From 426e85c2c9e4b520d60fcd8fad13ceda597d4118 Mon Sep 17 00:00:00 2001 From: Mattias Tall Date: Tue, 26 May 2026 10:55:25 +0200 Subject: [PATCH] Mobile nav, channel banners, search channel linking - Add bottom tab bar (Home/Following/Discover/Downloads/Settings) for mobile - Fetch and display channel banner images on channel pages - Fix ChannelCard: channels without a local DB id now follow+navigate on click Co-Authored-By: Claude Sonnet 4.6 --- backend/services/ytdlp.py | 15 ++++++- frontend/src/components/ChannelCard.jsx | 17 ++++++- frontend/src/components/Layout.jsx | 60 ++++++++++++++++++++++++- frontend/src/pages/Channel.jsx | 7 +++ 4 files changed, 95 insertions(+), 4 deletions(-) diff --git a/backend/services/ytdlp.py b/backend/services/ytdlp.py index c58a844..3c7c570 100644 --- a/backend/services/ytdlp.py +++ b/backend/services/ytdlp.py @@ -82,6 +82,19 @@ def _normalize_video(info: dict) -> dict: } +def _channel_banner(thumbnails: list | None) -> str | None: + if not thumbnails: + return None + for t in thumbnails: + if "banner" in str(t.get("id") or "").lower(): + return t.get("url") + wide = [t for t in thumbnails + if t.get("width") and t.get("height") and t["width"] > t["height"] * 3] + if wide: + return max(wide, key=lambda t: t.get("width") or 0).get("url") + return None + + def _channel_avatar(thumbnails: list | None) -> str | None: """Pick the channel avatar from yt-dlp's thumbnails list. @@ -109,7 +122,7 @@ def _normalize_channel(info: dict) -> dict: "name": info.get("channel") or info.get("title") or info.get("uploader") or None, "description": info.get("description") or None, "thumbnail_url": _channel_avatar(info.get("thumbnails")), - "banner_url": None, + "banner_url": _channel_banner(info.get("thumbnails")), "subscriber_count": info.get("channel_follower_count"), } diff --git a/frontend/src/components/ChannelCard.jsx b/frontend/src/components/ChannelCard.jsx index ece9080..38c6967 100644 --- a/frontend/src/components/ChannelCard.jsx +++ b/frontend/src/components/ChannelCard.jsx @@ -1,6 +1,6 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useNavigate } from "react-router-dom"; -import { followChannel, unfollowChannel } from "../api"; +import { followChannel, unfollowChannel, followChannelByUrl } from "../api"; function formatSubs(n) { if (!n) return null; @@ -21,6 +21,19 @@ export default function ChannelCard({ channel }) { onSuccess: () => qc.invalidateQueries({ queryKey: ["channels"] }), }); + const followAndNavMut = useMutation({ + mutationFn: () => followChannelByUrl({ youtube_channel_id: channel.youtube_channel_id }), + onSuccess: (res) => { + qc.invalidateQueries({ queryKey: ["channels"] }); + navigate(`/channels/${res.data.channel_id}`); + }, + }); + + const handleClick = () => { + if (channelId) navigate(`/channels/${channelId}`); + else if (channel.youtube_channel_id) followAndNavMut.mutate(); + }; + const subs = formatSubs(channel.subscriber_count); const meta = [ subs && `${subs} subscribers`, @@ -30,7 +43,7 @@ export default function ChannelCard({ channel }) { return (
channelId && navigate(`/channels/${channelId}`)} + onClick={handleClick} > {/* Avatar */} {channel.thumbnail_url ? ( diff --git a/frontend/src/components/Layout.jsx b/frontend/src/components/Layout.jsx index 1f84635..174703a 100644 --- a/frontend/src/components/Layout.jsx +++ b/frontend/src/components/Layout.jsx @@ -4,6 +4,62 @@ import { useAuth } from "../hooks/useAuth"; import SearchBar from "./SearchBar"; import { getDownloads, getChannels } from "../api"; +function BottomNav({ newCount }) { + const tabs = [ + { + to: "/", label: "Home", end: true, + icon: , + }, + { + to: "/following", label: "Following", badge: newCount, + icon: , + }, + { + to: "/discovery", label: "Discover", + icon: , + }, + { + to: "/downloads", label: "Downloads", + icon: , + }, + { + to: "/settings", label: "Settings", + icon: , + }, + ]; + + return ( + + ); +} + function DownloadIndicator() { const { data } = useQuery({ queryKey: ["downloads"], @@ -201,9 +257,11 @@ export default function Layout() { {/* Page content */} -
+
+ +
); } diff --git a/frontend/src/pages/Channel.jsx b/frontend/src/pages/Channel.jsx index 552afc4..bbb5c40 100644 --- a/frontend/src/pages/Channel.jsx +++ b/frontend/src/pages/Channel.jsx @@ -79,6 +79,13 @@ export default function ChannelPage() { return (
+ {/* Banner */} + {channel.banner_url && ( +
+ +
+ )} + {/* Channel header */}
{channel.thumbnail_url ? (