diff --git a/backend/app/routes/feed.py b/backend/app/routes/feed.py index 0ef733b..03863a1 100644 --- a/backend/app/routes/feed.py +++ b/backend/app/routes/feed.py @@ -275,10 +275,13 @@ SORTS = { "newest": Video.published_at.desc().nulls_last(), "oldest": Video.published_at.asc().nulls_last(), "views": Video.view_count.desc().nulls_last(), + "views_asc": Video.view_count.asc().nulls_last(), "duration_desc": Video.duration_seconds.desc().nulls_last(), "duration_asc": Video.duration_seconds.asc().nulls_last(), "title": func.lower(Video.title).asc().nulls_last(), + "title_desc": func.lower(Video.title).desc().nulls_last(), "subscribers": Channel.subscriber_count.desc().nulls_last(), + "subscribers_asc": Channel.subscriber_count.asc().nulls_last(), # Your per-channel priority (set in the channel manager), newest first within a tier. # coalesce keeps it null-safe in "all" scope where unsubscribed channels have no row. "priority": func.coalesce(Subscription.priority, 0).desc(), @@ -296,9 +299,10 @@ def get_feed( db: Session = Depends(get_db), ) -> dict: query, _status = _filtered_query(db, user, **params) - if sort == "priority": + if sort in ("priority", "priority_asc"): + prio = func.coalesce(Subscription.priority, 0) query = query.order_by( - func.coalesce(Subscription.priority, 0).desc(), + prio.asc() if sort == "priority_asc" else prio.desc(), Video.published_at.desc().nulls_last(), ) else: diff --git a/frontend/src/components/Feed.tsx b/frontend/src/components/Feed.tsx index eac7b72..7741586 100644 --- a/frontend/src/components/Feed.tsx +++ b/frontend/src/components/Feed.tsx @@ -1,7 +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 { ArrowDown, ArrowUp, RefreshCw } from "lucide-react"; import { api, type FeedFilters, type Video } from "../lib/api"; import i18n from "../i18n"; import { notify } from "../lib/notifications"; @@ -10,18 +10,9 @@ 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", -]; +// The "Show" view filter, content-type filter and ordering live in the feed toolbar +// (above the cards), not the filter sidebar. +const SHOW_IDS = ["unwatched", "in_progress", "all", "watched", "hidden"]; const CONTENT = [ { key: "includeNormal", label: "sidebar.content.normal" }, { key: "includeShorts", label: "sidebar.content.shorts" }, @@ -29,6 +20,33 @@ const CONTENT = [ ] as const; const rollSeed = () => Math.floor(Math.random() * 1_000_000_000); +// 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. +type SortKey = "date" | "popular" | "duration" | "title" | "subscribers" | "priority" | "shuffle"; +const SORT_KEYS: SortKey[] = ["date", "popular", "duration", "title", "subscribers", "priority", "shuffle"]; +const SORT_MAP: Record, { asc: string; desc: string }> = { + date: { desc: "newest", asc: "oldest" }, + popular: { desc: "views", asc: "views_asc" }, + duration: { desc: "duration_desc", asc: "duration_asc" }, + title: { asc: "title", desc: "title_desc" }, + subscribers: { desc: "subscribers", asc: "subscribers_asc" }, + priority: { desc: "priority", asc: "priority_asc" }, +}; +const SORT_DEFAULT_DIR: Record, "asc" | "desc"> = { + date: "desc", popular: "desc", duration: "desc", title: "asc", subscribers: "desc", priority: "desc", +}; +function parseSort(s: string): { key: SortKey; dir: "asc" | "desc" } { + if (s === "shuffle") return { key: "shuffle", dir: "desc" }; + for (const k of Object.keys(SORT_MAP) as (keyof typeof SORT_MAP)[]) { + if (SORT_MAP[k].asc === s) return { key: k, dir: "asc" }; + if (SORT_MAP[k].desc === s) return { key: k, dir: "desc" }; + } + return { key: "date", dir: "desc" }; +} +function buildSort(key: SortKey, dir: "asc" | "desc"): string { + return key === "shuffle" ? "shuffle" : SORT_MAP[key][dir]; +} + function matchesView(status: string, show: string): boolean { switch (show) { case "hidden": @@ -210,9 +228,26 @@ export default function Feed({ ); + const { key: sortKey, dir: sortDir } = parseSort(filters.sort); + const toolbar = (
+ {SHOW_IDS.map((id) => ( + + ))} +