2026-06-11 02:19:47 +02:00
|
|
|
import { useEffect, useState } from "react";
|
2026-06-15 00:30:34 +02:00
|
|
|
import { useTranslation } from "react-i18next";
|
2026-06-11 02:19:47 +02:00
|
|
|
import { useQuery } from "@tanstack/react-query";
|
|
|
|
|
import { api, HttpError, type FeedFilters } from "./lib/api";
|
2026-06-15 00:30:34 +02:00
|
|
|
import { setLanguage, isSupported, type LangCode } from "./i18n";
|
2026-06-11 02:19:47 +02:00
|
|
|
import {
|
|
|
|
|
applyTheme,
|
|
|
|
|
DEFAULT_THEME,
|
|
|
|
|
loadLocalTheme,
|
|
|
|
|
saveLocalTheme,
|
|
|
|
|
type ThemePrefs,
|
|
|
|
|
} from "./lib/theme";
|
2026-06-15 12:29:43 +02:00
|
|
|
import { hasFilterParams, paramsToFilters, readPage, stripUrlParams, type Page } from "./lib/urlState";
|
2026-06-11 19:21:48 +02:00
|
|
|
import {
|
|
|
|
|
loadLayout,
|
|
|
|
|
normalizeLayout,
|
|
|
|
|
saveLayoutLocal,
|
|
|
|
|
type SidebarLayout,
|
|
|
|
|
} from "./lib/sidebarLayout";
|
2026-06-12 01:43:07 +02:00
|
|
|
import { configureNotifications, notify } from "./lib/notifications";
|
feat(ui): liquid-glass design system, settings polish, hints, notif fixes
- Add a theme-aware glass surface system (.glass/.glass-card + ambient backdrop,
performance-mode opt-out) and apply it across panels, popovers, toasts, cards,
sidebar widgets, channel rows, video cards and login.
- SettingsPanel: slide in/out animation, glass styling, wrapping pill tabs (no
horizontal scrollbar) with a prominent active state.
- Notifications: auto-dismiss can be switched off (stays until closed); the test
notification now also triggers the alert sound; resume a suspended AudioContext.
- Add an app-wide, toggleable hint/tooltip system (lib/hints + Tooltip) and wire
hints across the settings and channel-manager surfaces; persisted per account.
2026-06-11 21:08:35 +02:00
|
|
|
import { setHintsEnabled } from "./lib/hints";
|
2026-06-11 02:19:47 +02:00
|
|
|
import Login from "./components/Login";
|
|
|
|
|
import Header from "./components/Header";
|
2026-06-16 00:42:23 +02:00
|
|
|
import NavSidebar from "./components/NavSidebar";
|
2026-06-11 02:19:47 +02:00
|
|
|
import Sidebar from "./components/Sidebar";
|
|
|
|
|
import Feed from "./components/Feed";
|
2026-06-14 07:08:59 +02:00
|
|
|
import Channels, { type ChannelStatusFilter } from "./components/Channels";
|
2026-06-15 14:45:18 +02:00
|
|
|
import Playlists from "./components/Playlists";
|
2026-06-12 02:47:55 +02:00
|
|
|
import Stats from "./components/Stats";
|
2026-06-16 14:38:51 +02:00
|
|
|
import Scheduler from "./components/Scheduler";
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
import SettingsPanel from "./components/SettingsPanel";
|
2026-06-14 01:11:29 +02:00
|
|
|
import OnboardingWizard from "./components/OnboardingWizard";
|
|
|
|
|
import { shouldAutoOpenOnboarding } from "./lib/onboarding";
|
fix: address reader-UI feedback (shorts, search, tags, login, UX)
- Shorts: confirm via youtube.com/shorts/<id> probe (SOCS cookie bypasses the
consent redirect) instead of a 60s heuristic; concurrent probing, shorts_probed
flag, scheduled refinement (migration 0005)
- Search: match title + channel name only (descriptions caused noisy results)
- Faceted tag filtering: AND across categories (language AND topic narrows),
OR within a category; any/all toggle applies to topics
- Language detection: majority vote over individual titles (fixes misdetections
like multipoleguy -> English; drops bogus Polish/Romanian)
- Login: drop forced consent so returning sign-in is quick (select_account)
- Feed cards: clickable channel name (opens channel), persistent saved badge,
undo toast on hide, Hidden view to restore; tag chips show counts in tooltip
2026-06-11 03:07:49 +02:00
|
|
|
import Toaster from "./components/Toaster";
|
2026-06-18 01:17:31 +02:00
|
|
|
import ErrorDialog from "./components/ErrorDialog";
|
2026-06-15 00:06:57 +02:00
|
|
|
import About from "./components/About";
|
|
|
|
|
import ReleaseNotes from "./components/ReleaseNotes";
|
|
|
|
|
import VersionBanner from "./components/VersionBanner";
|
2026-06-16 10:12:10 +02:00
|
|
|
import DemoBanner from "./components/DemoBanner";
|
2026-06-15 00:06:57 +02:00
|
|
|
import { CURRENT_VERSION } from "./lib/releaseNotes";
|
2026-06-11 02:19:47 +02:00
|
|
|
|
|
|
|
|
const DEFAULT_FILTERS: FeedFilters = {
|
|
|
|
|
tags: [],
|
|
|
|
|
tagMode: "or",
|
|
|
|
|
q: "",
|
|
|
|
|
sort: "newest",
|
2026-06-15 04:06:22 +02:00
|
|
|
scope: "my",
|
2026-06-11 03:47:51 +02:00
|
|
|
includeNormal: true,
|
2026-06-11 02:19:47 +02:00
|
|
|
includeShorts: false,
|
|
|
|
|
includeLive: false,
|
|
|
|
|
show: "unwatched",
|
|
|
|
|
};
|
|
|
|
|
|
2026-06-11 03:28:45 +02:00
|
|
|
const FILTERS_KEY = "subfeed.filters";
|
2026-06-15 14:59:24 +02:00
|
|
|
const PAGE_KEY = "siftlode.page";
|
|
|
|
|
|
|
|
|
|
// Page is navigation state, not a filter, but it's also kept out of the address bar — so
|
|
|
|
|
// persist it to localStorage to survive a reload. A share link's ?page= still wins.
|
|
|
|
|
function loadInitialPage(): Page {
|
|
|
|
|
const params = new URLSearchParams(window.location.search);
|
|
|
|
|
if (params.has("page")) return readPage();
|
|
|
|
|
const stored = localStorage.getItem(PAGE_KEY);
|
2026-06-16 01:05:05 +02:00
|
|
|
if (
|
|
|
|
|
stored === "channels" ||
|
|
|
|
|
stored === "stats" ||
|
|
|
|
|
stored === "playlists" ||
|
2026-06-16 14:38:51 +02:00
|
|
|
stored === "settings" ||
|
|
|
|
|
stored === "scheduler"
|
2026-06-16 01:05:05 +02:00
|
|
|
)
|
|
|
|
|
return stored;
|
2026-06-15 14:59:24 +02:00
|
|
|
return "feed";
|
|
|
|
|
}
|
2026-06-11 03:28:45 +02:00
|
|
|
|
2026-06-11 19:17:08 +02:00
|
|
|
function loadStoredFilters(): FeedFilters {
|
2026-06-11 03:28:45 +02:00
|
|
|
try {
|
|
|
|
|
return { ...DEFAULT_FILTERS, ...JSON.parse(localStorage.getItem(FILTERS_KEY) || "{}") };
|
|
|
|
|
} catch {
|
|
|
|
|
return DEFAULT_FILTERS;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 19:17:08 +02:00
|
|
|
// URL wins over localStorage so a pasted link reproduces the exact view.
|
|
|
|
|
function loadInitialFilters(): FeedFilters {
|
|
|
|
|
const params = new URLSearchParams(window.location.search);
|
|
|
|
|
if (hasFilterParams(params)) return paramsToFilters(params, DEFAULT_FILTERS);
|
|
|
|
|
return loadStoredFilters();
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 02:19:47 +02:00
|
|
|
export default function App() {
|
2026-06-17 14:28:29 +02:00
|
|
|
const { t, i18n } = useTranslation();
|
2026-06-11 02:19:47 +02:00
|
|
|
const [theme, setThemeState] = useState<ThemePrefs>(() => loadLocalTheme());
|
2026-06-11 19:17:08 +02:00
|
|
|
const [filters, setFiltersState] = useState<FeedFilters>(loadInitialFilters);
|
2026-06-11 02:19:47 +02:00
|
|
|
const [view, setView] = useState<"grid" | "list">("grid");
|
2026-06-11 19:21:48 +02:00
|
|
|
const [sidebarLayout, setSidebarLayoutState] = useState<SidebarLayout>(loadLayout);
|
2026-06-15 14:59:24 +02:00
|
|
|
const [page, setPageState] = useState<Page>(loadInitialPage);
|
2026-06-14 01:11:29 +02:00
|
|
|
const [wizardOpen, setWizardOpen] = useState(false);
|
2026-06-17 19:16:23 +02:00
|
|
|
const CHANNEL_FILTER_KEY = "siftlode.channelFilter";
|
|
|
|
|
const [channelFilter, setChannelFilterState] = useState<ChannelStatusFilter>(() => {
|
|
|
|
|
const v = localStorage.getItem(CHANNEL_FILTER_KEY);
|
|
|
|
|
return v === "needs_full" || v === "fully_synced" || v === "hidden" || v === "all"
|
|
|
|
|
? v
|
|
|
|
|
: "all";
|
|
|
|
|
});
|
|
|
|
|
// Persist the channel status chip so a reload (F5) keeps it.
|
|
|
|
|
const setChannelFilter = (f: ChannelStatusFilter) => {
|
|
|
|
|
setChannelFilterState(f);
|
|
|
|
|
localStorage.setItem(CHANNEL_FILTER_KEY, f);
|
|
|
|
|
};
|
2026-06-15 00:06:57 +02:00
|
|
|
const [aboutOpen, setAboutOpen] = useState(false);
|
|
|
|
|
const [notesOpen, setNotesOpen] = useState(false);
|
|
|
|
|
const [notesHighlight, setNotesHighlight] = useState<string | undefined>(undefined);
|
2026-06-18 01:17:31 +02:00
|
|
|
// "Focus this channel in the manager": jump to the Channels page and seed its name filter
|
|
|
|
|
// so the channel is isolated. Cleared when leaving the page so it doesn't re-apply later.
|
|
|
|
|
const [focusChannelName, setFocusChannelName] = useState<string | null>(null);
|
|
|
|
|
const focusChannel = (name: string) => {
|
|
|
|
|
setFocusChannelName(name);
|
|
|
|
|
setPage("channels");
|
|
|
|
|
};
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (page !== "channels") setFocusChannelName(null);
|
|
|
|
|
}, [page]);
|
2026-06-15 00:06:57 +02:00
|
|
|
|
|
|
|
|
function openReleaseNotes(highlight?: string) {
|
|
|
|
|
setNotesHighlight(highlight);
|
|
|
|
|
setNotesOpen(true);
|
|
|
|
|
}
|
2026-06-11 02:19:47 +02:00
|
|
|
|
2026-06-11 03:28:45 +02:00
|
|
|
function setFilters(next: FeedFilters) {
|
|
|
|
|
setFiltersState(next);
|
|
|
|
|
localStorage.setItem(FILTERS_KEY, JSON.stringify(next));
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setPage(next: Page) {
|
2026-06-16 01:27:49 +02:00
|
|
|
if (next === page) return;
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
setPageState(next);
|
2026-06-15 14:59:24 +02:00
|
|
|
localStorage.setItem(PAGE_KEY, next);
|
2026-06-16 01:27:49 +02:00
|
|
|
// Push an in-app history entry so the browser/mouse Back button steps through pages
|
|
|
|
|
// instead of leaving the app (e.g. back to the OAuth redirect). The URL stays clean —
|
|
|
|
|
// the page rides in history.state, not the query string (filters never go in the URL).
|
|
|
|
|
window.history.pushState({ ...window.history.state, sfPage: next }, "");
|
2026-06-11 03:28:45 +02:00
|
|
|
}
|
|
|
|
|
|
2026-06-11 19:21:48 +02:00
|
|
|
function setSidebarLayout(next: SidebarLayout) {
|
|
|
|
|
setSidebarLayoutState(next);
|
|
|
|
|
saveLayoutLocal(next);
|
|
|
|
|
api.savePrefs({ sidebarLayout: next }).catch(() => {});
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 02:19:47 +02:00
|
|
|
useEffect(() => applyTheme(theme), [theme]);
|
|
|
|
|
|
2026-06-15 12:29:43 +02:00
|
|
|
// Filters live in localStorage, not the address bar. If we arrived via a "Share view"
|
|
|
|
|
// link, its params have already hydrated the initial state — persist them and strip the
|
|
|
|
|
// query so the URL stays clean from here on.
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const params = new URLSearchParams(window.location.search);
|
|
|
|
|
if (hasFilterParams(params) || params.has("page")) {
|
|
|
|
|
localStorage.setItem(FILTERS_KEY, JSON.stringify(filters));
|
|
|
|
|
stripUrlParams();
|
|
|
|
|
}
|
|
|
|
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
2026-06-11 19:17:08 +02:00
|
|
|
|
2026-06-16 01:27:49 +02:00
|
|
|
// In-app Back/Forward: stamp the initial entry with the current page, then sync `page`
|
|
|
|
|
// from history.state on popstate. Runs after the strip-params effect above (which resets
|
|
|
|
|
// history.state to null), so the initial stamp isn't clobbered.
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
window.history.replaceState(
|
|
|
|
|
{ ...window.history.state, sfPage: page },
|
|
|
|
|
""
|
|
|
|
|
);
|
|
|
|
|
function onPop(e: PopStateEvent) {
|
|
|
|
|
const p = (e.state?.sfPage as Page) ?? "feed";
|
|
|
|
|
setPageState(p);
|
|
|
|
|
localStorage.setItem(PAGE_KEY, p);
|
|
|
|
|
}
|
|
|
|
|
window.addEventListener("popstate", onPop);
|
|
|
|
|
return () => window.removeEventListener("popstate", onPop);
|
|
|
|
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
|
|
2026-06-11 02:19:47 +02:00
|
|
|
const meQuery = useQuery({ queryKey: ["me"], queryFn: api.me });
|
|
|
|
|
|
2026-06-14 01:11:29 +02:00
|
|
|
// First-login onboarding: prompt the user to connect YouTube (and resume the flow after
|
|
|
|
|
// each consent redirect). Derived from granted scopes + storage flags so it's stable
|
|
|
|
|
// across the full-page OAuth round-trip; dismissible and reopenable from Settings.
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (meQuery.data && shouldAutoOpenOnboarding(meQuery.data)) setWizardOpen(true);
|
|
|
|
|
}, [meQuery.data?.id, meQuery.data?.can_read, meQuery.data?.can_write]);
|
|
|
|
|
|
2026-06-11 02:19:47 +02:00
|
|
|
// On login, adopt server-stored preferences.
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const prefs = meQuery.data?.preferences;
|
|
|
|
|
if (!prefs) return;
|
|
|
|
|
if (prefs.theme) {
|
|
|
|
|
const merged = { ...DEFAULT_THEME, ...prefs.theme };
|
|
|
|
|
setThemeState(merged);
|
|
|
|
|
saveLocalTheme(merged);
|
|
|
|
|
}
|
|
|
|
|
if (prefs.view === "grid" || prefs.view === "list") setView(prefs.view);
|
2026-06-11 19:21:48 +02:00
|
|
|
if (prefs.sidebarLayout) {
|
|
|
|
|
const l = normalizeLayout(prefs.sidebarLayout);
|
|
|
|
|
setSidebarLayoutState(l);
|
|
|
|
|
saveLayoutLocal(l);
|
|
|
|
|
}
|
2026-06-15 00:30:34 +02:00
|
|
|
if (isSupported(prefs.language)) setLanguage(prefs.language);
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
if (prefs.notifications) configureNotifications(prefs.notifications);
|
feat(ui): liquid-glass design system, settings polish, hints, notif fixes
- Add a theme-aware glass surface system (.glass/.glass-card + ambient backdrop,
performance-mode opt-out) and apply it across panels, popovers, toasts, cards,
sidebar widgets, channel rows, video cards and login.
- SettingsPanel: slide in/out animation, glass styling, wrapping pill tabs (no
horizontal scrollbar) with a prominent active state.
- Notifications: auto-dismiss can be switched off (stays until closed); the test
notification now also triggers the alert sound; resume a suspended AudioContext.
- Add an app-wide, toggleable hint/tooltip system (lib/hints + Tooltip) and wire
hints across the settings and channel-manager surfaces; persisted per account.
2026-06-11 21:08:35 +02:00
|
|
|
if (typeof prefs.hints === "boolean") setHintsEnabled(prefs.hints);
|
|
|
|
|
if (typeof prefs.performanceMode === "boolean")
|
|
|
|
|
document.documentElement.dataset.perf = prefs.performanceMode ? "1" : "";
|
2026-06-12 01:43:07 +02:00
|
|
|
// Nudge admins when access requests are waiting (in lieu of a server-push bell).
|
|
|
|
|
const pending = meQuery.data?.pending_invites ?? 0;
|
|
|
|
|
if (meQuery.data?.role === "admin" && pending > 0) {
|
|
|
|
|
notify({
|
|
|
|
|
level: "warning",
|
2026-06-15 00:30:34 +02:00
|
|
|
title: t("common.accessRequestsTitle"),
|
|
|
|
|
message: t("common.accessRequestsMessage", { count: pending }),
|
2026-06-12 01:43:07 +02:00
|
|
|
});
|
|
|
|
|
}
|
2026-06-16 09:17:34 +02:00
|
|
|
// The demo account is shared: there are no subscriptions, so default it to the whole
|
2026-06-16 10:12:10 +02:00
|
|
|
// library (the "my" feed would be empty). The communal-state warning is a permanent
|
|
|
|
|
// banner (see DemoBanner below), not a toast that re-pops on every reload.
|
2026-06-16 09:17:34 +02:00
|
|
|
if (meQuery.data?.is_demo) {
|
|
|
|
|
setFilters({ ...loadStoredFilters(), scope: "all" });
|
|
|
|
|
}
|
2026-06-11 02:19:47 +02:00
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
|
}, [meQuery.data?.id]);
|
|
|
|
|
|
|
|
|
|
function setTheme(next: ThemePrefs) {
|
|
|
|
|
setThemeState(next);
|
|
|
|
|
saveLocalTheme(next);
|
|
|
|
|
api.savePrefs({ theme: next }).catch(() => {});
|
|
|
|
|
}
|
|
|
|
|
function changeView(v: "grid" | "list") {
|
|
|
|
|
setView(v);
|
|
|
|
|
api.savePrefs({ view: v }).catch(() => {});
|
|
|
|
|
}
|
2026-06-15 00:30:34 +02:00
|
|
|
function changeLanguage(code: LangCode) {
|
|
|
|
|
setLanguage(code);
|
|
|
|
|
api.savePrefs({ language: code }).catch(() => {});
|
|
|
|
|
}
|
2026-06-11 02:19:47 +02:00
|
|
|
|
|
|
|
|
if (meQuery.isLoading)
|
2026-06-15 00:30:34 +02:00
|
|
|
return (
|
|
|
|
|
<div className="min-h-screen grid place-items-center text-muted">{t("common.loading")}</div>
|
|
|
|
|
);
|
2026-06-11 02:19:47 +02:00
|
|
|
if (meQuery.error instanceof HttpError && meQuery.error.status === 401)
|
|
|
|
|
return <Login />;
|
|
|
|
|
if (meQuery.error)
|
|
|
|
|
return (
|
|
|
|
|
<div className="min-h-screen grid place-items-center text-muted">
|
2026-06-15 00:30:34 +02:00
|
|
|
{t("common.somethingWrong")}
|
2026-06-11 02:19:47 +02:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return (
|
2026-06-16 00:42:23 +02:00
|
|
|
<div className="h-screen flex bg-bg text-fg">
|
|
|
|
|
<NavSidebar
|
2026-06-11 02:19:47 +02:00
|
|
|
me={meQuery.data!}
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
page={page}
|
|
|
|
|
setPage={setPage}
|
2026-06-15 00:06:57 +02:00
|
|
|
onOpenAbout={() => setAboutOpen(true)}
|
2026-06-17 14:28:29 +02:00
|
|
|
onChangeLanguage={changeLanguage}
|
|
|
|
|
language={i18n.language as LangCode}
|
|
|
|
|
filters={filters}
|
|
|
|
|
setFilters={setFilters}
|
2026-06-11 02:19:47 +02:00
|
|
|
/>
|
2026-06-17 14:28:29 +02:00
|
|
|
<div className="relative flex-1 min-w-0 flex flex-col">
|
2026-06-16 00:42:23 +02:00
|
|
|
<Header
|
|
|
|
|
me={meQuery.data!}
|
|
|
|
|
filters={filters}
|
|
|
|
|
setFilters={setFilters}
|
|
|
|
|
page={page}
|
|
|
|
|
onGoToFullHistory={() => {
|
|
|
|
|
setChannelFilter("needs_full");
|
|
|
|
|
setPage("channels");
|
|
|
|
|
}}
|
|
|
|
|
/>
|
2026-06-16 10:12:10 +02:00
|
|
|
{meQuery.data!.is_demo && <DemoBanner />}
|
2026-06-16 00:42:23 +02:00
|
|
|
<VersionBanner onOpen={() => openReleaseNotes(CURRENT_VERSION)} />
|
|
|
|
|
<div className="flex flex-1 min-h-0">
|
|
|
|
|
{page === "feed" && (
|
|
|
|
|
<Sidebar
|
|
|
|
|
filters={filters}
|
|
|
|
|
setFilters={setFilters}
|
|
|
|
|
layout={sidebarLayout}
|
|
|
|
|
setLayout={setSidebarLayout}
|
2026-06-18 01:17:31 +02:00
|
|
|
onFocusChannel={focusChannel}
|
2026-06-16 00:42:23 +02:00
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
<main className="flex-1 min-w-0 overflow-y-auto">
|
2026-06-12 02:47:55 +02:00
|
|
|
{page === "channels" ? (
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
<Channels
|
2026-06-11 23:27:11 +02:00
|
|
|
canWrite={meQuery.data!.can_write}
|
2026-06-17 19:16:23 +02:00
|
|
|
isAdmin={meQuery.data!.role === "admin"}
|
2026-06-18 01:17:31 +02:00
|
|
|
focusChannelName={focusChannelName}
|
|
|
|
|
onFocusChannel={focusChannel}
|
2026-06-14 07:08:59 +02:00
|
|
|
statusFilter={channelFilter}
|
|
|
|
|
setStatusFilter={setChannelFilter}
|
2026-06-15 02:02:05 +02:00
|
|
|
onOpenWizard={() => setWizardOpen(true)}
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
onViewChannel={(id, name) => {
|
|
|
|
|
setFilters({ ...filters, channelId: id, channelName: name, show: "all" });
|
|
|
|
|
setPage("feed");
|
|
|
|
|
}}
|
2026-06-18 01:17:31 +02:00
|
|
|
onFilterByTag={(tagId, name) => {
|
|
|
|
|
setFilters({ ...filters, tags: [tagId], channelId: undefined, channelName: undefined });
|
|
|
|
|
setPage("feed");
|
|
|
|
|
notify({ level: "info", message: t("channels.notify.filteredByTag", { name }) });
|
|
|
|
|
}}
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
/>
|
2026-06-12 02:47:55 +02:00
|
|
|
) : page === "stats" && meQuery.data!.role === "admin" ? (
|
|
|
|
|
<Stats />
|
2026-06-16 14:38:51 +02:00
|
|
|
) : page === "scheduler" && meQuery.data!.role === "admin" ? (
|
|
|
|
|
<Scheduler />
|
2026-06-15 14:45:18 +02:00
|
|
|
) : page === "playlists" ? (
|
2026-06-15 21:23:13 +02:00
|
|
|
<Playlists canWrite={meQuery.data!.can_write} />
|
2026-06-16 01:05:05 +02:00
|
|
|
) : page === "settings" ? (
|
|
|
|
|
<SettingsPanel
|
|
|
|
|
me={meQuery.data!}
|
|
|
|
|
theme={theme}
|
|
|
|
|
setTheme={setTheme}
|
|
|
|
|
view={view}
|
|
|
|
|
setView={changeView}
|
|
|
|
|
onOpenWizard={() => setWizardOpen(true)}
|
|
|
|
|
/>
|
2026-06-12 02:47:55 +02:00
|
|
|
) : (
|
2026-06-14 06:36:12 +02:00
|
|
|
<Feed
|
|
|
|
|
filters={filters}
|
|
|
|
|
setFilters={setFilters}
|
|
|
|
|
view={view}
|
|
|
|
|
canRead={meQuery.data!.can_read}
|
2026-06-16 09:27:34 +02:00
|
|
|
isDemo={meQuery.data!.is_demo}
|
2026-06-14 06:36:12 +02:00
|
|
|
onOpenWizard={() => setWizardOpen(true)}
|
|
|
|
|
/>
|
feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags),
user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts).
Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in
tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status +
actions + admin pause/resume, Account); a channel manager with priority, hide,
per-channel user tags, sync badges and 'view in feed'. Notifications now honor the
configurable sound + auto-dismiss settings.
2026-06-11 20:45:48 +02:00
|
|
|
)}
|
2026-06-16 00:42:23 +02:00
|
|
|
</main>
|
|
|
|
|
</div>
|
2026-06-17 14:28:29 +02:00
|
|
|
{/* Toasts rise from the bottom-left, by the notification bell in the nav rail.
|
|
|
|
|
Anchored inside the content column so they clear the sidebar automatically
|
|
|
|
|
(collapsed or expanded) without tracking its width. */}
|
|
|
|
|
<Toaster />
|
2026-06-11 02:19:47 +02:00
|
|
|
</div>
|
2026-06-16 09:27:34 +02:00
|
|
|
{wizardOpen && meQuery.data && !meQuery.data.is_demo && (
|
2026-06-14 01:11:29 +02:00
|
|
|
<OnboardingWizard me={meQuery.data} onClose={() => setWizardOpen(false)} />
|
|
|
|
|
)}
|
2026-06-15 00:06:57 +02:00
|
|
|
{aboutOpen && (
|
|
|
|
|
<About
|
|
|
|
|
onClose={() => setAboutOpen(false)}
|
|
|
|
|
onOpenReleaseNotes={() => {
|
|
|
|
|
setAboutOpen(false);
|
|
|
|
|
openReleaseNotes();
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
{notesOpen && (
|
|
|
|
|
<ReleaseNotes onClose={() => setNotesOpen(false)} highlight={notesHighlight} />
|
|
|
|
|
)}
|
2026-06-18 01:17:31 +02:00
|
|
|
<ErrorDialog />
|
2026-06-11 02:19:47 +02:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|