feat: feedback round 3 — content-type toggles, channel filter, card polish

- Content type is now three independent toggles (Normal / Shorts / Live·Upcoming);
  the feed is the union of enabled types (backend show_normal + include_shorts +
  include_live)
- Per-channel filter: a button on each card scopes the feed to that channel, shown
  as a removable chip in the sidebar
- Hide now refetches the feed once the change is persisted, so a hidden video shows
  up in the Hidden view immediately (no refresh needed); optimistic updates respect
  the current view
- Grid cards are now distinct panels (card background, border, shadow) that lift on
  hover; clickable channel name in list view too
This commit is contained in:
npeter83 2026-06-11 03:47:51 +02:00
parent e07a37622d
commit ecbecbb9f4
6 changed files with 115 additions and 30 deletions

View file

@ -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: