| [053bf51] | 1 | <div class="container" style="max-width:640px;margin:1.5rem auto 3rem;padding:0 1rem">
|
|---|
| [9a00f28] | 2 | <h1 style="margin:0 0 .3rem"><%= t('push.t') %></h1>
|
|---|
| [053bf51] | 3 | <p style="color:var(--ink-soft,#888);margin:0 0 1.2rem">
|
|---|
| [9a00f28] | 4 | <%= t('push.intro') %>
|
|---|
| [053bf51] | 5 | </p>
|
|---|
| 6 |
|
|---|
| 7 | <% if (!vapidKey) { %>
|
|---|
| [9a00f28] | 8 | <p style="color:#c0392b"><%= t('push.unavailable') %></p>
|
|---|
| [053bf51] | 9 | <% } else { %>
|
|---|
| 10 |
|
|---|
| 11 | <div id="np-unsupported" hidden style="color:#c0392b;margin:0 0 1rem">
|
|---|
| [9a00f28] | 12 | <%= t('push.unsupported') %>
|
|---|
| [053bf51] | 13 | </div>
|
|---|
| 14 | <div id="np-ios-hint" hidden style="border:1px solid var(--border,#333);border-radius:10px;padding:.8rem 1rem;margin:0 0 1rem;font-size:.92rem;color:var(--ink-soft,#888)">
|
|---|
| [9a00f28] | 15 | <%= t('push.ios_hint') %>
|
|---|
| [053bf51] | 16 | </div>
|
|---|
| 17 |
|
|---|
| 18 | <section style="border:1px solid var(--border,#333);border-radius:12px;padding:1rem 1.1rem;margin:0 0 1.2rem">
|
|---|
| [9a00f28] | 19 | <p style="margin:0 0 .6rem"><strong><%= t('push.this_device') %></strong> <span id="np-state"><%= t('push.checking') %></span></p>
|
|---|
| [053bf51] | 20 | <div style="display:flex;gap:.6rem;flex-wrap:wrap">
|
|---|
| [9a00f28] | 21 | <button type="button" id="np-on" class="btn btn-primary" hidden><%= t('push.enable') %></button>
|
|---|
| 22 | <button type="button" id="np-off" class="btn" hidden><%= t('push.disable') %></button>
|
|---|
| 23 | <button type="button" id="np-test" class="btn" hidden><%= t('push.test') %></button>
|
|---|
| [053bf51] | 24 | </div>
|
|---|
| 25 | <fieldset id="np-alerts" hidden style="border:none;padding:0;margin:1rem 0 0">
|
|---|
| [9a00f28] | 26 | <legend style="font-weight:600;margin-bottom:.4rem"><%= t('push.what') %></legend>
|
|---|
| 27 | <label style="display:block"><input type="checkbox" data-alert="follow"> <%= t('push.a_follow') %></label>
|
|---|
| 28 | <label style="display:block"><input type="checkbox" data-alert="reply"> <%= t('push.a_reply') %></label>
|
|---|
| 29 | <label style="display:block"><input type="checkbox" data-alert="like"> <%= t('push.a_like') %></label>
|
|---|
| 30 | <label style="display:block"><input type="checkbox" data-alert="boost"> <%= t('push.a_boost') %></label>
|
|---|
| 31 | <label style="display:block"><input type="checkbox" data-alert="dm"> <%= t('push.a_dm') %></label>
|
|---|
| 32 | <p id="np-saved" hidden style="color:var(--accent);margin:.4rem 0 0"><%= t('push.saved') %></p>
|
|---|
| [053bf51] | 33 | </fieldset>
|
|---|
| 34 | </section>
|
|---|
| 35 |
|
|---|
| 36 | <% if (subscriptions.length) { %>
|
|---|
| [9a00f28] | 37 | <h2 style="font-size:1.05rem;margin:0 0 .5rem"><%= t('push.devices') %></h2>
|
|---|
| [053bf51] | 38 | <ul style="list-style:none;padding:0;margin:0 0 1.2rem">
|
|---|
| 39 | <% subscriptions.forEach(function (s) { %>
|
|---|
| 40 | <li data-endpoint="<%= s.endpoint %>" style="display:flex;justify-content:space-between;gap:.8rem;align-items:center;padding:.45rem 0;border-bottom:1px solid var(--border,#2a2a2a)">
|
|---|
| [9a00f28] | 41 | <span style="min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"><%= s.ua_label || t('push.device') %> <small style="color:var(--ink-soft,#888)"><%= t('push.since') %> <%= String(s.created_at || '').slice(0, 10) %></small></span>
|
|---|
| 42 | <button type="button" class="btn np-remove" data-endpoint="<%= s.endpoint %>"><%= t('push.remove') %></button>
|
|---|
| [053bf51] | 43 | </li>
|
|---|
| 44 | <% }); %>
|
|---|
| 45 | </ul>
|
|---|
| 46 | <% } %>
|
|---|
| 47 |
|
|---|
| 48 | <% } %>
|
|---|
| 49 |
|
|---|
| [9a00f28] | 50 | <p style="margin-top:1.5rem"><a href="/admin">← <%= t('admin.back') %></a></p>
|
|---|
| [053bf51] | 51 | </div>
|
|---|
| 52 |
|
|---|
| 53 | <% if (vapidKey) { %>
|
|---|
| 54 | <script id="np-data" type="application/json"><%- JSON.stringify({
|
|---|
| 55 | vapid: vapidKey,
|
|---|
| 56 | alerts: defaultAlerts,
|
|---|
| 57 | saved: subscriptions.reduce(function (m, s) { try { m[s.endpoint] = JSON.parse(s.alert_types || '{}'); } catch (e) { m[s.endpoint] = {}; } return m; }, {}),
|
|---|
| [9a00f28] | 58 | i18n: { on: t('push.state_on'), off: t('push.state_off'), denied: t('push.state_denied'), unknown: t('push.state_unknown'), unsupported: t('push.state_unsupported'), failed: t('push.enable_failed') },
|
|---|
| [053bf51] | 59 | }).replace(/</g, '\\u003c') %></script>
|
|---|
| 60 | <script>
|
|---|
| 61 | (function () {
|
|---|
| 62 | var cfg = JSON.parse(document.getElementById('np-data').textContent);
|
|---|
| 63 | var elState = document.getElementById('np-state');
|
|---|
| 64 | var btnOn = document.getElementById('np-on'), btnOff = document.getElementById('np-off'), btnTest = document.getElementById('np-test');
|
|---|
| 65 | var alertsBox = document.getElementById('np-alerts'), savedMsg = document.getElementById('np-saved');
|
|---|
| 66 | var currentEndpoint = null;
|
|---|
| 67 |
|
|---|
| 68 | var isIos = /iPad|iPhone|iPod/.test(navigator.userAgent);
|
|---|
| 69 | var standalone = window.matchMedia('(display-mode: standalone)').matches || window.navigator.standalone === true;
|
|---|
| 70 | if (isIos && !standalone) document.getElementById('np-ios-hint').hidden = false;
|
|---|
| 71 |
|
|---|
| 72 | if (!('serviceWorker' in navigator) || !('PushManager' in window) || !('Notification' in window)) {
|
|---|
| 73 | document.getElementById('np-unsupported').hidden = false;
|
|---|
| [9a00f28] | 74 | elState.textContent = cfg.i18n.unsupported;
|
|---|
| [053bf51] | 75 | return;
|
|---|
| 76 | }
|
|---|
| 77 |
|
|---|
| 78 | function b64ToU8(s) {
|
|---|
| 79 | var pad = '='.repeat((4 - (s.length % 4)) % 4);
|
|---|
| 80 | var raw = atob((s + pad).replace(/-/g, '+').replace(/_/g, '/'));
|
|---|
| 81 | var out = new Uint8Array(raw.length);
|
|---|
| 82 | for (var i = 0; i < raw.length; i++) out[i] = raw.charCodeAt(i);
|
|---|
| 83 | return out;
|
|---|
| 84 | }
|
|---|
| 85 | function post(url, body) {
|
|---|
| 86 | return fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body || {}) });
|
|---|
| 87 | }
|
|---|
| 88 | function deviceLabel() {
|
|---|
| 89 | var ua = navigator.userAgent;
|
|---|
| 90 | var browser = /Firefox\//.test(ua) ? 'Firefox' : /Edg\//.test(ua) ? 'Edge' : /Chrome\//.test(ua) ? 'Chrome' : /Safari\//.test(ua) ? 'Safari' : 'Browser';
|
|---|
| 91 | var os = /Android/.test(ua) ? 'Android' : /iPad|iPhone|iPod/.test(ua) ? 'iOS' : /Mac/.test(ua) ? 'macOS' : /Win/.test(ua) ? 'Windows' : /Linux/.test(ua) ? 'Linux' : '';
|
|---|
| 92 | return (browser + (os ? ' op ' + os : ''));
|
|---|
| 93 | }
|
|---|
| 94 | function readAlertBoxes() {
|
|---|
| 95 | var out = {};
|
|---|
| 96 | alertsBox.querySelectorAll('input[data-alert]').forEach(function (cb) { out[cb.getAttribute('data-alert')] = cb.checked ? 1 : 0; });
|
|---|
| 97 | return out;
|
|---|
| 98 | }
|
|---|
| 99 | function setAlertBoxes(alerts) {
|
|---|
| 100 | alertsBox.querySelectorAll('input[data-alert]').forEach(function (cb) {
|
|---|
| 101 | cb.checked = !!alerts[cb.getAttribute('data-alert')];
|
|---|
| 102 | });
|
|---|
| 103 | }
|
|---|
| 104 |
|
|---|
| 105 | function render(sub) {
|
|---|
| 106 | currentEndpoint = sub ? sub.endpoint : null;
|
|---|
| [9a00f28] | 107 | elState.textContent = sub ? cfg.i18n.on : (Notification.permission === 'denied' ? cfg.i18n.denied : cfg.i18n.off);
|
|---|
| [053bf51] | 108 | btnOn.hidden = !!sub || Notification.permission === 'denied';
|
|---|
| 109 | btnOff.hidden = !sub;
|
|---|
| 110 | btnTest.hidden = !sub;
|
|---|
| 111 | alertsBox.hidden = !sub;
|
|---|
| 112 | }
|
|---|
| 113 |
|
|---|
| 114 | navigator.serviceWorker.ready.then(function (reg) {
|
|---|
| 115 | return reg.pushManager.getSubscription();
|
|---|
| 116 | }).then(function (sub) {
|
|---|
| 117 | // Show this device's SAVED prefs when we know them, defaults otherwise.
|
|---|
| 118 | setAlertBoxes((sub && cfg.saved[sub.endpoint]) ? Object.assign({}, cfg.alerts, cfg.saved[sub.endpoint]) : cfg.alerts);
|
|---|
| 119 | render(sub);
|
|---|
| [9a00f28] | 120 | }).catch(function () { elState.textContent = cfg.i18n.unknown; });
|
|---|
| [053bf51] | 121 |
|
|---|
| 122 | btnOn.addEventListener('click', function () {
|
|---|
| 123 | btnOn.disabled = true;
|
|---|
| 124 | Notification.requestPermission().then(function (perm) {
|
|---|
| 125 | if (perm !== 'granted') { btnOn.disabled = false; render(null); return; }
|
|---|
| 126 | navigator.serviceWorker.ready.then(function (reg) {
|
|---|
| 127 | return reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: b64ToU8(cfg.vapid) });
|
|---|
| 128 | }).then(function (sub) {
|
|---|
| 129 | return post('/push/subscribe', { subscription: sub.toJSON(), alerts: readAlertBoxes(), uaLabel: deviceLabel() })
|
|---|
| 130 | .then(function (r) { if (!r.ok) throw new Error('subscribe failed'); render(sub); location.reload(); });
|
|---|
| [9a00f28] | 131 | }).catch(function () { btnOn.disabled = false; elState.textContent = cfg.i18n.failed; });
|
|---|
| [053bf51] | 132 | });
|
|---|
| 133 | });
|
|---|
| 134 |
|
|---|
| 135 | btnOff.addEventListener('click', function () {
|
|---|
| 136 | navigator.serviceWorker.ready.then(function (reg) { return reg.pushManager.getSubscription(); }).then(function (sub) {
|
|---|
| 137 | if (!sub) { render(null); return; }
|
|---|
| 138 | var ep = sub.endpoint;
|
|---|
| 139 | sub.unsubscribe().then(function () { return post('/push/unsubscribe', { endpoint: ep }); })
|
|---|
| 140 | .then(function () { location.reload(); });
|
|---|
| 141 | });
|
|---|
| 142 | });
|
|---|
| 143 |
|
|---|
| 144 | btnTest.addEventListener('click', function () {
|
|---|
| 145 | btnTest.disabled = true;
|
|---|
| 146 | post('/push/test').then(function () { setTimeout(function () { btnTest.disabled = false; }, 2000); });
|
|---|
| 147 | });
|
|---|
| 148 |
|
|---|
| 149 | alertsBox.addEventListener('change', function () {
|
|---|
| 150 | if (!currentEndpoint) return;
|
|---|
| 151 | post('/push/alerts', { endpoint: currentEndpoint, alerts: readAlertBoxes() }).then(function (r) {
|
|---|
| 152 | if (r.ok) { savedMsg.hidden = false; setTimeout(function () { savedMsg.hidden = true; }, 1500); }
|
|---|
| 153 | });
|
|---|
| 154 | });
|
|---|
| 155 |
|
|---|
| 156 | document.querySelectorAll('.np-remove').forEach(function (btn) {
|
|---|
| 157 | btn.addEventListener('click', function () {
|
|---|
| 158 | post('/push/unsubscribe', { endpoint: btn.getAttribute('data-endpoint') }).then(function () { location.reload(); });
|
|---|
| 159 | });
|
|---|
| 160 | });
|
|---|
| 161 | })();
|
|---|
| 162 | </script>
|
|---|
| 163 | <% } %>
|
|---|