feat(feed): pick the feed's view from its own toolbar
The view mode moves out of Settings into a switcher next to sort, on the feed and channel pages alike. It leaves the Settings prefs draft for its own eager save: a toolbar click must not mark that page dirty and trip its leave-page guard. lib/feedView.ts owns the vocabulary and maps the old grid/list prefs forward on read, so nobody's saved choice is lost.
This commit is contained in:
@@ -1,7 +1,18 @@
|
||||
import { lazy, Suspense, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { keepPreviousData, useInfiniteQuery, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { ArrowDown, ArrowUp, ArrowLeft, Info, RefreshCw, Trash2, Youtube } from "lucide-react";
|
||||
import {
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
ArrowLeft,
|
||||
Info,
|
||||
LayoutGrid,
|
||||
RefreshCw,
|
||||
Rows3,
|
||||
Trash2,
|
||||
Youtube,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
import { api, HttpError, type FeedFilters, type Video } from "../lib/api";
|
||||
import i18n from "../i18n";
|
||||
import { notify, resolveVideo } from "../lib/notifications";
|
||||
@@ -14,8 +25,10 @@ import {
|
||||
SORT_KEYS,
|
||||
type SortKey,
|
||||
} from "../lib/feedSort";
|
||||
import { FEED_VIEWS, type FeedView } from "../lib/feedView";
|
||||
import { clearedFilters, useActiveFilters } from "../lib/useActiveFilters";
|
||||
import ActiveFilterChips from "./ActiveFilterChips";
|
||||
import ViewSwitcher from "./ViewSwitcher";
|
||||
import VirtualFeed from "./VirtualFeed";
|
||||
// Lazy: the in-app YouTube player (IFrame API) loads only when a video is first opened.
|
||||
const PlayerModal = lazy(() => import("./PlayerModal"));
|
||||
@@ -32,6 +45,12 @@ const CONTENT = [
|
||||
] as const;
|
||||
const rollSeed = () => Math.floor(Math.random() * 1_000_000_000);
|
||||
|
||||
// Icon per view mode; the labels are i18n'd at render (see feed.view.*).
|
||||
const VIEW_ICON: Record<FeedView, LucideIcon> = {
|
||||
cards: LayoutGrid,
|
||||
rows: Rows3,
|
||||
};
|
||||
|
||||
// Ordering = a key + direction (like the Playlists page), mapped to the backend sort strings
|
||||
// (which encode both). One entry per concept; a single arrow flips the direction.
|
||||
// "relevance" (full-text search ranking) and "shuffle" are directionless single-string sorts.
|
||||
@@ -56,6 +75,7 @@ export default function Feed({
|
||||
filters,
|
||||
setFilters,
|
||||
view,
|
||||
setView,
|
||||
canRead,
|
||||
isDemo = false,
|
||||
onOpenWizard,
|
||||
@@ -67,7 +87,10 @@ export default function Feed({
|
||||
}: {
|
||||
filters: FeedFilters;
|
||||
setFilters: (f: FeedFilters) => void;
|
||||
view: "grid" | "list";
|
||||
// The view mode is an account pref, not a filter: App owns it and it persists on pick, so it
|
||||
// isn't part of FeedFilters (no share-link param, no saved view, no active-filter chip).
|
||||
view: FeedView;
|
||||
setView: (v: FeedView) => void;
|
||||
canRead: boolean;
|
||||
isDemo?: boolean;
|
||||
onOpenWizard: () => void;
|
||||
@@ -621,6 +644,13 @@ export default function Feed({
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
<span className="mx-0.5 h-5 w-px bg-border" aria-hidden="true" />
|
||||
<ViewSwitcher
|
||||
value={view}
|
||||
options={FEED_VIEWS.map((id) => ({ id, label: t("feed.view." + id), icon: VIEW_ICON[id] }))}
|
||||
onChange={setView}
|
||||
label={t("feed.viewLabel")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user