diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 5897f07..b03b2e9 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -19,7 +19,7 @@ import { } from "./lib/sidebarLayout"; import { configureNotifications, getNotifSettings, notify, removeByMetaKind, type NotifSettings } from "./lib/notifications"; import { hintsEnabled, setHintsEnabled } from "./lib/hints"; -import { LS, readJSON, readMerged, usePersistedState } from "./lib/storage"; +import { LS, readJSON, readMerged, usePersistedState, writeJSON } from "./lib/storage"; import { useConfirm } from "./components/ConfirmProvider"; import type { PrefsController } from "./components/SettingsPanel"; import Welcome from "./components/Welcome"; @@ -257,6 +257,26 @@ export default function App() { api.savePrefs({ sidebarLayout: next }).catch(() => {}); } + // Collapse state for the two full-height panels (left nav + filter sidebar). Persisted to the + // user's preferences so it follows the account across devices; a localStorage cache seeds the + // initial render synchronously so nothing flashes open before the server prefs arrive. + const [navCollapsed, setNavCollapsedState] = useState(() => + Boolean(readJSON(LS.navCollapsed, false)) + ); + const [filterCollapsed, setFilterCollapsedState] = useState(() => + Boolean(readJSON(LS.filterCollapsed, false)) + ); + function setNavCollapsed(next: boolean) { + setNavCollapsedState(next); + writeJSON(LS.navCollapsed, next); + api.savePrefs({ navCollapsed: next }).catch(() => {}); + } + function setFilterCollapsed(next: boolean) { + setFilterCollapsedState(next); + writeJSON(LS.filterCollapsed, next); + api.savePrefs({ filterCollapsed: next }).catch(() => {}); + } + useEffect(() => applyTheme(theme), [theme]); // Apply the draft prefs locally for instant preview (their localStorage mirrors update via @@ -423,6 +443,14 @@ export default function App() { setSidebarLayoutState(l); saveLayoutLocal(l); } + if (typeof prefs.navCollapsed === "boolean") { + setNavCollapsedState(prefs.navCollapsed); + writeJSON(LS.navCollapsed, prefs.navCollapsed); + } + if (typeof prefs.filterCollapsed === "boolean") { + setFilterCollapsedState(prefs.filterCollapsed); + writeJSON(LS.filterCollapsed, prefs.filterCollapsed); + } if (isSupported(prefs.language)) setLanguage(prefs.language); // The demo account is shared: there are no subscriptions, so default it to the whole // library (the "my" feed would be empty). The communal-state warning is a permanent @@ -564,7 +592,29 @@ export default function App() { onOpenAbout={() => setAboutOpen(true)} onChangeLanguage={changeLanguage} language={i18n.language as LangCode} + collapsed={navCollapsed} + onToggleCollapse={() => setNavCollapsed(!navCollapsed)} + onGoToFullHistory={() => { + setChannelFilter("needs_full"); + setChannelsView("subscribed"); // the status filter applies to the subscriptions tab + setChannelsFilterReset((n) => n + 1); // drop any stale column filter hiding the rows + setPage("channels"); + }} /> + {/* Full-height filter sidebar: feed only, and hidden while a channel page overlays the + content column. Sits beside the nav rail as its own collapsible column. */} + {page === "feed" && !channelView && ( + setFilterCollapsed(!filterCollapsed)} + /> + )}
{channelView ? ( { - setChannelFilter("needs_full"); - setChannelsView("subscribed"); // the status filter applies to the subscriptions tab - setChannelsFilterReset((n) => n + 1); // drop any stale column filter hiding the rows - setPage("channels"); - }} /> {meQuery.data!.is_demo && } openReleaseNotes(CURRENT_VERSION)} /> -
- {page === "feed" && ( - - )}
{page === "channels" ? ( )}
-
)} {/* Toasts rise from the bottom-left, by the notification bell in the nav rail. diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx index 9938a48..2dc8024 100644 --- a/frontend/src/components/Header.tsx +++ b/frontend/src/components/Header.tsx @@ -1,25 +1,22 @@ import { useTranslation } from "react-i18next"; -import { Library, Search, User, X, Youtube } from "lucide-react"; +import { Search, X, Youtube } from "lucide-react"; import type { FeedFilters, Me } from "../lib/api"; import type { Page } from "../lib/urlState"; -import SyncStatus from "./SyncStatus"; -// Contextual top bar. Primary navigation + account now live in the left NavSidebar; the -// header carries the global sync status, the feed's scope toggle + search (or the current -// page title), the language switcher and the notification bell. +// Contextual top bar over the content column. Primary navigation, account and the per-user sync +// status live in the left NavSidebar; the feed's scope toggle now lives in the filter sidebar. +// The header carries just the feed search (or the current page title). export default function Header({ me, filters, setFilters, page, - onGoToFullHistory, onYtSearch, }: { me: Me; filters: FeedFilters; setFilters: (f: FeedFilters) => void; page: Page; - onGoToFullHistory: () => void; // Trigger a live YouTube search for the current term (hidden for the demo account, which // can't spend the shared quota). onYtSearch: (q: string) => void; @@ -30,37 +27,6 @@ export default function Header({ return (
- - - {page === "feed" && ( -
- {(["my", "all"] as const).map((s) => ( - - ))} -
- )} - {page === "feed" ? (
diff --git a/frontend/src/components/NavSidebar.tsx b/frontend/src/components/NavSidebar.tsx index 0800b6a..0518be8 100644 --- a/frontend/src/components/NavSidebar.tsx +++ b/frontend/src/components/NavSidebar.tsx @@ -23,11 +23,11 @@ import { import { api, type Me } from "../lib/api"; import { useLiveQuery } from "../lib/useLiveQuery"; import { getUnreadCount, subscribe } from "../lib/notifications"; -import { LS } from "../lib/storage"; import type { Page } from "../lib/urlState"; import { type LangCode } from "../i18n"; import AvatarImg from "./Avatar"; import LanguageSwitcher from "./LanguageSwitcher"; +import SyncStatus from "./SyncStatus"; // Primary app navigation: a collapsible left rail with icon+label entries (Design C). The // modules used to hide under the avatar dropdown; they now live here. Collapsed, it becomes @@ -39,6 +39,9 @@ export default function NavSidebar({ onOpenAbout, onChangeLanguage, language, + collapsed, + onToggleCollapse, + onGoToFullHistory, }: { me: Me; page: Page; @@ -46,11 +49,13 @@ export default function NavSidebar({ onOpenAbout: () => void; onChangeLanguage: (code: LangCode) => void; language: LangCode; + // Collapse state is owned by App (persisted to the user's preferences); the rail just reflects + // it and asks App to flip it. + collapsed: boolean; + onToggleCollapse: () => void; + onGoToFullHistory: () => void; }) { const { t } = useTranslation(); - const [collapsed, setCollapsed] = useState( - () => localStorage.getItem(LS.navCollapsed) === "1" - ); const [acctOpen, setAcctOpen] = useState(false); const acctBtnRef = useRef(null); const acctPanelRef = useRef(null); @@ -88,14 +93,6 @@ export default function NavSidebar({ }; }, [acctOpen]); - function toggleCollapsed() { - setCollapsed((c) => { - const next = !c; - localStorage.setItem(LS.navCollapsed, next ? "1" : "0"); - return next; - }); - } - async function logout() { await fetch("/auth/logout", { method: "POST", credentials: "include" }); location.reload(); @@ -247,7 +244,7 @@ export default function NavSidebar({ )}
+ {/* Per-user sync status (video counts + live sync state), moved out of the old top bar. */} +
+ +
+
{userItems.map(renderItem)} {systemItems.length > 0 && diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 17d2b65..c835ef6 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -4,13 +4,17 @@ import { keepPreviousData, useQuery } from "@tanstack/react-query"; import { Check, ChevronDown, + ChevronLeft, + ChevronRight, Eye, EyeOff, GripVertical, + Library, Pencil, - RefreshCw, RotateCcw, Share2, + SlidersHorizontal, + User, X, } from "lucide-react"; import { @@ -120,6 +124,8 @@ export default function Sidebar({ setLayout, onFocusChannel, isDemo = false, + collapsed, + onToggleCollapse, }: { filters: FeedFilters; setFilters: (f: FeedFilters) => void; @@ -127,6 +133,10 @@ export default function Sidebar({ setLayout: (l: SidebarLayout) => void; onFocusChannel: (name: string) => void; isDemo?: boolean; + // Full-height collapse (state owned by App, persisted to the user's preferences), mirroring + // the left nav rail. + collapsed: boolean; + onToggleCollapse: () => void; }) { const { t } = useTranslation(); const tagsQuery = useQuery({ queryKey: ["tags"], queryFn: api.tags }); @@ -464,16 +474,81 @@ export default function Sidebar({ ? orderedAvailable : orderedAvailable.filter((id) => !layout.hidden[id]); - return ( -