feat(layout): full-height collapsible filter sidebar; sync status + scope relocated
Restructure the app shell into three top-level columns: - The per-user sync status (video counts + live sync state) moves from the top bar to a compact block at the top of the left nav rail (icon-only with a tooltip when collapsed). - The feed's Mine/Library scope toggle moves to the top of the filter sidebar. - The filter sidebar becomes a full-height sibling column with its own collapse control (a thin rail carrying the active-filter count), mirroring the nav rail. The top bar is now just the feed search / page title. - Both panels' collapsed state is persisted to the user's preferences (server-side, so it follows the account across devices), seeded from a localStorage cache to avoid a flash. Default: both panels open.
This commit is contained in:
parent
c6fe94450b
commit
072b3296a3
9 changed files with 255 additions and 78 deletions
|
|
@ -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>(() =>
|
||||
Boolean(readJSON(LS.navCollapsed, false))
|
||||
);
|
||||
const [filterCollapsed, setFilterCollapsedState] = useState<boolean>(() =>
|
||||
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 && (
|
||||
<Sidebar
|
||||
filters={filters}
|
||||
setFilters={setFilters}
|
||||
layout={sidebarLayout}
|
||||
setLayout={setSidebarLayout}
|
||||
onFocusChannel={focusChannel}
|
||||
isDemo={meQuery.data!.is_demo}
|
||||
collapsed={filterCollapsed}
|
||||
onToggleCollapse={() => setFilterCollapsed(!filterCollapsed)}
|
||||
/>
|
||||
)}
|
||||
<div className="relative flex-1 min-w-0 flex flex-col">
|
||||
{channelView ? (
|
||||
<ChannelPage
|
||||
|
|
@ -584,26 +634,9 @@ export default function App() {
|
|||
setFilters={setFilters}
|
||||
page={page}
|
||||
onYtSearch={enterYtSearch}
|
||||
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");
|
||||
}}
|
||||
/>
|
||||
{meQuery.data!.is_demo && <DemoBanner />}
|
||||
<VersionBanner onOpen={() => openReleaseNotes(CURRENT_VERSION)} />
|
||||
<div className="flex flex-1 min-h-0">
|
||||
{page === "feed" && (
|
||||
<Sidebar
|
||||
filters={filters}
|
||||
setFilters={setFilters}
|
||||
layout={sidebarLayout}
|
||||
setLayout={setSidebarLayout}
|
||||
onFocusChannel={focusChannel}
|
||||
isDemo={meQuery.data!.is_demo}
|
||||
/>
|
||||
)}
|
||||
<main className="flex-1 min-w-0 overflow-y-auto">
|
||||
{page === "channels" ? (
|
||||
<Channels
|
||||
|
|
@ -661,7 +694,6 @@ export default function App() {
|
|||
/>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{/* Toasts rise from the bottom-left, by the notification bell in the nav rail.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue