// arr-tui for webOS: the shell with the views, the status of the services, // the remote's hints and its buttons. import { app } from "./app.ts"; import { applyTheme } from "./theme.ts"; import { h, replace } from "./ui/dom.ts"; import { current, focus, focusFirst, initFocus, Keys } from "./ui/focus.ts"; import { button, confirmDialog, toast } from "./ui/widgets.ts"; import { CalendarView } from "./views/calendar.ts"; import { DownloadsView, watchDownloads } from "./views/downloads.ts"; import { LibraryView } from "./views/library.ts"; import { LogView } from "./views/log.ts"; import { SearchView } from "./views/search.ts"; import { SettingsView } from "./views/settings.ts"; import type { Hint, View } from "./views/view.ts"; interface Entry { name: string; view: View; tab: HTMLButtonElement; } function start(): void { applyTheme(app.cfg.ui.theme); const content = h("main.content"); const hints = h("div.footer-hints"); const dots = h("div.status-dots"); const tabs = h("nav.tabs.fgroup"); const entries: Entry[] = []; let cur: Entry | null = null; let switchTimer = 0; const show = (e: Entry, focusView: boolean) => { if (cur === e) { if (focusView) focusFirst(e.view.el); return; } cur?.view.hide(); cur?.tab.classList.remove("on"); cur = e; e.tab.classList.add("on"); replace(content, e.view.el); renderHints(e.view.hints); e.view.show(); if (!focusView && current() !== e.tab) focus(e.tab); }; const views: [string, View][] = [ ["Movies", new SearchView("movie")], ["Series", new SearchView("series")], ["Library", new LibraryView()], ["Downloads", new DownloadsView()], ["Calendar", new CalendarView()], ["Settings", new SettingsView()], ["Log", new LogView()], ]; views.forEach(([name, view], i) => { const tab = button(`${i + 1} ${name}`, () => show(entry, true), "tab"); const entry: Entry = { name, view, tab }; // Moving along the tabs switches views once the focus rests there. tab.addEventListener("focus", () => { clearTimeout(switchTimer); switchTimer = window.setTimeout(() => { if (document.activeElement === tab) show(entry, false); }, 250); }); entries.push(entry); tabs.appendChild(tab); }); function renderHints(list: Hint[]): void { replace(hints, ...list.map(([b, text]) => h("span.hint", h(`span.key.${b}` as "span", b === "ok" ? "OK" : b === "back" ? "Back" : ""), text)), h("span.hint", h("span.key.back", "Back"), "back")); } function renderDots(): void { replace(dots, ...(["Radarr", "Sonarr", "SABnzbd", "Trakt"] as const).map((name) => { const configured = name === "Radarr" ? app.radarr.configured() : name === "Sonarr" ? app.sonarr.configured() : name === "SABnzbd" ? app.sab.configured() : app.trakt.configured(); const err = app.status[name]; return h(`span.status-dot.${!configured ? "off" : err ? "bad" : "ok"}` as "span", { title: err || name }, name); })); } app.onStatus.push(renderDots); app.onConfig.push(renderDots); renderDots(); const root = h("div.app", h("header.topbar", h("div.brand", "arr-tui"), tabs, dots), content, h("footer.footer", hints), ); document.getElementById("root")!.appendChild(root); // Back: from a view to its tab; on the tabs, ask before leaving the app. const back = () => { const el = current(); if (cur && el && !tabs.contains(el)) { focus(cur.tab); return; } confirmDialog("Leave arr-tui?", "Leave", () => window.close()); }; const keys = (ev: KeyboardEvent): boolean => { // 1-7 open a view. const n = ev.keyCode >= 49 && ev.keyCode <= 57 ? ev.keyCode - 49 : ev.keyCode >= 97 && ev.keyCode <= 105 ? ev.keyCode - 97 : -1; if (n >= 0 && n < entries.length) { show(entries[n], true); return true; } // Desktop browsers: F1-F4 stand in for the colour buttons. const code = ({ 112: Keys.red, 113: Keys.green, 114: Keys.yellow, 115: Keys.blue } as Record)[ev.keyCode] ?? ev.keyCode; return !!cur?.view.keys?.(code); }; initFocus(root, back, keys); const first = app.radarr.configured() || app.sonarr.configured() || app.sab.configured() ? 0 : 5; // nothing set up: Settings show(entries[first], false); watchDownloads(); if (first === 5) toast("Nothing is configured yet: enter the addresses and API keys of your services.", "warn"); // Check the services once at start, for the status dots. for (const [name, check] of [ ["Radarr", () => app.radarr.systemStatus()], ["Sonarr", () => app.sonarr.systemStatus()], ["SABnzbd", () => app.sab.version()], ] as const) { const configured = name === "Radarr" ? app.radarr.configured() : name === "Sonarr" ? app.sonarr.configured() : app.sab.configured(); if (configured) check().then(() => app.setStatus(name, null), (e) => app.setStatus(name, e)); } } if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", start); else start();