diff --git a/frontend/src/components/Feed.tsx b/frontend/src/components/Feed.tsx index 9133f7b..eac7b72 100644 --- a/frontend/src/components/Feed.tsx +++ b/frontend/src/components/Feed.tsx @@ -1,6 +1,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { useInfiniteQuery, useQuery, useQueryClient } from "@tanstack/react-query"; +import { RefreshCw } from "lucide-react"; import { api, type FeedFilters, type Video } from "../lib/api"; import i18n from "../i18n"; import { notify } from "../lib/notifications"; @@ -9,6 +10,25 @@ import PlayerModal from "./PlayerModal"; const PAGE = 60; +// Sort + content-type live in the feed toolbar (above the cards), not the filter sidebar. +const SORT_IDS = [ + "newest", + "oldest", + "views", + "duration_desc", + "duration_asc", + "title", + "subscribers", + "priority", + "shuffle", +]; +const CONTENT = [ + { key: "includeNormal", label: "sidebar.content.normal" }, + { key: "includeShorts", label: "sidebar.content.shorts" }, + { key: "includeLive", label: "sidebar.content.live" }, +] as const; +const rollSeed = () => Math.floor(Math.random() * 1_000_000_000); + function matchesView(status: string, show: string): boolean { switch (show) { case "hidden": @@ -166,11 +186,10 @@ export default function Feed({ if (query.isLoading) return
{t("feed.loading")}
; if (query.isError) return
{t("feed.loadError")}
; - if (items.length === 0) { - // No YouTube connection and looking at their own (empty) subscriptions: offer both - // connecting YouTube and just browsing the shared library (no read scope needed). - if (!canRead && filters.scope === "my") - return ( + // Onboarding empty state (no YouTube + own subscriptions): a dedicated full-page prompt, + // with no toolbar (content-type/sort would be meaningless here). + if (items.length === 0 && !canRead && filters.scope === "my") + return (

{t("feed.emptyTitle")}

@@ -190,20 +209,73 @@ export default function Feed({
); - return
{t("feed.noMatches")}
; - } + + const toolbar = ( +
+
+ {CONTENT.map((c) => { + const on = filters[c.key]; + return ( + + ); + })} +
+
+ + {countQuery.data + ? t("feed.videoCount", { + count: countQuery.data.count, + formattedCount: countQuery.data.count.toLocaleString(), + }) + : " "} + +
+ {t("feed.sortLabel")} + + {filters.sort === "shuffle" && ( + + )} +
+
+ ); return (
-
- {countQuery.data - ? t("feed.videoCount", { - count: countQuery.data.count, - formattedCount: countQuery.data.count.toLocaleString(), - }) - : " "} -
- {view === "grid" ? ( + {toolbar} + {items.length === 0 ? ( +
{t("feed.noMatches")}
+ ) : view === "grid" ? (
{items.map((v) => ( = { show: true, - sort: true, date: true, - content: true, language: languages.length > 0, topic: topics.length > 0, }; @@ -257,41 +255,6 @@ export default function Sidebar({ ))}
); - case "sort": - return ( -
- - {filters.sort === "shuffle" && ( - - )} -
- ); case "date": return ( <> @@ -381,26 +344,6 @@ export default function Sidebar({ )} ); - case "content": - return ( - <> - setFilters({ ...filters, includeNormal: v })} - /> - setFilters({ ...filters, includeShorts: v })} - /> - setFilters({ ...filters, includeLive: v })} - /> - - ); case "language": { const chips = visibleChips(languages); return ( diff --git a/frontend/src/i18n/locales/de/feed.json b/frontend/src/i18n/locales/de/feed.json index 3a46ab8..582fcac 100644 --- a/frontend/src/i18n/locales/de/feed.json +++ b/frontend/src/i18n/locales/de/feed.json @@ -8,6 +8,7 @@ "noMatches": "Keine Videos entsprechen diesen Filtern.", "videoCount_one": "{{formattedCount}} Video", "videoCount_other": "{{formattedCount}} Videos", + "sortLabel": "Sortierung", "loadingMore": "Mehr wird geladen…", "hiddenNamed": "Ausgeblendet: „{{title}}”", "hidden": "Video ausgeblendet", diff --git a/frontend/src/i18n/locales/en/feed.json b/frontend/src/i18n/locales/en/feed.json index 8e30dda..3991cf5 100644 --- a/frontend/src/i18n/locales/en/feed.json +++ b/frontend/src/i18n/locales/en/feed.json @@ -8,6 +8,7 @@ "noMatches": "No videos match these filters.", "videoCount_one": "{{formattedCount}} video", "videoCount_other": "{{formattedCount}} videos", + "sortLabel": "Sort", "loadingMore": "Loading more…", "hiddenNamed": "Hidden “{{title}}”", "hidden": "Video hidden", diff --git a/frontend/src/i18n/locales/hu/feed.json b/frontend/src/i18n/locales/hu/feed.json index 2995492..62d94e0 100644 --- a/frontend/src/i18n/locales/hu/feed.json +++ b/frontend/src/i18n/locales/hu/feed.json @@ -8,6 +8,7 @@ "noMatches": "Egy videó sem felel meg ezeknek a szűrőknek.", "videoCount_one": "{{formattedCount}} videó", "videoCount_other": "{{formattedCount}} videó", + "sortLabel": "Rendezés", "loadingMore": "Továbbiak betöltése…", "hiddenNamed": "Elrejtve: „{{title}}”", "hidden": "Videó elrejtve", diff --git a/frontend/src/lib/sidebarLayout.ts b/frontend/src/lib/sidebarLayout.ts index a9c0676..29ea1ef 100644 --- a/frontend/src/lib/sidebarLayout.ts +++ b/frontend/src/lib/sidebarLayout.ts @@ -2,22 +2,15 @@ // collapsed, and which are hidden. Persisted to localStorage and the server-side // `preferences` blob so it follows the account. -export type WidgetId = "show" | "sort" | "date" | "content" | "language" | "topic"; +// `sort` and `content` moved to the feed toolbar (above the cards); they are no longer +// sidebar widgets. normalizeLayout drops them from any persisted layout automatically. +export type WidgetId = "show" | "date" | "language" | "topic"; -export const ALL_WIDGETS: WidgetId[] = [ - "show", - "sort", - "date", - "content", - "language", - "topic", -]; +export const ALL_WIDGETS: WidgetId[] = ["show", "date", "language", "topic"]; export const WIDGET_TITLES: Record = { show: "Show", - sort: "Sort", date: "Upload date", - content: "Content type", language: "Language", topic: "Topic", };