Skip to content

Commit 6e85eab

Browse files
committed
feat: 字幕支持
1 parent b80896f commit 6e85eab

File tree

10 files changed

+229
-9
lines changed

10 files changed

+229
-9
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ Miru App
3838
- [x] 漫画小说设置
3939
- [x] 漫画小说历史记录
4040
- [x] TMDB 元数据
41-
- [ ] 字幕支持
41+
- [x] 字幕支持
4242
- [ ] BT 种子播放
4343
- [ ] 数据同步
4444
- [ ] 自动搜寻字幕

assets/i18n/en.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,10 @@
9393
"watch-now": "Watch Now",
9494
"no-episodes": "No episodes",
9595
"play-complete": "Playback complete",
96-
"resume-last-playback": "Resume last playback"
96+
"resume-last-playback": "Resume last playback",
97+
"subtitle": "Subtitle",
98+
"subtitle-change": "Change Subtitle {title}",
99+
"subtitle-file": "Subtitle File"
97100
},
98101

99102
"comic-settings": {

assets/i18n/zh.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,11 @@
9898
"watch-now": "立即观看",
9999
"no-episodes": "暂无剧集",
100100
"play-complete": "播放完成",
101-
"resume-last-playback": "恢复上次播放位置"
101+
"resume-last-playback": "恢复上次播放位置",
102+
"subtitle-none": "不使用字幕",
103+
"subtitle": "字幕",
104+
"subtitle-change": "切换字幕 {title}",
105+
"subtitle-file": "选择字幕文件"
102106
},
103107

