From f73cbdb4901f97cb6e06d7de5f93941ede202658 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Thu, 11 Jun 2026 04:00:17 +0200 Subject: [PATCH] feat: unhide action, date-range filter, more sort options - Hidden view shows an Unhide action (eye icon) instead of Hide - Upload-date filter: From/To date range (inclusive); feed shows only videos published in that window (backend published_after / published_before) - New sort options: Name (A-Z) and Channel subscribers, alongside date/views/ duration/shuffle - Native controls follow the theme via color-scheme (dark date picker) --- backend/app/routes/feed.py | 17 ++++++++++- frontend/src/components/Sidebar.tsx | 41 ++++++++++++++++++++++++++- frontend/src/components/VideoCard.tsx | 15 +++++++--- frontend/src/index.css | 8 ++++++ frontend/src/lib/api.ts | 4 +++ 5 files changed, 79 insertions(+), 6 deletions(-) diff --git a/backend/app/routes/feed.py b/backend/app/routes/feed.py index 458d8c9..c0d54c3 100644 --- a/backend/app/routes/feed.py +++ b/backend/app/routes/feed.py @@ -1,4 +1,4 @@ -from datetime import datetime, timezone +from datetime import date, datetime, timedelta, timezone from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy import and_, false, func, or_, select @@ -50,6 +50,8 @@ def get_feed( min_duration: int | None = None, max_duration: int | None = None, max_age_days: int | None = None, + published_after: date | None = None, + published_before: date | None = None, show_normal: bool = True, include_shorts: bool = False, include_live: bool = False, @@ -119,6 +121,17 @@ def get_feed( query = query.where( Video.published_at >= datetime.fromtimestamp(cutoff, tz=timezone.utc) ) + if published_after is not None: + start = datetime.combine( + published_after, datetime.min.time(), tzinfo=timezone.utc + ) + query = query.where(Video.published_at >= start) + if published_before is not None: + # Inclusive of the end day. + end = datetime.combine( + published_before, datetime.min.time(), tzinfo=timezone.utc + ) + timedelta(days=1) + query = query.where(Video.published_at < end) if q: # Title (and channel name) only — searching descriptions produced noisy matches. like = f"%{q}%" @@ -168,6 +181,8 @@ def get_feed( "views": Video.view_count.desc().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(), + "subscribers": Channel.subscriber_count.desc().nulls_last(), "shuffle": func.md5(func.concat(Video.id, str(seed))), } query = query.order_by(sorts.get(sort, sorts["newest"])) diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 2be3530..dcc1a0a 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -8,6 +8,8 @@ const SORTS = [ { id: "views", label: "Most viewed" }, { id: "duration_desc", label: "Longest" }, { id: "duration_asc", label: "Shortest" }, + { id: "title", label: "Name (A–Z)" }, + { id: "subscribers", label: "Channel subscribers" }, { id: "shuffle", label: "Surprise me" }, ]; @@ -70,7 +72,9 @@ export default function Sidebar({ filters.includeLive || filters.show !== "unwatched" || filters.sort !== "newest" || - !!filters.channelId; + !!filters.channelId || + !!filters.dateFrom || + !!filters.dateTo; return (