mirror of
https://github.com/kodjodevf/mangayomi.git
synced 2026-03-11 17:25:32 +00:00
added video chapter timestamps
This commit is contained in:
parent
163bc9cec9
commit
5e1e526785
20 changed files with 491 additions and 73 deletions
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
200
lib/modules/anime/widgets/custom_track_shape.dart
Normal file
200
lib/modules/anime/widgets/custom_track_shape.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue