Files
hub/public/today.js
T

49 lines
6.9 KiB
JavaScript

'use strict';
// Home: "Pick up where you left off" (Continue watching and Continue listening), shown above the day's agenda in the evening and below it in the morning.
async function homeResume(el) {
if (!el) return;
const bits = await Promise.allSettled([
allowed('video') ? (async () => {
const st = await jfetch('/api/plex/status'); if (!st.signedIn || !st.server) return '';
px.serverKey = st.server;
const items = ((await px.get('/library/onDeck?X-Plex-Container-Start=0&X-Plex-Container-Size=8')).Metadata || []).filter(m => m.type === 'movie' || m.type === 'episode').slice(0, 6);
return items.length ? `<h3 class="sub" style="margin:10px 0 4px">Continue watching</h3><div class="vrow">${items.map(m => vdCard(m, true)).join('')}</div>` : '';
})() : '',
allowed('audio') ? (async () => {
const [data] = await Promise.all([abApi('/books'), abLoadProg()]), books = Array.isArray(data) ? data : (data.books || Object.values(data));
const cont = books.filter(b => { const p = abProg[abId(b)]; return p && p.pos > 10 && p.dur && p.pos < p.dur - 30; }).sort((a, b) => abProg[abId(b)].t - abProg[abId(a)].t).slice(0, 6);
today.books = cont;
return cont.length ? `<h3 class="sub" style="margin:10px 0 4px">Continue listening</h3><div class="vrow">${cont.map(b => { const p = abProg[abId(b)], pc = Math.min(100, p.pos / p.dur * 100);
return `<a class="vcard" href="#/audio" data-book="${esc(abId(b))}" style="flex:none;width:130px"><span class="vimg" style="aspect-ratio:1/1;background:linear-gradient(135deg,hsl(${abHue(abTitle(b))} 55% 40%),hsl(${abHue(abTitle(b)) + 40} 55% 28%))"><img loading="lazy" alt="" src="${abCover(b)}" onerror="this.remove()">${pc > 1 ? `<i class="vprog"><b style="width:${pc}%"></b></i>` : ''}</span><b>${esc(abTitle(b))}</b><small>${esc(abAuthor(b))}</small></a>`; }).join('')}</div>` : '';
})() : '',
allowed('read') ? (async () => {
const list = await jfetch('/api/read/list');
const cont = list.filter(b => b.pct > 0.01 && b.pct < 0.98).sort((a, b) => b.read - a.read).slice(0, 6);
return cont.length ? `<h3 class="sub" style="margin:10px 0 4px">Continue reading</h3><div class="vrow">${cont.map(b => `<a class="vcard" href="#/read/open/${esc(b.id)}" style="flex:none;width:110px"><span class="vimg rdcover" style="aspect-ratio:2/3">${b.cover ? `<img loading="lazy" alt="" src="/api/read/book/${esc(b.id)}/cover" onerror="this.remove()">` : ''}<em>${esc(b.title)}</em><u class="rdprog"><s style="width:${Math.round(b.pct * 100)}%"></s></u></span><b>${esc(b.title)}</b><small>${Math.round(b.pct * 100)}%</small></a>`).join('')}</div>` : '';
})() : '',
]);
const html = bits.map(b => b.status === 'fulfilled' ? b.value : '').join('');
if (!html || !el.isConnected) return;
el.innerHTML = `<h2>Pick up where you left off</h2>${html}`;
el.querySelectorAll('a[data-book]').forEach(a => a.onclick = e => { e.preventDefault(); const b = (today.books || []).find(x => abId(x) === a.dataset.book); if (b) abSheet(b); });
}
const today = { books: [] };
// Home: how much you listened this week (podcasts and audiobooks), the day streak, and a yearly goal for audiobooks finished. The goal is kept in your synced settings.
const lsTime = sec => { const m = Math.round(sec / 60); return m < 60 ? m + ' min' : Math.floor(m / 60) + ' h' + (m % 60 ? ' ' + (m % 60) + ' min' : ''); };
async function homeListening(after) {
if (!after || !(allowed('podcast') || allowed('audio'))) return;
let s; try { s = await jfetch('/api/listening/stats'); } catch { return; }
const goal = +store.get('read_goal', 0) || 0; if (!after.isConnected) return;
const max = Math.max(60, ...s.last14.map(d => d.s)), card = document.createElement('div'); card.id = 'listening';
card.innerHTML = `<h2>Listening</h2><div class="card" style="max-width:none"><div class="lsrow"><span><b>${lsTime(s.week)}</b><small>this week</small></span><span><b>${s.streak}</b><small>day${s.streak === 1 ? '' : 's'} in a row</small></span><span><b>${s.finishedYear}${goal ? ' / ' + goal : ''}</b><small>books finished this year</small></span></div>
<div class="lsbars" role="img" aria-label="Minutes listened each of the last 14 days">${s.last14.map(d => `<i style="height:${Math.max(3, Math.round(d.s / max * 100))}%" title="${esc(d.day)}: ${lsTime(d.s)}"></i>`).join('')}</div>
${goal ? `<div class="lsgoal"><i style="width:${Math.min(100, Math.round(s.finishedYear / goal * 100))}%"></i></div>` : ''}
<div id="lsfam" class="lsfam"></div><div class="bar" style="margin:8px 0 0"><button class="btn ghost" id="lsgoal">${goal ? 'Change yearly goal' : 'Set a yearly goal'}</button></div></div>`;
after.insertAdjacentElement('afterend', card);
jfetch('/api/listening/family').then(f => { const box = card.querySelector('#lsfam'); if (!box) return; if (!f.goal && !f.finished) { box.innerHTML = `<button class="btn ghost" id="lsfg" style="min-height:36px;margin-top:8px">Set a family reading challenge</button>`; } else box.innerHTML = `<div class="lsrow" style="margin-top:12px"><span><b>${f.finished}${f.goal ? ' of ' + f.goal : ''}</b><small>family books finished in ${f.year}${f.people.length ? ' · ' + esc(f.people.map(p => p.name + ' ' + p.n).join(', ')) : ''}</small></span></div>${f.goal ? `<div class="lsgoal"><i style="width:${Math.min(100, Math.round(f.finished / f.goal * 100))}%"></i></div>` : ''}<button class="btn ghost" id="lsfg" style="min-height:36px;margin-top:8px">${f.goal ? 'Change the family goal' : 'Set a family goal'}</button>`;
const b = box.querySelector('#lsfg'); if (b) b.onclick = () => { const el = openSheet('Family reading challenge', 'One goal for everyone: how many books the family finishes this year (audiobooks and e-books together).', `<form id="lsff"><label for="lsfn">Books</label><input type="number" id="lsfn" min="0" max="1000" value="${f.goal || ''}" placeholder="40" inputmode="numeric"><div class="bar"><button class="btn" style="flex:1;justify-content:center">Save</button></div></form>`); el.querySelector('#lsff').onsubmit = async e => { e.preventDefault(); try { await jfetch('/api/listening/family', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ goal: +el.querySelector('#lsfn').value || 0 }) }); closeSheet(); card.remove(); homeListening(after); } catch (er) { toast(er.message); } }; };
}).catch(() => {});
card.querySelector('#lsgoal').onclick = () => { const el = openSheet('Audiobooks to finish this year', '', `<form id="lsf"><label for="lsn">Goal</label><input type="number" id="lsn" min="0" max="500" value="${goal || ''}" placeholder="12" inputmode="numeric"><div class="bar"><button class="btn" style="flex:1;justify-content:center">Save</button></div></form>`); el.querySelector('#lsf').onsubmit = e => { e.preventDefault(); const n = Math.max(0, Math.min(500, Math.round(+el.querySelector('#lsn').value) || 0)); if (n) store.set('read_goal', n); else store.del('read_goal'); closeSheet(); card.remove(); homeListening(after); }; };
}