diff --git a/lumen/src/extras.ts b/lumen/src/extras.ts index b7a3507..e7e1adf 100644 --- a/lumen/src/extras.ts +++ b/lumen/src/extras.ts @@ -113,7 +113,8 @@ export function useExtras() { music: svc.music, comic: svc.comics, magazine: svc.magazines, - game: svc.software, + // consoles can come from ROMarrNG alone; each category is checked below + game: svc.software || svc.romarr, sports: svc.sports, adult: true, }; @@ -125,6 +126,8 @@ export function useExtras() { ? !!on.adult && !!pub?.adultEnabled : !!on[k]); const [working, setWorking] = useState | null>(null); + // the game categories whose catalogue answers (PC games need QuestarrNG, consoles may not) + const [gamesWorking, setGamesWorking] = useState(null); const key = EXTRAS.map((e) => (enabled(e.key) ? e.key : '')).join(',') + games.join(','); @@ -139,7 +142,10 @@ export function useExtras() { games.map((c) => probe(`/request/software/catalog/popular?category=${c}&limit=1`) ) - ).then((r) => r.some(Boolean)) + ).then((r) => { + if (live) setGamesWorking(games.filter((_, i) => r[i])); + return r.some(Boolean); + }) : PROBE[k] ? probe(PROBE[k]!) : Promise.resolve(true) @@ -153,7 +159,10 @@ export function useExtras() { return { ready: !!pub, enabled, - games, + // until the checks answer, list none (so Games doesn't load a dead category first) + games: !enabled('game') + ? games + : games.filter((c) => gamesWorking?.includes(c)), list: EXTRAS.filter((e) => enabled(e.key) && working?.has(e.key)), }; } diff --git a/lumen/src/mediaServer.ts b/lumen/src/mediaServer.ts index 4a9aad0..583c3ef 100644 --- a/lumen/src/mediaServer.ts +++ b/lumen/src/mediaServer.ts @@ -176,7 +176,8 @@ export interface Services { comics: boolean; magazines: boolean; sports: boolean; - software: boolean; + software: boolean; // QuestarrNG: PC games, and consoles unless ROMarrNG serves them + romarr: boolean; // ROMarrNG (RomM): retro and modern consoles } export function resolveServices(pub: Record | null): Services { const has = (k: string) => pub?.[k] !== false; @@ -190,6 +191,7 @@ export function resolveServices(pub: Record | null): Services { magazines: has('magazinesEnabled'), sports: has('sportsEnabled'), software: has('softwareEnabled'), + romarr: pub?.romarrEnabled === true, }; } let currentServices: Services = resolveServices(null); diff --git a/lumen/src/pages/Login.tsx b/lumen/src/pages/Login.tsx index 5f57dc4..dc2a0d5 100644 --- a/lumen/src/pages/Login.tsx +++ b/lumen/src/pages/Login.tsx @@ -12,6 +12,26 @@ import { BrandMark } from '../components/brand'; import { mediaServer, services, site } from '../mediaServer'; type Mode = 'start' | 'server' | 'local' | 'quick'; +type Method = 'server' | 'local' | 'quick'; + +// The way this device last signed in successfully. The next sign-in opens straight to it, +// so a family member picks Jellyfin (or email) once and then just types their password. +const LAST_KEY = 'lumen-last-sign-in'; +export function lastSignIn(): Method | null { + try { + const v = localStorage.getItem(LAST_KEY); + return v === 'server' || v === 'local' || v === 'quick' ? v : null; + } catch { + return null; + } +} +function rememberSignIn(m: Method) { + try { + localStorage.setItem(LAST_KEY, m); + } catch { + // private window or blocked storage: the choice just isn't remembered + } +} // SeerrNG answers sign-in failures with codes like INVALID_CREDENTIALS; say it in words export function friendly(e: unknown, serverName: string): string { @@ -46,12 +66,19 @@ export default function Login() { const [busy, setBusy] = useState(false); const [err, setErr] = useState(''); // nothing to choose between: open the only form straight away + // The admin's form setting wins; otherwise open the way this device last signed in. + // (Quick Connect only remembers the start page: a code shouldn't be asked for unprompted.) + const last = lastSignIn(); const [mode, setMode] = useState( family ? 'server' : !server.serverLogin && server.localLogin ? 'local' - : 'start' + : last === 'server' && passwordForm && server.serverLogin + ? 'server' + : last === 'local' && server.localLogin + ? 'local' + : 'start' ); const home: Mode = family ? 'server' : 'start'; const [code, setCode] = useState(''); @@ -66,13 +93,14 @@ export default function Login() { [] ); - const run = async (fn: () => Promise) => { + const run = async (fn: () => Promise, method?: Method) => { if (active.current) return; active.current = true; setBusy(true); setErr(''); try { await fn(); + if (method) rememberSignIn(method); await refresh(); } catch (e) { if (!(e instanceof QuickConnectCancelled)) @@ -92,7 +120,8 @@ export default function Login() { email: f.get('email'), password: f.get('password'), }), - }) + }), + 'local' ); }; const jellyfin = (e: FormEvent) => { @@ -102,7 +131,8 @@ export default function Login() { signInWithJellyfin( String(f.get('username') || ''), String(f.get('password') || '') - ) + ), + 'server' ); }; const quick = () => { @@ -127,7 +157,7 @@ export default function Login() { if (runId === quickRun.current) setMode(home); throw e; } - }); + }, 'quick'); }; const cancelQuick = () => { quickRun.current++; diff --git a/lumen/src/pages/LumenSettings.tsx b/lumen/src/pages/LumenSettings.tsx index 74e9c6a..45702c2 100644 --- a/lumen/src/pages/LumenSettings.tsx +++ b/lumen/src/pages/LumenSettings.tsx @@ -333,7 +333,8 @@ export default function LumenSettings() { Sign-in page Jellyfin and Emby: show the username and password boxes straight - away, without naming the media server + away, without naming the media server. With buttons, each device + opens the way it last signed in.