From 2b79160f955dbbddf1df81af1a64d9f69b1f1b29 Mon Sep 17 00:00:00 2001 From: nmemmert <67961292+nmemmert@users.noreply.github.com> Date: Fri, 23 Oct 2026 09:54:33 -0400 Subject: [PATCH] Tasks: an All view with every list, NE Hub and iCloud, together Co-Authored-By: Claude Sonnet 5.5 --- public/sw.js | 2 +- public/tasks.js | 31 ++++++++++++++++++------------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/public/sw.js b/public/sw.js index 8ba9f89..a8c73d3 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,5 +1,5 @@ // App-shell cache only; Seafile/API traffic always goes to the network. -const CACHE = 'hub-v24'; +const CACHE = 'hub-v25'; 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']; diff --git a/public/tasks.js b/public/tasks.js index e29306c..a7386b7 100644 --- a/public/tasks.js +++ b/public/tasks.js @@ -3,6 +3,7 @@ const dueInfo = d => { if (!d) return null; const t = ymd(new Date()); const label = new Date(d + 'T00:00:00').toLocaleDateString([], { month: 'short', day: 'numeric' }); return d < t ? { text: 'Overdue · ' + label, color: 'var(--danger)' } : d === t ? { text: 'Today', color: 'var(--accent)' } : { text: label, color: 'var(--muted)' }; }; const taskStore = []; // tasks on screen, so a tap can open the right one +let taskLists = []; // the lists (NE Hub and iCloud), so a task seen in "All" opens with its own list's options async function viewTasks() { const v = $('#view'); @@ -10,11 +11,12 @@ async function viewTasks() { try { const { lists, icloud } = await jfetch('/api/tasks/lists'); if (!location.hash.startsWith('#/tasks')) return; - let cur = store.get('task_list'); if (!lists.some(l => l.id === cur)) cur = lists[0].id; - const list = lists.find(l => l.id === cur); + taskLists = lists; + let cur = store.get('task_list'); if (cur !== 'all' && !lists.some(l => l.id === cur)) cur = lists.length > 1 ? 'all' : lists[0].id; // (All: every list, NE Hub and iCloud, together) + const list = cur === 'all' ? lists.find(l => l.id === 'local:inbox') || lists.find(l => l.local) || lists[0] : lists.find(l => l.id === cur); // (where a task typed into All goes) v.innerHTML = `
${plannerHead('tasks')} - -
+
${lists.length > 1 ? `All` : ''}${lists.map(l => `${esc(l.name)}${l.icloud ? ' (iCloud)' : ''}`).join('')}+ List
+

Loading…

`; $('#tn').textContent = !icloud.connected ? 'Tip: connect iCloud (Calendar → Manage calendars) to include any reminder lists your account shares with apps.' @@ -23,7 +25,7 @@ async function viewTasks() { $('#nl2').onclick = e => { e.preventDefault(); newListSheet(lists, list); }; $('#qa').onsubmit = async e => { e.preventDefault(); const t = $('#qt').value.trim(); if (!t) return; - try { await jfetch('/api/tasks', { method: 'POST', body: JSON.stringify({ list: cur, fields: { title: t, notes: '', due: '', done: false } }) }); $('#qt').value = ''; loadTaskList(cur, list); } + try { await jfetch('/api/tasks', { method: 'POST', body: JSON.stringify({ list: list.id, fields: { title: t, notes: '', due: '', done: false } }) }); $('#qt').value = ''; loadTaskList(cur, list); } catch (err) { toast(err.message); } }; loadTaskList(cur, list); @@ -33,7 +35,9 @@ async function viewTasks() { async function loadTaskList(cur, list) { const box = $('#tl'); if (!box) return; try { - const tasks = await jfetch('/api/tasks?list=' + encodeURIComponent(cur)); + const all = cur === 'all', byId = Object.fromEntries(taskLists.map(l => [l.id, l])); + const tasks = all ? (await Promise.all(taskLists.map(l => jfetch('/api/tasks?list=' + encodeURIComponent(l.id)).then(r => r.map(t => ({ ...t, list: l.id, _ln: l.name + (l.icloud ? ' (iCloud)' : '') }))).catch(() => [])))) + .flat() : await jfetch('/api/tasks?list=' + encodeURIComponent(cur)); if (!$('#tl')) return; taskStore.length = 0; const ids = new Set(tasks.map(t => t.id)), byParent = {}; @@ -43,7 +47,7 @@ async function loadTaskList(cur, list) { const done = tasks.filter(t => top(t) && t.done).sort((a, b) => (b.completedAt || 0) - (a.completedAt || 0) || a.title.localeCompare(b.title)); const REPEAT_LABEL = { daily: 'Daily', weekdays: 'Weekdays', weekly: 'Weekly', monthly: 'Monthly', yearly: 'Yearly' }; const row = (t, sub) => { const i = taskStore.push(t) - 1, du = dueInfo(t.due), kids = byParent[t.id] || [], rep = REPEAT_LABEL[t.repeat], prog = kids.length ? kids.filter(k => k.done).length + '/' + kids.length + ' subtasks' : ''; - const extra = [rep ? '↻ ' + rep : '', prog].filter(Boolean).join(' · '); + const extra = [all && t._ln ? t._ln : '', rep ? '↻ ' + rep : '', prog].filter(Boolean).join(' · '); return `
`; }; const showDone = store.get('task_show_done', false); @@ -52,14 +56,15 @@ async function loadTaskList(cur, list) { const sd = $('#sd'); if (sd) sd.onclick = e => { e.preventDefault(); store.set('task_show_done', !showDone); loadTaskList(cur, list); }; box.querySelectorAll('button[data-t]').forEach(b => b.onclick = async () => { const t = taskStore[+b.dataset.t]; b.disabled = true; - try { const r = await jfetch('/api/tasks/toggle', { method: 'POST', body: JSON.stringify({ list: cur, id: t.id, href: t.href, done: !t.done }) }); if (r && r.rolled) toast('Repeats: next due ' + r.rolled); loadTaskList(cur, list); } + try { const r = await jfetch('/api/tasks/toggle', { method: 'POST', body: JSON.stringify({ list: t.list, id: t.id, href: t.href, done: !t.done }) }); if (r && r.rolled) toast('Repeats: next due ' + r.rolled); loadTaskList(cur, list); } catch (err) { b.disabled = false; toast(err.message); } }); - box.querySelectorAll('button[data-e]').forEach(b => b.onclick = () => taskSheet(taskStore[+b.dataset.e], cur, list)); + box.querySelectorAll('button[data-e]').forEach(b => b.onclick = () => { const t = taskStore[+b.dataset.e]; taskSheet(t, t.list, byId[t.list] || list, cur, list); }); } catch (e) { box.innerHTML = `

