diff --git a/backend/app/routes/feed.py b/backend/app/routes/feed.py
index f7c6f1e..458d8c9 100644
--- a/backend/app/routes/feed.py
+++ b/backend/app/routes/feed.py
@@ -1,7 +1,7 @@
from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException, Query
-from sqlalchemy import and_, func, or_, select
+from sqlalchemy import and_, false, func, or_, select
from sqlalchemy.orm import Session, aliased
from app.auth import current_user
@@ -50,6 +50,7 @@ def get_feed(
min_duration: int | None = None,
max_duration: int | None = None,
max_age_days: int | None = None,
+ show_normal: bool = True,
include_shorts: bool = False,
include_live: bool = False,
show: str = "unwatched", # all | unwatched | watched | saved | hidden
@@ -92,13 +93,21 @@ def get_feed(
)
)
- # In the explicit Watched/Saved/Hidden views, show the complete set regardless of
- # the Shorts / live default-hiding (so e.g. a hidden Short still shows up there).
+ # Content-type filter: Normal (regular videos incl. past-stream VODs), Shorts and
+ # Live/Upcoming are independent toggles; the feed is the union of the enabled types.
+ # The explicit Watched/Saved/Hidden views show every type so nothing goes missing.
explicit_view = show in ("watched", "saved", "hidden")
- if not include_shorts and not explicit_view:
- query = query.where(Video.is_short.is_(False))
- if not include_live and not explicit_view:
- query = query.where(Video.live_status.notin_(HIDDEN_LIVE))
+ if not explicit_view:
+ type_clauses = []
+ if show_normal:
+ type_clauses.append(
+ and_(Video.is_short.is_(False), Video.live_status.notin_(HIDDEN_LIVE))
+ )
+ if include_shorts:
+ type_clauses.append(Video.is_short.is_(True))
+ if include_live:
+ type_clauses.append(Video.live_status.in_(HIDDEN_LIVE))
+ query = query.where(or_(*type_clauses) if type_clauses else false())
if channel_id:
query = query.where(Video.channel_id == channel_id)
if min_duration is not None:
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 4c375dd..ce58cda 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -19,6 +19,7 @@ const DEFAULT_FILTERS: FeedFilters = {
tagMode: "or",
q: "",
sort: "newest",
+ includeNormal: true,
includeShorts: false,
includeLive: false,
show: "unwatched",
@@ -96,7 +97,7 @@ export default function App() {
-
+
diff --git a/frontend/src/components/Feed.tsx b/frontend/src/components/Feed.tsx
index ef6ae68..a4270cb 100644
--- a/frontend/src/components/Feed.tsx
+++ b/frontend/src/components/Feed.tsx
@@ -1,19 +1,37 @@
import { useEffect, useRef, useState } from "react";
-import { useInfiniteQuery } from "@tanstack/react-query";
+import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
import { api, type FeedFilters, type Video } from "../lib/api";
import { toast } from "../lib/toast";
import VideoCard from "./VideoCard";
const PAGE = 60;
+function matchesView(status: string, show: string): boolean {
+ switch (show) {
+ case "hidden":
+ return status === "hidden";
+ case "watched":
+ return status === "watched";
+ case "saved":
+ return status === "saved";
+ case "unwatched":
+ return status !== "watched" && status !== "hidden";
+ default:
+ return status !== "hidden"; // all
+ }
+}
+
export default function Feed({
filters,
+ setFilters,
view,
}: {
filters: FeedFilters;
+ setFilters: (f: FeedFilters) => void;
view: "grid" | "list";
}) {
const [overrides, setOverrides] = useState>({});
+ const qc = useQueryClient();
const query = useInfiniteQuery({
queryKey: ["feed", filters],
@@ -43,20 +61,24 @@ export default function Feed({
function onState(id: string, status: string) {
setOverrides((o) => ({ ...o, [id]: status }));
- api.setState(id, status).catch(() => {});
+ // Refetch once the server has the change so other views (e.g. Hidden) are in sync.
+ api
+ .setState(id, status)
+ .then(() => qc.invalidateQueries({ queryKey: ["feed"] }))
+ .catch(() => {});
if (status === "hidden") {
toast("Video hidden", { label: "Undo", onClick: () => onState(id, "new") });
}
}
+ function onChannelFilter(channelId: string, channelName: string) {
+ setFilters({ ...filters, channelId, channelName });
+ }
+
const items: Video[] = (query.data?.pages ?? [])
.flatMap((p) => p.items)
.map((v) => (overrides[v.id] ? { ...v, status: overrides[v.id] } : v))
- .filter((v) => {
- const ov = overrides[v.id];
- // In the Hidden view, drop just-unhidden items; elsewhere drop just-hidden ones.
- return filters.show === "hidden" ? ov !== "new" : ov !== "hidden";
- });
+ .filter((v) => matchesView(v.status, filters.show));
if (query.isLoading) return Loading feed…
;
if (query.isError) return Couldn't load the feed.
;
@@ -66,15 +88,27 @@ export default function Feed({
return (
{view === "grid" ? (
-
+
{items.map((v) => (
-
+
))}
) : (
{items.map((v) => (
-
+
))}
)}
diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx
index 5ec01e8..2be3530 100644
--- a/frontend/src/components/Sidebar.tsx
+++ b/frontend/src/components/Sidebar.tsx
@@ -1,4 +1,5 @@
import { useQuery } from "@tanstack/react-query";
+import { X } from "lucide-react";
import { api, type FeedFilters, type Tag } from "../lib/api";
const SORTS = [
@@ -64,13 +65,27 @@ export default function Sidebar({
const active =
filters.tags.length > 0 ||
+ !filters.includeNormal ||
filters.includeShorts ||
filters.includeLive ||
filters.show !== "unwatched" ||
- filters.sort !== "newest";
+ filters.sort !== "newest" ||
+ !!filters.channelId;
return (
-
+
);
}
@@ -146,12 +163,12 @@ export default function VideoCard({
return (
-
+
{video.channel_thumbnail && (
{meta}
-
+
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts
index f6628c2..5707360 100644
--- a/frontend/src/lib/api.ts
+++ b/frontend/src/lib/api.ts
@@ -45,10 +45,12 @@ export interface FeedFilters {
tagMode: "or" | "and";
q: string;
sort: string;
+ includeNormal: boolean;
includeShorts: boolean;
includeLive: boolean;
show: string;
channelId?: string;
+ channelName?: string;
maxAgeDays?: number;
minDuration?: number;
maxDuration?: number;
@@ -78,6 +80,7 @@ function feedQuery(f: FeedFilters, offset: number, limit: number): string {
p.set("tag_mode", f.tagMode);
if (f.q) p.set("q", f.q);
p.set("sort", f.sort);
+ p.set("show_normal", String(f.includeNormal));
p.set("include_shorts", String(f.includeShorts));
p.set("include_live", String(f.includeLive));
p.set("show", f.show);