PWA: install button, one icon badge for Signal, mail and tasks due, share target for files, manifest launch handler
build-container / build (push) Successful in 22m24s
build-container / build (push) Successful in 22m24s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5c5a7b5155
commit
924a024b0b
9 files changed
+88
-6
No files matched your search
@@ -22,7 +22,9 @@
|
||||
addEventListener('hashchange', route);
|
||||
restorePlayer();
|
||||
route();
|
||||
shareCheck();
|
||||
mailPoll(); setInterval(mailPoll, 2 * 60e3);
|
||||
taskPoll(); setInterval(taskPoll, 10 * 60e3);
|
||||
if ('serviceWorker' in navigator) navigator.serviceWorker.register('/sw.js').catch(() => {});
|
||||
})();
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@
|
||||
<script src="/file-actions.js"></script>
|
||||
<script src="/signal-view.js"></script>
|
||||
<script src="/mail.js"></script>
|
||||
<script src="/share.js"></script>
|
||||
<script src="/boot.js"></script>
|
||||
<script src="/shell.js"></script>
|
||||
</body>
|
||||
|
||||
@@ -91,5 +91,28 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
"dir": "ltr",
|
||||
"prefers_related_applications": false,
|
||||
"launch_handler": {
|
||||
"client_mode": [
|
||||
"navigate-existing",
|
||||
"auto"
|
||||
]
|
||||
},
|
||||
"share_target": {
|
||||
"action": "/share-target",
|
||||
"method": "POST",
|
||||
"enctype": "multipart/form-data",
|
||||
"params": {
|
||||
"files": [
|
||||
{
|
||||
"name": "files",
|
||||
"accept": [
|
||||
"*/*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
-1
@@ -91,8 +91,9 @@ function viewSettings() {
|
||||
</div><div id="sec-signal"><h2>Signal alerts</h2><p class="sub">How this computer gets your attention when a Signal message arrives (not while you are looking at that chat). Saved on this device only.</p>
|
||||
<div class="list"><label class="row" style="cursor:pointer"><span class="grow"><b>Play a sound</b><small>A short chime</small></span><input type="checkbox" id="sga-s"></label>
|
||||
<label class="row" style="cursor:pointer"><span class="grow"><b>Desktop notification</b><small id="sga-ns">Pops up even when this tab is in the background</small></span><input type="checkbox" id="sga-n"></label></div>
|
||||
<div class="bar"><button class="btn ghost" id="sga-t">Play test sound</button></div></div>${remindersHtml()}<h2>Install</h2><p class="sub">iPhone/iPad: Share → Add to Home Screen. Desktop Chrome/Edge: install icon in the address bar.</p></div>`;
|
||||
<div class="bar"><button class="btn ghost" id="sga-t">Play test sound</button></div></div>${remindersHtml()}<h2>Install</h2>${installHtml()}</div>`;
|
||||
remindersInit();
|
||||
installInit();
|
||||
// only show what this person may use
|
||||
const hide = (cond, sel) => { if (cond) document.querySelectorAll(sel).forEach(e => e.remove()); };
|
||||
hide(!((allowed('mail') && cfg.mail) || cfg.mailAdmin), '#sec-mail'); hide(!allowed('audio'), '#sec-audio'); hide(!allowed('signal'), '#sec-signal'); hide(!(allowed('files') || allowed('office')), '#row-sf, #out');
|
||||
@@ -173,3 +174,19 @@ function viewSettings() {
|
||||
$('#hubout').onclick = async () => { try { mcClear(); for (const k of Object.keys(localStorage)) if (k.startsWith('dc:')) localStorage.removeItem(k); } catch {} await authPost('/api/auth/logout'); location.replace('/login'); };
|
||||
const out = $('#out'); if (out) out.onclick = () => { store.del('sf_token'); toast('Signed out'); viewSettings(); };
|
||||
}
|
||||
|
||||
// ---- Install: the browser's own prompt where it offers one (Chrome, Edge, Android); iPhone/iPad have to use Share
|
||||
let installEvt = null;
|
||||
addEventListener('beforeinstallprompt', e => { e.preventDefault(); installEvt = e; const b = document.getElementById('install-btn'); if (b) installInit(); });
|
||||
addEventListener('appinstalled', () => { installEvt = null; const b = document.getElementById('install-box'); if (b) b.innerHTML = '<p class="sub">NE Hub is installed on this device.</p>'; });
|
||||
const isInstalled = () => matchMedia('(display-mode: standalone)').matches || navigator.standalone === true;
|
||||
function installHtml() {
|
||||
const body = isInstalled() ? '<p class="sub">NE Hub is installed on this device.</p>'
|
||||
: '<p class="sub">iPhone/iPad: Share → Add to Home Screen. Desktop Chrome/Edge: install icon in the address bar.</p><div class="bar"><button class="btn" id="install-btn" hidden>Install NE Hub</button></div>';
|
||||
return '<div id="install-box">' + body + '</div>';
|
||||
}
|
||||
function installInit() {
|
||||
const b = document.getElementById('install-btn'); if (!b || !installEvt) return;
|
||||
b.hidden = false;
|
||||
b.onclick = async () => { const ev = installEvt; installEvt = null; b.hidden = true; try { await ev.prompt(); } catch {} };
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
// Files shared into NE Hub from other apps (the manifest's share target; sw.js keeps them): choose a Seafile folder, then upload.
|
||||
async function shareCheck() {
|
||||
if (!('caches' in window)) return;
|
||||
let c, keys;
|
||||
try { c = await caches.open('hub-share'); keys = await c.keys(); } catch { return; }
|
||||
if (!keys.length) return;
|
||||
const clear = async () => { for (const k of await c.keys()) await c.delete(k); };
|
||||
if (!allowed('files')) { await clear(); toast('You do not have the Files tab, so shared files were discarded'); return; }
|
||||
const files = [];
|
||||
for (const k of keys) { const r = await c.match(k); if (r) files.push(new File([await r.blob()], decodeURIComponent(r.headers.get('x-name') || 'file'), { type: r.headers.get('content-type') || '' })); }
|
||||
const loc = store.get('office_loc');
|
||||
const dst = await chooseFolder('Save ' + (files.length === 1 ? '“' + files[0].name + '”' : files.length + ' shared files') + ' to…', loc ? { repo: loc.repo, repoName: loc.repoName || '', dir: loc.dir } : null, { okLabel: 'Upload here', newFolder: true });
|
||||
await clear();
|
||||
if (!dst) return toast('Shared files discarded');
|
||||
enqueueUploads(dst.repo, dst.dir, files.map(file => ({ file })));
|
||||
}
|
||||
@@ -28,9 +28,10 @@ function sgChime() {
|
||||
g.gain.setValueAtTime(0.0001, t + d); g.gain.exponentialRampToValueAtTime(0.22, t + d + 0.02); g.gain.exponentialRampToValueAtTime(0.0001, t + d + 0.4);
|
||||
o.connect(g); g.connect(sgAC.destination); o.start(t + d); o.stop(t + d + 0.45); });
|
||||
}
|
||||
function sgBadgeEverywhere() {
|
||||
document.title = (sg.unread ? '(' + (sg.unread > 99 ? '99+' : sg.unread) + ') ' : '') + 'NE Hub';
|
||||
try { if (navigator.setAppBadge) { sg.unread ? navigator.setAppBadge(sg.unread) : navigator.clearAppBadge(); } } catch {}
|
||||
function sgBadgeEverywhere() { // Signal chats, unread mail and tasks due: one number on the tab title and the installed app's icon
|
||||
const n = (sg.unread || 0) + (typeof mailUnread === 'number' ? mailUnread : 0) + (typeof taskDue === 'number' ? taskDue : 0);
|
||||
document.title = (n ? '(' + (n > 99 ? '99+' : n) + ') ' : '') + 'NE Hub';
|
||||
try { if (navigator.setAppBadge) { n ? navigator.setAppBadge(n) : navigator.clearAppBadge(); } } catch {}
|
||||
}
|
||||
function sgAlert(ev) { // a message from someone else arrived and you are not looking at that chat
|
||||
const a = sgAlerts();
|
||||
|
||||
+13
-1
@@ -1,5 +1,6 @@
|
||||
// App-shell cache only; Seafile/API traffic always goes to the network.
|
||||
const CACHE = 'hub-v8';
|
||||
const CACHE = 'hub-v9';
|
||||
const SHARE = 'hub-share'; // files shared into the app from other apps, held until the person picks a folder
|
||||
const SHELL = ['/icon.svg', '/manifest.webmanifest', '/icons/icon-192.png', '/icons/apple-touch-icon.png'];
|
||||
|
||||
// Install: keep the icons, and (when signed in) the page itself with the scripts and styles it names, so the app opens offline.
|
||||
@@ -21,8 +22,19 @@ self.addEventListener('activate', e => {
|
||||
e.waitUntil(caches.keys().then(ks => Promise.all(ks.filter(k => k !== CACHE).map(k => caches.delete(k)))));
|
||||
self.clients.claim();
|
||||
});
|
||||
// Share target: other apps send files here (POST); keep them and open the app, which asks where to put them.
|
||||
async function takeShare(request) {
|
||||
try {
|
||||
const form = await request.formData(), c = await caches.open(SHARE);
|
||||
for (const k of await c.keys()) await c.delete(k);
|
||||
let i = 0;
|
||||
for (const f of form.getAll('files')) if (f && f.name) await c.put('/share-files/' + (i++), new Response(f, { headers: { 'x-name': encodeURIComponent(f.name), 'content-type': f.type || 'application/octet-stream' } }));
|
||||
} catch { /* nothing usable was shared */ }
|
||||
return Response.redirect(self.location.origin + '/#/files', 303);
|
||||
}
|
||||
self.addEventListener('fetch', e => {
|
||||
const u = new URL(e.request.url);
|
||||
if (e.request.method === 'POST' && u.origin === location.origin && u.pathname === '/share-target') return e.respondWith(takeShare(e.request));
|
||||
if (e.request.method !== 'GET' || u.origin !== location.origin || u.pathname.startsWith('/sf/') || u.pathname.startsWith('/plex/') || u.pathname.startsWith('/api/')) return;
|
||||
e.respondWith(fetch(e.request).then(r => {
|
||||
if (r.ok) { const copy = r.clone(); caches.open(CACHE).then(c => c.put(e.request, copy)); }
|
||||
|
||||
@@ -99,6 +99,13 @@ function newListSheet(lists, list) {
|
||||
el.querySelectorAll('[data-rm]').forEach(b => b.onclick = () => confirmSheet('Delete list?', 'Its tasks are deleted too.', 'Delete', async () => { await jfetch('/api/tasks/lists/delete', { method: 'POST', body: JSON.stringify({ id: b.dataset.rm }) }); store.del('task_list'); viewTasks(); }));
|
||||
}
|
||||
|
||||
// Overdue and due-today tasks, for the icon badge
|
||||
let taskDue = 0;
|
||||
async function taskPoll() {
|
||||
if (!allowed('tasks')) return;
|
||||
try { const s = await jfetch('/api/tasks/summary?today=' + ymd(new Date())), n = (s.overdue || 0) + (s.today || 0); if (n !== taskDue) { taskDue = n; renderNav(); } } catch {}
|
||||
}
|
||||
|
||||
// Home: a card only when something is overdue or due today
|
||||
async function renderTasksCard(el) {
|
||||
if (!el) return;
|
||||
|
||||
Reference in new issue
Block a user