${esc(e.message)}

`; } } -function taskSheet(t, cur, list) { +function taskSheet(t, cur, list, viewCur, viewList) { // (cur and list: the task's own list; viewCur and viewList: what the page is showing, to redraw) + const redraw = () => loadTaskList(viewCur || cur, viewList || list); const el = openSheet('Task', list.name, `
@@ -73,18 +78,18 @@ function taskSheet(t, cur, list) { el.querySelector('#sx').onclick = closeSheet; el.querySelector('#tclr').onclick = () => { el.querySelector('#tdue').value = ''; }; el.querySelector('#tf').onsubmit = async e => { e.preventDefault(); const btn = el.querySelector('form .btn:not(.ghost):not(.danger)'); btn.disabled = true; - try { await jfetch('/api/tasks', { method: 'POST', body: JSON.stringify({ list: cur, id: t.id, href: t.href, fields: { title: el.querySelector('#tt').value.trim(), notes: el.querySelector('#tnote').value.trim(), due: el.querySelector('#tdue').value, done: el.querySelector('#tdone').checked, ...(el.querySelector('#trep') ? { repeat: el.querySelector('#trep').value } : {}) } }) }).then(r => { if (r && r.rolled) toast('Repeats: next due ' + r.rolled); }); closeSheet(); loadTaskList(cur, list); } + try { await jfetch('/api/tasks', { method: 'POST', body: JSON.stringify({ list: cur, id: t.id, href: t.href, fields: { title: el.querySelector('#tt').value.trim(), notes: el.querySelector('#tnote').value.trim(), due: el.querySelector('#tdue').value, done: el.querySelector('#tdone').checked, ...(el.querySelector('#trep') ? { repeat: el.querySelector('#trep').value } : {}) } }) }).then(r => { if (r && r.rolled) toast('Repeats: next due ' + r.rolled); }); closeSheet(); redraw(); } catch (err) { btn.disabled = false; el.querySelector('#te').textContent = err.message; } }; if (el.querySelector('#tsubadd')) el.querySelector('#tsubadd').onclick = async () => { const inp = el.querySelector('#tsub'), title = inp.value.trim(); if (!title) return; try { await jfetch('/api/tasks', { method: 'POST', body: JSON.stringify({ list: cur, fields: { title, notes: '', due: '', done: false, parent: t.id } }) }); - el.querySelector('#tsubs').insertAdjacentHTML('beforeend', `
${esc(title)}
`); inp.value = ''; inp.focus(); loadTaskList(cur, list); } + el.querySelector('#tsubs').insertAdjacentHTML('beforeend', `
${esc(title)}
`); inp.value = ''; inp.focus(); redraw(); } catch (err) { el.querySelector('#te').textContent = err.message; } }; if (el.querySelector('#tsub')) el.querySelector('#tsub').onkeydown = e => { if (e.key === 'Enter') { e.preventDefault(); el.querySelector('#tsubadd').click(); } }; el.querySelector('#td').onclick = () => confirmSheet('Delete task?', t.title + (taskStore.some(k => k.parent === t.id) ? ' and its subtasks' : ''), 'Delete', async () => { - try { await jfetch('/api/tasks/delete', { method: 'POST', body: JSON.stringify({ list: cur, id: t.id, href: t.href, etag: t.etag }) }); toast('Task deleted'); loadTaskList(cur, list); } + try { await jfetch('/api/tasks/delete', { method: 'POST', body: JSON.stringify({ list: cur, id: t.id, href: t.href, etag: t.etag }) }); toast('Task deleted'); redraw(); } catch (err) { toast(err.message); } }); }