104108
"reader": {

lib/models/extension.dart

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,16 +125,35 @@ class ExtensionBangumiWatch {
125125
ExtensionBangumiWatch({
126126
required this.type,
127127
required this.url,
128+
this.subtitles,
128129
});
129130
final ExtensionWatchBangumiType type;
130131
final String url;
132+
final List<ExtensionBangumiWatchSubtitle>? subtitles;
131133

132134
factory ExtensionBangumiWatch.fromJson(Map<String, dynamic> json) =>
133135
_$ExtensionBangumiWatchFromJson(json);
134136

135137
Map<String, dynamic> toJson() => _$ExtensionBangumiWatchToJson(this);
136138
}
137139

140+
@JsonSerializable()
141+
class ExtensionBangumiWatchSubtitle {
142+
final String? language;
143+
final String title;
144+
final String url;
145+
ExtensionBangumiWatchSubtitle({
146+
required this.title,
147+
required this.url,
148+
this.language,
149+
});
150+
151+
factory ExtensionBangumiWatchSubtitle.fromJson(Map<String, dynamic> json) =>
152+
_$ExtensionBangumiWatchSubtitleFromJson(json);
153+
154+
Map<String, dynamic> toJson() => _$ExtensionBangumiWatchSubtitleToJson(this);
155+
}
156+
138157
@JsonSerializable()
139158
class ExtensionMangaWatch {
140159
final List<String> urls;

lib/models/extension.g.dart

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/models/index.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ export 'history.dart';
44
export 'extension_setting.dart';
55
export 'manga_setting.dart';
66
export 'miru_detail.dart';
7+
export 'tmdb.dart';

lib/pages/watch/video_controller.dart

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import 'dart:async';
22
import 'dart:io';
33

4+
import 'package:file_picker/file_picker.dart';
45
import 'package:fluent_ui/fluent_ui.dart';
56
import 'package:flutter/services.dart';
7+
import 'package:flutter_i18n/flutter_i18n.dart';
68
import 'package:get/get.dart';
79
import 'package:media_kit/media_kit.dart';
810
import 'package:media_kit_video/media_kit_video.dart';
911
import 'package:miru_app/models/index.dart';
1012
import 'package:miru_app/pages/home/controller.dart';
13+
import 'package:miru_app/router/router.dart';
1114
import 'package:miru_app/utils/database.dart';
1215
import 'package:miru_app/utils/extension_runtime.dart';
1316
import 'package:miru_app/utils/i18n.dart';
@@ -38,6 +41,9 @@ class VideoPlayerController extends GetxController {
3841
final isOpenSidebar = false.obs;
3942
final isFullScreen = false.obs;
4043
late final index = playIndex.obs;
44+
final List<ExtensionBangumiWatchSubtitle> subtitles =
45+
<ExtensionBangumiWatchSubtitle>[].obs;
46+
final selectedSubtitle = 0.obs;
4147

4248
// 是否已经自动跳转到上次播放进度
4349
bool _isAutoSeekPosition = false;
@@ -55,14 +61,69 @@ class VideoPlayerController extends GetxController {
5561
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
5662
}
5763
play();
64+
65+
// 切换剧集
5866
ever(index, (callback) {
5967
play();
6068
});
69+
70+
// 显示剧集列表
6171
ever(showPlayList, (callback) {
6272
if (!showPlayList.value) {
6373
isOpenSidebar.value = false;
6474
}
6575
});
76+
77+
// 切换字幕
78+
ever(selectedSubtitle, (callback) {
79+
if (callback == -1) {
80+
player.setSubtitleTrack(SubtitleTrack.no());
81+
return;
82+
}
83+
if (callback == -2) {
84+
// 选择文件 srt 或者 vtt
85+
FilePicker.platform.pickFiles(
86+
type: FileType.custom,
87+
allowedExtensions: ['srt', 'vtt'],
88+
).then((value) {
89+
if (value == null) {
90+
selectedSubtitle.value = -1;
91+
return;
92+
}
93+
94+
// 读取文件
95+
final data = File(value.files.first.path!).readAsStringSync();
96+
player.setSubtitleTrack(SubtitleTrack.data(data));
97+
sendMessage(
98+
Message(
99+
Text(
100+
FlutterI18n.translate(
101+
cuurentContext,
102+
"video.subtitle-change",
103+
translationParams: {"title": value.files.first.name},
104+
),
105+
),
106+
),
107+
);
108+
});
109+
return;
110+
}
111+
player.setSubtitleTrack(
112+
SubtitleTrack.uri(subtitles[callback].url),
113+
);
114+
sendMessage(
115+
Message(
116+
Text(
117+
FlutterI18n.translate(
118+
cuurentContext,
119+
"video.subtitle-change",
120+
translationParams: {"title": subtitles[callback].title},
121+
),
122+
),
123+
),
124+
);
125+
});
126+
66127
// 自动切换下一集
67128
player.stream.completed.listen((event) {
68129
if (index.value == playList.length - 1 && event) {
@@ -84,6 +145,7 @@ class VideoPlayerController extends GetxController {
84145
runtime.extension.package,
85146
detailUrl,
86147
);
148+
87149
if (history != null &&
88150
history.progress.isNotEmpty &&
89151
history.episodeId == index.value &&
@@ -104,10 +166,12 @@ class VideoPlayerController extends GetxController {
104166

105167
play() async {
106168
try {
169+
subtitles.clear();
170+
selectedSubtitle.value = -1;
107171
final playUrl = playList[index.value].url;
108-
final m3u8Url =
109-
(await runtime.watch(playUrl) as ExtensionBangumiWatch).url;
110-
player.open(Media(m3u8Url));
172+
final watchData = await runtime.watch(playUrl) as ExtensionBangumiWatch;
173+
player.open(Media(watchData.url));
174+
subtitles.addAll(watchData.subtitles ?? []);
111175
} catch (e) {
112176
debugPrint(e.toString());
113177
sendMessage(
@@ -203,6 +267,5 @@ class VideoPlayerController extends GetxController {
203267
class Message {
204268
final Widget child;
205269
final Duration time;
206-
207270
Message(this.child, {this.time = const Duration(seconds: 3)});
208271
}

lib/pages/watch/widgets/video/video_player_content.dart

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
22
import 'package:get/get.dart';
33
import 'package:media_kit_video/media_kit_video.dart';
44
import 'package:miru_app/pages/watch/video_controller.dart';
5+
import 'package:miru_app/utils/i18n.dart';
56
import 'package:miru_app/utils/router.dart';
67
import 'package:miru_app/widgets/platform_widget.dart';
78
import 'package:window_manager/window_manager.dart';
@@ -88,6 +89,52 @@ class _VideoPlayerContenState extends State<VideoPlayerConten> {
8889
const MaterialDesktopVolumeButton(),
8990
const MaterialDesktopPositionIndicator(),
9091
const Spacer(),
92+
PopupMenuButton(
93+
icon: const Icon(
94+
Icons.subtitles,
95+
color: Colors.white,
96+
),
97+
itemBuilder: (context) {
98+
return [
99+
// 是否显示字幕
100+
PopupMenuItem(
101+
child: Obx(
102+
() => CheckboxListTile(
103+
value: _c.selectedSubtitle.value == -1,
104+
onChanged: (value) {
105+
_c.selectedSubtitle.value = -1;
106+
},
107+
title: Text('video.subtitle-none'.i18n),
108+
),
109+
),
110+
),
111+
// 选择文件
112+
PopupMenuItem(
113+
child: Obx(
114+
() => CheckboxListTile(
115+
value: _c.selectedSubtitle.value == -2,
116+
onChanged: (value) {
117+
_c.selectedSubtitle.value = -2;
118+
},
119+
title: Text("video.subtitle-file".i18n),
120+
),
121+
),
122+
),
123+
for (int i = 0; i < _c.subtitles.length; i++)
124+
PopupMenuItem(
125+
child: Obx(
126+
() => CheckboxListTile(
127+
value: _c.selectedSubtitle.value == i,
128+
onChanged: (value) {
129+
_c.selectedSubtitle.value = i;
130+
},
131+
title: Text(_c.subtitles[i].title),
132+
),
133+
),
134+
),
135+
];
136+
},
137+
),
91138
MaterialDesktopCustomButton(
92139
onPressed: () {
93140
_c.showPlayList.value = !_c.showPlayList.value;
@@ -164,6 +211,51 @@ class _VideoPlayerContenState extends State<VideoPlayerConten> {
164211
}),
165212
const MaterialPositionIndicator(),
166213
const Spacer(),
214+
PopupMenuButton(
215+
icon: const Icon(
216+
Icons.subtitles,
217+
color: Colors.white,
218+
),
219+
itemBuilder: (context) {
220+
return [
221+
// 是否显示字幕
222+
PopupMenuItem(
223+
value: -1,
224+
child: Obx(
225+
() => CheckboxListTile(
226+
value: _c.selectedSubtitle.value == -1,
227+
onChanged: (value) {
228+
_c.selectedSubtitle.value = -1;
229+
},
230+
title: Text('video.subtitle-none'.i18n),
231+
),
232+
),
233+
), // 选择文件
234+
PopupMenuItem(
235+
child: CheckboxListTile(
236+
value: _c.selectedSubtitle.value == -2,
237+
onChanged: (value) {
238+
_c.selectedSubtitle.value = -2;
239+
},
240+
title: Text("video.subtitle-file".i18n),
241+
),
242+
),
243+
for (int i = 0; i < _c.subtitles.length; i++)
244+
PopupMenuItem(
245+
value: i,
246+
child: Obx(
247+
() => CheckboxListTile(
248+
value: _c.selectedSubtitle.value == i,
249+
onChanged: (value) {
250+
_c.selectedSubtitle.value = i;
251+
},
252+
title: Text(_c.subtitles[i].title),
253+
),
254+
),
255+
),
256+
];
257+
},
258+
),
167259
MaterialCustomButton(
168260
onPressed: () {
169261
_c.showPlayList.value = !_c.showPlayList.value;

pubspec.lock

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,14 @@ packages:
305305
url: "https://pub.dev"
306306
source: hosted
307307
version: "7.0.0"
308+
file_picker:
309+
dependency: "direct main"
310+
description:
311+
name: file_picker
312+
sha256: "21145c9c268d54b1f771d8380c195d2d6f655e0567dc1ca2f9c134c02c819e0a"
313+
url: "https://pub.dev"
314+
source: hosted
315+
version: "5.3.3"
308316
fixnum:
309317
dependency: transitive
310318
description:
@@ -403,6 +411,14 @@ packages:
403411
url: "https://pub.dev"
404412
source: hosted
405413
version: "0.6.17+1"
414+
flutter_plugin_android_lifecycle:
415+
dependency: transitive
416+
description:
417+
name: flutter_plugin_android_lifecycle
418+
sha256: "950e77c2bbe1692bc0874fc7fb491b96a4dc340457f4ea1641443d0a6c1ea360"
419+
url: "https://pub.dev"
420+
source: hosted
421+
version: "2.0.15"
406422
flutter_test:
407423
dependency: "direct dev"
408424
description: flutter

0 commit comments

Comments
 (0)