Tasks: an All view with every list, NE Hub and iCloud, together
build-container / build (push) Successful in 12m21s
build-container / build (push) Successful in 12m21s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
732906bc7a
commit
2b79160f95
2 files changed
+19
-14
No files matched your search
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
// App-shell cache only; Seafile/API traffic always goes to the network.
|
// 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 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'];
|
const SHELL = ['/icon.svg', '/manifest.webmanifest', '/icons/icon-192.png', '/icons/apple-touch-icon.png'];
|
||||||
|
|
||||||
|
|||||||
+18
-13
@@ -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' });
|
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)' }; };
|
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
|
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() {
|
async function viewTasks() {
|
||||||
const v = $('#view');
|
const v = $('#view');
|
||||||
@@ -10,11 +11,12 @@ async function viewTasks() {
|
|||||||
try {
|
try {
|
||||||
const { lists, icloud } = await jfetch('/api/tasks/lists');
|
const { lists, icloud } = await jfetch('/api/tasks/lists');
|
||||||
if (!location.hash.startsWith('#/tasks')) return;
|
if (!location.hash.startsWith('#/tasks')) return;
|
||||||
let cur = store.get('task_list'); if (!lists.some(l => l.id === cur)) cur = lists[0].id;
|
taskLists = lists;
|
||||||
const list = lists.find(l => l.id === cur);
|
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 = `<div class="wrap">${plannerHead('tasks')}
|
v.innerHTML = `<div class="wrap">${plannerHead('tasks')}
|
||||||
<div class="seg">${lists.map(l => `<a href="#" data-l="${esc(l.id)}" class="${l.id === cur ? 'on' : ''}">${esc(l.name)}${l.icloud ? ' (iCloud)' : ''}</a>`).join('')}<a href="#" id="nl2" aria-label="New list">+ List</a></div>
|
<div class="seg">${lists.length > 1 ? `<a href="#" data-l="all" class="${cur === 'all' ? 'on' : ''}">All</a>` : ''}${lists.map(l => `<a href="#" data-l="${esc(l.id)}" class="${l.id === cur ? 'on' : ''}">${esc(l.name)}${l.icloud ? ' (iCloud)' : ''}</a>`).join('')}<a href="#" id="nl2" aria-label="New list">+ List</a></div>
|
||||||
<form id="qa" class="bar" style="margin:6px 0 12px;flex-wrap:nowrap"><input type="text" id="qt" placeholder="Add a task" autocomplete="off" style="flex:1"><button class="btn" aria-label="Add">${icon('check')}Add</button></form>
|
<form id="qa" class="bar" style="margin:6px 0 12px;flex-wrap:nowrap"><input type="text" id="qt" placeholder="${cur === 'all' ? 'Add a task to ' + esc(list.name) : 'Add a task'}" autocomplete="off" style="flex:1"><button class="btn" aria-label="Add">${icon('check')}Add</button></form>
|
||||||
<div id="tl"><p class="empty">Loading…</p></div>
|
<div id="tl"><p class="empty">Loading…</p></div>
|
||||||
<p class="sub" id="tn" style="margin-top:16px"></p></div>`;
|
<p class="sub" id="tn" style="margin-top:16px"></p></div>`;
|
||||||
$('#tn').textContent = !icloud.connected ? 'Tip: connect iCloud (Calendar → Manage calendars) to include any reminder lists your account shares with apps.'
|
$('#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); };
|
$('#nl2').onclick = e => { e.preventDefault(); newListSheet(lists, list); };
|
||||||
$('#qa').onsubmit = async e => {
|
$('#qa').onsubmit = async e => {
|
||||||
e.preventDefault(); const t = $('#qt').value.trim(); if (!t) return;
|
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); }
|
catch (err) { toast(err.message); }
|
||||||
};
|
};
|
||||||
loadTaskList(cur, list);
|
loadTaskList(cur, list);
|
||||||
@@ -33,7 +35,9 @@ async function viewTasks() {
|
|||||||
async function loadTaskList(cur, list) {
|
async function loadTaskList(cur, list) {
|
||||||
const box = $('#tl'); if (!box) return;
|
const box = $('#tl'); if (!box) return;
|
||||||
try {
|
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;
|
if (!$('#tl')) return;
|
||||||
taskStore.length = 0;
|
taskStore.length = 0;
|
||||||
const ids = new Set(tasks.map(t => t.id)), byParent = {};
|
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 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 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 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 `<div class="row it${t.done ? ' donetask' : ''}${sub ? ' subtask' : ''}"><button class="ibtn tick" data-t="${i}" aria-label="${t.done ? 'Mark not done' : 'Mark done'}">${icon('check')}</button>
|
return `<div class="row it${t.done ? ' donetask' : ''}${sub ? ' subtask' : ''}"><button class="ibtn tick" data-t="${i}" aria-label="${t.done ? 'Mark not done' : 'Mark done'}">${icon('check')}</button>
|
||||||
<button class="rmain" data-e="${i}"><span class="grow"><b>${esc(t.title)}</b>${du || t.notes || extra ? `<small>${du ? `<span style="color:${du.color}">${esc(du.text)}</span>` : ''}${du && (extra || t.notes) ? ' · ' : ''}${esc(extra)}${extra && t.notes ? ' · ' : ''}${esc((t.notes || '').split('\n')[0])}</small>` : ''}</span></button></div>`; };
|
<button class="rmain" data-e="${i}"><span class="grow"><b>${esc(t.title)}</b>${du || t.notes || extra ? `<small>${du ? `<span style="color:${du.color}">${esc(du.text)}</span>` : ''}${du && (extra || t.notes) ? ' · ' : ''}${esc(extra)}${extra && t.notes ? ' · ' : ''}${esc((t.notes || '').split('\n')[0])}</small>` : ''}</span></button></div>`; };
|
||||||
const showDone = store.get('task_show_done', false);
|
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); };
|
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 () => {
|
box.querySelectorAll('button[data-t]').forEach(b => b.onclick = async () => {
|
||||||
const t = taskStore[+b.dataset.t]; b.disabled = true;
|
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); }
|
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 = `<p class="err">${esc(e.message)}</p>`; }
|
} catch (e) { box.innerHTML = `<p class="err">${esc(e.message)}</p>`; }
|
||||||
}
|
}
|
||||||
|
|
||||||
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, `<form id="tf"><input type="text" id="tt" value="${esc(t.title)}" autocomplete="off" required style="margin-top:6px">
|
const el = openSheet('Task', list.name, `<form id="tf"><input type="text" id="tt" value="${esc(t.title)}" autocomplete="off" required style="margin-top:6px">
|
||||||
<label for="tdue">Due date</label><div style="display:flex;gap:8px"><input type="date" id="tdue" value="${esc(t.due)}" style="flex:1"><button type="button" class="btn ghost" id="tclr">Clear</button></div>
|
<label for="tdue">Due date</label><div style="display:flex;gap:8px"><input type="date" id="tdue" value="${esc(t.due)}" style="flex:1"><button type="button" class="btn ghost" id="tclr">Clear</button></div>
|
||||||
<label for="tnote">Notes</label><textarea id="tnote" rows="3">${esc(t.notes)}</textarea>
|
<label for="tnote">Notes</label><textarea id="tnote" rows="3">${esc(t.notes)}</textarea>
|
||||||
@@ -73,18 +78,18 @@ function taskSheet(t, cur, list) {
|
|||||||
el.querySelector('#sx').onclick = closeSheet; el.querySelector('#tclr').onclick = () => { el.querySelector('#tdue').value = ''; };
|
el.querySelector('#sx').onclick = closeSheet; el.querySelector('#tclr').onclick = () => { el.querySelector('#tdue').value = ''; };
|
||||||
el.querySelector('#tf').onsubmit = async e => {
|
el.querySelector('#tf').onsubmit = async e => {
|
||||||
e.preventDefault(); const btn = el.querySelector('form .btn:not(.ghost):not(.danger)'); btn.disabled = true;
|
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; }
|
catch (err) { btn.disabled = false; el.querySelector('#te').textContent = err.message; }
|
||||||
};
|
};
|
||||||
if (el.querySelector('#tsubadd')) el.querySelector('#tsubadd').onclick = async () => {
|
if (el.querySelector('#tsubadd')) el.querySelector('#tsubadd').onclick = async () => {
|
||||||
const inp = el.querySelector('#tsub'), title = inp.value.trim(); if (!title) return;
|
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 } }) });
|
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', `<div class="row"><span class="grow"><b>${esc(title)}</b></span></div>`); inp.value = ''; inp.focus(); loadTaskList(cur, list); }
|
el.querySelector('#tsubs').insertAdjacentHTML('beforeend', `<div class="row"><span class="grow"><b>${esc(title)}</b></span></div>`); inp.value = ''; inp.focus(); redraw(); }
|
||||||
catch (err) { el.querySelector('#te').textContent = err.message; }
|
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(); } };
|
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 () => {
|
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); }
|
catch (err) { toast(err.message); }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user