From 2f2bfbfcdc085d7ac79c121df701fae70ee9c666 Mon Sep 17 00:00:00 2001 From: nianzhibai <177086871+nianzhibai@users.noreply.github.com> Date: Sun, 7 Jun 2026 15:17:08 +0800 Subject: [PATCH] Improve video detail player controls and layout --- src/components/AppShell.tsx | 82 ++- src/components/RecommendedRail.tsx | 12 +- src/components/VideoActions.tsx | 25 +- src/components/VideoInfoPanel.tsx | 6 +- src/components/VideoMetaHeader.tsx | 28 +- src/components/VideoPlayer.tsx | 701 ++++++++++++++++++------ src/pages/HomePage.tsx | 2 +- src/pages/VideoDetailPage.tsx | 80 ++- src/styles/layout.css | 4 + src/styles/navigation.css | 18 + src/styles/video-detail.css | 842 +++++++++++++++++++---------- tests/videoActions.test.ts | 33 ++ tests/videoPlayerPoster.test.ts | 155 ++++++ 13 files changed, 1482 insertions(+), 506 deletions(-) create mode 100644 tests/videoActions.test.ts create mode 100644 tests/videoPlayerPoster.test.ts diff --git a/src/components/AppShell.tsx b/src/components/AppShell.tsx index 5c957de..caeae11 100644 --- a/src/components/AppShell.tsx +++ b/src/components/AppShell.tsx @@ -1,4 +1,4 @@ -import { ReactNode } from "react"; +import { ReactNode, useEffect, useState } from "react"; import { TopBar } from "./TopBar"; import { MainNav } from "./MainNav"; import { SubNav } from "./SubNav"; @@ -7,14 +7,84 @@ import { BackToTop } from "./BackToTop"; type Props = { children: ReactNode; + mobileAutoHideNav?: boolean; }; -export function AppShell({ children }: Props) { +const MOBILE_NAV_QUERY = "(max-width: 768px)"; +const SCROLL_DELTA_THRESHOLD = 6; +const HIDE_AFTER_SCROLL_Y = 56; + +export function AppShell({ children, mobileAutoHideNav = false }: Props) { + const [mobileNavHidden, setMobileNavHidden] = useState(false); + + useEffect(() => { + if (!mobileAutoHideNav) { + setMobileNavHidden(false); + return; + } + + const mediaQuery = window.matchMedia(MOBILE_NAV_QUERY); + let lastScrollY = Math.max(window.scrollY, 0); + let ticking = false; + + const showNav = () => setMobileNavHidden(false); + + const updateNavVisibility = () => { + ticking = false; + const currentScrollY = Math.max(window.scrollY, 0); + + if (!mediaQuery.matches || currentScrollY <= 0) { + showNav(); + lastScrollY = currentScrollY; + return; + } + + const delta = currentScrollY - lastScrollY; + if (Math.abs(delta) < SCROLL_DELTA_THRESHOLD) return; + + if (delta > 0 && currentScrollY > HIDE_AFTER_SCROLL_Y) { + setMobileNavHidden(true); + } else if (delta < 0) { + showNav(); + } + + lastScrollY = currentScrollY; + }; + + const handleScroll = () => { + if (ticking) return; + ticking = true; + window.requestAnimationFrame(updateNavVisibility); + }; + + const handleMediaChange = () => { + lastScrollY = Math.max(window.scrollY, 0); + showNav(); + }; + + handleMediaChange(); + window.addEventListener("scroll", handleScroll, { passive: true }); + mediaQuery.addEventListener("change", handleMediaChange); + + return () => { + window.removeEventListener("scroll", handleScroll); + mediaQuery.removeEventListener("change", handleMediaChange); + }; + }, [mobileAutoHideNav]); + + const className = [ + "app-shell", + mobileAutoHideNav ? "app-shell--mobile-auto-hide-nav" : "", + mobileNavHidden ? "is-mobile-nav-hidden" : "", + ].filter(Boolean).join(" "); + return ( -