From e8f62bc89a47de7cf172bbe25cbce065f204763c Mon Sep 17 00:00:00 2001 From: Marcus <1922576605@qq.com> Date: Tue, 6 Oct 2026 09:21:03 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=B8=83=E5=B1=80=E4=BA=A4=E4=BA=92?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/design-system.md | 10 +- src/app/(app)/components/app-header.tsx | 11 +- src/app/(app)/components/library-filters.tsx | 160 ++++++++++++++ src/app/(app)/components/library-toolbar.tsx | 96 +-------- .../(app)/components/library-video-dialog.tsx | 204 ++++++++---------- src/app/(app)/components/logout-button.tsx | 16 +- src/app/(app)/components/mobile-menu.tsx | 138 ++++++++++++ src/app/(app)/components/playlist-menu.tsx | 23 +- src/app/(app)/components/search-dialog.tsx | 11 +- .../(app)/components/search-result-list.tsx | 12 ++ .../components/search-result-toolbar.tsx | 35 ++- src/app/(app)/components/video-playlist.tsx | 70 +----- src/components/ui/alert.tsx | 2 +- src/components/ui/badge.tsx | 2 +- src/components/ui/button.tsx | 2 +- src/components/ui/card.tsx | 2 +- src/components/ui/dialog.tsx | 2 +- src/components/ui/dropdown-menu.tsx | 2 +- src/components/ui/empty.tsx | 2 +- src/components/ui/field.tsx | 2 +- src/components/ui/input-group.tsx | 2 +- src/components/ui/input.tsx | 2 +- src/components/ui/kbd.tsx | 2 +- src/components/ui/label.tsx | 2 +- src/components/ui/progress.tsx | 2 +- src/components/ui/separator.tsx | 2 +- src/components/ui/sheet.tsx | 138 ++++++++++++ src/components/ui/skeleton.tsx | 2 +- src/components/ui/spinner.tsx | 2 +- src/components/ui/textarea.tsx | 2 +- src/components/ui/toggle-group.tsx | 2 +- src/components/ui/toggle.tsx | 2 +- src/components/ui/tooltip.tsx | 2 +- src/components/video-player.tsx | 24 ++- src/components/videojs/video/skin.tsx | 31 ++- src/constants/motion.ts | 8 +- src/hooks/use-close-on-back.ts | 31 +++ src/hooks/use-playlist.ts | 17 ++ src/hooks/use-search-results.ts | 19 +- src/server/mock-search.ts | 11 + src/server/search.ts | 40 +++- src/server/torbox.ts | 27 +++ src/stores/library-filter-store.ts | 14 +- src/stores/search-store.ts | 19 +- src/stores/search-view-store.ts | 8 + src/types/search.ts | 2 + src/utils/magnet.ts | 5 + 47 files changed, 877 insertions(+), 343 deletions(-) create mode 100644 src/app/(app)/components/library-filters.tsx create mode 100644 src/app/(app)/components/mobile-menu.tsx create mode 100644 src/components/ui/sheet.tsx create mode 100644 src/hooks/use-close-on-back.ts create mode 100644 src/hooks/use-playlist.ts diff --git a/docs/design-system.md b/docs/design-system.md index 7016cc3..64e4f26 100644 --- a/docs/design-system.md +++ b/docs/design-system.md @@ -76,7 +76,7 @@ div.flex.h-dvh.flex-col ← (app)/layout.tsx - **例外三·分组标题吸顶**:资源库的分组标题行用原生 `sticky`(全站唯一),由浏览器合成,滚动时不跑 JS。虚拟列表把每个分组的可见行包进一个与整组等高的容器(未渲染部分用占位块撑出),标题以它为边界,到分组末尾时被下一组自然推出;`rangeExtractor` 保证当前分组的标题始终渲染。吸附时的底边线用 `stuck-divider`(scroll-state 容器查询);容器查询改不了容器自身,吸附时标题行两侧各外扩 6px 由 `useStuck`(IntersectionObserver,观察边界比吸附位置低 1px)驱动。 - 单格 grid 里放 canvas 时,轨道必须是 `grid-cols-1 grid-rows-1`(`minmax(0, 1fr)`),否则画布的像素尺寸会反过来撑大布局。 - shadcn 自带浮层(Dialog、Toast、Tooltip、DropdownMenu)与图片预览灯箱内部的定位不受此限。 -- **浮层层级**:Dialog、DropdownMenu、Tooltip 为 `z-50`,图片灯箱 `z-90`(`image-lightbox.tsx` 覆盖库默认的 9999),Toast 视口 `z-100`。通知必须盖过所有浮层,否则在弹窗内触发的 `message.*` 会被弹窗挡住;新增浮层组件不要超过 `z-90`。 +- **浮层层级**:Dialog、Sheet、DropdownMenu、Tooltip 为 `z-50`,图片灯箱 `z-90`(`image-lightbox.tsx` 覆盖库默认的 9999),Toast 视口 `z-100`。通知必须盖过所有浮层,否则在弹窗内触发的 `message.*` 会被弹窗挡住;新增浮层组件不要超过 `z-90`。 ### 长列表(虚拟滚动) @@ -104,8 +104,9 @@ div.flex.h-dvh.flex-col ← (app)/layout.tsx | 文件卡片 | `Card size="sm"` + `CardContent` + `CardFooter`;底栏左侧一个主操作(视频「播放」、图片「预览」,其他文件没有),右侧图标操作(视频多一个 PotPlayer,所有文件都有下载、复制直链),名称放在 `Tooltip` 与 `aria-label`;卡片是 `@container/card`,窄于 186px(手机两列)时主操作只显示图标 | | 标签 | `Badge`(来源:`variant="outline"` + `SourceDot`) | | 下拉操作 | `DropdownMenu` | +| 侧滑面板 | `Sheet`(右侧滑出、高度撑满、右上角关闭):手机顶栏菜单 | | 图标按钮说明 | `Tooltip` | -| 视频播放 | `VideoPlayer`(Video.js v10 + `components/videojs` 皮肤,`I18nProvider` 固定中文);资源库在 `Dialog` 中播放,`next/dynamic` 按需加载;MP4、M4V、WebM 直接播放原文件,其他格式经 TorBox 转码为 HLS(`HlsJsVideo`,hls.js 只在此时加载),标题下注明「TorBox 转码」;底栏提供下载与「PotPlayer 播放」;播放错误说明替换为中文并提示换用 PotPlayer | +| 视频播放 | `VideoPlayer`(Video.js v10 + `components/videojs` 皮肤,`I18nProvider` 固定中文);资源库在全屏 `Dialog` 中播放(类似视频网站的播放页),`next/dynamic` 按需加载;MP4、M4V、WebM 直接播放原文件,其他格式经 TorBox 转码为 HLS(`HlsJsVideo`,hls.js 只在此时加载),信息行注明「TorBox 转码」;`onClose` 在画面左上角放关闭按钮(皮肤的 `topStart` 插槽,样式与显隐同右上角按钮组);标题右侧提供下载与「PotPlayer 播放」(手机宽度 `< md` 隐藏 PotPlayer);播放错误说明替换为中文并提示换用 PotPlayer | | 图片预览 | `ImageLightbox`(yet-another-react-lightbox + Zoom):传入一组图片与当前序号;资源库以一个资源分组内的图片为一组,组内首尾循环,不跨组;列表只渲染一个灯箱 | | 提示消息 | `src/lib/message.ts`(不直接调用 `toast`) | | 错误提示 | `Alert variant="destructive"` | @@ -203,8 +204,9 @@ div.flex.h-dvh.flex-col ← (app)/layout.tsx | 分组标题 | 24 小时内加入的资源显示呼吸的 NEW | `NewBadge`,以数据拉取时间为「现在」 | | 分组吸顶 | 滚过分组标题后标题吸附在顶部,下一分组到达时被推出;吸附时可直接收起该分组并回到其标题处 | 原生 `sticky` + 分组容器(`layoutVirtualRows`),无滚动监听 | | 全部展开 / 收起 | 工具栏图标按钮,有展开的资源时收起全部,否则展开全部;当前分组保持在顶部 | `setAllExpanded` + 列表订阅 `bulkToggleCount` | -| 视频播放 | 点击视频封面或「播放」在弹窗中网页播放并自动开始,焦点交给播放器(空格、方向键等快捷键直接可用);「PotPlayer 播放」交给外部播放器后关闭弹窗 | `LibraryVideoDialog` + `VideoPlayer`;弹窗与灯箱都在列表层只渲染一个,全屏或缩放窗口重建卡片行时不会中断播放 | -| 视频播放列表 | 弹窗里同一资源的视频超过一个时,底栏左侧出现「播放列表 3 / 12」开关,列表默认收起(收起时播放器占满宽度),展开状态在弹窗关闭后保留;展开时列表从右侧滑入、播放器随之收窄,收起时滑出(窄屏改为在播放器下方展开 / 收起),参数见 `PLAYLIST_SLIDE`:宽屏在播放器右侧、高度跟随播放器,窄屏在下方限高;点击切换、上一个 / 下一个按钮,播完自动接下一个(最后一个不循环),当前项高亮并滚入视野;切换同时转移封面的「上次播放」标记。Video.js v10 没有播放列表功能(官方列为已知缺口),所以列表与接续逻辑由项目自己实现 | `VideoPlaylist` + `LibraryVideoDialog`(`VideoPlayer` 的 `onEnded`);顺序与资源列表一致,受类型、搜索筛选影响 | +| 视频播放 | 点击视频封面或「播放」全屏打开并自动开始,焦点交给播放器(空格、方向键等快捷键直接可用);视频在顶部(手机贴边),下方依次是标题、信息行与操作;视频宽度上限按视口高度换算(`(100dvh - 9rem) × 16/9`),矮屏时等比缩小居中;关闭按钮在视频左上角、随控制栏显隐,也可 Esc 或返回键 / 返回手势关闭(打开时压入一条历史记录,用其他方式关闭时撤回,不留多余记录);「PotPlayer 播放」交给外部播放器后关闭 | `LibraryVideoDialog` + `VideoPlayer` + `useCloseOnBack`;弹窗与灯箱都在列表层只渲染一个,全屏或缩放窗口重建卡片行时不会中断播放 | +| 视频播放列表 | 同一资源的视频超过一个时始终显示,无边框,标题行只有「播放列表 3 / 12」:宽屏(`≥ lg`)在右侧占满高度,窄屏在信息行下方占满剩余高度(最低 12rem,更矮时整块滚动),内容多时只滚动列表;点击切换,播完自动接下一个(最后一个不循环),当前项高亮并滚入视野;切换同时转移封面的「上次播放」标记。Video.js v10 没有播放列表功能(官方列为已知缺口),所以列表与接续逻辑由项目自己实现 | `VideoPlaylist` + `LibraryVideoDialog`(`VideoPlayer` 的 `onEnded`);顺序与资源列表一致,受类型、搜索筛选影响 | | 图片预览 | 点击图片封面或「预览」打开灯箱:左右键 / 按钮 / 滑动在同一分组内循环切换;滚轮、双击、双指或工具栏按钮缩放;Esc、点击空白处或下拉关闭,焦点回到打开它的按钮 | `ImageLightbox`;配色通过 `--yarl__*` 变量映射到主题变量,图标换成 lucide,加载超过 0.4s 才淡入转圈;减弱动态效果由灯箱自行处理 | +| 手机顶栏菜单 | 宽度 `< md` 时顶栏右侧的播放列表、退出登录与工具栏的来源 / 下载状态 / 类型三组筛选收进右上角汉堡按钮;点击后菜单从右侧滑出(高度撑满,右上角关闭):筛选按等宽网格排列并带「重置」(只清这三项,保留关键词),下面是 M3U 的「复制 M3U 地址」(手机上没有 PotPlayer,不提供「用 PotPlayer 打开」),退出登录固定在底部;三项中任一项不是「全部」时汉堡图标右上角弹出翠绿提示点,读屏名为「菜单(已启用筛选)」 | `MobileMenu`(`Sheet`)+ `LibraryFilters layout="stacked"`(与工具栏共用);提示点用单格 grid 叠层 + Motion `scale`,参数见 `INDICATOR_DOT`;来源计数只在菜单打开时计算 | | 键盘 | `/` 聚焦搜索,搜索框内 Esc 先清空再失焦;方向键在卡片间移动(跨分组、自动滚动),Enter 执行主操作 | `useHotkey`、`useGridKeyboardNav` + `utils/grid-nav` | diff --git a/src/app/(app)/components/app-header.tsx b/src/app/(app)/components/app-header.tsx index 17e6f38..eabfea5 100644 --- a/src/app/(app)/components/app-header.tsx +++ b/src/app/(app)/components/app-header.tsx @@ -5,6 +5,7 @@ import { Separator } from "@/components/ui/separator"; import { AddMagnetButton } from "./add-magnet-button"; import { LogoutButton } from "./logout-button"; +import { MobileMenu } from "./mobile-menu"; import { PlaylistMenu } from "./playlist-menu"; import { SearchButton } from "./search-button"; @@ -33,9 +34,13 @@ export const AppHeader = ({ token, mock, searchEnabled }: AppHeaderProps) => (