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.
This commit is contained in:
@@ -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_play_records_username ON play_records(username);
|
||||||
CREATE INDEX IF NOT EXISTS idx_favorites_username ON favorites(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_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_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);
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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. **测试验证**: 设置后可以快进到设定时间测试效果
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
🎉 **享受更流畅的观影体验!**
|
||||||
+1
-1
@@ -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")});
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
* 模拟 Docker 构建过程中的 Edge Runtime 转换
|
* 模拟 Docker 构建过程中的 Edge Runtime 转换
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
/* eslint-disable no-console */
|
||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
|
|||||||
@@ -1567,9 +1567,10 @@ function PlayPageClient() {
|
|||||||
title={videoTitle}
|
title={videoTitle}
|
||||||
artPlayerRef={artPlayerRef}
|
artPlayerRef={artPlayerRef}
|
||||||
currentTime={currentPlayTime}
|
currentTime={currentPlayTime}
|
||||||
_duration={videoDuration}
|
duration={videoDuration}
|
||||||
isSettingMode={isSkipSettingMode}
|
isSettingMode={isSkipSettingMode}
|
||||||
onSettingModeChange={setIsSkipSettingMode}
|
onSettingModeChange={setIsSkipSettingMode}
|
||||||
|
onNextEpisode={handleNextEpisode}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -17,9 +17,10 @@ interface SkipControllerProps {
|
|||||||
title: string;
|
title: string;
|
||||||
artPlayerRef: React.MutableRefObject<any>;
|
artPlayerRef: React.MutableRefObject<any>;
|
||||||
currentTime?: number;
|
currentTime?: number;
|
||||||
_duration?: number; // 使用下划线前缀标识未使用的参数
|
duration?: number;
|
||||||
isSettingMode?: boolean;
|
isSettingMode?: boolean;
|
||||||
onSettingModeChange?: (isOpen: boolean) => void;
|
onSettingModeChange?: (isOpen: boolean) => void;
|
||||||
|
onNextEpisode?: () => void; // 新增:跳转下一集的回调
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SkipController({
|
export default function SkipController({
|
||||||
@@ -28,17 +29,57 @@ export default function SkipController({
|
|||||||
title,
|
title,
|
||||||
artPlayerRef,
|
artPlayerRef,
|
||||||
currentTime = 0,
|
currentTime = 0,
|
||||||
_duration = 0,
|
duration = 0,
|
||||||
isSettingMode = false,
|
isSettingMode = false,
|
||||||
onSettingModeChange,
|
onSettingModeChange,
|
||||||
|
onNextEpisode,
|
||||||
}: SkipControllerProps) {
|
}: SkipControllerProps) {
|
||||||
const [skipConfig, setSkipConfig] = useState<EpisodeSkipConfig | null>(null);
|
const [skipConfig, setSkipConfig] = useState<EpisodeSkipConfig | null>(null);
|
||||||
const [showSkipButton, setShowSkipButton] = useState(false);
|
const [showSkipButton, setShowSkipButton] = useState(false);
|
||||||
const [currentSkipSegment, setCurrentSkipSegment] = useState<SkipSegment | null>(null);
|
const [currentSkipSegment, setCurrentSkipSegment] = useState<SkipSegment | null>(null);
|
||||||
const [newSegment, setNewSegment] = useState<Partial<SkipSegment>>({});
|
const [newSegment, setNewSegment] = useState<Partial<SkipSegment>>({});
|
||||||
|
|
||||||
|
// 新增状态:批量设置模式 - 支持分:秒格式
|
||||||
|
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<number>(0);
|
const lastSkipTimeRef = useRef<number>(0);
|
||||||
const skipTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const skipTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const autoSkipTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const countdownIntervalRef = useRef<NodeJS.Timeout | null>(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 () => {
|
const loadSkipConfig = useCallback(async () => {
|
||||||
@@ -50,6 +91,69 @@ export default function SkipController({
|
|||||||
}
|
}
|
||||||
}, [source, id]);
|
}, [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(
|
const checkSkipSegment = useCallback(
|
||||||
(time: number) => {
|
(time: number) => {
|
||||||
@@ -61,25 +165,48 @@ export default function SkipController({
|
|||||||
|
|
||||||
if (currentSegment && currentSegment !== currentSkipSegment) {
|
if (currentSegment && currentSegment !== currentSkipSegment) {
|
||||||
setCurrentSkipSegment(currentSegment);
|
setCurrentSkipSegment(currentSegment);
|
||||||
setShowSkipButton(true);
|
|
||||||
|
// 检查是否开启自动跳过
|
||||||
// 自动隐藏跳过按钮
|
const hasAutoSkipSetting = skipConfig.segments.some(s => s.autoSkip !== false);
|
||||||
if (skipTimeoutRef.current) {
|
|
||||||
clearTimeout(skipTimeoutRef.current);
|
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) {
|
} else if (!currentSegment && currentSkipSegment) {
|
||||||
setCurrentSkipSegment(null);
|
setCurrentSkipSegment(null);
|
||||||
setShowSkipButton(false);
|
setShowSkipButton(false);
|
||||||
if (skipTimeoutRef.current) {
|
if (skipTimeoutRef.current) {
|
||||||
clearTimeout(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]);
|
}, [currentSkipSegment, artPlayerRef]);
|
||||||
|
|
||||||
// 保存新的跳过片段
|
// 保存新的跳过片段(单个片段模式)
|
||||||
const handleSaveSegment = useCallback(async () => {
|
const handleSaveSegment = useCallback(async () => {
|
||||||
if (!newSegment.start || !newSegment.end || !newSegment.type) {
|
if (!newSegment.start || !newSegment.end || !newSegment.type) {
|
||||||
alert('请填写完整的跳过片段信息');
|
alert('请填写完整的跳过片段信息');
|
||||||
@@ -122,6 +249,8 @@ export default function SkipController({
|
|||||||
end: newSegment.end,
|
end: newSegment.end,
|
||||||
type: newSegment.type as 'opening' | 'ending',
|
type: newSegment.type as 'opening' | 'ending',
|
||||||
title: newSegment.title || (newSegment.type === 'opening' ? '片头' : '片尾'),
|
title: newSegment.title || (newSegment.type === 'opening' ? '片头' : '片尾'),
|
||||||
|
autoSkip: true, // 默认开启自动跳过
|
||||||
|
autoNextEpisode: newSegment.type === 'ending', // 片尾默认开启自动下一集
|
||||||
};
|
};
|
||||||
|
|
||||||
const updatedConfig: EpisodeSkipConfig = {
|
const updatedConfig: EpisodeSkipConfig = {
|
||||||
@@ -144,6 +273,98 @@ export default function SkipController({
|
|||||||
}
|
}
|
||||||
}, [newSegment, skipConfig, source, id, title, onSettingModeChange]);
|
}, [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(
|
const handleDeleteSegment = useCallback(
|
||||||
async (index: number) => {
|
async (index: number) => {
|
||||||
@@ -201,11 +422,42 @@ export default function SkipController({
|
|||||||
if (skipTimeoutRef.current) {
|
if (skipTimeoutRef.current) {
|
||||||
clearTimeout(skipTimeoutRef.current);
|
clearTimeout(skipTimeoutRef.current);
|
||||||
}
|
}
|
||||||
|
if (autoSkipTimeoutRef.current) {
|
||||||
|
clearTimeout(autoSkipTimeoutRef.current);
|
||||||
|
}
|
||||||
|
if (countdownIntervalRef.current) {
|
||||||
|
clearInterval(countdownIntervalRef.current);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="skip-controller">
|
<div className="skip-controller">
|
||||||
|
{/* 倒计时显示 - 片尾自动跳转下一集 */}
|
||||||
|
{showCountdown && (
|
||||||
|
<div className="fixed top-20 left-1/2 transform -translate-x-1/2 z-50 bg-blue-600/90 text-white px-6 py-3 rounded-lg backdrop-blur-sm border border-white/20 shadow-lg animate-fade-in">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<svg className="w-5 h-5 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{countdownSeconds}秒后自动播放下一集
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setShowCountdown(false);
|
||||||
|
if (countdownIntervalRef.current) {
|
||||||
|
clearInterval(countdownIntervalRef.current);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="px-2 py-1 bg-white/20 hover:bg-white/30 rounded text-xs transition-colors"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 跳过按钮 */}
|
{/* 跳过按钮 */}
|
||||||
{showSkipButton && currentSkipSegment && (
|
{showSkipButton && currentSkipSegment && (
|
||||||
<div className="fixed top-20 right-4 z-50 bg-black/80 text-white px-4 py-2 rounded-lg backdrop-blur-sm border border-white/20 shadow-lg animate-fade-in">
|
<div className="fixed top-20 right-4 z-50 bg-black/80 text-white px-4 py-2 rounded-lg backdrop-blur-sm border border-white/20 shadow-lg animate-fade-in">
|
||||||
@@ -223,119 +475,268 @@ export default function SkipController({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 设置模式面板 */}
|
{/* 设置模式面板 - 增强版批量设置 */}
|
||||||
{isSettingMode && (
|
{isSettingMode && (
|
||||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 w-full max-w-md">
|
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 w-full max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||||
<h3 className="text-lg font-semibold mb-4 text-gray-900 dark:text-gray-100">
|
<h3 className="text-lg font-semibold mb-4 text-gray-900 dark:text-gray-100">
|
||||||
添加跳过片段
|
智能跳过设置
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div className="space-y-4">
|
{/* 全局开关 */}
|
||||||
<div>
|
<div className="bg-blue-50 dark:bg-blue-900/20 p-4 rounded-lg mb-6">
|
||||||
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">
|
<div className="flex items-center justify-between mb-2">
|
||||||
类型
|
<label className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={batchSettings.autoSkip}
|
||||||
|
onChange={(e) => setBatchSettings({...batchSettings, autoSkip: e.target.checked})}
|
||||||
|
className="rounded"
|
||||||
|
/>
|
||||||
|
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
启用自动跳过
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<select
|
</div>
|
||||||
value={newSegment.type || ''}
|
<div className="flex items-center justify-between">
|
||||||
onChange={(e) => setNewSegment({ ...newSegment, type: e.target.value as 'opening' | 'ending' })}
|
<label className="flex items-center space-x-2">
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
|
<input
|
||||||
>
|
type="checkbox"
|
||||||
<option value="">选择类型</option>
|
checked={batchSettings.autoNextEpisode}
|
||||||
<option value="opening">片头</option>
|
onChange={(e) => setBatchSettings({...batchSettings, autoNextEpisode: e.target.checked})}
|
||||||
<option value="ending">片尾</option>
|
className="rounded"
|
||||||
</select>
|
/>
|
||||||
|
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
片尾自动播放下一集
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
|
||||||
|
开启后将自动跳过设定的片头片尾,无需手动点击
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
{/* 片头设置 */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h4 className="font-medium text-gray-900 dark:text-gray-100 border-b pb-2">
|
||||||
|
🎬 片头设置
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">
|
||||||
|
开始时间 (分:秒)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={batchSettings.openingStart}
|
||||||
|
onChange={(e) => setBatchSettings({...batchSettings, openingStart: e.target.value})}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
|
||||||
|
placeholder="0:00"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">格式: 分:秒 (如 0:00)</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">
|
||||||
|
结束时间 (分:秒)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={batchSettings.openingEnd}
|
||||||
|
onChange={(e) => setBatchSettings({...batchSettings, openingEnd: e.target.value})}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
|
||||||
|
placeholder="1:30"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">格式: 分:秒 (如 1:30)</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
{/* 片尾设置 */}
|
||||||
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">
|
<div className="space-y-4">
|
||||||
开始时间 (秒)
|
<h4 className="font-medium text-gray-900 dark:text-gray-100 border-b pb-2">
|
||||||
</label>
|
🎭 片尾设置
|
||||||
<input
|
</h4>
|
||||||
type="number"
|
|
||||||
value={newSegment.start || ''}
|
<div>
|
||||||
onChange={(e) => setNewSegment({ ...newSegment, start: parseFloat(e.target.value) })}
|
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
|
开始时间 (分:秒)
|
||||||
placeholder="例如: 0"
|
</label>
|
||||||
/>
|
<input
|
||||||
</div>
|
type="text"
|
||||||
|
value={batchSettings.endingStart}
|
||||||
|
onChange={(e) => setBatchSettings({...batchSettings, endingStart: e.target.value})}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
|
||||||
|
placeholder="20:00"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">从此时间开始检测片尾</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">
|
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">
|
||||||
结束时间 (秒)
|
结束时间 (分:秒) - 可选
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="text"
|
||||||
value={newSegment.end || ''}
|
value={batchSettings.endingEnd}
|
||||||
onChange={(e) => setNewSegment({ ...newSegment, end: parseFloat(e.target.value) })}
|
onChange={(e) => setBatchSettings({...batchSettings, endingEnd: e.target.value})}
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
|
||||||
placeholder="例如: 90"
|
placeholder="留空直接跳下一集"
|
||||||
/>
|
/>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">空白=直接跳下一集</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div className="mt-6 p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||||||
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">
|
<div className="text-sm text-gray-600 dark:text-gray-400 space-y-1">
|
||||||
描述 (可选)
|
<p><strong>当前播放时间:</strong> {secondsToTime(currentTime)}</p>
|
||||||
</label>
|
{duration > 0 && (
|
||||||
<input
|
<p><strong>视频总长度:</strong> {secondsToTime(duration)}</p>
|
||||||
type="text"
|
)}
|
||||||
value={newSegment.title || ''}
|
<div className="text-xs mt-2 text-gray-500 space-y-1">
|
||||||
onChange={(e) => setNewSegment({ ...newSegment, title: e.target.value })}
|
<p>💡 <strong>片头示例:</strong> 从 0:00 自动跳到 1:30</p>
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
|
<p>💡 <strong>片尾示例:</strong> 从 20:00 开始倒计时,自动跳下一集</p>
|
||||||
placeholder="例如: 片头曲"
|
<p>💡 支持格式: 1:30 (1分30秒) 或 90 (90秒)</p>
|
||||||
/>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
当前播放时间: {formatTime(currentTime)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex space-x-3 mt-6">
|
<div className="flex space-x-3 mt-6">
|
||||||
<button
|
<button
|
||||||
onClick={handleSaveSegment}
|
onClick={handleSaveBatchSettings}
|
||||||
className="flex-1 px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded font-medium transition-colors"
|
className="flex-1 px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded font-medium transition-colors"
|
||||||
>
|
>
|
||||||
保存
|
保存智能配置
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onSettingModeChange?.(false);
|
onSettingModeChange?.(false);
|
||||||
setNewSegment({});
|
setBatchSettings({
|
||||||
|
openingStart: '0:00',
|
||||||
|
openingEnd: '1:30',
|
||||||
|
endingStart: '20:00',
|
||||||
|
endingEnd: '',
|
||||||
|
autoSkip: true,
|
||||||
|
autoNextEpisode: true,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
className="flex-1 px-4 py-2 bg-gray-500 hover:bg-gray-600 text-white rounded font-medium transition-colors"
|
className="flex-1 px-4 py-2 bg-gray-500 hover:bg-gray-600 text-white rounded font-medium transition-colors"
|
||||||
>
|
>
|
||||||
取消
|
取消
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 分割线 */}
|
||||||
|
<div className="my-6 border-t border-gray-200 dark:border-gray-600"></div>
|
||||||
|
|
||||||
|
{/* 传统单个设置模式 */}
|
||||||
|
<details className="mb-4">
|
||||||
|
<summary className="cursor-pointer text-sm font-medium text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200">
|
||||||
|
高级设置:添加单个片段
|
||||||
|
</summary>
|
||||||
|
<div className="mt-4 space-y-4 pl-4 border-l-2 border-gray-200 dark:border-gray-600">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">
|
||||||
|
类型
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={newSegment.type || ''}
|
||||||
|
onChange={(e) => setNewSegment({ ...newSegment, type: e.target.value as 'opening' | 'ending' })}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
|
||||||
|
>
|
||||||
|
<option value="">选择类型</option>
|
||||||
|
<option value="opening">片头</option>
|
||||||
|
<option value="ending">片尾</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">
|
||||||
|
开始时间 (秒)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={newSegment.start || ''}
|
||||||
|
onChange={(e) => setNewSegment({ ...newSegment, start: parseFloat(e.target.value) })}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">
|
||||||
|
结束时间 (秒)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={newSegment.end || ''}
|
||||||
|
onChange={(e) => setNewSegment({ ...newSegment, end: parseFloat(e.target.value) })}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleSaveSegment}
|
||||||
|
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm font-medium transition-colors"
|
||||||
|
>
|
||||||
|
添加片段
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 管理已有片段 */}
|
{/* 管理已有片段 - 优化布局避免重叠 */}
|
||||||
{skipConfig && skipConfig.segments && skipConfig.segments.length > 0 && !isSettingMode && (
|
{skipConfig && skipConfig.segments && skipConfig.segments.length > 0 && !isSettingMode && (
|
||||||
<div className="mt-4 p-4 bg-gray-100 dark:bg-gray-800 rounded-lg">
|
<div className="fixed bottom-4 right-4 z-40 max-w-sm bg-white/95 dark:bg-gray-800/95 backdrop-blur-sm rounded-lg shadow-lg border border-gray-200 dark:border-gray-600 animate-fade-in">
|
||||||
<h4 className="font-medium mb-2 text-gray-900 dark:text-gray-100">
|
<div className="p-3">
|
||||||
已设置的跳过片段:
|
<h4 className="font-medium mb-2 text-gray-900 dark:text-gray-100 text-sm flex items-center">
|
||||||
</h4>
|
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<div className="space-y-2">
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 5l7 7-7 7M5 5l7 7-7 7" />
|
||||||
{skipConfig.segments.map((segment, index) => (
|
</svg>
|
||||||
<div
|
跳过配置
|
||||||
key={index}
|
</h4>
|
||||||
className="flex items-center justify-between p-2 bg-white dark:bg-gray-700 rounded text-sm"
|
<div className="space-y-1">
|
||||||
>
|
{skipConfig.segments.map((segment, index) => (
|
||||||
<span className="text-gray-900 dark:text-gray-100">
|
<div
|
||||||
{segment.type === 'opening' ? '片头' : '片尾'}: {formatTime(segment.start)} - {formatTime(segment.end)}
|
key={index}
|
||||||
{segment.title && ` (${segment.title})`}
|
className="flex items-center justify-between p-2 bg-gray-50 dark:bg-gray-700 rounded text-xs"
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
onClick={() => handleDeleteSegment(index)}
|
|
||||||
className="px-2 py-1 bg-red-500 hover:bg-red-600 text-white rounded text-xs transition-colors"
|
|
||||||
>
|
>
|
||||||
删除
|
<span className="text-gray-800 dark:text-gray-200 flex-1 mr-2">
|
||||||
</button>
|
<span className="font-medium">
|
||||||
</div>
|
{segment.type === 'opening' ? '🎬片头' : '🎭片尾'}
|
||||||
))}
|
</span>
|
||||||
|
<br />
|
||||||
|
<span className="text-gray-600 dark:text-gray-400">
|
||||||
|
{formatTime(segment.start)} - {formatTime(segment.end)}
|
||||||
|
</span>
|
||||||
|
{segment.autoSkip && (
|
||||||
|
<span className="ml-1 px-1 bg-green-100 dark:bg-green-900 text-green-600 dark:text-green-400 rounded text-xs">
|
||||||
|
自动
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDeleteSegment(index)}
|
||||||
|
className="px-1.5 py-0.5 bg-red-500 hover:bg-red-600 text-white rounded text-xs transition-colors flex-shrink-0"
|
||||||
|
title="删除"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 pt-2 border-t border-gray-200 dark:border-gray-600">
|
||||||
|
<button
|
||||||
|
onClick={() => onSettingModeChange?.(true)}
|
||||||
|
className="w-full px-2 py-1 bg-blue-100 hover:bg-blue-200 dark:bg-blue-900 dark:hover:bg-blue-800 text-blue-700 dark:text-blue-300 rounded text-xs transition-colors"
|
||||||
|
>
|
||||||
|
修改配置
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ export interface SkipSegment {
|
|||||||
end: number; // 结束时间(秒)
|
end: number; // 结束时间(秒)
|
||||||
type: 'opening' | 'ending'; // 片头或片尾
|
type: 'opening' | 'ending'; // 片头或片尾
|
||||||
title?: string; // 可选的描述
|
title?: string; // 可选的描述
|
||||||
|
autoSkip?: boolean; // 是否自动跳过(默认true)
|
||||||
|
autoNextEpisode?: boolean; // 片尾是否自动跳转下一集(默认true,仅对ending类型有效)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EpisodeSkipConfig {
|
export interface EpisodeSkipConfig {
|
||||||
|
|||||||
Reference in New Issue
Block a user