Skip to content

Commit 6420630

Browse files
authored
Merge pull request #127 from solidSpoon/自动播放下一个视频
自动播放下一个视频
2 parents c1fd84e + 8f0f500 commit 6420630

File tree

12 files changed

+302
-6
lines changed

12 files changed

+302
-6
lines changed

.eslintrc.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@
1414
"react-app"
1515
],
1616
"parser": "@typescript-eslint/parser",
17+
"ignorePatterns": [
18+
"vite.*.config.ts",
19+
"node_modules/**",
20+
".vite/**",
21+
"out/**"
22+
],
1723
"settings": {
1824
"import/resolver": {
1925
"node": {

CLAUDE.md

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# DashPlayer 项目基本情况
2+
3+
## 项目概述
4+
DashPlayer 是一款专为英语学习打造的视频播放器,采用 Electron + React + TypeScript 技术栈开发。
5+
6+
## 技术架构
7+
8+
### 前端技术栈
9+
- **框架**: React 18 + TypeScript
10+
- **状态管理**: Zustand
11+
- **UI组件**: Radix UI + Tailwind CSS + Lucide React
12+
- **路由**: React Router DOM
13+
- **视频播放**: React Player
14+
- **数据请求**: SWR
15+
- **构建工具**: Vite + Electron Forge
16+
17+
### 后端技术栈
18+
- **运行时**: Electron (Node.js)
19+
- **数据库**: SQLite + Drizzle ORM
20+
- **依赖注入**: Inversify
21+
- **AI集成**: OpenAI SDK
22+
- **媒体处理**: FFmpeg
23+
24+
## 项目结构
25+
26+
### 主要目录
27+
```
28+
src/
29+
├── backend/ # 后端代码 (Electron Main Process)
30+
│ ├── controllers/ # API控制器
31+
│ ├── services/ # 业务逻辑服务
32+
│ ├── db/ # 数据库相关
33+
│ └── utils/ # 工具函数
34+
├── fronted/ # 前端代码 (Electron Renderer Process)
35+
│ ├── components/ # React组件
36+
│ ├── hooks/ # 自定义Hooks
37+
│ ├── pages/ # 页面组件
38+
│ └── lib/ # 前端工具库
39+
└── common/ # 前后端共享代码
40+
├── api/ # API定义
41+
├── types/ # 类型定义
42+
└── utils/ # 通用工具
43+
```
44+
45+
### 关键组件
46+
- **Player.tsx**: 视频播放器主组件
47+
- **PlayerControlPanel.tsx**: 播放控制面板
48+
- **ControlBox.tsx**: Player Controls控制区域
49+
- **FileBrowser.tsx**: Video Explorer文件浏览器
50+
- **usePlayerController**: 播放器状态管理Hook
51+
- **useFile**: 文件状态管理Hook
52+
53+
## 核心功能
54+
1. **视频播放**: 支持多种格式的视频播放
55+
2. **字幕处理**: SRT字幕解析、显示和时间调整
56+
3. **AI辅助**: 字幕翻译、语法分析、词汇解释
57+
4. **播放历史**: 记录观看进度和历史
58+
5. **文件管理**: 文件/文件夹模式播放列表
59+
6. **快捷键**: 丰富的键盘快捷键支持
60+
7. **主题**: 支持明暗主题切换
61+
62+
## 开发相关
63+
64+
### 常用命令
65+
```bash
66+
yarn start # 启动开发环境
67+
yarn lint # 代码检查
68+
yarn test # 运行测试
69+
yarn make # 构建应用
70+
```
71+
72+
### 数据库
73+
- 使用 Drizzle ORM 管理 SQLite 数据库
74+
- 主要表: watchHistory, videoClip, tag, words 等
75+
- 支持数据库迁移
76+
77+
### API架构
78+
- 基于 Electron IPC 的前后端通信
79+
- 控制器-服务模式的后端架构
80+
- 统一的 API 注册和调用机制
81+
82+
### 状态管理
83+
- Zustand store 分片管理
84+
- 播放器状态: usePlayerController
85+
- 文件状态: useFile
86+
- 布局状态: useLayout
87+
- 设置状态: useSetting
88+
89+
## API开发规范
90+
91+
### 重要提醒 ⚠️
92+
**修改API时必须在以下文件中更新类型定义:**
93+
- `src/common/api/api-def.ts` - API类型定义文件
94+
- 所有新增的API都必须在对应的interface中定义参数和返回值类型
95+
96+
### API定义结构
97+
```typescript
98+
interface WatchHistoryDef {
99+
'watch-history/get-next-video': { params: string, return: WatchHistoryVO | null };
100+
// 其他API定义...
101+
}
102+
```
103+
104+
### API开发流程
105+
1.`api-def.ts` 中定义API类型
106+
2. 在对应的Controller中实现方法
107+
3. 在Service接口中添加方法定义
108+
4. 在ServiceImpl中实现具体逻辑
109+
5. 在Controller的registerRoutes中注册路由
110+
111+
## 当前开发任务
112+
✅ 已完成:实现自动播放下一个视频功能,在文件夹模式下视频播放结束后自动播放下一个视频。
113+
114+
## 注意事项
115+
- 项目使用 TypeScript 严格模式
116+
- 遵循 React Hooks 最佳实践
117+
- 后端使用依赖注入模式
118+
- 前端组件采用 shadcn/ui 设计规范
119+
- **API开发必须先在 `api-def.ts` 中定义类型**

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "dash-player",
33
"productName": "DashPlayer",
4-
"version": "5.1.6",
4+
"version": "5.1.7",
55
"description": "My Electron application description",
66
"main": ".vite/build/main.js",
77
"scripts": {

src/backend/controllers/WatchHistoryController.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ export default class WatchHistoryController implements Controller {
5555
return this.watchHistoryService.analyseFolder(path);
5656
}
5757

58+
public async getNextVideo(currentId: string): Promise<WatchHistoryVO | null> {
59+
return this.watchHistoryService.getNextVideo(currentId);
60+
}
61+
5862
registerRoutes(): void {
5963
registerRoute('watch-history/list', (p) => this.list(p));
6064
registerRoute('watch-history/progress/update', (p) => this.updateProgress(p));
@@ -65,5 +69,6 @@ export default class WatchHistoryController implements Controller {
6569
registerRoute('watch-history/attach-srt', (p) => this.attachSrt(p));
6670
registerRoute('watch-history/suggest-srt', (p) => this.suggestSrt(p));
6771
registerRoute('watch-history/analyse-folder', (p) => this.analyseFolder(p));
72+
registerRoute('watch-history/get-next-video', (p) => this.getNextVideo(p));
6873
}
6974
}

src/backend/services/WatchHistoryService.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ interface WatchHistoryService {
2424
* @param file 视频文件路径
2525
*/
2626
suggestSrt(file: string): Promise<string[]>;
27+
28+
/**
29+
* 获取下一个视频
30+
* @param currentId 当前视频ID
31+
*/
32+
getNextVideo(currentId: string): Promise<WatchHistoryVO | null>;
2733
}
2834

2935
export default WatchHistoryService;

src/backend/services/impl/WatchHistoryServiceImpl.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,4 +430,34 @@ export default class WatchHistoryServiceImpl implements WatchHistoryService {
430430
}
431431
}
432432
}
433+
434+
public async getNextVideo(currentId: string): Promise<WatchHistoryVO | null> {
435+
const [currentRecord] = await db.select().from(watchHistory)
436+
.where(eq(watchHistory.id, currentId));
437+
438+
if (!currentRecord) {
439+
return null;
440+
}
441+
442+
const folderVideos = await db.select().from(watchHistory)
443+
.where(
444+
and(
445+
eq(watchHistory.base_path, currentRecord.base_path),
446+
eq(watchHistory.project_type, WatchHistoryType.DIRECTORY)
447+
)
448+
).orderBy(asc(watchHistory.file_name));
449+
450+
if (CollUtil.isEmpty(folderVideos)) {
451+
return null;
452+
}
453+
454+
const currentIndex = folderVideos.findIndex(video => video.id === currentId);
455+
456+
if (currentIndex >= 0 && currentIndex < folderVideos.length - 1) {
457+
const nextVideo = folderVideos[currentIndex + 1];
458+
return await this.buildVoFromFile(nextVideo);
459+
}
460+
461+
return null;
462+
}
433463
}

src/common/api/api-def.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ interface WatchHistoryDef {
104104
'watch-history/attach-srt': { params: { videoPath: string, srtPath: string | 'same' }, return: void };
105105
'watch-history/suggest-srt': { params: string, return: string[] };
106106
'watch-history/analyse-folder': { params: string, return: { supported: number, unsupported: number } };
107+
'watch-history/get-next-video': { params: string, return: WatchHistoryVO | null };
107108
}
108109

109110
interface SubtitleControllerDef {

src/fronted/components/ControlBox.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,9 @@ const ControlBox = () => {
8888
changeSyncSide,
8989
changeSingleRepeat,
9090
autoPause,
91-
changeAutoPause
91+
changeAutoPause,
92+
autoPlayNext,
93+
changeAutoPlayNext
9294
} = usePlayerController(
9395
useShallow((s) => ({
9496
showEn: s.showEn,
@@ -102,7 +104,9 @@ const ControlBox = () => {
102104
singleRepeat: s.singleRepeat,
103105
changeSingleRepeat: s.changeSingleRepeat,
104106
autoPause: s.autoPause,
105-
changeAutoPause: s.changeAutoPause
107+
changeAutoPause: s.changeAutoPause,
108+
autoPlayNext: s.autoPlayNext,
109+
changeAutoPlayNext: s.changeAutoPlayNext
106110
}))
107111
);
108112
const setSetting = useSetting((s) => s.setSetting);
@@ -196,6 +200,13 @@ const ControlBox = () => {
196200
label: '自动暂停',
197201
tooltip: `当前句子结束自动暂停 快捷键为 ${getShortcut('shortcut.autoPause')}`
198202
})}
203+
{controlItem({
204+
checked: autoPlayNext,
205+
onCheckedChange: changeAutoPlayNext,
206+
id: 'autoPlayNext',
207+
label: '自动播放下一个',
208+
tooltip: '文件夹模式下视频结束后自动播放下一个视频'
209+
})}
199210
{controlItem({
200211
checked: setting('appearance.theme') === 'dark',
201212
onCheckedChange: () => {

src/fronted/components/Player.tsx

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import PlayerToaster from '@/fronted/components/PlayerToaster';
1212
import UrlUtil from '@/common/utils/UrlUtil';
1313
import StrUtil from '@/common/utils/str-util';
1414
import ReactPlayer from 'react-player/file';
15+
import { useNavigate } from 'react-router-dom';
1516

1617
const api = window.electron;
1718

@@ -26,7 +27,8 @@ export default function Player({ className }: { className?: string }): ReactElem
2627
updateExactPlayTime,
2728
setDuration,
2829
seekTo,
29-
playbackRate
30+
playbackRate,
31+
autoPlayNext
3032
} = usePlayerController(
3133
useShallow((state) => ({
3234
playing: state.playing,
@@ -38,7 +40,8 @@ export default function Player({ className }: { className?: string }): ReactElem
3840
updateExactPlayTime: state.updateExactPlayTime,
3941
setDuration: state.setDuration,
4042
seekTo: state.seekTo,
41-
playbackRate: state.playbackRate
43+
playbackRate: state.playbackRate,
44+
autoPlayNext: state.autoPlayNext
4245
}))
4346
);
4447
const videoPath = useFile((s) => s.videoPath);
@@ -47,6 +50,7 @@ export default function Player({ className }: { className?: string }): ReactElem
4750
const videoLoaded = useFile((s) => s.videoLoaded);
4851
const playerRef = useRef<ReactPlayer>(null);
4952
const playerRefBackground = useRef<HTMLCanvasElement>(null);
53+
const navigate = useNavigate();
5054
let lastFile: string | undefined;
5155

5256
const fullScreen = useLayout((s) => s.fullScreen);
@@ -182,6 +186,24 @@ export default function Player({ className }: { className?: string }): ReactElem
182186
lastFile = file;
183187
};
184188

189+
const handleAutoPlayNext = async () => {
190+
if (!autoPlayNext || !videoId) {
191+
return;
192+
}
193+
194+
try {
195+
const nextVideo = await api.call('watch-history/get-next-video', videoId);
196+
if (nextVideo) {
197+
console.log('Auto playing next video:', nextVideo.fileName);
198+
navigate(`/player/${nextVideo.id}`);
199+
} else {
200+
console.log('No next video found');
201+
}
202+
} catch (error) {
203+
console.error('Failed to get next video:', error);
204+
}
205+
};
206+
185207
console.log('videoPath', videoPath);
186208
const render = (): ReactElement => {
187209
if (StrUtil.isBlank(videoPath)) {
@@ -230,6 +252,7 @@ export default function Player({ className }: { className?: string }): ReactElem
230252
await jumpToHistoryProgress(videoPath);
231253
loadedVideo(videoPath);
232254
}}
255+
onEnded={handleAutoPlayNext}
233256
/>
234257
{!fullScreen && (!showControlPanel && (
235258
<PlayerControlPanel

src/fronted/hooks/usePlayerControllerSlices/SliceTypes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export interface ModeSlice {
6262
singleRepeat: boolean;
6363
autoPause: boolean;
6464
showWordLevel: boolean;
65+
autoPlayNext: boolean;
6566

6667
changeShowEn: () => void;
6768
changeShowCn: () => void;
@@ -70,6 +71,7 @@ export interface ModeSlice {
7071
changeSingleRepeat: (target?:boolean) => void;
7172
changeShowWordLevel: () => void;
7273
changeAutoPause: (target?:boolean) => void;
74+
changeAutoPlayNext: (target?:boolean) => void;
7375
}
7476

7577
export interface ControllerSlice {

0 commit comments

Comments
 (0)