diff --git a/README.md b/README.md index 927172d..f43aec0 100644 --- a/README.md +++ b/README.md @@ -206,6 +206,8 @@ Files are kept in the data folder under `mystore/` (up to 1 GB each; extensions **Notifications** (the bell on Home and Family): a list of what you have not seen yet: chat messages (one line per conversation, with a count), reminders for tasks, events and mail, and family announcements. Tap one to open it; mark all read or clear. Kept per person in `inbox.json`. +**Movies & TV search:** search by title, actor or director (results grouped into Movies, TV shows, Actors, Directors and Episodes); tap an actor or director to see everything they are in; filter the library by genre, decade, rating (G, PG, PG-13, R, TV ratings) and watched or unwatched, together with the sort. + **Name tiles on the sign-in page:** on a device you have remembered (Settings → Family → *Remember this device*, or any family device) the sign-in page shows everyone's name, children marked KID. Tap yours, then enter your PIN (it signs in as soon as the last digit is typed) or use your passkey; a child with no PIN just taps. Any other browser still shows only *Sign in with passkey*, so strangers see no names and cannot guess PINs. A remembered personal device gets a normal long session; a family device keeps its short sessions. *Forget* switches it off again. **Family devices** (Settings, under Passkeys): for a kitchen tablet or the family computer. An admin turns on *Make this a family device* while signed in on it; from then on it opens on the **family board** (announcement, today's family calendar, chores with progress, shopping list, family tasks and notes) and a row of name tiles, children marked **KID**. Tap your name: a child signs in with one tap, everyone else enters a PIN they set in Settings (five wrong tries lock that person for five minutes; there is also a passkey link). A label with your name and **Switch user** stays on screen, and the device goes back to the board after five idle minutes (music playing keeps it signed in). Sessions on a family device last 30 minutes at a time, not months. Turn it off from the same place. Each board card has a **+** to add to the shopping list, the family tasks, a shared checklist (pick which) or the Family calendar (all day, or at a time): write it, tap your name (PIN unless you are a child) and it is added as you, if that part of the hub is turned on for you. Chores on the board are tappable: tap one (up-for-grabs chores ask *I did it* or *Take it for later*), tap your name, and enter your PIN if you are not a child; the chore is then done or claimed as you, with all the usual chore rules, without signing in. The admin picks who appears on the board (a **Show on the family board** switch in each person's settings; people left off do not get a tile or show in family chore progress). The Family calendar and Family task list are shared by everyone and appear on the board. diff --git a/public/video.js b/public/video.js index 2ec2b57..fb57458 100644 --- a/public/video.js +++ b/public/video.js @@ -80,9 +80,9 @@ async function videoHome(st, libKey) { const cur = secs.find(s => s.key === (libKey || store.get('video_lib'))) || secs[0]; store.set('video_lib', cur.key); v.innerHTML = `
Loading…
Loading…
Nothing matches.
${$('#vunwc').checked || genre ? '' : ''}${esc(e.message)}
`; }); }; - $('#vsort').onchange = () => { store.set('video_sort', $('#vsort').value); reload(); }; $('#vunwc').onchange = () => { store.set('video_unw', $('#vunwc').checked ? '1' : '0'); reload(); }; $('#vgen').onchange = () => { genre = $('#vgen').value; reload(); }; + $('#vsort').onchange = () => { store.set('video_sort', $('#vsort').value); reload(); }; $('#vunwc').onchange = () => { store.set('video_unw', $('#vunwc').checked ? '1' : '0'); reload(); }; $('#vgen').onchange = () => { genre = $('#vgen').value; reload(); }; for (const id of ['#vdec', '#vcr', '#vwatch']) $(id).onchange = () => reload(); reload(); // press and hold (or right-click) a card: quick actions const vv = $('#view .wrap') || $('#view'); let hold = 0, held = false; // (the page's own wrapper, new on every draw, so the handlers never pile up) @@ -133,14 +135,19 @@ async function videoHome(st, libKey) { vv.addEventListener('touchstart', e => { const c = e.target.closest('.vcard'); held = false; clearTimeout(hold); if (c && c.dataset.rk) hold = setTimeout(() => { held = true; menu(c); }, 550); }, { passive: true }); for (const ev of ['touchend', 'touchmove', 'touchcancel']) vv.addEventListener(ev, () => clearTimeout(hold), { passive: true }); vv.addEventListener('click', e => { if (held) { held = false; e.preventDefault(); e.stopPropagation(); return; } const x = e.target.closest('.vx'); if (x) { e.preventDefault(); const rk = x.dataset.rm; vdRemove((vd.deck || []).find(m => m.ratingKey === rk) || { ratingKey: rk, type: 'movie' }).then(loadDeck); } }, true); + const personBar = () => { const t = $('#vlt'); if (t) t.innerHTML = vd.person ? `${esc(vd.person.name)} ` : esc(cur.title); const c = $('#vpc'); if (c) c.onclick = () => { vd.person = null; personBar(); reload(); }; }; + vd.person = null; let t; $('#vq').oninput = e => { clearTimeout(t); const q = e.target.value.trim(); if (q.length < 2) { $('#vsr').innerHTML = ''; return; } t = setTimeout(async () => { try { - const mc = await px.get(`/hubs/search?query=${vdEnc(q)}&limit=10`), rows = []; - for (const h of mc.Hub || []) if (['movie', 'show', 'episode'].includes(h.type)) for (const m of h.Metadata || []) rows.push(m); + const mc = await px.get(`/hubs/search?query=${vdEnc(q)}&limit=24`), groups = []; + for (const h of mc.Hub || []) { const items = h.Metadata || []; if (!items.length) continue; if (h.type === 'movie') groups.push(['Movies', items, 'card']); else if (h.type === 'show') groups.push(['TV shows', items, 'card']); else if (h.type === 'episode') groups.push(['Episodes', items, 'card']); else if (h.type === 'actor' || h.type === 'director') groups.push([h.type === 'actor' ? 'Actors' : 'Directors', items.filter(m => m.filter), 'person']); } if (!$('#vsr') || $('#vq').value.trim() !== q) return; - $('#vsr').innerHTML = rows.length ? `Nothing found.
'; + const order = ['Movies', 'TV shows', 'Actors', 'Directors', 'Episodes']; + groups.sort((x, y) => order.indexOf(x[0]) - order.indexOf(y[0])); + $('#vsr').innerHTML = groups.some(g => g[1].length) ? groups.filter(g => g[1].length).map(([title, items, kind]) => kind === 'card' ? `Nothing found. Try fewer words, or part of a name.
'; + $('#vsr').querySelectorAll('[data-vp]').forEach(a => a.onclick = e => { e.preventDefault(); vd.person = { filter: a.dataset.vp, name: a.dataset.vn }; $('#vq').value = ''; $('#vsr').innerHTML = ''; personBar(); reload(); $('#vlt').scrollIntoView({ behavior: 'smooth' }); }); } catch (err) { if ($('#vsr')) $('#vsr').innerHTML = `${esc(err.message)}
`; } }, 350); };