From d5726c4f077b6c62112d300d64ec0ffe62c0fa81 Mon Sep 17 00:00:00 2001 From: katelya Date: Tue, 2 Sep 2025 14:49:56 +0800 Subject: [PATCH] feat: Implement intelligent skip feature with enhanced settings - Added support for time input in minutes:seconds format for skipping segments. - Introduced automatic skipping functionality for both opening and ending segments. - Enhanced UI for skip settings with a floating configuration card. - Implemented countdown timer for automatic next episode playback. - Added batch settings for configuring multiple skip segments at once. - Updated SkipController component to handle new skip logic and UI changes. - Created comprehensive usage guide for the new skip feature. --- D1初始化.md | 7 + SKIP_FEATURE_GUIDE.md | 123 ++++++ public/sw.js | 2 +- scripts/test-docker-compatibility.js | 3 + src/app/play/page.tsx | 3 +- src/components/SkipController.tsx | 585 ++++++++++++++++++++++----- src/lib/db.client.ts | 2 + 7 files changed, 631 insertions(+), 94 deletions(-) create mode 100644 SKIP_FEATURE_GUIDE.md diff --git a/D1初始化.md b/D1初始化.md index abe3571..2e281fa 100644 --- a/D1初始化.md +++ b/D1初始化.md @@ -65,6 +65,7 @@ CREATE TABLE IF NOT EXISTS skip_configs ( CREATE INDEX IF NOT EXISTS idx_play_records_username ON play_records(username); CREATE INDEX IF NOT EXISTS idx_favorites_username ON favorites(username); CREATE INDEX IF NOT EXISTS idx_search_history_username ON search_history(username); +CREATE INDEX IF NOT EXISTS idx_skip_configs_username ON skip_configs(username); -- 复合索引优化查询性能 -- 播放记录:用户名+键值的复合索引,用于快速查找特定记录 @@ -84,4 +85,10 @@ CREATE INDEX IF NOT EXISTS idx_search_history_username_created_at ON search_hist -- 搜索历史清理查询的优化索引 CREATE INDEX IF NOT EXISTS idx_search_history_username_id_created_at ON search_history(username, id, created_at DESC); + +-- 跳过配置索引 +-- 跳过配置:用户名+键值的复合索引,用于快速查找特定配置 +CREATE INDEX IF NOT EXISTS idx_skip_configs_username_key ON skip_configs(username, key); +-- 跳过配置:用户名+更新时间的复合索引,用于按时间排序的查询 +CREATE INDEX IF NOT EXISTS idx_skip_configs_username_updated_time ON skip_configs(username, updated_time DESC); ``` diff --git a/SKIP_FEATURE_GUIDE.md b/SKIP_FEATURE_GUIDE.md new file mode 100644 index 0000000..160dff1 --- /dev/null +++ b/SKIP_FEATURE_GUIDE.md @@ -0,0 +1,123 @@ +# 🎬 智能跳过功能使用指南 + +## ✨ 功能特色 + +### 🚀 全新智能跳过体验 + +- **🎯 分:秒格式输入** - 更符合观影习惯,如 `2:10` 表示 2 分 10 秒 +- **⚡ 自动跳过** - 无需手动点击,到达设定时间自动跳转 +- **🎭 智能片尾** - 可设置从指定时间直接跳转下一集 +- **🎨 优化布局** - 悬浮式配置显示,不与内容重叠 + +## 📱 使用方法 + +### 1. 开启跳过设置 + +在播放页面,点击标题右侧的 **"跳过设置"** 按钮 + +### 2. 智能配置模式(推荐) + +#### 全局开关 + +- ✅ **启用自动跳过** - 到达时间自动跳转,无需手动点击 +- ✅ **片尾自动播放下一集** - 片尾时显示倒计时,自动播放下一集 + +#### 片头设置 + +- **开始时间**: `0:00` (通常从视频开始) +- **结束时间**: `2:10` (跳过 2 分 10 秒的片头) + +#### 片尾设置 + +- **开始时间**: `19:20` (从 19 分 20 秒开始检测片尾) +- **结束时间**: 留空 (直接跳转下一集) 或 `20:50` (跳过片尾到此时间) + +### 3. 支持的时间格式 + +- **分:秒格式**: `2:10`, `19:20`, `1:30.5` +- **秒数格式**: `130`, `1160`, `90.5` +- **自动识别**: 系统会自动识别并转换格式 + +## 🎯 使用场景示例 + +### 场景一:跳过片头 + +``` +片头设置: +开始时间: 0:00 +结束时间: 2:10 + +效果: 视频开始播放时,自动从0秒跳转到2分10秒 +``` + +### 场景二:片尾直接下一集 + +``` +片尾设置: +开始时间: 19:20 +结束时间: 留空 + +效果: 播放到19分20秒时,显示倒计时,自动播放下一集 +``` + +### 场景三:跳过片头+片尾 + +``` +片头: 0:00 → 2:10 (跳过片头) +片尾: 19:20 → 留空 (直接下一集) + +效果: 完全自动化的观影体验 +``` + +## 🎨 界面说明 + +### 播放时显示 + +- **倒计时器**: 片尾时屏幕中央显示"X 秒后自动播放下一集" +- **跳过提示**: 自动跳过时显示"自动跳过片头/片尾" +- **取消按钮**: 可随时取消自动操作 + +### 配置显示 + +- **悬浮卡片**: 右下角显示当前跳过配置 +- **状态标识**: 显示自动跳过状态 +- **快速修改**: 点击卡片可快速修改配置 + +## ⚙️ 高级功能 + +### 精确设置 + +- 支持小数点精度: `90.5` 秒 +- 支持多段跳过: 可设置多个片头/片尾段落 +- 智能检测: 自动识别当前播放时间是否在跳过区间 + +### 数据同步 + +- **LocalStorage**: 单设备本地存储 +- **云端同步**: 支持 Redis、D1、Upstash 跨设备同步 +- **实时更新**: 配置修改后立即生效 + +## 🔧 故障排除 + +### 常见问题 + +1. **时间格式错误**: 确保使用 `分:秒` 格式,如 `2:10` +2. **配置不生效**: 检查是否开启了"启用自动跳过"开关 +3. **重叠显示**: 新版本已修复,配置卡片不会与内容重叠 + +### 兼容性 + +- ✅ 支持所有部署方式 (Docker, Vercel, Cloudflare Pages) +- ✅ 支持所有存储后端 (LocalStorage, Redis, D1, Upstash) +- ✅ 支持桌面和移动设备 + +## 💡 使用技巧 + +1. **首次设置**: 建议先观看一遍内容,记录片头片尾时间 +2. **批量配置**: 使用智能配置模式一次性设置片头和片尾 +3. **个性化**: 不同类型的内容可以设置不同的跳过规则 +4. **测试验证**: 设置后可以快进到设定时间测试效果 + +--- + +🎉 **享受更流畅的观影体验!** diff --git a/public/sw.js b/public/sw.js index e9477c0..ea6e261 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1 +1 @@ -if(!self.define){let e,s={};const n=(n,c)=>(n=new URL(n+".js",c).href,s[n]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=n,e.onload=s,document.head.appendChild(e)}else e=n,importScripts(n),s()}).then(()=>{let e=s[n];if(!e)throw new Error(`Module ${n} didn’t register its module`);return e}));self.define=(c,a)=>{const i=e||("document"in self?document.currentScript.src:"")||location.href;if(s[i])return;let t={};const o=e=>n(e,i),r={module:{uri:i},exports:t,require:o};s[i]=Promise.all(c.map(e=>r[e]||o(e))).then(e=>(a(...e),t))}}define(["./workbox-e9849328"],function(e){"use strict";importScripts(),self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"/_next/app-build-manifest.json",revision:"0e33962d6467364e379933cb15dccaf5"},{url:"/_next/static/chunks/110-4c63c94070455926.js",revision:"gMKKQpGImN2Xpwm8-RogA"},{url:"/_next/static/chunks/154-de4a84fd5b2e0100.js",revision:"gMKKQpGImN2Xpwm8-RogA"},{url:"/_next/static/chunks/29-0844689411ca7d55.js",revision:"gMKKQpGImN2Xpwm8-RogA"},{url:"/_next/static/chunks/459-6bec40a8423cc309.js",revision:"gMKKQpGImN2Xpwm8-RogA"},{url:"/_next/static/chunks/51b697cb-f464f3017ac1ea30.js",revision:"gMKKQpGImN2Xpwm8-RogA"},{url:"/_next/static/chunks/682-d1dca8d17a3a8e6f.js",revision:"gMKKQpGImN2Xpwm8-RogA"},{url:"/_next/static/chunks/900-fb094d8873768e88.js",revision:"gMKKQpGImN2Xpwm8-RogA"},{url:"/_next/static/chunks/967-217cdcb80ae3beeb.js",revision:"gMKKQpGImN2Xpwm8-RogA"},{url:"/_next/static/chunks/998-568996670b543597.js",revision:"gMKKQpGImN2Xpwm8-RogA"},{url:"/_next/static/chunks/app/_not-found/page-ac328df06cf68f14.js",revision:"gMKKQpGImN2Xpwm8-RogA"},{url:"/_next/static/chunks/app/admin/page-d0def26e413c060d.js",revision:"gMKKQpGImN2Xpwm8-RogA"},{url:"/_next/static/chunks/app/douban/page-2d0023184aa37aff.js",revision:"gMKKQpGImN2Xpwm8-RogA"},{url:"/_next/static/chunks/app/layout-bd0bfbfdb401e15f.js",revision:"gMKKQpGImN2Xpwm8-RogA"},{url:"/_next/static/chunks/app/login/page-fcbddca77bc41b81.js",revision:"gMKKQpGImN2Xpwm8-RogA"},{url:"/_next/static/chunks/app/page-6a58e37ab3250691.js",revision:"gMKKQpGImN2Xpwm8-RogA"},{url:"/_next/static/chunks/app/play/page-253340b78ab57cef.js",revision:"gMKKQpGImN2Xpwm8-RogA"},{url:"/_next/static/chunks/app/search/page-63fe30b91e0539a7.js",revision:"gMKKQpGImN2Xpwm8-RogA"},{url:"/_next/static/chunks/app/warning/page-11cba4cf9332a238.js",revision:"gMKKQpGImN2Xpwm8-RogA"},{url:"/_next/static/chunks/c72274ce-06682d6fc8197e6d.js",revision:"gMKKQpGImN2Xpwm8-RogA"},{url:"/_next/static/chunks/da9543df-bf6da1a431d8604f.js",revision:"gMKKQpGImN2Xpwm8-RogA"},{url:"/_next/static/chunks/framework-6e06c675866dc992.js",revision:"gMKKQpGImN2Xpwm8-RogA"},{url:"/_next/static/chunks/main-95de9e33689c098a.js",revision:"gMKKQpGImN2Xpwm8-RogA"},{url:"/_next/static/chunks/main-app-dbd320e104e1a5dc.js",revision:"gMKKQpGImN2Xpwm8-RogA"},{url:"/_next/static/chunks/pages/_app-792b631a362c29e1.js",revision:"gMKKQpGImN2Xpwm8-RogA"},{url:"/_next/static/chunks/pages/_error-9fde6601392a2a99.js",revision:"gMKKQpGImN2Xpwm8-RogA"},{url:"/_next/static/chunks/polyfills-42372ed130431b0a.js",revision:"846118c33b2c0e922d7b3a7676f81f6f"},{url:"/_next/static/chunks/webpack-17170f1d90853b2d.js",revision:"gMKKQpGImN2Xpwm8-RogA"},{url:"/_next/static/css/23100062f5d4aac0.css",revision:"23100062f5d4aac0"},{url:"/_next/static/css/275ed64cc4367444.css",revision:"275ed64cc4367444"},{url:"/_next/static/css/4e3e7863e443c949.css",revision:"4e3e7863e443c949"},{url:"/_next/static/gMKKQpGImN2Xpwm8-RogA/_buildManifest.js",revision:"046380ae5bc74b46b6d5eac3eed65355"},{url:"/_next/static/gMKKQpGImN2Xpwm8-RogA/_ssgManifest.js",revision:"b6652df95db52feb4daf4eca35380933"},{url:"/_next/static/media/26a46d62cd723877-s.woff2",revision:"befd9c0fdfa3d8a645d5f95717ed6420"},{url:"/_next/static/media/55c55f0601d81cf3-s.woff2",revision:"43828e14271c77b87e3ed582dbff9f74"},{url:"/_next/static/media/581909926a08bbc8-s.woff2",revision:"f0b86e7c24f455280b8df606b89af891"},{url:"/_next/static/media/8e9860b6e62d6359-s.woff2",revision:"01ba6c2a184b8cba08b0d57167664d75"},{url:"/_next/static/media/97e0cb1ae144a2a9-s.woff2",revision:"e360c61c5bd8d90639fd4503c829c2dc"},{url:"/_next/static/media/df0a9ae256c0569c-s.woff2",revision:"d54db44de5ccb18886ece2fda72bdfe0"},{url:"/_next/static/media/e4af272ccee01ff0-s.p.woff2",revision:"65850a373e258f1c897a2b3d75eb74de"},{url:"/favicon.ico",revision:"c5de6e56c5664adda146825f75ea6ecf"},{url:"/icons/icon-192x192.png",revision:"4a56c090828a1ad254c903c7aec0389d"},{url:"/icons/icon-256x256.png",revision:"f6409eb1a001f754121e3a8281c0319c"},{url:"/icons/icon-384x384.png",revision:"f6efc3e357b9ffdf4e0d8c14b2ed0ac1"},{url:"/icons/icon-512x512.png",revision:"9c008cbbeb6a576fe07bb1284a83f4d2"},{url:"/logo.png",revision:"40de611b143c47c6291c7bdad2c959ca"},{url:"/manifest.json",revision:"7bd3dabc1cfbfe40f09577efca223d31"},{url:"/robots.txt",revision:"e2b2cd8514443456bc6fb9d77b3b1f3e"},{url:"/screenshot1.png",revision:"10572bfcea54dc93ac4c5f7c9057fc98"},{url:"/screenshot2.png",revision:"f815a8990973a221899976867365c239"},{url:"/screenshot3.png",revision:"49709e96345dfeeab1d8083821d4b44e"},{url:"/screenshot4.png",revision:"a76c751e41e37556048a487e4f8b8b1c"},{url:"/wechat.jpg",revision:"d0f601311802667cd6ca5a37dc69bfa7"}],{ignoreURLParametersMatching:[]}),e.cleanupOutdatedCaches(),e.registerRoute("/",new e.NetworkFirst({cacheName:"start-url",plugins:[{cacheWillUpdate:async({request:e,response:s,event:n,state:c})=>s&&"opaqueredirect"===s.type?new Response(s.body,{status:200,statusText:"OK",headers:s.headers}):s}]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:gstatic)\.com\/.*/i,new e.CacheFirst({cacheName:"google-fonts-webfonts",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:31536e3})]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:googleapis)\.com\/.*/i,new e.StaleWhileRevalidate({cacheName:"google-fonts-stylesheets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:eot|otf|ttc|ttf|woff|woff2|font.css)$/i,new e.StaleWhileRevalidate({cacheName:"static-font-assets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:jpg|jpeg|gif|png|svg|ico|webp)$/i,new e.StaleWhileRevalidate({cacheName:"static-image-assets",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/_next\/image\?url=.+$/i,new e.StaleWhileRevalidate({cacheName:"next-image",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:mp3|wav|ogg)$/i,new e.CacheFirst({cacheName:"static-audio-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:mp4)$/i,new e.CacheFirst({cacheName:"static-video-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:js)$/i,new e.StaleWhileRevalidate({cacheName:"static-js-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:css|less)$/i,new e.StaleWhileRevalidate({cacheName:"static-style-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/_next\/data\/.+\/.+\.json$/i,new e.StaleWhileRevalidate({cacheName:"next-data",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:json|xml|csv)$/i,new e.NetworkFirst({cacheName:"static-data-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({url:e})=>{if(!(self.origin===e.origin))return!1;const s=e.pathname;return!s.startsWith("/api/auth/")&&!!s.startsWith("/api/")},new e.NetworkFirst({cacheName:"apis",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:16,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({url:e})=>{if(!(self.origin===e.origin))return!1;return!e.pathname.startsWith("/api/")},new e.NetworkFirst({cacheName:"others",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({url:e})=>!(self.origin===e.origin),new e.NetworkFirst({cacheName:"cross-origin",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:3600})]}),"GET")}); +if(!self.define){let e,s={};const n=(n,c)=>(n=new URL(n+".js",c).href,s[n]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=n,e.onload=s,document.head.appendChild(e)}else e=n,importScripts(n),s()}).then(()=>{let e=s[n];if(!e)throw new Error(`Module ${n} didn’t register its module`);return e}));self.define=(c,a)=>{const i=e||("document"in self?document.currentScript.src:"")||location.href;if(s[i])return;let t={};const o=e=>n(e,i),r={module:{uri:i},exports:t,require:o};s[i]=Promise.all(c.map(e=>r[e]||o(e))).then(e=>(a(...e),t))}}define(["./workbox-e9849328"],function(e){"use strict";importScripts(),self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"/_next/app-build-manifest.json",revision:"94824925af6265ddd7901dd5a5bc2ced"},{url:"/_next/static/DW0c5RnMDGosaxigGfJBA/_buildManifest.js",revision:"046380ae5bc74b46b6d5eac3eed65355"},{url:"/_next/static/DW0c5RnMDGosaxigGfJBA/_ssgManifest.js",revision:"b6652df95db52feb4daf4eca35380933"},{url:"/_next/static/chunks/110-4c63c94070455926.js",revision:"DW0c5RnMDGosaxigGfJBA"},{url:"/_next/static/chunks/154-de4a84fd5b2e0100.js",revision:"DW0c5RnMDGosaxigGfJBA"},{url:"/_next/static/chunks/29-0844689411ca7d55.js",revision:"DW0c5RnMDGosaxigGfJBA"},{url:"/_next/static/chunks/459-6bec40a8423cc309.js",revision:"DW0c5RnMDGosaxigGfJBA"},{url:"/_next/static/chunks/51b697cb-f464f3017ac1ea30.js",revision:"DW0c5RnMDGosaxigGfJBA"},{url:"/_next/static/chunks/682-d1dca8d17a3a8e6f.js",revision:"DW0c5RnMDGosaxigGfJBA"},{url:"/_next/static/chunks/900-fb094d8873768e88.js",revision:"DW0c5RnMDGosaxigGfJBA"},{url:"/_next/static/chunks/967-217cdcb80ae3beeb.js",revision:"DW0c5RnMDGosaxigGfJBA"},{url:"/_next/static/chunks/998-568996670b543597.js",revision:"DW0c5RnMDGosaxigGfJBA"},{url:"/_next/static/chunks/app/_not-found/page-ac328df06cf68f14.js",revision:"DW0c5RnMDGosaxigGfJBA"},{url:"/_next/static/chunks/app/admin/page-d0def26e413c060d.js",revision:"DW0c5RnMDGosaxigGfJBA"},{url:"/_next/static/chunks/app/douban/page-2d0023184aa37aff.js",revision:"DW0c5RnMDGosaxigGfJBA"},{url:"/_next/static/chunks/app/layout-bd0bfbfdb401e15f.js",revision:"DW0c5RnMDGosaxigGfJBA"},{url:"/_next/static/chunks/app/login/page-fcbddca77bc41b81.js",revision:"DW0c5RnMDGosaxigGfJBA"},{url:"/_next/static/chunks/app/page-6a58e37ab3250691.js",revision:"DW0c5RnMDGosaxigGfJBA"},{url:"/_next/static/chunks/app/play/page-49382538d6f6adc3.js",revision:"DW0c5RnMDGosaxigGfJBA"},{url:"/_next/static/chunks/app/search/page-63fe30b91e0539a7.js",revision:"DW0c5RnMDGosaxigGfJBA"},{url:"/_next/static/chunks/app/warning/page-11cba4cf9332a238.js",revision:"DW0c5RnMDGosaxigGfJBA"},{url:"/_next/static/chunks/c72274ce-06682d6fc8197e6d.js",revision:"DW0c5RnMDGosaxigGfJBA"},{url:"/_next/static/chunks/da9543df-bf6da1a431d8604f.js",revision:"DW0c5RnMDGosaxigGfJBA"},{url:"/_next/static/chunks/framework-6e06c675866dc992.js",revision:"DW0c5RnMDGosaxigGfJBA"},{url:"/_next/static/chunks/main-95de9e33689c098a.js",revision:"DW0c5RnMDGosaxigGfJBA"},{url:"/_next/static/chunks/main-app-dbd320e104e1a5dc.js",revision:"DW0c5RnMDGosaxigGfJBA"},{url:"/_next/static/chunks/pages/_app-792b631a362c29e1.js",revision:"DW0c5RnMDGosaxigGfJBA"},{url:"/_next/static/chunks/pages/_error-9fde6601392a2a99.js",revision:"DW0c5RnMDGosaxigGfJBA"},{url:"/_next/static/chunks/polyfills-42372ed130431b0a.js",revision:"846118c33b2c0e922d7b3a7676f81f6f"},{url:"/_next/static/chunks/webpack-17170f1d90853b2d.js",revision:"DW0c5RnMDGosaxigGfJBA"},{url:"/_next/static/css/23100062f5d4aac0.css",revision:"23100062f5d4aac0"},{url:"/_next/static/css/275ed64cc4367444.css",revision:"275ed64cc4367444"},{url:"/_next/static/css/38baf1075069f639.css",revision:"38baf1075069f639"},{url:"/_next/static/media/26a46d62cd723877-s.woff2",revision:"befd9c0fdfa3d8a645d5f95717ed6420"},{url:"/_next/static/media/55c55f0601d81cf3-s.woff2",revision:"43828e14271c77b87e3ed582dbff9f74"},{url:"/_next/static/media/581909926a08bbc8-s.woff2",revision:"f0b86e7c24f455280b8df606b89af891"},{url:"/_next/static/media/8e9860b6e62d6359-s.woff2",revision:"01ba6c2a184b8cba08b0d57167664d75"},{url:"/_next/static/media/97e0cb1ae144a2a9-s.woff2",revision:"e360c61c5bd8d90639fd4503c829c2dc"},{url:"/_next/static/media/df0a9ae256c0569c-s.woff2",revision:"d54db44de5ccb18886ece2fda72bdfe0"},{url:"/_next/static/media/e4af272ccee01ff0-s.p.woff2",revision:"65850a373e258f1c897a2b3d75eb74de"},{url:"/favicon.ico",revision:"c5de6e56c5664adda146825f75ea6ecf"},{url:"/icons/icon-192x192.png",revision:"4a56c090828a1ad254c903c7aec0389d"},{url:"/icons/icon-256x256.png",revision:"f6409eb1a001f754121e3a8281c0319c"},{url:"/icons/icon-384x384.png",revision:"f6efc3e357b9ffdf4e0d8c14b2ed0ac1"},{url:"/icons/icon-512x512.png",revision:"9c008cbbeb6a576fe07bb1284a83f4d2"},{url:"/logo.png",revision:"40de611b143c47c6291c7bdad2c959ca"},{url:"/manifest.json",revision:"7bd3dabc1cfbfe40f09577efca223d31"},{url:"/robots.txt",revision:"e2b2cd8514443456bc6fb9d77b3b1f3e"},{url:"/screenshot1.png",revision:"10572bfcea54dc93ac4c5f7c9057fc98"},{url:"/screenshot2.png",revision:"f815a8990973a221899976867365c239"},{url:"/screenshot3.png",revision:"49709e96345dfeeab1d8083821d4b44e"},{url:"/screenshot4.png",revision:"a76c751e41e37556048a487e4f8b8b1c"},{url:"/wechat.jpg",revision:"d0f601311802667cd6ca5a37dc69bfa7"}],{ignoreURLParametersMatching:[]}),e.cleanupOutdatedCaches(),e.registerRoute("/",new e.NetworkFirst({cacheName:"start-url",plugins:[{cacheWillUpdate:async({request:e,response:s,event:n,state:c})=>s&&"opaqueredirect"===s.type?new Response(s.body,{status:200,statusText:"OK",headers:s.headers}):s}]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:gstatic)\.com\/.*/i,new e.CacheFirst({cacheName:"google-fonts-webfonts",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:31536e3})]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:googleapis)\.com\/.*/i,new e.StaleWhileRevalidate({cacheName:"google-fonts-stylesheets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:eot|otf|ttc|ttf|woff|woff2|font.css)$/i,new e.StaleWhileRevalidate({cacheName:"static-font-assets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:jpg|jpeg|gif|png|svg|ico|webp)$/i,new e.StaleWhileRevalidate({cacheName:"static-image-assets",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/_next\/image\?url=.+$/i,new e.StaleWhileRevalidate({cacheName:"next-image",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:mp3|wav|ogg)$/i,new e.CacheFirst({cacheName:"static-audio-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:mp4)$/i,new e.CacheFirst({cacheName:"static-video-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:js)$/i,new e.StaleWhileRevalidate({cacheName:"static-js-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:css|less)$/i,new e.StaleWhileRevalidate({cacheName:"static-style-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/_next\/data\/.+\/.+\.json$/i,new e.StaleWhileRevalidate({cacheName:"next-data",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:json|xml|csv)$/i,new e.NetworkFirst({cacheName:"static-data-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({url:e})=>{if(!(self.origin===e.origin))return!1;const s=e.pathname;return!s.startsWith("/api/auth/")&&!!s.startsWith("/api/")},new e.NetworkFirst({cacheName:"apis",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:16,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({url:e})=>{if(!(self.origin===e.origin))return!1;return!e.pathname.startsWith("/api/")},new e.NetworkFirst({cacheName:"others",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({url:e})=>!(self.origin===e.origin),new e.NetworkFirst({cacheName:"cross-origin",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:3600})]}),"GET")}); diff --git a/scripts/test-docker-compatibility.js b/scripts/test-docker-compatibility.js index ffe82b9..b5b90c8 100644 --- a/scripts/test-docker-compatibility.js +++ b/scripts/test-docker-compatibility.js @@ -5,6 +5,9 @@ * 模拟 Docker 构建过程中的 Edge Runtime 转换 */ +/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-disable no-console */ + const fs = require('fs'); const path = require('path'); diff --git a/src/app/play/page.tsx b/src/app/play/page.tsx index e92a549..d0e51e4 100644 --- a/src/app/play/page.tsx +++ b/src/app/play/page.tsx @@ -1567,9 +1567,10 @@ function PlayPageClient() { title={videoTitle} artPlayerRef={artPlayerRef} currentTime={currentPlayTime} - _duration={videoDuration} + duration={videoDuration} isSettingMode={isSkipSettingMode} onSettingModeChange={setIsSkipSettingMode} + onNextEpisode={handleNextEpisode} /> )} diff --git a/src/components/SkipController.tsx b/src/components/SkipController.tsx index 630bba0..8f2011c 100644 --- a/src/components/SkipController.tsx +++ b/src/components/SkipController.tsx @@ -17,9 +17,10 @@ interface SkipControllerProps { title: string; artPlayerRef: React.MutableRefObject; currentTime?: number; - _duration?: number; // 使用下划线前缀标识未使用的参数 + duration?: number; isSettingMode?: boolean; onSettingModeChange?: (isOpen: boolean) => void; + onNextEpisode?: () => void; // 新增:跳转下一集的回调 } export default function SkipController({ @@ -28,17 +29,57 @@ export default function SkipController({ title, artPlayerRef, currentTime = 0, - _duration = 0, + duration = 0, isSettingMode = false, onSettingModeChange, + onNextEpisode, }: SkipControllerProps) { const [skipConfig, setSkipConfig] = useState(null); const [showSkipButton, setShowSkipButton] = useState(false); const [currentSkipSegment, setCurrentSkipSegment] = useState(null); const [newSegment, setNewSegment] = useState>({}); + + // 新增状态:批量设置模式 - 支持分:秒格式 + const [batchSettings, setBatchSettings] = useState({ + openingStart: '0:00', // 片头开始时间(分:秒格式) + openingEnd: '1:30', // 片头结束时间(分:秒格式,90秒=1分30秒) + endingStart: '20:00', // 片尾开始时间(分:秒格式) + endingEnd: '', // 片尾结束时间(可选,空表示直接跳转下一集) + autoSkip: true, // 自动跳过开关 + autoNextEpisode: true, // 自动下一集开关 + }); + const [showCountdown, setShowCountdown] = useState(false); + const [countdownSeconds, setCountdownSeconds] = useState(0); const lastSkipTimeRef = useRef(0); const skipTimeoutRef = useRef(null); + const autoSkipTimeoutRef = useRef(null); + const countdownIntervalRef = useRef(null); + + // 时间格式转换函数 + const timeToSeconds = useCallback((timeStr: string): number => { + if (!timeStr || timeStr.trim() === '') return 0; + + // 支持多种格式: "2:10", "2:10.5", "130", "130.5" + if (timeStr.includes(':')) { + const parts = timeStr.split(':'); + const minutes = parseInt(parts[0]) || 0; + const seconds = parseFloat(parts[1]) || 0; + return minutes * 60 + seconds; + } else { + return parseFloat(timeStr) || 0; + } + }, []); + + const secondsToTime = useCallback((seconds: number): string => { + const mins = Math.floor(seconds / 60); + const secs = Math.floor(seconds % 60); + const decimal = seconds % 1; + if (decimal > 0) { + return `${mins}:${secs.toString().padStart(2, '0')}.${Math.floor(decimal * 10)}`; + } + return `${mins}:${secs.toString().padStart(2, '0')}`; + }, []); // 加载跳过配置 const loadSkipConfig = useCallback(async () => { @@ -50,6 +91,69 @@ export default function SkipController({ } }, [source, id]); + // 自动跳过逻辑 + const handleAutoSkip = useCallback((segment: SkipSegment) => { + if (!artPlayerRef.current) return; + + const targetTime = segment.end + 1; + artPlayerRef.current.currentTime = targetTime; + lastSkipTimeRef.current = Date.now(); + + // 显示跳过提示 + if (artPlayerRef.current.notice) { + const segmentName = segment.type === 'opening' ? '片头' : '片尾'; + artPlayerRef.current.notice.show = `自动跳过${segmentName}`; + } + + setCurrentSkipSegment(null); + }, [artPlayerRef]); + + // 开始片尾倒计时 + const startEndingCountdown = useCallback((seconds: number) => { + setShowCountdown(true); + setCountdownSeconds(seconds); + + if (countdownIntervalRef.current) { + clearInterval(countdownIntervalRef.current); + } + + countdownIntervalRef.current = setInterval(() => { + setCountdownSeconds(prev => { + if (prev <= 1) { + // 倒计时结束,跳转下一集 + if (onNextEpisode) { + onNextEpisode(); + } + setShowCountdown(false); + if (countdownIntervalRef.current) { + clearInterval(countdownIntervalRef.current); + } + return 0; + } + return prev - 1; + }); + }, 1000); + }, [onNextEpisode]); + + // 检查片尾倒计时 + const checkEndingCountdown = useCallback((time: number) => { + if (!skipConfig?.segments?.length || !duration || !onNextEpisode) return; + + const endingSegments = skipConfig.segments.filter(s => s.type === 'ending' && s.autoNextEpisode !== false); + if (!endingSegments.length) return; + + for (const segment of endingSegments) { + const timeToEnd = duration - time; + const timeToSegmentStart = duration - segment.start; + + // 当距离视频结束的时间等于设定的片尾开始时间时,开始倒计时 + if (timeToEnd <= timeToSegmentStart && timeToEnd > 0 && !showCountdown) { + startEndingCountdown(Math.ceil(timeToEnd)); + break; + } + } + }, [skipConfig, duration, onNextEpisode, showCountdown, startEndingCountdown]); + // 检查当前播放时间是否在跳过区间内 const checkSkipSegment = useCallback( (time: number) => { @@ -61,25 +165,48 @@ export default function SkipController({ if (currentSegment && currentSegment !== currentSkipSegment) { setCurrentSkipSegment(currentSegment); - setShowSkipButton(true); - - // 自动隐藏跳过按钮 - if (skipTimeoutRef.current) { - clearTimeout(skipTimeoutRef.current); + + // 检查是否开启自动跳过 + const hasAutoSkipSetting = skipConfig.segments.some(s => s.autoSkip !== false); + + if (hasAutoSkipSetting) { + // 自动跳过:延迟1秒执行跳过 + if (autoSkipTimeoutRef.current) { + clearTimeout(autoSkipTimeoutRef.current); + } + autoSkipTimeoutRef.current = setTimeout(() => { + handleAutoSkip(currentSegment); + }, 1000); + + setShowSkipButton(false); // 自动跳过时不显示按钮 + } else { + // 手动模式:显示跳过按钮 + setShowSkipButton(true); + + // 自动隐藏跳过按钮 + if (skipTimeoutRef.current) { + clearTimeout(skipTimeoutRef.current); + } + skipTimeoutRef.current = setTimeout(() => { + setShowSkipButton(false); + setCurrentSkipSegment(null); + }, 8000); } - skipTimeoutRef.current = setTimeout(() => { - setShowSkipButton(false); - setCurrentSkipSegment(null); - }, 8000); // 8秒后自动隐藏 } else if (!currentSegment && currentSkipSegment) { setCurrentSkipSegment(null); setShowSkipButton(false); if (skipTimeoutRef.current) { clearTimeout(skipTimeoutRef.current); } + if (autoSkipTimeoutRef.current) { + clearTimeout(autoSkipTimeoutRef.current); + } } + + // 检查片尾倒计时 + checkEndingCountdown(time); }, - [skipConfig, currentSkipSegment] + [skipConfig, currentSkipSegment, handleAutoSkip, checkEndingCountdown] ); // 执行跳过 @@ -104,7 +231,7 @@ export default function SkipController({ } }, [currentSkipSegment, artPlayerRef]); - // 保存新的跳过片段 + // 保存新的跳过片段(单个片段模式) const handleSaveSegment = useCallback(async () => { if (!newSegment.start || !newSegment.end || !newSegment.type) { alert('请填写完整的跳过片段信息'); @@ -122,6 +249,8 @@ export default function SkipController({ end: newSegment.end, type: newSegment.type as 'opening' | 'ending', title: newSegment.title || (newSegment.type === 'opening' ? '片头' : '片尾'), + autoSkip: true, // 默认开启自动跳过 + autoNextEpisode: newSegment.type === 'ending', // 片尾默认开启自动下一集 }; const updatedConfig: EpisodeSkipConfig = { @@ -144,6 +273,98 @@ export default function SkipController({ } }, [newSegment, skipConfig, source, id, title, onSettingModeChange]); + // 保存批量设置的跳过配置 + const handleSaveBatchSettings = useCallback(async () => { + const segments: SkipSegment[] = []; + + // 添加片头设置 + if (batchSettings.openingStart && batchSettings.openingEnd) { + const start = timeToSeconds(batchSettings.openingStart); + const end = timeToSeconds(batchSettings.openingEnd); + + if (start >= end) { + alert('片头开始时间必须小于结束时间'); + return; + } + + segments.push({ + start, + end, + type: 'opening', + title: '片头', + autoSkip: batchSettings.autoSkip, + }); + } + + // 添加片尾设置 + if (batchSettings.endingStart) { + const endingStartSeconds = timeToSeconds(batchSettings.endingStart); + + // 如果没有设置结束时间,则直接跳转到下一集 + if (!batchSettings.endingEnd || batchSettings.endingEnd.trim() === '') { + // 直接从指定时间跳转下一集 + segments.push({ + start: endingStartSeconds, + end: duration, // 设置为视频总长度 + type: 'ending', + title: '片尾跳转下一集', + autoSkip: batchSettings.autoSkip, + autoNextEpisode: batchSettings.autoNextEpisode, + }); + } else { + const endingEndSeconds = timeToSeconds(batchSettings.endingEnd); + + if (endingStartSeconds >= endingEndSeconds) { + alert('片尾开始时间必须小于结束时间'); + return; + } + + segments.push({ + start: endingStartSeconds, + end: endingEndSeconds, + type: 'ending', + title: '片尾', + autoSkip: batchSettings.autoSkip, + autoNextEpisode: batchSettings.autoNextEpisode, + }); + } + } + + if (segments.length === 0) { + alert('请至少设置片头或片尾时间'); + return; + } + + try { + const updatedConfig: EpisodeSkipConfig = { + source, + id, + title, + segments, + updated_time: Date.now(), + }; + + await saveSkipConfig(source, id, updatedConfig); + setSkipConfig(updatedConfig); + onSettingModeChange?.(false); + + // 重置批量设置 + setBatchSettings({ + openingStart: '0:00', + openingEnd: '1:30', + endingStart: '20:00', + endingEnd: '', + autoSkip: true, + autoNextEpisode: true, + }); + + alert('跳过配置已保存'); + } catch (err) { + console.error('保存跳过配置失败:', err); + alert('保存失败,请重试'); + } + }, [batchSettings, duration, source, id, title, onSettingModeChange, timeToSeconds]); + // 删除跳过片段 const handleDeleteSegment = useCallback( async (index: number) => { @@ -201,11 +422,42 @@ export default function SkipController({ if (skipTimeoutRef.current) { clearTimeout(skipTimeoutRef.current); } + if (autoSkipTimeoutRef.current) { + clearTimeout(autoSkipTimeoutRef.current); + } + if (countdownIntervalRef.current) { + clearInterval(countdownIntervalRef.current); + } }; }, []); return (
+ {/* 倒计时显示 - 片尾自动跳转下一集 */} + {showCountdown && ( +
+
+ + + + + {countdownSeconds}秒后自动播放下一集 + + +
+
+ )} + {/* 跳过按钮 */} {showSkipButton && currentSkipSegment && (
@@ -223,119 +475,268 @@ export default function SkipController({
)} - {/* 设置模式面板 */} + {/* 设置模式面板 - 增强版批量设置 */} {isSettingMode && (
-
+

- 添加跳过片段 + 智能跳过设置

-
-
-
)} diff --git a/src/lib/db.client.ts b/src/lib/db.client.ts index 626a110..0662ab5 100644 --- a/src/lib/db.client.ts +++ b/src/lib/db.client.ts @@ -47,6 +47,8 @@ export interface SkipSegment { end: number; // 结束时间(秒) type: 'opening' | 'ending'; // 片头或片尾 title?: string; // 可选的描述 + autoSkip?: boolean; // 是否自动跳过(默认true) + autoNextEpisode?: boolean; // 片尾是否自动跳转下一集(默认true,仅对ending类型有效) } export interface EpisodeSkipConfig {