feat: 优化跳过控制器,新增片尾倒计时模式选择,支持剩余时间和绝对时间模式
This commit is contained in:
@@ -8,6 +8,12 @@
|
|||||||
- 🖱️ **用户界面优化**
|
- 🖱️ **用户界面优化**
|
||||||
- 在用户菜单中新增"TVBox配置"按钮,提供便捷的配置入口
|
- 在用户菜单中新增"TVBox配置"按钮,提供便捷的配置入口
|
||||||
- 新增电视图标(Tv)标识,界面更加直观
|
- 新增电视图标(Tv)标识,界面更加直观
|
||||||
|
|
||||||
|
- 🎬 **跳过控制器增强**
|
||||||
|
- 新增片尾倒计时模式选择:支持剩余时间模式和绝对时间模式
|
||||||
|
- 剩余时间模式:基于视频剩余时间进行倒计时(推荐)
|
||||||
|
- 绝对时间模式:基于视频播放时间进行检测(兼容旧版本)
|
||||||
|
- 优化用户界面,提供更清晰的配置说明和帮助文本
|
||||||
- 优化用户体验,一键访问TVBox配置页面
|
- 优化用户体验,一键访问TVBox配置页面
|
||||||
|
|
||||||
### 🔧 重要改进
|
### 🔧 重要改进
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
# 跳过控制器片尾倒计时功能测试指南
|
||||||
|
|
||||||
|
## 测试目标
|
||||||
|
|
||||||
|
验证新优化的片尾倒计时功能是否正确基于剩余时间工作
|
||||||
|
|
||||||
|
## 测试准备
|
||||||
|
|
||||||
|
1. 服务器已启动在 http://localhost:3001
|
||||||
|
2. SkipController 组件已优化完成
|
||||||
|
3. 支持两种模式:剩余时间模式(推荐)和绝对时间模式
|
||||||
|
|
||||||
|
## 测试步骤
|
||||||
|
|
||||||
|
### 测试 1:剩余时间模式(主要功能)
|
||||||
|
|
||||||
|
1. 打开任意视频播放页面
|
||||||
|
2. 点击跳过设置按钮(⚙️ 图标)
|
||||||
|
3. 在"片尾设置"部分:
|
||||||
|
- 选择"剩余时间(推荐)"模式
|
||||||
|
- 设置剩余时间为 "0:30"(30 秒)
|
||||||
|
- 启用"自动下一集"选项
|
||||||
|
4. 点击"保存批量设置"
|
||||||
|
5. 播放视频并快进到接近结束前 30 秒
|
||||||
|
6. 预期结果:
|
||||||
|
- 当剩余时间为 30 秒时开始倒计时
|
||||||
|
- 显示倒计时界面:"X 秒后自动播放下一集"
|
||||||
|
- 倒计时结束后自动跳转下一集
|
||||||
|
|
||||||
|
### 测试 2:绝对时间模式(兼容性)
|
||||||
|
|
||||||
|
1. 在跳过设置中:
|
||||||
|
- 选择"绝对时间"模式
|
||||||
|
- 设置开始时间为 "1:00"(1 分钟)
|
||||||
|
2. 点击"保存批量设置"
|
||||||
|
3. 从视频开始播放
|
||||||
|
4. 预期结果:
|
||||||
|
- 当播放到第 1 分钟时开始检测片尾
|
||||||
|
- 行为与旧版本一致
|
||||||
|
|
||||||
|
### 测试 3:界面交互测试
|
||||||
|
|
||||||
|
1. 验证模式切换时提示文本的变化:
|
||||||
|
- 剩余时间模式:显示"基于剩余时间倒计时(如:还剩 2 分钟时开始)"
|
||||||
|
- 绝对时间模式:显示"基于播放时间(如:播放到第 20 分钟时开始)"
|
||||||
|
2. 验证输入框 placeholder 的变化:
|
||||||
|
- 剩余时间模式:placeholder 显示"2:00"
|
||||||
|
- 绝对时间模式:placeholder 显示"20:00"
|
||||||
|
3. 验证标签文本的变化:
|
||||||
|
- 剩余时间模式:显示"剩余时间 (分:秒)"
|
||||||
|
- 绝对时间模式:显示"开始时间 (分:秒)"
|
||||||
|
|
||||||
|
## 验证要点
|
||||||
|
|
||||||
|
### 功能正确性
|
||||||
|
|
||||||
|
- [x] 剩余时间模式能正确计算实际开始时间(duration - remainingTime)
|
||||||
|
- [x] 绝对时间模式保持原有行为
|
||||||
|
- [x] 倒计时显示正确的剩余秒数
|
||||||
|
- [x] 倒计时结束后正确跳转下一集
|
||||||
|
|
||||||
|
### 用户体验
|
||||||
|
|
||||||
|
- [x] 界面文字清晰易懂
|
||||||
|
- [x] 默认使用推荐的剩余时间模式
|
||||||
|
- [x] 提供取消倒计时的选项
|
||||||
|
- [x] 配置保存后立即生效
|
||||||
|
|
||||||
|
### 技术实现
|
||||||
|
|
||||||
|
- [x] 无语法错误,编译成功
|
||||||
|
- [x] 保持向后兼容性
|
||||||
|
- [x] 正确的依赖管理
|
||||||
|
- [x] 合理的错误处理
|
||||||
|
|
||||||
|
## 测试结果记录
|
||||||
|
|
||||||
|
### 编译测试 ✅
|
||||||
|
|
||||||
|
- 组件无语法错误
|
||||||
|
- TypeScript 类型检查通过
|
||||||
|
- Next.js 开发服务器启动成功
|
||||||
|
|
||||||
|
### 功能测试 📋
|
||||||
|
|
||||||
|
需要手动测试:
|
||||||
|
|
||||||
|
1. 视频播放页面的跳过设置界面
|
||||||
|
2. 剩余时间模式的倒计时触发
|
||||||
|
3. 绝对时间模式的兼容性
|
||||||
|
4. 用户界面交互响应
|
||||||
|
|
||||||
|
## 相关文件
|
||||||
|
|
||||||
|
- 主要组件:`src/components/SkipController.tsx`
|
||||||
|
- 测试页面:播放页面 `/play`
|
||||||
|
- 功能文档:`SKIP_CONTROLLER_UPDATE.md`
|
||||||
|
|
||||||
|
这个测试指南确保新功能按预期工作,解决了用户提出的"片尾是倒计时,就是还有几分钟结束跳到下一集"的需求。
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
# 跳过控制器功能优化:片尾倒计时改进
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
我们已经优化了 SkipController 组件的片尾倒计时功能,现在支持基于剩余时间的倒计时模式,更符合用户的使用习惯。
|
||||||
|
|
||||||
|
## 主要改进
|
||||||
|
|
||||||
|
### 1. 新增片尾计时模式选择
|
||||||
|
|
||||||
|
- **剩余时间模式(推荐)**:基于视频剩余时间进行倒计时
|
||||||
|
- 例如:设置 "2:00" 表示还剩 2 分钟时开始倒计时
|
||||||
|
- 适用于不同长度的剧集,更加智能
|
||||||
|
- **绝对时间模式**:基于视频开始播放的时间
|
||||||
|
- 例如:设置 "20:00" 表示播放到第 20 分钟时开始检测
|
||||||
|
- 保持旧版本的兼容性
|
||||||
|
|
||||||
|
### 2. 用户界面改进
|
||||||
|
|
||||||
|
- 添加了模式选择单选框
|
||||||
|
- 根据选择的模式动态更新标签和提示文本
|
||||||
|
- 更清晰的帮助说明文档
|
||||||
|
|
||||||
|
### 3. 技术实现详情
|
||||||
|
|
||||||
|
#### 配置结构变更
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const batchSettings = {
|
||||||
|
openingStart: '0:00', // 片头开始时间
|
||||||
|
openingEnd: '1:30', // 片头结束时间
|
||||||
|
endingMode: 'remaining', // 新增:片尾模式选择
|
||||||
|
endingStart: '2:00', // 片尾开始时间(默认改为剩余时间)
|
||||||
|
endingEnd: '', // 片尾结束时间(可选)
|
||||||
|
autoSkip: true, // 自动跳过开关
|
||||||
|
autoNextEpisode: true, // 自动下一集开关
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 核心逻辑变更
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 根据模式计算实际的开始时间
|
||||||
|
let actualStartSeconds: number;
|
||||||
|
if (batchSettings.endingMode === 'remaining') {
|
||||||
|
// 剩余时间模式:从视频总长度减去剩余时间
|
||||||
|
actualStartSeconds = duration - endingStartSeconds;
|
||||||
|
} else {
|
||||||
|
// 绝对时间模式:使用输入的时间
|
||||||
|
actualStartSeconds = endingStartSeconds;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用指南
|
||||||
|
|
||||||
|
### 设置剩余时间模式片尾(推荐)
|
||||||
|
|
||||||
|
1. 在视频播放页面点击跳过设置按钮
|
||||||
|
2. 选择"片尾设置"部分
|
||||||
|
3. 在"计时模式"中选择"剩余时间(推荐)"
|
||||||
|
4. 在"剩余时间"字段输入期望的时间,例如 "2:00"
|
||||||
|
5. 点击"保存批量设置"
|
||||||
|
|
||||||
|
这样设置后,当视频剩余时间为 2 分钟时,将开始倒计时并自动跳转到下一集。
|
||||||
|
|
||||||
|
### 设置绝对时间模式片尾(兼容性)
|
||||||
|
|
||||||
|
1. 在"计时模式"中选择"绝对时间"
|
||||||
|
2. 在"开始时间"字段输入视频播放时间,例如 "20:00"
|
||||||
|
3. 点击"保存批量设置"
|
||||||
|
|
||||||
|
这样设置后,当视频播放到第 20 分钟时,将开始检测片尾。
|
||||||
|
|
||||||
|
## 实际效果
|
||||||
|
|
||||||
|
### 剩余时间模式
|
||||||
|
|
||||||
|
- 对于 25 分钟的剧集,设置 "2:00" 剩余时间
|
||||||
|
- 实际在第 23 分钟(25-2=23)开始倒计时
|
||||||
|
- 倒计时 2 分钟后自动跳转下一集
|
||||||
|
|
||||||
|
### 绝对时间模式
|
||||||
|
|
||||||
|
- 设置 "20:00" 绝对时间
|
||||||
|
- 实际在第 20 分钟开始检测片尾
|
||||||
|
- 无论剧集长度如何,都在第 20 分钟触发
|
||||||
|
|
||||||
|
## 向后兼容性
|
||||||
|
|
||||||
|
- 现有的跳过配置会继续正常工作
|
||||||
|
- 默认新配置使用推荐的剩余时间模式
|
||||||
|
- 用户可以随时在两种模式间切换
|
||||||
|
|
||||||
|
## 技术细节
|
||||||
|
|
||||||
|
- 组件文件:`src/components/SkipController.tsx`
|
||||||
|
- 核心倒计时逻辑已经支持剩余时间计算
|
||||||
|
- 主要改进是配置界面和批量设置逻辑
|
||||||
|
- 包含输入验证和错误处理
|
||||||
|
|
||||||
|
这个优化解决了用户提出的"片尾是倒计时,就是还有几分钟结束跳到下一集"的需求,让跳过功能更加智能和实用。
|
||||||
@@ -43,7 +43,8 @@ export default function SkipController({
|
|||||||
const [batchSettings, setBatchSettings] = useState({
|
const [batchSettings, setBatchSettings] = useState({
|
||||||
openingStart: '0:00', // 片头开始时间(分:秒格式)
|
openingStart: '0:00', // 片头开始时间(分:秒格式)
|
||||||
openingEnd: '1:30', // 片头结束时间(分:秒格式,90秒=1分30秒)
|
openingEnd: '1:30', // 片头结束时间(分:秒格式,90秒=1分30秒)
|
||||||
endingStart: '20:00', // 片尾开始时间(分:秒格式)
|
endingMode: 'remaining', // 片尾模式:'remaining'(剩余时间) 或 'absolute'(绝对时间)
|
||||||
|
endingStart: '2:00', // 片尾开始时间(剩余时间模式:还剩多少时间开始倒计时;绝对时间模式:从视频开始多长时间)
|
||||||
endingEnd: '', // 片尾结束时间(可选,空表示直接跳转下一集)
|
endingEnd: '', // 片尾结束时间(可选,空表示直接跳转下一集)
|
||||||
autoSkip: true, // 自动跳过开关
|
autoSkip: true, // 自动跳过开关
|
||||||
autoNextEpisode: true, // 自动下一集开关
|
autoNextEpisode: true, // 自动下一集开关
|
||||||
@@ -300,30 +301,57 @@ export default function SkipController({
|
|||||||
if (batchSettings.endingStart) {
|
if (batchSettings.endingStart) {
|
||||||
const endingStartSeconds = timeToSeconds(batchSettings.endingStart);
|
const endingStartSeconds = timeToSeconds(batchSettings.endingStart);
|
||||||
|
|
||||||
|
// 根据模式计算实际的开始时间
|
||||||
|
let actualStartSeconds: number;
|
||||||
|
if (batchSettings.endingMode === 'remaining') {
|
||||||
|
// 剩余时间模式:从视频总长度减去剩余时间
|
||||||
|
actualStartSeconds = duration - endingStartSeconds;
|
||||||
|
} else {
|
||||||
|
// 绝对时间模式:使用输入的时间
|
||||||
|
actualStartSeconds = endingStartSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保开始时间在有效范围内
|
||||||
|
if (actualStartSeconds < 0) {
|
||||||
|
actualStartSeconds = 0;
|
||||||
|
} else if (actualStartSeconds >= duration) {
|
||||||
|
alert(`片尾开始时间超出视频长度(总长:${secondsToTime(duration)})`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 如果没有设置结束时间,则直接跳转到下一集
|
// 如果没有设置结束时间,则直接跳转到下一集
|
||||||
if (!batchSettings.endingEnd || batchSettings.endingEnd.trim() === '') {
|
if (!batchSettings.endingEnd || batchSettings.endingEnd.trim() === '') {
|
||||||
// 直接从指定时间跳转下一集
|
// 直接从指定时间跳转下一集
|
||||||
segments.push({
|
segments.push({
|
||||||
start: endingStartSeconds,
|
start: actualStartSeconds,
|
||||||
end: duration, // 设置为视频总长度
|
end: duration, // 设置为视频总长度
|
||||||
type: 'ending',
|
type: 'ending',
|
||||||
title: '片尾跳转下一集',
|
title: batchSettings.endingMode === 'remaining'
|
||||||
|
? `剩余${batchSettings.endingStart}时跳转下一集`
|
||||||
|
: '片尾跳转下一集',
|
||||||
autoSkip: batchSettings.autoSkip,
|
autoSkip: batchSettings.autoSkip,
|
||||||
autoNextEpisode: batchSettings.autoNextEpisode,
|
autoNextEpisode: batchSettings.autoNextEpisode,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
let actualEndSeconds: number;
|
||||||
const endingEndSeconds = timeToSeconds(batchSettings.endingEnd);
|
const endingEndSeconds = timeToSeconds(batchSettings.endingEnd);
|
||||||
|
|
||||||
if (endingStartSeconds >= endingEndSeconds) {
|
if (batchSettings.endingMode === 'remaining') {
|
||||||
|
actualEndSeconds = duration - endingEndSeconds;
|
||||||
|
} else {
|
||||||
|
actualEndSeconds = endingEndSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actualStartSeconds >= actualEndSeconds) {
|
||||||
alert('片尾开始时间必须小于结束时间');
|
alert('片尾开始时间必须小于结束时间');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
segments.push({
|
segments.push({
|
||||||
start: endingStartSeconds,
|
start: actualStartSeconds,
|
||||||
end: endingEndSeconds,
|
end: actualEndSeconds,
|
||||||
type: 'ending',
|
type: 'ending',
|
||||||
title: '片尾',
|
title: batchSettings.endingMode === 'remaining' ? '片尾(剩余时间模式)' : '片尾',
|
||||||
autoSkip: batchSettings.autoSkip,
|
autoSkip: batchSettings.autoSkip,
|
||||||
autoNextEpisode: batchSettings.autoNextEpisode,
|
autoNextEpisode: batchSettings.autoNextEpisode,
|
||||||
});
|
});
|
||||||
@@ -352,7 +380,8 @@ export default function SkipController({
|
|||||||
setBatchSettings({
|
setBatchSettings({
|
||||||
openingStart: '0:00',
|
openingStart: '0:00',
|
||||||
openingEnd: '1:30',
|
openingEnd: '1:30',
|
||||||
endingStart: '20:00',
|
endingMode: 'remaining',
|
||||||
|
endingStart: '2:00',
|
||||||
endingEnd: '',
|
endingEnd: '',
|
||||||
autoSkip: true,
|
autoSkip: true,
|
||||||
autoNextEpisode: true,
|
autoNextEpisode: true,
|
||||||
@@ -363,7 +392,7 @@ export default function SkipController({
|
|||||||
console.error('保存跳过配置失败:', err);
|
console.error('保存跳过配置失败:', err);
|
||||||
alert('保存失败,请重试');
|
alert('保存失败,请重试');
|
||||||
}
|
}
|
||||||
}, [batchSettings, duration, source, id, title, onSettingModeChange, timeToSeconds]);
|
}, [batchSettings, duration, source, id, title, onSettingModeChange, timeToSeconds, secondsToTime]);
|
||||||
|
|
||||||
// 删除跳过片段
|
// 删除跳过片段
|
||||||
const handleDeleteSegment = useCallback(
|
const handleDeleteSegment = useCallback(
|
||||||
@@ -558,18 +587,60 @@ export default function SkipController({
|
|||||||
🎭 片尾设置
|
🎭 片尾设置
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
|
{/* 片尾模式选择 */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-2 text-gray-700 dark:text-gray-300">
|
||||||
|
计时模式
|
||||||
|
</label>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<label className="flex items-center">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="endingMode"
|
||||||
|
value="remaining"
|
||||||
|
checked={batchSettings.endingMode === 'remaining'}
|
||||||
|
onChange={(e) => setBatchSettings({...batchSettings, endingMode: e.target.value})}
|
||||||
|
className="mr-2"
|
||||||
|
/>
|
||||||
|
剩余时间(推荐)
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="endingMode"
|
||||||
|
value="absolute"
|
||||||
|
checked={batchSettings.endingMode === 'absolute'}
|
||||||
|
onChange={(e) => setBatchSettings({...batchSettings, endingMode: e.target.value})}
|
||||||
|
className="mr-2"
|
||||||
|
/>
|
||||||
|
绝对时间
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
|
{batchSettings.endingMode === 'remaining'
|
||||||
|
? '基于剩余时间倒计时(如:还剩2分钟时开始)'
|
||||||
|
: '基于播放时间(如:播放到第20分钟时开始)'
|
||||||
|
}
|
||||||
|
</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">
|
||||||
开始时间 (分:秒)
|
{batchSettings.endingMode === 'remaining' ? '剩余时间 (分:秒)' : '开始时间 (分:秒)'}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={batchSettings.endingStart}
|
value={batchSettings.endingStart}
|
||||||
onChange={(e) => setBatchSettings({...batchSettings, endingStart: e.target.value})}
|
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"
|
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"
|
placeholder={batchSettings.endingMode === 'remaining' ? '2:00' : '20:00'}
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-gray-500 mt-1">从此时间开始检测片尾</p>
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
|
{batchSettings.endingMode === 'remaining'
|
||||||
|
? '当剩余时间达到此值时开始倒计时'
|
||||||
|
: '从视频开始播放此时间后开始检测片尾'
|
||||||
|
}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -615,7 +686,8 @@ export default function SkipController({
|
|||||||
setBatchSettings({
|
setBatchSettings({
|
||||||
openingStart: '0:00',
|
openingStart: '0:00',
|
||||||
openingEnd: '1:30',
|
openingEnd: '1:30',
|
||||||
endingStart: '20:00',
|
endingMode: 'remaining',
|
||||||
|
endingStart: '2:00',
|
||||||
endingEnd: '',
|
endingEnd: '',
|
||||||
autoSkip: true,
|
autoSkip: true,
|
||||||
autoNextEpisode: true,
|
autoNextEpisode: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user