Search
@@ -60,7 +60,7 @@ function viewSearch() {
const part = (st, off, label) => st === undefined ? off : st.err ? bad('error: ' + st.err) : `✓${st.n !== undefined ? ' ' + st.n : ''}`;
const filesOff = !sf.token() ? link('#/files', 'sign in to search files') : '✓';
const musicOff = !plexSt || !plexSt.signedIn ? link('#/music', 'sign in to search music') : !plexSt.server ? link('#/music', 'choose a Plex server') : '✓';
- el.innerHTML = `Searching: Tasks ${part(r.tasks, '✓')} · Events ${part(r.events, '✓')} · Files ${part(r.files, filesOff)} · Music ${part(r.music, musicOff)} · Movies & TV ${part(r.video, musicOff)} · Podcasts ${part(r.pods, '✓')} · Audiobooks ${part(r.books, '✓')}`;
+ el.innerHTML = `Searching: Tasks ${part(r.tasks, '✓')} · Events ${part(r.events, '✓')} · Files ${part(r.files, filesOff)} · Music ${part(r.music, musicOff)} · Movies & TV ${part(r.video, musicOff)} · Podcasts ${part(r.pods, '✓')} · Audiobooks ${part(r.books, '✓')}${allowed('mail') && cfg.mail ? ' · Mail ' + part(r.mail, '✓') : ''}`;
};
jfetch('/api/plex/status').then(st => { plexSt = st; showSources(); }).catch(() => showSources());
showSources();
@@ -74,7 +74,7 @@ function viewSearch() {
const enc = encodeURIComponent, lower = term.toLowerCase();
const hasSf = !!sf.token(), plexReady = !!(plexSt && plexSt.signedIn && plexSt.server);
const libsP = hasSf ? sf.req('/api2/repos/') : Promise.reject(new Error('signed out'));
- const [libs, files, music, video, pods, books, ddg, hub] = await Promise.allSettled([
+ const [libs, files, music, video, pods, books, ddg, hub, mailR] = await Promise.allSettled([
libsP,
libsP.then(l => searchFiles(term, l)),
plexReady ? searchMusic(term) : Promise.reject(new Error('not connected')),
@@ -83,6 +83,7 @@ function viewSearch() {
allowed('audio') ? searchBooks(term) : Promise.reject(new Error('not allowed')),
jfetch('/api/ddg?q=' + enc(term)),
jfetch('/api/search?q=' + enc(term)),
+ allowed('mail') && cfg.mail ? jfetch('/api/mail/messages?folder=inbox&q=' + enc(term)) : Promise.reject(new Error('not allowed')),
]);
if (mySeq !== seq || !location.hash.startsWith('#/search')) return;
const sec = (title, html) => html ? `
${title} ${html}
` : '';
@@ -103,6 +104,7 @@ function viewSearch() {
const videoHtml = (video.status === 'fulfilled' ? video.value : []).map(m => vdCard(m)).join('');
const podHtml = (pods.status === 'fulfilled' ? pods.value : []).map(s => `
${esc(pdF(s, 'title'))} Podcast${pdF(s, 'author') ? ' · ' + esc(pdF(s, 'author')) : ''} `).join('');
const bookHtml = (books.status === 'fulfilled' ? books.value : []).map(b => `
${esc(abTitle(b))} Audiobook${abAuthor(b) ? ' · ' + esc(abAuthor(b)) : ''} `).join('');
+ const mailHtml = (mailR.status === 'fulfilled' ? mailR.value.items || [] : []).slice(0, 8).map(m => `
${icon('mail')} ${esc(m.subject)} ${esc((m.from && (m.from.name || m.from.address)) || '')} · ${esc(new Date(m.date).toLocaleDateString())} `).join('');
const d = ddg.status === 'fulfilled' ? ddg.value : {};
const card = (d.text || d.answer) ? `
${d.image ? `
` : ''}
@@ -123,12 +125,13 @@ function viewSearch() {
music: !plexReady ? undefined : music.status === 'fulfilled' ? { n: music.value.length } : { err: music.reason && music.reason.message },
pods: !allowed('podcast') ? undefined : pods.status === 'fulfilled' ? { n: pods.value.length } : { err: pods.reason && pods.reason.message },
books: !allowed('audio') ? undefined : books.status === 'fulfilled' ? { n: books.value.length } : { err: books.reason && books.reason.message },
+ mail: !(allowed('mail') && cfg.mail) ? undefined : mailR.status === 'fulfilled' ? { n: (mailR.value.items || []).length } : { err: mailR.reason && mailR.reason.message },
video: !plexReady ? undefined : video.status === 'fulfilled' ? { n: video.value.length } : { err: video.reason && video.reason.message },
});
const notes = [...(h.errors || [])].map(e => `
${esc(e)}
`).join('');
const linkHtml = allowed('links') ? allLinks().filter(l => (l.title + ' ' + l.url + ' ' + (l.group || '')).toLowerCase().includes(lower)).slice(0, 8).map(l => `
${icon('link')} ${esc(l.title || l.url)} ${esc(new URL(safeUrl(l.url)).host)}${l.group ? ' · ' + esc(l.group) : ''} `).join('') : '';
- res.innerHTML = card + sec('Tasks', taskHtml) + sec('Links', linkHtml) + sec('Events', eventHtml) + sec('Libraries', libHtml) + sec('Files', fileHtml) + sec('Music', musicHtml) + (podHtml ? sec('Podcasts', podHtml) : '') + (bookHtml ? sec('Audiobooks', bookHtml) : '') + (videoHtml ? `
Movies & TV ${videoHtml}
` : '') + notes;
- if (!taskHtml && !linkHtml && !eventHtml && !libHtml && !fileHtml && !musicHtml && !videoHtml && !podHtml && !bookHtml && !card) res.innerHTML = '
No results. The line above shows what was searched.
' + notes;
+ res.innerHTML = card + sec('Mail', mailHtml) + sec('Tasks', taskHtml) + sec('Links', linkHtml) + sec('Events', eventHtml) + sec('Libraries', libHtml) + sec('Files', fileHtml) + sec('Music', musicHtml) + (podHtml ? sec('Podcasts', podHtml) : '') + (bookHtml ? sec('Audiobooks', bookHtml) : '') + (videoHtml ? `
Movies & TV ${videoHtml}
` : '') + notes;
+ if (!taskHtml && !linkHtml && !eventHtml && !libHtml && !fileHtml && !musicHtml && !videoHtml && !podHtml && !bookHtml && !mailHtml && !card) res.innerHTML = '
No results. The line above shows what was searched.
' + notes;
res.querySelectorAll('button[data-f]').forEach(b => b.onclick = () => { const [r, p, n, dd] = JSON.parse(b.dataset.f); openFile(r, p, n, dd); });
res.querySelectorAll('a[data-book]').forEach(a => a.onclick = e => { e.preventDefault(); const b = (books.value || []).find(x => abId(x) === a.dataset.book); if (b) abSheet(b); });
res.querySelectorAll('button[data-t]').forEach(b => b.onclick = () => playQueue([JSON.parse(b.dataset.t)], 0));
diff --git a/public/sw.js b/public/sw.js
index 2b6696c..dc3150c 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-v22';
+const CACHE = 'hub-v23';
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/today.js b/public/today.js
new file mode 100644
index 0000000..98d1b04
--- /dev/null
+++ b/public/today.js
@@ -0,0 +1,25 @@
+'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 ? `
Continue watching ${items.map(m => vdCard(m, true)).join('')}
` : '';
+ })() : '',
+ 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 ? `
Continue listening ` : '';
+ })() : '',
+ ]);
+ const html = bits.map(b => b.status === 'fulfilled' ? b.value : '').join('');
+ if (!html || !el.isConnected) return;
+ el.innerHTML = `
Pick up where you left off ${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: [] };
diff --git a/public/video.js b/public/video.js
index aa34f38..b66c44d 100644
--- a/public/video.js
+++ b/public/video.js
@@ -234,7 +234,7 @@ function videoMount() {
const b = (idn, ic, label, extra = '') => ``;
v.innerHTML = `
${icon('back')}Back ${vdCrumbs(m)} ${esc(title)}
-
${b('vmute', icon('vol'), 'Mute')} ${b('vspd', vp.speed + '×', 'Speed', ' txt')}${b('vset', 'Quality & tracks', 'Quality and tracks', ' txt')}${document.pictureInPictureEnabled ? b('vpip', icon('pip'), 'Picture in picture') : ''}${b('vfs', icon('full'), 'Full screen')}${b('vclose', icon('x'), 'Close the video')}
+
${b('vmute', icon('vol'), 'Mute')} ${b('vspd', vp.speed + '×', 'Speed', ' txt')}${castOk(vp.el) ? b('vcast', 'Cast', 'Play on a TV or speaker', ' txt') : ''}${b('vset', 'Quality & tracks', 'Quality and tracks', ' txt')}${document.pictureInPictureEnabled ? b('vpip', icon('pip'), 'Picture in picture') : ''}${b('vfs', icon('full'), 'Full screen')}${b('vclose', icon('x'), 'Close the video')}
0:00 -0:00
${ep ? b('vprev', icon('prev'), 'Previous episode') : ''}${b('vb10', `${icon('rew')}10 `, 'Back 10 seconds')}${b('vpp', icon('play'), 'Play', ' big')}${b('vf10', `${icon('fwd')}10 `, 'Forward 10 seconds')}${ep ? b('vnextep', icon('next'), 'Next episode') : ''}
@@ -249,6 +249,7 @@ function videoMount() {
$('#vpp').onclick = () => { el.paused ? el.play().catch(() => {}) : el.pause(); };
$('#vb10').onclick = () => videoSkip(-10); $('#vf10').onclick = () => videoSkip(10);
if ($('#vprev')) $('#vprev').onclick = () => videoGo(-1); if ($('#vnextep')) $('#vnextep').onclick = () => videoGo(1);
+ if ($('#vcast')) $('#vcast').onclick = () => castTo(vp && vp.el);
$('#vset').onclick = videoSettings; $('#vfs').onclick = videoFullscreen;
if ($('#vpip')) $('#vpip').onclick = () => { try { document.pictureInPictureElement ? document.exitPictureInPicture() : el.requestPictureInPicture(); } catch {} };
$('#vmute').onclick = () => { el.muted = !el.muted; };