refactor(frontend): the module stores mount from the registry, and the feed and Plex each own their saved views

A module's definition names its store (feed: filters, view, saved views; Plex: browse state, saved
views; channels, files, playlists their provider), and ModuleStores mounts them at the root in
registry order instead of a hand-nested tree in main.tsx. A contract test holds that every store's
read side is in reach and that no store's static imports leave its own module, so the order cannot
matter.

Saved views were one provider that switched between the feed and Plex by the active page and reset
on each switch. The logic is now one hook (useSavedViewsStore) with an instance per module; the
shared chip, popover and modal read the views their page or rail hands down. A feed view applied
and edited now survives a visit to Plex. Plex's views are still asked for only on its page, and a
module's manage modal still closes when its page is left. The feed's URL-override check applies
only on the feed page, where its filter params live.

The Channels store's tab type moves from the page into the store's context.
This commit is contained in:
2026-10-23 04:59:10 +02:00
parent cde5a98fd2
commit 6ae7825a14
43 changed files with 653 additions and 289 deletions
+8 -5
View File
@@ -17,7 +17,8 @@ import FilterChipGroup from "../../components/ui/FilterChipGroup";
import Combobox, { type ComboboxOption } from "../../components/ui/Combobox";
import SavedViewsPopover from "../../features/filters/SavedViewsPopover";
import PersonalViewsModal from "../../features/filters/PersonalViewsModal";
import { useSavedViews } from "../../features/filters/savedViewsContext";
import { SavedViewsScope } from "../../features/filters/savedViewsContext";
import { usePlexSavedViews } from "./plexSavedViews";
import { usePlex, usePlexActions } from "./plexContext";
import { DismissibleChip } from "../../components/ui/Chip";
import { Input } from "../../components/ui/Input";
@@ -47,7 +48,9 @@ export default function PlexSidebar() {
const { setFilters } = usePlexActions();
// Personal ("saved") views for the Plex module: the rail header icon opens a quick-pick popover, the
// title opens the full manager modal (same shared components as the feed).
const { openModal, modalOpen } = useSavedViews();
// They reach the shared views UI in this rail through the scope below.
const savedViews = usePlexSavedViews();
const { openModal, modalOpen } = savedViews;
// This rail's media source (the Plex library) — names the abstraction the feed and Plex rails
// share; drives the panel layout key. `FilterRail` owns the customize `editing` state now.
const source: MediaSourceKind = "plex";
@@ -293,7 +296,7 @@ export default function PlexSidebar() {
];
return (
<>
<SavedViewsScope value={savedViews}>
<FilterRail
kind={source}
title={t("sidebar.filters")}
@@ -308,14 +311,14 @@ export default function PlexSidebar() {
// feed rail). The rail's own "Clear all" is dropped too — the chip row above the grid owns it.
collapsible={false}
// Saved ("personal") views: header icon → quick-pick popover, title → the manager modal.
// Same shared components as the feed rail (SavedViewsProvider switches source).
// The same shared components as the feed rail, over Plex's own views.
iconExpanded={<SavedViewsPopover trigger={<SlidersHorizontal className="w-4 h-4" />} />}
onTitleClick={openModal}
titleActionLabel={t("views.manageTitle")}
/>
{/* The manager modal lives here (rail is always mounted on plex); Modal portals to <body>. */}
{modalOpen && <PersonalViewsModal />}
</>
</SavedViewsScope>
);
}