added video chapter timestamps

This commit is contained in:
Schnitzel5 2025-07-27 02:44:04 +02:00
parent 163bc9cec9
commit 5e1e526785
20 changed files with 491 additions and 73 deletions

View file

@ -472,8 +472,10 @@
"sync_enable_histories": "Sync history data",
"sync_enable_updates": "Sync update data",
"sync_enable_settings": "Sync settings",
"anime4K": "Enable Anime4K",
"anime4K_info": "Supports .js scripts under /mpv/scripts/",
"anime4K_download": "MPV config files are required!\nDownload now?",
"enable_mpv": "Enable mpv shaders / scripts",
"mpv_info": "Supports .js scripts under mpv/scripts/",
"mpv_redownload": "Redownload mpv config files",
"mpv_redownload_info": "Replaces old config files with new one!",
"mpv_download": "MPV config files are required!\nDownload now?",
"n_days": "{n} days"
}

View file

@ -2903,23 +2903,35 @@ abstract class AppLocalizations {
/// **'Sync settings'**
String get sync_enable_settings;
/// No description provided for @anime4K.
/// No description provided for @enable_mpv.
///
/// In en, this message translates to:
/// **'Enable Anime4K'**
String get anime4K;
/// **'Enable mpv shaders / scripts'**
String get enable_mpv;
/// No description provided for @anime4K_info.
/// No description provided for @mpv_info.
///
/// In en, this message translates to:
/// **'Supports .js scripts under /mpv/scripts/'**
String get anime4K_info;
/// **'Supports .js scripts under mpv/scripts/'**
String get mpv_info;
/// No description provided for @anime4K_download.
/// No description provided for @mpv_redownload.
///
/// In en, this message translates to:
/// **'Redownload mpv config files'**
String get mpv_redownload;
/// No description provided for @mpv_redownload_info.
///
/// In en, this message translates to:
/// **'Replaces old config files with new one!'**
String get mpv_redownload_info;
/// No description provided for @mpv_download.
///
/// In en, this message translates to:
/// **'MPV config files are required!\nDownload now?'**
String get anime4K_download;
String get mpv_download;
/// No description provided for @n_days.
///

View file

@ -1496,14 +1496,19 @@ class AppLocalizationsAr extends AppLocalizations {
String get sync_enable_settings => 'Sync settings';
@override
String get anime4K => 'Enable Anime4K';
String get enable_mpv => 'Enable mpv shaders / scripts';
@override
String get anime4K_info => 'Supports .js scripts under /mpv/scripts/';
String get mpv_info => 'Supports .js scripts under mpv/scripts/';
@override
String get anime4K_download =>
'MPV config files are required!\nDownload now?';
String get mpv_redownload => 'Redownload mpv config files';
@override
String get mpv_redownload_info => 'Replaces old config files with new one!';
@override
String get mpv_download => 'MPV config files are required!\nDownload now?';
@override
String n_days(Object n) {

View file

@ -1509,14 +1509,19 @@ class AppLocalizationsDe extends AppLocalizations {
String get sync_enable_settings => 'Sync settings';
@override
String get anime4K => 'Enable Anime4K';
String get enable_mpv => 'Enable mpv shaders / scripts';
@override
String get anime4K_info => 'Supports .js scripts under /mpv/scripts/';
String get mpv_info => 'Supports .js scripts under mpv/scripts/';
@override
String get anime4K_download =>
'MPV config files are required!\nDownload now?';
String get mpv_redownload => 'Redownload mpv config files';
@override
String get mpv_redownload_info => 'Replaces old config files with new one!';
@override
String get mpv_download => 'MPV config files are required!\nDownload now?';
@override
String n_days(Object n) {

View file

@ -1497,14 +1497,19 @@ class AppLocalizationsEn extends AppLocalizations {
String get sync_enable_settings => 'Sync settings';
@override
String get anime4K => 'Enable Anime4K';
String get enable_mpv => 'Enable mpv shaders / scripts';
@override
String get anime4K_info => 'Supports .js scripts under /mpv/scripts/';
String get mpv_info => 'Supports .js scripts under mpv/scripts/';
@override
String get anime4K_download =>
'MPV config files are required!\nDownload now?';
String get mpv_redownload => 'Redownload mpv config files';
@override
String get mpv_redownload_info => 'Replaces old config files with new one!';
@override
String get mpv_download => 'MPV config files are required!\nDownload now?';
@override
String n_days(Object n) {

View file

@ -1514,14 +1514,19 @@ class AppLocalizationsEs extends AppLocalizations {
String get sync_enable_settings => 'Sync settings';
@override
String get anime4K => 'Enable Anime4K';
String get enable_mpv => 'Enable mpv shaders / scripts';
@override
String get anime4K_info => 'Supports .js scripts under /mpv/scripts/';
String get mpv_info => 'Supports .js scripts under mpv/scripts/';
@override
String get anime4K_download =>
'MPV config files are required!\nDownload now?';
String get mpv_redownload => 'Redownload mpv config files';
@override
String get mpv_redownload_info => 'Replaces old config files with new one!';
@override
String get mpv_download => 'MPV config files are required!\nDownload now?';
@override
String n_days(Object n) {

View file

@ -1515,14 +1515,19 @@ class AppLocalizationsFr extends AppLocalizations {
String get sync_enable_settings => 'Sync settings';
@override
String get anime4K => 'Enable Anime4K';
String get enable_mpv => 'Enable mpv shaders / scripts';
@override
String get anime4K_info => 'Supports .js scripts under /mpv/scripts/';
String get mpv_info => 'Supports .js scripts under mpv/scripts/';
@override
String get anime4K_download =>
'MPV config files are required!\nDownload now?';
String get mpv_redownload => 'Redownload mpv config files';
@override
String get mpv_redownload_info => 'Replaces old config files with new one!';
@override
String get mpv_download => 'MPV config files are required!\nDownload now?';
@override
String n_days(Object n) {

View file

@ -1503,14 +1503,19 @@ class AppLocalizationsId extends AppLocalizations {
String get sync_enable_settings => 'Sync settings';
@override
String get anime4K => 'Enable Anime4K';
String get enable_mpv => 'Enable mpv shaders / scripts';
@override
String get anime4K_info => 'Supports .js scripts under /mpv/scripts/';
String get mpv_info => 'Supports .js scripts under mpv/scripts/';
@override
String get anime4K_download =>
'MPV config files are required!\nDownload now?';
String get mpv_redownload => 'Redownload mpv config files';
@override
String get mpv_redownload_info => 'Replaces old config files with new one!';
@override
String get mpv_download => 'MPV config files are required!\nDownload now?';
@override
String n_days(Object n) {

View file

@ -1512,14 +1512,19 @@ class AppLocalizationsIt extends AppLocalizations {
String get sync_enable_settings => 'Sync settings';
@override
String get anime4K => 'Enable Anime4K';
String get enable_mpv => 'Enable mpv shaders / scripts';
@override
String get anime4K_info => 'Supports .js scripts under /mpv/scripts/';
String get mpv_info => 'Supports .js scripts under mpv/scripts/';
@override
String get anime4K_download =>
'MPV config files are required!\nDownload now?';
String get mpv_redownload => 'Redownload mpv config files';
@override
String get mpv_redownload_info => 'Replaces old config files with new one!';
@override
String get mpv_download => 'MPV config files are required!\nDownload now?';
@override
String n_days(Object n) {

View file

@ -1511,14 +1511,19 @@ class AppLocalizationsPt extends AppLocalizations {
String get sync_enable_settings => 'Sync settings';
@override
String get anime4K => 'Enable Anime4K';
String get enable_mpv => 'Enable mpv shaders / scripts';
@override
String get anime4K_info => 'Supports .js scripts under /mpv/scripts/';
String get mpv_info => 'Supports .js scripts under mpv/scripts/';
@override
String get anime4K_download =>
'MPV config files are required!\nDownload now?';
String get mpv_redownload => 'Redownload mpv config files';
@override
String get mpv_redownload_info => 'Replaces old config files with new one!';
@override
String get mpv_download => 'MPV config files are required!\nDownload now?';
@override
String n_days(Object n) {

View file

@ -1513,14 +1513,19 @@ class AppLocalizationsRu extends AppLocalizations {
String get sync_enable_settings => 'Sync settings';
@override
String get anime4K => 'Enable Anime4K';
String get enable_mpv => 'Enable mpv shaders / scripts';
@override
String get anime4K_info => 'Supports .js scripts under /mpv/scripts/';
String get mpv_info => 'Supports .js scripts under mpv/scripts/';
@override
String get anime4K_download =>
'MPV config files are required!\nDownload now?';
String get mpv_redownload => 'Redownload mpv config files';
@override
String get mpv_redownload_info => 'Replaces old config files with new one!';
@override
String get mpv_download => 'MPV config files are required!\nDownload now?';
@override
String n_days(Object n) {

View file

@ -1497,14 +1497,19 @@ class AppLocalizationsTh extends AppLocalizations {
String get sync_enable_settings => 'Sync settings';
@override
String get anime4K => 'Enable Anime4K';
String get enable_mpv => 'Enable mpv shaders / scripts';
@override
String get anime4K_info => 'Supports .js scripts under /mpv/scripts/';
String get mpv_info => 'Supports .js scripts under mpv/scripts/';
@override
String get anime4K_download =>
'MPV config files are required!\nDownload now?';
String get mpv_redownload => 'Redownload mpv config files';
@override
String get mpv_redownload_info => 'Replaces old config files with new one!';
@override
String get mpv_download => 'MPV config files are required!\nDownload now?';
@override
String n_days(Object n) {

View file

@ -1503,14 +1503,19 @@ class AppLocalizationsTr extends AppLocalizations {
String get sync_enable_settings => 'Sync settings';
@override
String get anime4K => 'Enable Anime4K';
String get enable_mpv => 'Enable mpv shaders / scripts';
@override
String get anime4K_info => 'Supports .js scripts under /mpv/scripts/';
String get mpv_info => 'Supports .js scripts under mpv/scripts/';
@override
String get anime4K_download =>
'MPV config files are required!\nDownload now?';
String get mpv_redownload => 'Redownload mpv config files';
@override
String get mpv_redownload_info => 'Replaces old config files with new one!';
@override
String get mpv_download => 'MPV config files are required!\nDownload now?';
@override
String n_days(Object n) {

View file

@ -1468,14 +1468,19 @@ class AppLocalizationsZh extends AppLocalizations {
String get sync_enable_settings => 'Sync settings';
@override
String get anime4K => 'Enable Anime4K';
String get enable_mpv => 'Enable mpv shaders / scripts';
@override
String get anime4K_info => 'Supports .js scripts under /mpv/scripts/';
String get mpv_info => 'Supports .js scripts under mpv/scripts/';
@override
String get anime4K_download =>
'MPV config files are required!\nDownload now?';
String get mpv_redownload => 'Redownload mpv config files';
@override
String get mpv_redownload_info => 'Replaces old config files with new one!';
@override
String get mpv_download => 'MPV config files are required!\nDownload now?';
@override
String n_days(Object n) {

View file

@ -1,6 +1,8 @@
import 'dart:async';
import 'dart:convert';
import 'dart:ffi';
import 'dart:io';
import 'dart:math';
import 'package:bot_toast/bot_toast.dart';
import 'package:ffi/ffi.dart';
import 'package:file_picker/file_picker.dart';
@ -188,7 +190,35 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
libass: useLibass,
config: true,
configDir: useAnime4K ? widget.mpvDirectory?.path ?? "" : "",
observeProperties: {},
observeProperties: {
"user-data/aniyomi/show_text": generated.mpv_format.MPV_FORMAT_NODE,
"user-data/aniyomi/toggle_ui": generated.mpv_format.MPV_FORMAT_NODE,
"user-data/aniyomi/show_panel": generated.mpv_format.MPV_FORMAT_NODE,
"user-data/aniyomi/software_keyboard":
generated.mpv_format.MPV_FORMAT_NODE,
"user-data/aniyomi/set_button_title":
generated.mpv_format.MPV_FORMAT_NODE,
"user-data/aniyomi/reset_button_title":
generated.mpv_format.MPV_FORMAT_NODE,
"user-data/aniyomi/toggle_button": generated.mpv_format.MPV_FORMAT_NODE,
"user-data/aniyomi/switch_episode":
generated.mpv_format.MPV_FORMAT_NODE,
"user-data/aniyomi/pause": generated.mpv_format.MPV_FORMAT_NODE,
"user-data/aniyomi/seek_by": generated.mpv_format.MPV_FORMAT_NODE,
"user-data/aniyomi/seek_to": generated.mpv_format.MPV_FORMAT_NODE,
"user-data/aniyomi/seek_by_with_text":
generated.mpv_format.MPV_FORMAT_NODE,
"user-data/aniyomi/seek_to_with_text":
generated.mpv_format.MPV_FORMAT_NODE,
"user-data/aniyomi/launch_int_picker":
generated.mpv_format.MPV_FORMAT_NODE,
"user-data/current-anime/intro-length":
generated.mpv_format.MPV_FORMAT_INT64,
"user-data/mangayomi/chapter_titles":
generated.mpv_format.MPV_FORMAT_NODE,
"user-data/mangayomi/current_chapter":
generated.mpv_format.MPV_FORMAT_INT64,
},
eventHandler: _handleMpvEvents,
),
);
@ -221,6 +251,8 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
final ValueNotifier<bool> _isCompleted = ValueNotifier(false);
final ValueNotifier<Duration?> _tempPosition = ValueNotifier(null);
final ValueNotifier<BoxFit> _fit = ValueNotifier(BoxFit.contain);
final ValueNotifier<List<(String, int)>> _chapterMarks = ValueNotifier([]);
final ValueNotifier<int?> _currentChapterMark = ValueNotifier(null);
late final ValueNotifier<_AniSkipPhase> _skipPhase = ValueNotifier(
_AniSkipPhase.none,
);
@ -268,11 +300,15 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
if (event.ref.event_id ==
generated.mpv_event_id.MPV_EVENT_PROPERTY_CHANGE) {
final prop = event.ref.data.cast<generated.mpv_event_property>();
if (prop.ref.name.cast<Utf8>().toDartString() ==
"user-data/aniyomi/dummy_number" &&
final propName = prop.ref.name.cast<Utf8>().toDartString();
if (propName.startsWith("user-data/") &&
prop.ref.format == generated.mpv_format.MPV_FORMAT_NODE) {
final value = prop.ref.data.cast<generated.mpv_node>();
_handleMpvNodeEvents(propName, value);
} else if (propName.startsWith("user-data/") &&
prop.ref.format == generated.mpv_format.MPV_FORMAT_INT64) {
final number = prop.ref.data.cast<Int64>().value;
botToast("Dummy number: $number");
final value = prop.ref.data.cast<Int64>().value;
_handleMpvNumberEvents(propName, value);
}
}
} catch (e) {
@ -282,6 +318,37 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
}
}
Future<void> _handleMpvNodeEvents(
String propName,
Pointer<generated.mpv_node> value,
) async {
switch (propName.substring(10)) {
case "aniyomi/show_text":
if (value.ref.format == generated.mpv_format.MPV_FORMAT_STRING) {
final text = value.ref.u.string.cast<Utf8>().toDartString();
botToast(text);
}
break;
case "mangayomi/chapter_titles":
if (value.ref.format == generated.mpv_format.MPV_FORMAT_STRING) {
final text = value.ref.u.string.cast<Utf8>().toDartString();
final data = jsonDecode(text) as List<dynamic>;
_chapterMarks.value = data
.map((e) => (e["title"] as String, (e["time"] as int) * 1000))
.toList();
}
break;
}
}
Future<void> _handleMpvNumberEvents(String propName, int value) async {
switch (propName.substring(10)) {
case "mangayomi/current_chapter":
_currentChapterMark.value = max(value, 0);
break;
}
}
void pushToNewEpisode(BuildContext context, Chapter episode) {
widget.desktopFullScreenPlayer.call(ref.read(fullscreenProvider));
if (context.mounted) {
@ -893,6 +960,45 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
);
}
Widget _chapterMarkWidget() {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 5),
child: SizedBox(
height: 35,
child: ValueListenableBuilder(
valueListenable: _currentChapterMark,
builder: (context, value, child) => value != null
? PopupMenuButton<int>(
tooltip: '',
itemBuilder: (context) => _chapterMarks.value
.map(
(mark) => PopupMenuItem<int>(
value: mark.$2,
child: Text(
"${mark.$1} - ${Duration(milliseconds: mark.$2).label()}",
),
onTap: () =>
_player.seek(Duration(milliseconds: mark.$2)),
),
)
.toList(),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
"${_chapterMarks.value[value].$1} - ${Duration(milliseconds: _chapterMarks.value[value].$2).label()}",
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
)
: Container(),
),
),
);
}
Widget _mobileBottomButtonBar(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 30),
@ -903,7 +1009,11 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [_seekToWidget(), _buildSettingsButtons(context)],
children: [
_seekToWidget(),
_chapterMarkWidget(),
_buildSettingsButtons(context),
],
),
),
],
@ -1047,6 +1157,7 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
controller: _controller,
),
),
_chapterMarkWidget(),
],
),
_buildSettingsButtons(context),
@ -1330,6 +1441,7 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
},
defaultSkipIntroLength: skipIntroLength,
desktopFullScreenPlayer: widget.desktopFullScreenPlayer,
chapterMarks: _chapterMarks,
)
: MobileControllerWidget(
videoController: _controller,
@ -1340,6 +1452,7 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
doubleSpeed: (value) {
_isDoubleSpeed.value = value ?? false;
},
chapterMarks: _chapterMarks,
),
controller: _controller,
width: context.width(1),

View file

@ -1,6 +1,7 @@
import 'dart:io';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:mangayomi/modules/anime/widgets/custom_track_shape.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/media_kit_video_controls/src/controls/extensions/duration.dart';
@ -9,6 +10,7 @@ class CustomSeekBar extends StatefulWidget {
final Duration? delta;
final Function(Duration)? onSeekStart;
final Function(Duration)? onSeekEnd;
final ValueNotifier<List<(String, int)>> chapterMarks;
const CustomSeekBar({
super.key,
@ -16,6 +18,7 @@ class CustomSeekBar extends StatefulWidget {
this.onSeekEnd,
required this.player,
this.delta,
required this.chapterMarks,
});
@override
@ -90,6 +93,14 @@ class CustomSeekBarState extends State<CustomSeekBar> {
data: SliderTheme.of(context).copyWith(
trackHeight: isDesktop ? null : 3,
overlayShape: const RoundSliderOverlayShape(overlayRadius: 5.0),
trackShape: CustomTrackShape(
currentPosition: clampedValue,
bufferPosition: max(buffer.inMilliseconds.toDouble(), 0),
maxValue: maxValue < 1 ? 1 : maxValue,
minValue: 0,
chapterMarks: widget.chapterMarks.value,
chapterMarkWidth: 10,
),
),
child: Slider(
max: maxValue,

View file

@ -0,0 +1,200 @@
import 'dart:math';
import 'package:flutter/material.dart';
class CustomTrackShape extends SliderTrackShape {
final double maxValue;
final double minValue;
final double currentPosition;
final double bufferPosition;
final List<(String, int)> chapterMarks;
final double chapterMarkWidth;
double trackWidth;
CustomTrackShape({
required this.maxValue,
required this.minValue,
required this.currentPosition,
required this.bufferPosition,
required this.chapterMarks,
this.chapterMarkWidth = 3,
this.trackWidth = 5,
});
@override
Rect getPreferredRect({
required RenderBox parentBox,
Offset offset = Offset.zero,
required SliderThemeData sliderTheme,
bool? isEnabled,
bool? isDiscrete,
}) {
final double thumbWidth = sliderTheme.thumbShape!
.getPreferredSize(isEnabled ?? true, isDiscrete ?? false)
.width;
final double trackHeight = sliderTheme.trackHeight!;
final double trackTop =
offset.dy + (parentBox.size.height - trackHeight) / 2;
final double trackLeft = offset.dx + thumbWidth / 2;
trackWidth = parentBox.size.width - thumbWidth;
return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight);
}
@override
void paint(
PaintingContext context,
Offset offset, {
required RenderBox parentBox,
required SliderThemeData sliderTheme,
required Animation<double> enableAnimation,
required Offset thumbCenter,
Offset? secondaryOffset,
bool? isEnabled,
bool? isDiscrete,
required TextDirection textDirection,
}) {
if (sliderTheme.trackHeight == 0) return;
final Rect trackRect = getPreferredRect(
parentBox: parentBox,
offset: offset,
sliderTheme: sliderTheme,
isEnabled: isEnabled,
isDiscrete: isDiscrete,
);
double currentPositionWidth = (trackWidth / maxValue) * currentPosition;
double bufferPositionWidth = (trackWidth / maxValue) * bufferPosition;
_drawActiveThumb(context, sliderTheme, trackRect, currentPositionWidth);
_drawBufferThumb(
context,
sliderTheme,
trackRect,
currentPositionWidth,
bufferPositionWidth,
);
_drawInactiveThumb(context, sliderTheme, trackRect, currentPositionWidth);
for (final mark in chapterMarks) {
double markPositionWidth = (trackWidth / maxValue) * mark.$2;
_drawChapterMark(context, sliderTheme, trackRect, markPositionWidth);
}
}
void _drawActiveThumb(
PaintingContext context,
SliderThemeData sliderTheme,
Rect trackRect,
double currentPositionWidth,
) {
final Paint defaultPathPaint = Paint()
..color = sliderTheme.activeTrackColor!
..style = PaintingStyle.fill;
final defaultPathSegment = Path()
..addRect(
Rect.fromPoints(
Offset(trackRect.left, trackRect.top),
Offset(trackRect.left + currentPositionWidth, trackRect.bottom),
),
)
..lineTo(trackRect.left, trackRect.bottom)
..arcTo(
Rect.fromPoints(
Offset(trackRect.left + 5, trackRect.top),
Offset(trackRect.left - 5, trackRect.bottom),
),
-pi * 3 / 2,
pi,
false,
);
context.canvas.drawPath(defaultPathSegment, defaultPathPaint);
}
void _drawBufferThumb(
PaintingContext context,
SliderThemeData sliderTheme,
Rect trackRect,
double currentPositionWidth,
double bufferPositionWidth,
) {
final Paint defaultPathPaint = Paint()
..color = sliderTheme.secondaryActiveTrackColor!
..style = PaintingStyle.fill;
final defaultPathSegment = Path()
..addRect(
Rect.fromPoints(
Offset(trackRect.left + currentPositionWidth, trackRect.top),
Offset(trackRect.left + bufferPositionWidth, trackRect.bottom),
),
)
..lineTo(trackRect.left, trackRect.bottom)
..arcTo(
Rect.fromPoints(
Offset(trackRect.left + 5, trackRect.top),
Offset(trackRect.left - 5, trackRect.bottom),
),
-pi * 3 / 2,
pi,
false,
);
context.canvas.drawPath(defaultPathSegment, defaultPathPaint);
}
void _drawInactiveThumb(
PaintingContext context,
SliderThemeData sliderTheme,
Rect trackRect,
double currentPositionWidth,
) {
final unselectedPathPaint = Paint()
..style = PaintingStyle.fill
..color = sliderTheme.inactiveTrackColor!;
final unselectedPathSegment = Path()
..addRect(
Rect.fromPoints(
Offset(trackRect.right, trackRect.top),
Offset(trackRect.left + currentPositionWidth, trackRect.bottom),
),
)
..addArc(
Rect.fromPoints(
Offset(trackRect.right - 5, trackRect.bottom),
Offset(trackRect.right + 5, trackRect.top),
),
-pi / 2,
pi,
);
context.canvas.drawPath(unselectedPathSegment, unselectedPathPaint);
}
void _drawChapterMark(
PaintingContext context,
SliderThemeData sliderTheme,
Rect trackRect,
double markPositionWidth,
) {
final Paint borderPaint = Paint()
..color = Colors.white
..style = PaintingStyle.fill;
final pathSegmentSelected = Path()
..addRect(
Rect.fromPoints(
Offset(trackRect.left + markPositionWidth, trackRect.top),
Offset(
trackRect.left + markPositionWidth + chapterMarkWidth,
trackRect.bottom,
),
),
);
context.canvas.drawPath(pathSegmentSelected, borderPaint);
}
}

View file

@ -24,6 +24,7 @@ class DesktopControllerWidget extends ConsumerStatefulWidget {
final Widget seekToWidget;
final int defaultSkipIntroLength;
final void Function(bool) desktopFullScreenPlayer;
final ValueNotifier<List<(String, int)>> chapterMarks;
const DesktopControllerWidget({
super.key,
required this.videoController,
@ -36,6 +37,7 @@ class DesktopControllerWidget extends ConsumerStatefulWidget {
required this.doubleSpeed,
required this.defaultSkipIntroLength,
required this.desktopFullScreenPlayer,
required this.chapterMarks,
});
@override
@ -481,6 +483,7 @@ class _DesktopControllerWidgetState
widget.tempDuration(null);
},
player: widget.videoController.player,
chapterMarks: widget.chapterMarks,
),
),
),

View file

@ -24,6 +24,7 @@ class MobileControllerWidget extends ConsumerStatefulWidget {
final Widget topButtonBarWidget;
final GlobalKey<VideoState> videoStatekey;
final Widget bottomButtonBarWidget;
final ValueNotifier<List<(String, int)>> chapterMarks;
const MobileControllerWidget({
super.key,
required this.videoController,
@ -32,6 +33,7 @@ class MobileControllerWidget extends ConsumerStatefulWidget {
required this.streamController,
required this.videoStatekey,
required this.doubleSpeed,
required this.chapterMarks,
});
@override
@ -464,6 +466,7 @@ class _MobileControllerWidgetState
});
},
player: widget.videoController.player,
chapterMarks: widget.chapterMarks,
),
),
widget.bottomButtonBarWidget,
@ -491,6 +494,7 @@ class _MobileControllerWidgetState
child: CustomSeekBar(
delta: _seekBarDeltaValueNotifier,
player: widget.videoController.player,
chapterMarks: widget.chapterMarks,
),
),
],

View file

@ -485,18 +485,28 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
),
SwitchListTile(
value: useAnime4K,
title: Text(context.l10n.anime4K),
title: Text(context.l10n.enable_mpv),
subtitle: Text(
context.l10n.anime4K_info,
context.l10n.mpv_info,
style: TextStyle(fontSize: 11, color: context.secondaryColor),
),
onChanged: (value) async {
if (value && !(await _checkAnime4K(context))) {
if (value && !(await _checkMpvConfig(context))) {
return;
}
ref.read(useAnime4KStateProvider.notifier).set(value);
},
),
ListTile(
onTap: () {
_checkMpvConfig(context, redownload: true);
},
title: Text(context.l10n.mpv_redownload),
subtitle: Text(
context.l10n.mpv_redownload_info,
style: TextStyle(fontSize: 11, color: context.secondaryColor),
),
),
SwitchListTile(
value: fullScreenPlayer,
title: Text(context.l10n.full_screen_player),
@ -592,7 +602,10 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
);
}
Future<bool> _checkAnime4K(BuildContext context) async {
Future<bool> _checkMpvConfig(
BuildContext context, {
bool redownload = false,
}) async {
var status = await Permission.storage.status;
if (!status.isGranted) {
await Permission.storage.request();
@ -601,9 +614,9 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
final dir = await provider.getMpvDirectory();
final mpvFile = File('${dir!.path}/mpv.conf');
final inputFile = File('${dir.path}/input.conf');
if (!(await mpvFile.exists()) &&
!(await inputFile.exists()) &&
context.mounted) {
final filesMissing =
!(await mpvFile.exists()) && !(await inputFile.exists());
if ((redownload || filesMissing) && context.mounted) {
final res = await showDialog(
context: context,
builder: (context) {
@ -611,7 +624,7 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
content: SingleChildScrollView(
child: Column(
children: [
Text(context.l10n.anime4K_download),
Text(context.l10n.mpv_download),
_total > 0
? Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,