Merge branch 'feature/mpv-anime4k' into enhance/mpv

This commit is contained in:
Schnitzel5 2025-07-31 02:01:23 +02:00
commit c73e1f3800
81 changed files with 8994 additions and 1186 deletions

BIN
assets/mangayomi_mpv.zip Normal file

Binary file not shown.

View file

@ -12,6 +12,11 @@ class MChapterBridge {
url: namedArgs.get<String?>('url'),
dateUpload: namedArgs.get<String?>('dateUpload'),
scanlator: namedArgs.get<String?>('scanlator'),
isFiller: namedArgs.get<bool?>('isFiller'),
thumbnailUrl: namedArgs.get<String?>('scanlator'),
description: namedArgs.get<String?>('scanlator'),
downloadSize: namedArgs.get<String?>('scanlator'),
duration: namedArgs.get<String?>('scanlator'),
);
},
},
@ -20,6 +25,11 @@ class MChapterBridge {
'url': (visitor, target) => (target as MChapter).url,
'dateUpload': (visitor, target) => (target as MChapter).dateUpload,
'scanlator': (visitor, target) => (target as MChapter).scanlator,
'isFiller': (visitor, target) => (target as MChapter).isFiller,
'thumbnailUrl': (visitor, target) => (target as MChapter).thumbnailUrl,
'description': (visitor, target) => (target as MChapter).description,
'downloadSize': (visitor, target) => (target as MChapter).downloadSize,
'duration': (visitor, target) => (target as MChapter).duration,
},
setters: {
'name': (visitor, target, value) =>
@ -30,6 +40,16 @@ class MChapterBridge {
(target as MChapter).dateUpload = value as String?,
'scanlator': (visitor, target, value) =>
(target as MChapter).scanlator = value as String?,
'isFiller': (visitor, target, value) =>
(target as MChapter).isFiller = value as bool?,
'thumbnailUrl': (visitor, target, value) =>
(target as MChapter).thumbnailUrl = value as String?,
'description': (visitor, target, value) =>
(target as MChapter).description = value as String?,
'downloadSize': (visitor, target, value) =>
(target as MChapter).downloadSize = value as String?,
'duration': (visitor, target, value) =>
(target as MChapter).duration = value as String?,
},
);
void registerBridgedClasses(D4rt interpreter) {

View file

@ -6,13 +6,41 @@ class MChapter {
String? dateUpload;
String? scanlator;
MChapter({this.name, this.url, this.dateUpload, this.scanlator});
bool? isFiller;
String? thumbnailUrl;
String? description;
/// video size
String? downloadSize;
/// video duration
String? duration;
MChapter({
this.name,
this.url,
this.dateUpload,
this.scanlator,
this.isFiller = false,
this.thumbnailUrl,
this.description,
this.downloadSize,
this.duration,
});
factory MChapter.fromJson(Map<String, dynamic> json) {
return MChapter(
name: json['name'],
url: json['url'],
dateUpload: json['dateUpload'],
scanlator: json['scanlator'],
isFiller: json['isFiller'] ?? false,
thumbnailUrl: json['thumbnailUrl'],
description: json['description'],
downloadSize: json['downloadSize'],
duration: json['duration'],
);
}
Map<String, dynamic> toJson() => {
@ -20,5 +48,10 @@ class MChapter {
'url': url,
'dateUpload': dateUpload,
'scanlator': scanlator,
'isFiller': isFiller,
'thumbnailUrl': thumbnailUrl,
'description': description,
'downloadSize': downloadSize,
'duration': duration,
};
}

View file

@ -335,6 +335,7 @@
"installed": "Installed",
"auto_scroll": "Auto scroll",
"video_audio": "Audio",
"video_audio_info": "Preferred languages, pitch correction, audio channels",
"player": "Player",
"markEpisodeAsSeenSetting": "At what point to mark the episode as seen",
"default_skip_intro_length": "Default Skip intro length",
@ -406,6 +407,7 @@
"torrent_url": "Torrent url",
"or": "OR",
"advanced": "Advanced",
"advanced_info": "mpv config",
"use_native_http_client": "Use native http client",
"use_native_http_client_info": "it automatically supports platform features such VPNs, support more HTTP features such as HTTP/3 and custom redirect handling",
"n_hour_ago": "{hour} hour ago",
@ -458,6 +460,7 @@
"you_have_finished_reading": "You have finished reading",
"return_to_the_list_of_chapters": "Return to the list of chapters",
"hwdec": "Hardware Decoder",
"track_library_navigate": "Go to existing local entry",
"track_library_add": "Add to local library",
"track_library_add_confirm": "Add tracked item to local library",
"track_library_not_logged": "Login to the corresponding tracker to use this feature!",
@ -471,5 +474,40 @@
"rpc_show_cover_image": "Show current cover image in Discord",
"sync_enable_histories": "Sync history data",
"sync_enable_updates": "Sync update data",
"sync_enable_settings": "Sync settings"
"sync_enable_settings": "Sync settings",
"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?",
"custom_buttons": "Custom buttons",
"custom_buttons_info": "Execute Javascript code with custom buttons",
"custom_buttons_edit": "Edit custom buttons",
"custom_buttons_add": "Add custom button",
"custom_buttons_delete": "Delete custom button",
"custom_buttons_text": "Button text",
"custom_buttons_text_req": "Button text required",
"custom_buttons_js_code": "Javascript code",
"custom_buttons_js_code_req": "Javascript code required",
"custom_buttons_js_code_long": "Javascript code (on long press)",
"custom_buttons_startup": "Javascript code (on startup)",
"n_days": "{n} days",
"decoder": "Decoder",
"decoder_info": "Hardware decoding, pixel format, debanding",
"enable_gpu_next": "Enable gpu-next (Android only)",
"enable_gpu_next_info": "A new video rendering backend",
"debanding": "Debanding",
"use_yuv420p": "Use YUV420P pixel format",
"use_yuv420p_info": "May fix black screens on some video codecs, can also improve performance at the cost of quality",
"audio_preferred_languages": "Preferred langauages",
"audio_preferred_languages_info": "Audio langauage(s) to be selected by default on a video with multiple audio streams, 2/3-letter languages codes (e.g.: en, de, fr) work. Multiple values can be delimited by a comma.",
"enable_audio_pitch_correction": "Enable audio pitch correction",
"enable_audio_pitch_correction_info": "Prevents the audio from becoming high-pitched at faster speeds and low-pitched at slower speeds",
"audio_channels": "Audio channels",
"volume_boost_cap": "Volume boost cap",
"internal_player": "Internal player",
"internal_player_info": "Progress, controls, orientation",
"subtitle_delay_text": "Subtitle delay",
"subtitle_delay": "Delay (ms)",
"subtitle_speed": "Speed"
}

View file

@ -2081,6 +2081,12 @@ abstract class AppLocalizations {
/// **'Audio'**
String get video_audio;
/// No description provided for @video_audio_info.
///
/// In en, this message translates to:
/// **'Preferred languages, pitch correction, audio channels'**
String get video_audio_info;
/// No description provided for @player.
///
/// In en, this message translates to:
@ -2507,6 +2513,12 @@ abstract class AppLocalizations {
/// **'Advanced'**
String get advanced;
/// No description provided for @advanced_info.
///
/// In en, this message translates to:
/// **'mpv config'**
String get advanced_info;
/// No description provided for @use_native_http_client.
///
/// In en, this message translates to:
@ -2819,6 +2831,12 @@ abstract class AppLocalizations {
/// **'Hardware Decoder'**
String get hwdec;
/// No description provided for @track_library_navigate.
///
/// In en, this message translates to:
/// **'Go to existing local entry'**
String get track_library_navigate;
/// No description provided for @track_library_add.
///
/// In en, this message translates to:
@ -2902,6 +2920,216 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Sync settings'**
String get sync_enable_settings;
/// No description provided for @enable_mpv.
///
/// In en, this message translates to:
/// **'Enable mpv shaders / scripts'**
String get enable_mpv;
/// No description provided for @mpv_info.
///
/// In en, this message translates to:
/// **'Supports .js scripts under mpv/scripts/'**
String get mpv_info;
/// 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 mpv_download;
/// No description provided for @custom_buttons.
///
/// In en, this message translates to:
/// **'Custom buttons'**
String get custom_buttons;
/// No description provided for @custom_buttons_info.
///
/// In en, this message translates to:
/// **'Execute Javascript code with custom buttons'**
String get custom_buttons_info;
/// No description provided for @custom_buttons_edit.
///
/// In en, this message translates to:
/// **'Edit custom buttons'**
String get custom_buttons_edit;
/// No description provided for @custom_buttons_add.
///
/// In en, this message translates to:
/// **'Add custom button'**
String get custom_buttons_add;
/// No description provided for @custom_buttons_delete.
///
/// In en, this message translates to:
/// **'Delete custom button'**
String get custom_buttons_delete;
/// No description provided for @custom_buttons_text.
///
/// In en, this message translates to:
/// **'Button text'**
String get custom_buttons_text;
/// No description provided for @custom_buttons_text_req.
///
/// In en, this message translates to:
/// **'Button text required'**
String get custom_buttons_text_req;
/// No description provided for @custom_buttons_js_code.
///
/// In en, this message translates to:
/// **'Javascript code'**
String get custom_buttons_js_code;
/// No description provided for @custom_buttons_js_code_req.
///
/// In en, this message translates to:
/// **'Javascript code required'**
String get custom_buttons_js_code_req;
/// No description provided for @custom_buttons_js_code_long.
///
/// In en, this message translates to:
/// **'Javascript code (on long press)'**
String get custom_buttons_js_code_long;
/// No description provided for @custom_buttons_startup.
///
/// In en, this message translates to:
/// **'Javascript code (on startup)'**
String get custom_buttons_startup;
/// No description provided for @n_days.
///
/// In en, this message translates to:
/// **'{n} days'**
String n_days(Object n);
/// No description provided for @decoder.
///
/// In en, this message translates to:
/// **'Decoder'**
String get decoder;
/// No description provided for @decoder_info.
///
/// In en, this message translates to:
/// **'Hardware decoding, pixel format, debanding'**
String get decoder_info;
/// No description provided for @enable_gpu_next.
///
/// In en, this message translates to:
/// **'Enable gpu-next (Android only)'**
String get enable_gpu_next;
/// No description provided for @enable_gpu_next_info.
///
/// In en, this message translates to:
/// **'A new video rendering backend'**
String get enable_gpu_next_info;
/// No description provided for @debanding.
///
/// In en, this message translates to:
/// **'Debanding'**
String get debanding;
/// No description provided for @use_yuv420p.
///
/// In en, this message translates to:
/// **'Use YUV420P pixel format'**
String get use_yuv420p;
/// No description provided for @use_yuv420p_info.
///
/// In en, this message translates to:
/// **'May fix black screens on some video codecs, can also improve performance at the cost of quality'**
String get use_yuv420p_info;
/// No description provided for @audio_preferred_languages.
///
/// In en, this message translates to:
/// **'Preferred langauages'**
String get audio_preferred_languages;
/// No description provided for @audio_preferred_languages_info.
///
/// In en, this message translates to:
/// **'Audio langauage(s) to be selected by default on a video with multiple audio streams, 2/3-letter languages codes (e.g.: en, de, fr) work. Multiple values can be delimited by a comma.'**
String get audio_preferred_languages_info;
/// No description provided for @enable_audio_pitch_correction.
///
/// In en, this message translates to:
/// **'Enable audio pitch correction'**
String get enable_audio_pitch_correction;
/// No description provided for @enable_audio_pitch_correction_info.
///
/// In en, this message translates to:
/// **'Prevents the audio from becoming high-pitched at faster speeds and low-pitched at slower speeds'**
String get enable_audio_pitch_correction_info;
/// No description provided for @audio_channels.
///
/// In en, this message translates to:
/// **'Audio channels'**
String get audio_channels;
/// No description provided for @volume_boost_cap.
///
/// In en, this message translates to:
/// **'Volume boost cap'**
String get volume_boost_cap;
/// No description provided for @internal_player.
///
/// In en, this message translates to:
/// **'Internal player'**
String get internal_player;
/// No description provided for @internal_player_info.
///
/// In en, this message translates to:
/// **'Progress, controls, orientation'**
String get internal_player_info;
/// No description provided for @subtitle_delay_text.
///
/// In en, this message translates to:
/// **'Subtitle delay'**
String get subtitle_delay_text;
/// No description provided for @subtitle_delay.
///
/// In en, this message translates to:
/// **'Delay (ms)'**
String get subtitle_delay;
/// No description provided for @subtitle_speed.
///
/// In en, this message translates to:
/// **'Speed'**
String get subtitle_speed;
}
class _AppLocalizationsDelegate

View file

@ -1050,6 +1050,10 @@ class AppLocalizationsAr extends AppLocalizations {
@override
String get video_audio => 'الصوت';
@override
String get video_audio_info =>
'Preferred languages, pitch correction, audio channels';
@override
String get player => 'لاعب';
@ -1272,6 +1276,9 @@ class AppLocalizationsAr extends AppLocalizations {
@override
String get advanced => 'متقدم';
@override
String get advanced_info => 'mpv config';
@override
String get use_native_http_client => 'استخدام عميل HTTP الأصلي';
@ -1450,6 +1457,9 @@ class AppLocalizationsAr extends AppLocalizations {
@override
String get hwdec => 'Hardware Decoder';
@override
String get track_library_navigate => 'Go to existing local entry';
@override
String get track_library_add => 'Add to local library';
@ -1494,4 +1504,115 @@ class AppLocalizationsAr extends AppLocalizations {
@override
String get sync_enable_settings => 'Sync settings';
@override
String get enable_mpv => 'Enable mpv shaders / scripts';
@override
String get mpv_info => 'Supports .js scripts under mpv/scripts/';
@override
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 get custom_buttons => 'Custom buttons';
@override
String get custom_buttons_info =>
'Execute Javascript code with custom buttons';
@override
String get custom_buttons_edit => 'Edit custom buttons';
@override
String get custom_buttons_add => 'Add custom button';
@override
String get custom_buttons_delete => 'Delete custom button';
@override
String get custom_buttons_text => 'Button text';
@override
String get custom_buttons_text_req => 'Button text required';
@override
String get custom_buttons_js_code => 'Javascript code';
@override
String get custom_buttons_js_code_req => 'Javascript code required';
@override
String get custom_buttons_js_code_long => 'Javascript code (on long press)';
@override
String get custom_buttons_startup => 'Javascript code (on startup)';
@override
String n_days(Object n) {
return '$n days';
}
@override
String get decoder => 'Decoder';
@override
String get decoder_info => 'Hardware decoding, pixel format, debanding';
@override
String get enable_gpu_next => 'Enable gpu-next (Android only)';
@override
String get enable_gpu_next_info => 'A new video rendering backend';
@override
String get debanding => 'Debanding';
@override
String get use_yuv420p => 'Use YUV420P pixel format';
@override
String get use_yuv420p_info =>
'May fix black screens on some video codecs, can also improve performance at the cost of quality';
@override
String get audio_preferred_languages => 'Preferred langauages';
@override
String get audio_preferred_languages_info =>
'Audio langauage(s) to be selected by default on a video with multiple audio streams, 2/3-letter languages codes (e.g.: en, de, fr) work. Multiple values can be delimited by a comma.';
@override
String get enable_audio_pitch_correction => 'Enable audio pitch correction';
@override
String get enable_audio_pitch_correction_info =>
'Prevents the audio from becoming high-pitched at faster speeds and low-pitched at slower speeds';
@override
String get audio_channels => 'Audio channels';
@override
String get volume_boost_cap => 'Volume boost cap';
@override
String get internal_player => 'Internal player';
@override
String get internal_player_info => 'Progress, controls, orientation';
@override
String get subtitle_delay_text => 'Subtitle delay';
@override
String get subtitle_delay => 'Delay (ms)';
@override
String get subtitle_speed => 'Speed';
}

View file

@ -1055,6 +1055,10 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get video_audio => 'Audio';
@override
String get video_audio_info =>
'Preferred languages, pitch correction, audio channels';
@override
String get player => 'Player';
@ -1282,6 +1286,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get advanced => 'Erweitert';
@override
String get advanced_info => 'mpv config';
@override
String get use_native_http_client => 'Nativen HTTP-Client verwenden';
@ -1462,6 +1469,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get hwdec => 'Hardware Decoder';
@override
String get track_library_navigate => 'Go to existing local entry';
@override
String get track_library_add => 'Zur lokalen Bibliothek hinzufügen';
@ -1507,4 +1517,115 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get sync_enable_settings => 'Sync settings';
@override
String get enable_mpv => 'Enable mpv shaders / scripts';
@override
String get mpv_info => 'Supports .js scripts under mpv/scripts/';
@override
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 get custom_buttons => 'Custom buttons';
@override
String get custom_buttons_info =>
'Execute Javascript code with custom buttons';
@override
String get custom_buttons_edit => 'Edit custom buttons';
@override
String get custom_buttons_add => 'Add custom button';
@override
String get custom_buttons_delete => 'Delete custom button';
@override
String get custom_buttons_text => 'Button text';
@override
String get custom_buttons_text_req => 'Button text required';
@override
String get custom_buttons_js_code => 'Javascript code';
@override
String get custom_buttons_js_code_req => 'Javascript code required';
@override
String get custom_buttons_js_code_long => 'Javascript code (on long press)';
@override
String get custom_buttons_startup => 'Javascript code (on startup)';
@override
String n_days(Object n) {
return '$n days';
}
@override
String get decoder => 'Decoder';
@override
String get decoder_info => 'Hardware decoding, pixel format, debanding';
@override
String get enable_gpu_next => 'Enable gpu-next (Android only)';
@override
String get enable_gpu_next_info => 'A new video rendering backend';
@override
String get debanding => 'Debanding';
@override
String get use_yuv420p => 'Use YUV420P pixel format';
@override
String get use_yuv420p_info =>
'May fix black screens on some video codecs, can also improve performance at the cost of quality';
@override
String get audio_preferred_languages => 'Preferred langauages';
@override
String get audio_preferred_languages_info =>
'Audio langauage(s) to be selected by default on a video with multiple audio streams, 2/3-letter languages codes (e.g.: en, de, fr) work. Multiple values can be delimited by a comma.';
@override
String get enable_audio_pitch_correction => 'Enable audio pitch correction';
@override
String get enable_audio_pitch_correction_info =>
'Prevents the audio from becoming high-pitched at faster speeds and low-pitched at slower speeds';
@override
String get audio_channels => 'Audio channels';
@override
String get volume_boost_cap => 'Volume boost cap';
@override
String get internal_player => 'Internal player';
@override
String get internal_player_info => 'Progress, controls, orientation';
@override
String get subtitle_delay_text => 'Subtitle delay';
@override
String get subtitle_delay => 'Delay (ms)';
@override
String get subtitle_speed => 'Speed';
}

View file

@ -1049,6 +1049,10 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get video_audio => 'Audio';
@override
String get video_audio_info =>
'Preferred languages, pitch correction, audio channels';
@override
String get player => 'Player';
@ -1272,6 +1276,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get advanced => 'Advanced';
@override
String get advanced_info => 'mpv config';
@override
String get use_native_http_client => 'Use native http client';
@ -1451,6 +1458,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get hwdec => 'Hardware Decoder';
@override
String get track_library_navigate => 'Go to existing local entry';
@override
String get track_library_add => 'Add to local library';
@ -1495,4 +1505,115 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get sync_enable_settings => 'Sync settings';
@override
String get enable_mpv => 'Enable mpv shaders / scripts';
@override
String get mpv_info => 'Supports .js scripts under mpv/scripts/';
@override
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 get custom_buttons => 'Custom buttons';
@override
String get custom_buttons_info =>
'Execute Javascript code with custom buttons';
@override
String get custom_buttons_edit => 'Edit custom buttons';
@override
String get custom_buttons_add => 'Add custom button';
@override
String get custom_buttons_delete => 'Delete custom button';
@override
String get custom_buttons_text => 'Button text';
@override
String get custom_buttons_text_req => 'Button text required';
@override
String get custom_buttons_js_code => 'Javascript code';
@override
String get custom_buttons_js_code_req => 'Javascript code required';
@override
String get custom_buttons_js_code_long => 'Javascript code (on long press)';
@override
String get custom_buttons_startup => 'Javascript code (on startup)';
@override
String n_days(Object n) {
return '$n days';
}
@override
String get decoder => 'Decoder';
@override
String get decoder_info => 'Hardware decoding, pixel format, debanding';
@override
String get enable_gpu_next => 'Enable gpu-next (Android only)';
@override
String get enable_gpu_next_info => 'A new video rendering backend';
@override
String get debanding => 'Debanding';
@override
String get use_yuv420p => 'Use YUV420P pixel format';
@override
String get use_yuv420p_info =>
'May fix black screens on some video codecs, can also improve performance at the cost of quality';
@override
String get audio_preferred_languages => 'Preferred langauages';
@override
String get audio_preferred_languages_info =>
'Audio langauage(s) to be selected by default on a video with multiple audio streams, 2/3-letter languages codes (e.g.: en, de, fr) work. Multiple values can be delimited by a comma.';
@override
String get enable_audio_pitch_correction => 'Enable audio pitch correction';
@override
String get enable_audio_pitch_correction_info =>
'Prevents the audio from becoming high-pitched at faster speeds and low-pitched at slower speeds';
@override
String get audio_channels => 'Audio channels';
@override
String get volume_boost_cap => 'Volume boost cap';
@override
String get internal_player => 'Internal player';
@override
String get internal_player_info => 'Progress, controls, orientation';
@override
String get subtitle_delay_text => 'Subtitle delay';
@override
String get subtitle_delay => 'Delay (ms)';
@override
String get subtitle_speed => 'Speed';
}

View file

@ -1059,6 +1059,10 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get video_audio => 'Audio';
@override
String get video_audio_info =>
'Preferred languages, pitch correction, audio channels';
@override
String get player => 'Jugador';
@ -1286,6 +1290,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get advanced => 'Avanzado';
@override
String get advanced_info => 'mpv config';
@override
String get use_native_http_client => 'Utilizar cliente HTTP nativo';
@ -1468,6 +1475,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get hwdec => 'Hardware Decoder';
@override
String get track_library_navigate => 'Go to existing local entry';
@override
String get track_library_add => 'Add to local library';
@ -1512,6 +1522,117 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get sync_enable_settings => 'Sync settings';
@override
String get enable_mpv => 'Enable mpv shaders / scripts';
@override
String get mpv_info => 'Supports .js scripts under mpv/scripts/';
@override
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 get custom_buttons => 'Custom buttons';
@override
String get custom_buttons_info =>
'Execute Javascript code with custom buttons';
@override
String get custom_buttons_edit => 'Edit custom buttons';
@override
String get custom_buttons_add => 'Add custom button';
@override
String get custom_buttons_delete => 'Delete custom button';
@override
String get custom_buttons_text => 'Button text';
@override
String get custom_buttons_text_req => 'Button text required';
@override
String get custom_buttons_js_code => 'Javascript code';
@override
String get custom_buttons_js_code_req => 'Javascript code required';
@override
String get custom_buttons_js_code_long => 'Javascript code (on long press)';
@override
String get custom_buttons_startup => 'Javascript code (on startup)';
@override
String n_days(Object n) {
return '$n days';
}
@override
String get decoder => 'Decoder';
@override
String get decoder_info => 'Hardware decoding, pixel format, debanding';
@override
String get enable_gpu_next => 'Enable gpu-next (Android only)';
@override
String get enable_gpu_next_info => 'A new video rendering backend';
@override
String get debanding => 'Debanding';
@override
String get use_yuv420p => 'Use YUV420P pixel format';
@override
String get use_yuv420p_info =>
'May fix black screens on some video codecs, can also improve performance at the cost of quality';
@override
String get audio_preferred_languages => 'Preferred langauages';
@override
String get audio_preferred_languages_info =>
'Audio langauage(s) to be selected by default on a video with multiple audio streams, 2/3-letter languages codes (e.g.: en, de, fr) work. Multiple values can be delimited by a comma.';
@override
String get enable_audio_pitch_correction => 'Enable audio pitch correction';
@override
String get enable_audio_pitch_correction_info =>
'Prevents the audio from becoming high-pitched at faster speeds and low-pitched at slower speeds';
@override
String get audio_channels => 'Audio channels';
@override
String get volume_boost_cap => 'Volume boost cap';
@override
String get internal_player => 'Internal player';
@override
String get internal_player_info => 'Progress, controls, orientation';
@override
String get subtitle_delay_text => 'Subtitle delay';
@override
String get subtitle_delay => 'Delay (ms)';
@override
String get subtitle_speed => 'Speed';
}
/// The translations for Spanish Castilian, as used in Latin America and the Caribbean (`es_419`).

View file

@ -1061,6 +1061,10 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get video_audio => 'Audio';
@override
String get video_audio_info =>
'Preferred languages, pitch correction, audio channels';
@override
String get player => 'Lecteur';
@ -1288,6 +1292,9 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get advanced => 'Avancé';
@override
String get advanced_info => 'mpv config';
@override
String get use_native_http_client => 'Utiliser le client HTTP natif';
@ -1469,6 +1476,9 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get hwdec => 'Hardware Decoder';
@override
String get track_library_navigate => 'Go to existing local entry';
@override
String get track_library_add => 'Add to local library';
@ -1513,4 +1523,115 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get sync_enable_settings => 'Sync settings';
@override
String get enable_mpv => 'Enable mpv shaders / scripts';
@override
String get mpv_info => 'Supports .js scripts under mpv/scripts/';
@override
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 get custom_buttons => 'Custom buttons';
@override
String get custom_buttons_info =>
'Execute Javascript code with custom buttons';
@override
String get custom_buttons_edit => 'Edit custom buttons';
@override
String get custom_buttons_add => 'Add custom button';
@override
String get custom_buttons_delete => 'Delete custom button';
@override
String get custom_buttons_text => 'Button text';
@override
String get custom_buttons_text_req => 'Button text required';
@override
String get custom_buttons_js_code => 'Javascript code';
@override
String get custom_buttons_js_code_req => 'Javascript code required';
@override
String get custom_buttons_js_code_long => 'Javascript code (on long press)';
@override
String get custom_buttons_startup => 'Javascript code (on startup)';
@override
String n_days(Object n) {
return '$n days';
}
@override
String get decoder => 'Decoder';
@override
String get decoder_info => 'Hardware decoding, pixel format, debanding';
@override
String get enable_gpu_next => 'Enable gpu-next (Android only)';
@override
String get enable_gpu_next_info => 'A new video rendering backend';
@override
String get debanding => 'Debanding';
@override
String get use_yuv420p => 'Use YUV420P pixel format';
@override
String get use_yuv420p_info =>
'May fix black screens on some video codecs, can also improve performance at the cost of quality';
@override
String get audio_preferred_languages => 'Preferred langauages';
@override
String get audio_preferred_languages_info =>
'Audio langauage(s) to be selected by default on a video with multiple audio streams, 2/3-letter languages codes (e.g.: en, de, fr) work. Multiple values can be delimited by a comma.';
@override
String get enable_audio_pitch_correction => 'Enable audio pitch correction';
@override
String get enable_audio_pitch_correction_info =>
'Prevents the audio from becoming high-pitched at faster speeds and low-pitched at slower speeds';
@override
String get audio_channels => 'Audio channels';
@override
String get volume_boost_cap => 'Volume boost cap';
@override
String get internal_player => 'Internal player';
@override
String get internal_player_info => 'Progress, controls, orientation';
@override
String get subtitle_delay_text => 'Subtitle delay';
@override
String get subtitle_delay => 'Delay (ms)';
@override
String get subtitle_speed => 'Speed';
}

View file

@ -1055,6 +1055,10 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get video_audio => 'Audio';
@override
String get video_audio_info =>
'Preferred languages, pitch correction, audio channels';
@override
String get player => 'Pemain';
@ -1279,6 +1283,9 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get advanced => 'Lanjutan';
@override
String get advanced_info => 'mpv config';
@override
String get use_native_http_client => 'Gunakan klien http asli';
@ -1457,6 +1464,9 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get hwdec => 'Hardware Decoder';
@override
String get track_library_navigate => 'Go to existing local entry';
@override
String get track_library_add => 'Add to local library';
@ -1501,4 +1511,115 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get sync_enable_settings => 'Sync settings';
@override
String get enable_mpv => 'Enable mpv shaders / scripts';
@override
String get mpv_info => 'Supports .js scripts under mpv/scripts/';
@override
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 get custom_buttons => 'Custom buttons';
@override
String get custom_buttons_info =>
'Execute Javascript code with custom buttons';
@override
String get custom_buttons_edit => 'Edit custom buttons';
@override
String get custom_buttons_add => 'Add custom button';
@override
String get custom_buttons_delete => 'Delete custom button';
@override
String get custom_buttons_text => 'Button text';
@override
String get custom_buttons_text_req => 'Button text required';
@override
String get custom_buttons_js_code => 'Javascript code';
@override
String get custom_buttons_js_code_req => 'Javascript code required';
@override
String get custom_buttons_js_code_long => 'Javascript code (on long press)';
@override
String get custom_buttons_startup => 'Javascript code (on startup)';
@override
String n_days(Object n) {
return '$n days';
}
@override
String get decoder => 'Decoder';
@override
String get decoder_info => 'Hardware decoding, pixel format, debanding';
@override
String get enable_gpu_next => 'Enable gpu-next (Android only)';
@override
String get enable_gpu_next_info => 'A new video rendering backend';
@override
String get debanding => 'Debanding';
@override
String get use_yuv420p => 'Use YUV420P pixel format';
@override
String get use_yuv420p_info =>
'May fix black screens on some video codecs, can also improve performance at the cost of quality';
@override
String get audio_preferred_languages => 'Preferred langauages';
@override
String get audio_preferred_languages_info =>
'Audio langauage(s) to be selected by default on a video with multiple audio streams, 2/3-letter languages codes (e.g.: en, de, fr) work. Multiple values can be delimited by a comma.';
@override
String get enable_audio_pitch_correction => 'Enable audio pitch correction';
@override
String get enable_audio_pitch_correction_info =>
'Prevents the audio from becoming high-pitched at faster speeds and low-pitched at slower speeds';
@override
String get audio_channels => 'Audio channels';
@override
String get volume_boost_cap => 'Volume boost cap';
@override
String get internal_player => 'Internal player';
@override
String get internal_player_info => 'Progress, controls, orientation';
@override
String get subtitle_delay_text => 'Subtitle delay';
@override
String get subtitle_delay => 'Delay (ms)';
@override
String get subtitle_speed => 'Speed';
}

View file

@ -1058,6 +1058,10 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get video_audio => 'Audio';
@override
String get video_audio_info =>
'Preferred languages, pitch correction, audio channels';
@override
String get player => 'Giocatore';
@ -1286,6 +1290,9 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get advanced => 'Avanzate';
@override
String get advanced_info => 'mpv config';
@override
String get use_native_http_client => 'Usa il client HTTP nativo';
@ -1466,6 +1473,9 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get hwdec => 'Hardware Decoder';
@override
String get track_library_navigate => 'Go to existing local entry';
@override
String get track_library_add => 'Add to local library';
@ -1510,4 +1520,115 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get sync_enable_settings => 'Sync settings';
@override
String get enable_mpv => 'Enable mpv shaders / scripts';
@override
String get mpv_info => 'Supports .js scripts under mpv/scripts/';
@override
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 get custom_buttons => 'Custom buttons';
@override
String get custom_buttons_info =>
'Execute Javascript code with custom buttons';
@override
String get custom_buttons_edit => 'Edit custom buttons';
@override
String get custom_buttons_add => 'Add custom button';
@override
String get custom_buttons_delete => 'Delete custom button';
@override
String get custom_buttons_text => 'Button text';
@override
String get custom_buttons_text_req => 'Button text required';
@override
String get custom_buttons_js_code => 'Javascript code';
@override
String get custom_buttons_js_code_req => 'Javascript code required';
@override
String get custom_buttons_js_code_long => 'Javascript code (on long press)';
@override
String get custom_buttons_startup => 'Javascript code (on startup)';
@override
String n_days(Object n) {
return '$n days';
}
@override
String get decoder => 'Decoder';
@override
String get decoder_info => 'Hardware decoding, pixel format, debanding';
@override
String get enable_gpu_next => 'Enable gpu-next (Android only)';
@override
String get enable_gpu_next_info => 'A new video rendering backend';
@override
String get debanding => 'Debanding';
@override
String get use_yuv420p => 'Use YUV420P pixel format';
@override
String get use_yuv420p_info =>
'May fix black screens on some video codecs, can also improve performance at the cost of quality';
@override
String get audio_preferred_languages => 'Preferred langauages';
@override
String get audio_preferred_languages_info =>
'Audio langauage(s) to be selected by default on a video with multiple audio streams, 2/3-letter languages codes (e.g.: en, de, fr) work. Multiple values can be delimited by a comma.';
@override
String get enable_audio_pitch_correction => 'Enable audio pitch correction';
@override
String get enable_audio_pitch_correction_info =>
'Prevents the audio from becoming high-pitched at faster speeds and low-pitched at slower speeds';
@override
String get audio_channels => 'Audio channels';
@override
String get volume_boost_cap => 'Volume boost cap';
@override
String get internal_player => 'Internal player';
@override
String get internal_player_info => 'Progress, controls, orientation';
@override
String get subtitle_delay_text => 'Subtitle delay';
@override
String get subtitle_delay => 'Delay (ms)';
@override
String get subtitle_speed => 'Speed';
}

View file

@ -1057,6 +1057,10 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get video_audio => 'Áudio';
@override
String get video_audio_info =>
'Preferred languages, pitch correction, audio channels';
@override
String get player => 'Jogador';
@ -1283,6 +1287,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get advanced => 'Avançado';
@override
String get advanced_info => 'mpv config';
@override
String get use_native_http_client => 'Usar cliente HTTP nativo';
@ -1465,6 +1472,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get hwdec => 'Hardware Decoder';
@override
String get track_library_navigate => 'Go to existing local entry';
@override
String get track_library_add => 'Add to local library';
@ -1509,6 +1519,117 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get sync_enable_settings => 'Sync settings';
@override
String get enable_mpv => 'Enable mpv shaders / scripts';
@override
String get mpv_info => 'Supports .js scripts under mpv/scripts/';
@override
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 get custom_buttons => 'Custom buttons';
@override
String get custom_buttons_info =>
'Execute Javascript code with custom buttons';
@override
String get custom_buttons_edit => 'Edit custom buttons';
@override
String get custom_buttons_add => 'Add custom button';
@override
String get custom_buttons_delete => 'Delete custom button';
@override
String get custom_buttons_text => 'Button text';
@override
String get custom_buttons_text_req => 'Button text required';
@override
String get custom_buttons_js_code => 'Javascript code';
@override
String get custom_buttons_js_code_req => 'Javascript code required';
@override
String get custom_buttons_js_code_long => 'Javascript code (on long press)';
@override
String get custom_buttons_startup => 'Javascript code (on startup)';
@override
String n_days(Object n) {
return '$n days';
}
@override
String get decoder => 'Decoder';
@override
String get decoder_info => 'Hardware decoding, pixel format, debanding';
@override
String get enable_gpu_next => 'Enable gpu-next (Android only)';
@override
String get enable_gpu_next_info => 'A new video rendering backend';
@override
String get debanding => 'Debanding';
@override
String get use_yuv420p => 'Use YUV420P pixel format';
@override
String get use_yuv420p_info =>
'May fix black screens on some video codecs, can also improve performance at the cost of quality';
@override
String get audio_preferred_languages => 'Preferred langauages';
@override
String get audio_preferred_languages_info =>
'Audio langauage(s) to be selected by default on a video with multiple audio streams, 2/3-letter languages codes (e.g.: en, de, fr) work. Multiple values can be delimited by a comma.';
@override
String get enable_audio_pitch_correction => 'Enable audio pitch correction';
@override
String get enable_audio_pitch_correction_info =>
'Prevents the audio from becoming high-pitched at faster speeds and low-pitched at slower speeds';
@override
String get audio_channels => 'Audio channels';
@override
String get volume_boost_cap => 'Volume boost cap';
@override
String get internal_player => 'Internal player';
@override
String get internal_player_info => 'Progress, controls, orientation';
@override
String get subtitle_delay_text => 'Subtitle delay';
@override
String get subtitle_delay => 'Delay (ms)';
@override
String get subtitle_speed => 'Speed';
}
/// The translations for Portuguese, as used in Brazil (`pt_BR`).

View file

@ -1060,6 +1060,10 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get video_audio => 'Аудио';
@override
String get video_audio_info =>
'Preferred languages, pitch correction, audio channels';
@override
String get player => 'Игрок';
@ -1286,6 +1290,9 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get advanced => 'Продвинутые';
@override
String get advanced_info => 'mpv config';
@override
String get use_native_http_client => 'Использовать нативный HTTP-клиент';
@ -1467,6 +1474,9 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get hwdec => 'Hardware Decoder';
@override
String get track_library_navigate => 'Go to existing local entry';
@override
String get track_library_add => 'Add to local library';
@ -1511,4 +1521,115 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get sync_enable_settings => 'Sync settings';
@override
String get enable_mpv => 'Enable mpv shaders / scripts';
@override
String get mpv_info => 'Supports .js scripts under mpv/scripts/';
@override
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 get custom_buttons => 'Custom buttons';
@override
String get custom_buttons_info =>
'Execute Javascript code with custom buttons';
@override
String get custom_buttons_edit => 'Edit custom buttons';
@override
String get custom_buttons_add => 'Add custom button';
@override
String get custom_buttons_delete => 'Delete custom button';
@override
String get custom_buttons_text => 'Button text';
@override
String get custom_buttons_text_req => 'Button text required';
@override
String get custom_buttons_js_code => 'Javascript code';
@override
String get custom_buttons_js_code_req => 'Javascript code required';
@override
String get custom_buttons_js_code_long => 'Javascript code (on long press)';
@override
String get custom_buttons_startup => 'Javascript code (on startup)';
@override
String n_days(Object n) {
return '$n days';
}
@override
String get decoder => 'Decoder';
@override
String get decoder_info => 'Hardware decoding, pixel format, debanding';
@override
String get enable_gpu_next => 'Enable gpu-next (Android only)';
@override
String get enable_gpu_next_info => 'A new video rendering backend';
@override
String get debanding => 'Debanding';
@override
String get use_yuv420p => 'Use YUV420P pixel format';
@override
String get use_yuv420p_info =>
'May fix black screens on some video codecs, can also improve performance at the cost of quality';
@override
String get audio_preferred_languages => 'Preferred langauages';
@override
String get audio_preferred_languages_info =>
'Audio langauage(s) to be selected by default on a video with multiple audio streams, 2/3-letter languages codes (e.g.: en, de, fr) work. Multiple values can be delimited by a comma.';
@override
String get enable_audio_pitch_correction => 'Enable audio pitch correction';
@override
String get enable_audio_pitch_correction_info =>
'Prevents the audio from becoming high-pitched at faster speeds and low-pitched at slower speeds';
@override
String get audio_channels => 'Audio channels';
@override
String get volume_boost_cap => 'Volume boost cap';
@override
String get internal_player => 'Internal player';
@override
String get internal_player_info => 'Progress, controls, orientation';
@override
String get subtitle_delay_text => 'Subtitle delay';
@override
String get subtitle_delay => 'Delay (ms)';
@override
String get subtitle_speed => 'Speed';
}

View file

@ -1049,6 +1049,10 @@ class AppLocalizationsTh extends AppLocalizations {
@override
String get video_audio => 'เสียง';
@override
String get video_audio_info =>
'Preferred languages, pitch correction, audio channels';
@override
String get player => 'ตัวเล่น';
@ -1273,6 +1277,9 @@ class AppLocalizationsTh extends AppLocalizations {
@override
String get advanced => 'ขั้นสูง';
@override
String get advanced_info => 'mpv config';
@override
String get use_native_http_client => 'ใช้ไคลเอนต์ HTTP พื้นเมือง';
@ -1451,6 +1458,9 @@ class AppLocalizationsTh extends AppLocalizations {
@override
String get hwdec => 'Hardware Decoder';
@override
String get track_library_navigate => 'Go to existing local entry';
@override
String get track_library_add => 'Add to local library';
@ -1495,4 +1505,115 @@ class AppLocalizationsTh extends AppLocalizations {
@override
String get sync_enable_settings => 'Sync settings';
@override
String get enable_mpv => 'Enable mpv shaders / scripts';
@override
String get mpv_info => 'Supports .js scripts under mpv/scripts/';
@override
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 get custom_buttons => 'Custom buttons';
@override
String get custom_buttons_info =>
'Execute Javascript code with custom buttons';
@override
String get custom_buttons_edit => 'Edit custom buttons';
@override
String get custom_buttons_add => 'Add custom button';
@override
String get custom_buttons_delete => 'Delete custom button';
@override
String get custom_buttons_text => 'Button text';
@override
String get custom_buttons_text_req => 'Button text required';
@override
String get custom_buttons_js_code => 'Javascript code';
@override
String get custom_buttons_js_code_req => 'Javascript code required';
@override
String get custom_buttons_js_code_long => 'Javascript code (on long press)';
@override
String get custom_buttons_startup => 'Javascript code (on startup)';
@override
String n_days(Object n) {
return '$n days';
}
@override
String get decoder => 'Decoder';
@override
String get decoder_info => 'Hardware decoding, pixel format, debanding';
@override
String get enable_gpu_next => 'Enable gpu-next (Android only)';
@override
String get enable_gpu_next_info => 'A new video rendering backend';
@override
String get debanding => 'Debanding';
@override
String get use_yuv420p => 'Use YUV420P pixel format';
@override
String get use_yuv420p_info =>
'May fix black screens on some video codecs, can also improve performance at the cost of quality';
@override
String get audio_preferred_languages => 'Preferred langauages';
@override
String get audio_preferred_languages_info =>
'Audio langauage(s) to be selected by default on a video with multiple audio streams, 2/3-letter languages codes (e.g.: en, de, fr) work. Multiple values can be delimited by a comma.';
@override
String get enable_audio_pitch_correction => 'Enable audio pitch correction';
@override
String get enable_audio_pitch_correction_info =>
'Prevents the audio from becoming high-pitched at faster speeds and low-pitched at slower speeds';
@override
String get audio_channels => 'Audio channels';
@override
String get volume_boost_cap => 'Volume boost cap';
@override
String get internal_player => 'Internal player';
@override
String get internal_player_info => 'Progress, controls, orientation';
@override
String get subtitle_delay_text => 'Subtitle delay';
@override
String get subtitle_delay => 'Delay (ms)';
@override
String get subtitle_speed => 'Speed';
}

View file

@ -1053,6 +1053,10 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get video_audio => 'Ses';
@override
String get video_audio_info =>
'Preferred languages, pitch correction, audio channels';
@override
String get player => 'Oyuncu';
@ -1278,6 +1282,9 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get advanced => 'Gelişmiş';
@override
String get advanced_info => 'mpv config';
@override
String get use_native_http_client => 'Yerel http istemcisini kullan';
@ -1457,6 +1464,9 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get hwdec => 'Hardware Decoder';
@override
String get track_library_navigate => 'Go to existing local entry';
@override
String get track_library_add => 'Add to local library';
@ -1501,4 +1511,115 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get sync_enable_settings => 'Sync settings';
@override
String get enable_mpv => 'Enable mpv shaders / scripts';
@override
String get mpv_info => 'Supports .js scripts under mpv/scripts/';
@override
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 get custom_buttons => 'Custom buttons';
@override
String get custom_buttons_info =>
'Execute Javascript code with custom buttons';
@override
String get custom_buttons_edit => 'Edit custom buttons';
@override
String get custom_buttons_add => 'Add custom button';
@override
String get custom_buttons_delete => 'Delete custom button';
@override
String get custom_buttons_text => 'Button text';
@override
String get custom_buttons_text_req => 'Button text required';
@override
String get custom_buttons_js_code => 'Javascript code';
@override
String get custom_buttons_js_code_req => 'Javascript code required';
@override
String get custom_buttons_js_code_long => 'Javascript code (on long press)';
@override
String get custom_buttons_startup => 'Javascript code (on startup)';
@override
String n_days(Object n) {
return '$n days';
}
@override
String get decoder => 'Decoder';
@override
String get decoder_info => 'Hardware decoding, pixel format, debanding';
@override
String get enable_gpu_next => 'Enable gpu-next (Android only)';
@override
String get enable_gpu_next_info => 'A new video rendering backend';
@override
String get debanding => 'Debanding';
@override
String get use_yuv420p => 'Use YUV420P pixel format';
@override
String get use_yuv420p_info =>
'May fix black screens on some video codecs, can also improve performance at the cost of quality';
@override
String get audio_preferred_languages => 'Preferred langauages';
@override
String get audio_preferred_languages_info =>
'Audio langauage(s) to be selected by default on a video with multiple audio streams, 2/3-letter languages codes (e.g.: en, de, fr) work. Multiple values can be delimited by a comma.';
@override
String get enable_audio_pitch_correction => 'Enable audio pitch correction';
@override
String get enable_audio_pitch_correction_info =>
'Prevents the audio from becoming high-pitched at faster speeds and low-pitched at slower speeds';
@override
String get audio_channels => 'Audio channels';
@override
String get volume_boost_cap => 'Volume boost cap';
@override
String get internal_player => 'Internal player';
@override
String get internal_player_info => 'Progress, controls, orientation';
@override
String get subtitle_delay_text => 'Subtitle delay';
@override
String get subtitle_delay => 'Delay (ms)';
@override
String get subtitle_speed => 'Speed';
}

View file

@ -1033,6 +1033,10 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get video_audio => '音频';
@override
String get video_audio_info =>
'Preferred languages, pitch correction, audio channels';
@override
String get player => '播放器';
@ -1250,6 +1254,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get advanced => '高级';
@override
String get advanced_info => 'mpv config';
@override
String get use_native_http_client => '使用本地 HTTP 客户端';
@ -1422,6 +1429,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get hwdec => 'Hardware Decoder';
@override
String get track_library_navigate => 'Go to existing local entry';
@override
String get track_library_add => 'Add to local library';
@ -1466,4 +1476,115 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get sync_enable_settings => 'Sync settings';
@override
String get enable_mpv => 'Enable mpv shaders / scripts';
@override
String get mpv_info => 'Supports .js scripts under mpv/scripts/';
@override
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 get custom_buttons => 'Custom buttons';
@override
String get custom_buttons_info =>
'Execute Javascript code with custom buttons';
@override
String get custom_buttons_edit => 'Edit custom buttons';
@override
String get custom_buttons_add => 'Add custom button';
@override
String get custom_buttons_delete => 'Delete custom button';
@override
String get custom_buttons_text => 'Button text';
@override
String get custom_buttons_text_req => 'Button text required';
@override
String get custom_buttons_js_code => 'Javascript code';
@override
String get custom_buttons_js_code_req => 'Javascript code required';
@override
String get custom_buttons_js_code_long => 'Javascript code (on long press)';
@override
String get custom_buttons_startup => 'Javascript code (on startup)';
@override
String n_days(Object n) {
return '$n days';
}
@override
String get decoder => 'Decoder';
@override
String get decoder_info => 'Hardware decoding, pixel format, debanding';
@override
String get enable_gpu_next => 'Enable gpu-next (Android only)';
@override
String get enable_gpu_next_info => 'A new video rendering backend';
@override
String get debanding => 'Debanding';
@override
String get use_yuv420p => 'Use YUV420P pixel format';
@override
String get use_yuv420p_info =>
'May fix black screens on some video codecs, can also improve performance at the cost of quality';
@override
String get audio_preferred_languages => 'Preferred langauages';
@override
String get audio_preferred_languages_info =>
'Audio langauage(s) to be selected by default on a video with multiple audio streams, 2/3-letter languages codes (e.g.: en, de, fr) work. Multiple values can be delimited by a comma.';
@override
String get enable_audio_pitch_correction => 'Enable audio pitch correction';
@override
String get enable_audio_pitch_correction_info =>
'Prevents the audio from becoming high-pitched at faster speeds and low-pitched at slower speeds';
@override
String get audio_channels => 'Audio channels';
@override
String get volume_boost_cap => 'Volume boost cap';
@override
String get internal_player => 'Internal player';
@override
String get internal_player_info => 'Progress, controls, orientation';
@override
String get subtitle_delay_text => 'Subtitle delay';
@override
String get subtitle_delay => 'Delay (ms)';
@override
String get subtitle_speed => 'Speed';
}

View file

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:app_links/app_links.dart';
import 'package:archive/archive.dart';
import 'package:bot_toast/bot_toast.dart';
import 'package:desktop_webview_window/desktop_webview_window.dart';
import 'package:flutter/foundation.dart';
@ -31,9 +32,11 @@ import 'package:mangayomi/utils/url_protocol/api.dart';
import 'package:mangayomi/modules/more/settings/appearance/providers/theme_provider.dart';
import 'package:mangayomi/modules/library/providers/file_scanner.dart';
import 'package:media_kit/media_kit.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
import 'package:window_manager/window_manager.dart';
import 'package:path/path.dart' as p;
import 'package:flutter/services.dart' show rootBundle;
late Isar isar;
DiscordRPC? discordRpc;
@ -94,6 +97,7 @@ class _MyAppState extends ConsumerState<MyApp> {
super.initState();
initializeDateFormatting();
_initDeepLinks();
_setupMpvConfig();
unawaited(ref.read(scanLocalLibraryProvider.future));
WidgetsBinding.instance.addPostFrameCallback((_) {
@ -242,6 +246,38 @@ class _MyAppState extends ConsumerState<MyApp> {
}
return true;
}
Future<void> _setupMpvConfig() async {
final provider = StorageProvider();
final dir = await provider.getMpvDirectory();
final mpvFile = File('${dir!.path}/mpv.conf');
final inputFile = File('${dir.path}/input.conf');
final filesMissing =
!(await mpvFile.exists()) && !(await inputFile.exists());
if (filesMissing) {
final bytes = await rootBundle.load("assets/mangayomi_mpv.zip");
final archive = ZipDecoder().decodeBytes(bytes.buffer.asUint8List());
String shadersDir = path.join(dir.path, 'shaders');
await Directory(shadersDir).create(recursive: true);
String scriptsDir = path.join(dir.path, 'scripts');
await Directory(scriptsDir).create(recursive: true);
for (final file in archive.files) {
if (file.name == "mpv.conf") {
await mpvFile.writeAsBytes(file.content);
} else if (file.name == "input.conf") {
await inputFile.writeAsBytes(file.content);
} else if (file.name.startsWith("shaders/") &&
file.name.endsWith(".glsl")) {
final shaderFile = File('$shadersDir/${file.name.split("/").last}');
await shaderFile.writeAsBytes(file.content);
} else if (file.name.startsWith("scripts/") &&
file.name.endsWith(".js")) {
final scriptFile = File('$scriptsDir/${file.name.split("/").last}');
await scriptFile.writeAsBytes(file.content);
}
}
}
}
}
class AllowScrollBehavior extends MaterialScrollBehavior {

View file

@ -26,6 +26,18 @@ class Chapter {
///Only for local archive Comic
String? archivePath;
bool? isFiller;
String? thumbnailUrl;
String? description;
/// video size
String? downloadSize;
/// video duration
String? duration;
int? updatedAt;
final manga = IsarLink<Manga>();
@ -41,6 +53,11 @@ class Chapter {
this.isRead = false,
this.lastPageRead = '',
this.archivePath = '',
this.isFiller = false,
this.thumbnailUrl,
this.description,
this.downloadSize,
this.duration,
this.updatedAt = 0,
});
@ -55,6 +72,11 @@ class Chapter {
name = json['name'];
scanlator = json['scanlator'];
url = json['url'];
isFiller = json['isFiller'] ?? false;
thumbnailUrl = json['thumbnailUrl'];
description = json['description'];
downloadSize = json['downloadSize'];
duration = json['duration'];
updatedAt = json['updatedAt'];
}
@ -69,6 +91,11 @@ class Chapter {
'name': name,
'scanlator': scanlator,
'url': url,
'isFiller': isFiller,
'thumbnailUrl': thumbnailUrl,
'description': description,
'downloadSize': downloadSize,
'duration': duration,
'updatedAt': updatedAt ?? 0,
};
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,95 @@
import 'package:isar/isar.dart';
part 'custom_button.g.dart';
@collection
@Name("CustomButton")
class CustomButton {
Id? id;
String? title;
String? codePress;
String? codeLongPress;
String? codeStartup;
bool? isFavourite;
int? pos;
int? updatedAt;
CustomButton({
this.id = Isar.autoIncrement,
required this.title,
required this.codePress,
this.codeLongPress = "",
this.codeStartup = "",
this.isFavourite = false,
required this.pos,
this.updatedAt = 0,
});
String getButtonStartup(int primaryId) {
final isPrimary = primaryId == id ? "true" : "false";
return codeStartup
?.replaceAll("\$id", "$id")
.replaceAll("\$isPrimary", isPrimary) ??
"";
}
String getButtonPress(int primaryId) {
final isPrimary = primaryId == id ? "true" : "false";
return codePress
?.replaceAll("\$id", "$id")
.replaceAll("\$isPrimary", isPrimary) ??
"";
}
String getButtonLongPress(int primaryId) {
final isPrimary = primaryId == id ? "true" : "false";
return codeLongPress
?.replaceAll("\$id", "$id")
.replaceAll("\$isPrimary", isPrimary) ??
"";
}
CustomButton.fromJson(Map<String, dynamic> json) {
id = json['id'];
title = json['title'];
codePress = json['codePress'];
codeLongPress = json['codeLongPress'];
codeStartup = json['codeStartup'];
isFavourite = json['isFavourite'];
pos = json['pos'];
updatedAt = json['updatedAt'];
}
Map<String, dynamic> toJson() => {
'id': id,
'title': title,
'codePress': codePress,
'codeLongPress': codeLongPress,
'codeStartup': codeStartup,
'isFavourite': isFavourite,
'pos': pos,
'updatedAt': updatedAt ?? 0,
};
}
class ActiveCustomButton {
String currentTitle;
bool visible;
CustomButton button;
Function() onPress;
Function() onLongPress;
ActiveCustomButton({
required this.currentTitle,
required this.visible,
required this.button,
required this.onPress,
required this.onLongPress,
});
}

File diff suppressed because it is too large Load diff

View file

@ -49,6 +49,9 @@ class Manga {
String? customCoverFromTracker;
/// only update X days after `lastUpdate`
int? smartUpdateDays;
int? updatedAt;
@Backlink(to: "manga")
@ -76,6 +79,7 @@ class Manga {
this.isLocalArchive = false,
this.customCoverImage,
this.customCoverFromTracker,
this.smartUpdateDays,
this.updatedAt = 0,
});
@ -101,6 +105,7 @@ class Manga {
source = json['source'];
status = Status.values[json['status']];
customCoverFromTracker = json['customCoverFromTracker'];
smartUpdateDays = json['smartUpdateDays'];
updatedAt = json['updatedAt'];
}
@ -125,6 +130,7 @@ class Manga {
'source': source,
'status': status.index,
'customCoverFromTracker': customCoverFromTracker,
'smartUpdateDays': smartUpdateDays,
'updatedAt': updatedAt ?? 0,
};
}

View file

@ -108,19 +108,24 @@ const MangaSchema = CollectionSchema(
name: r'name',
type: IsarType.string,
),
r'source': PropertySchema(
r'smartUpdateDays': PropertySchema(
id: 18,
name: r'smartUpdateDays',
type: IsarType.long,
),
r'source': PropertySchema(
id: 19,
name: r'source',
type: IsarType.string,
),
r'status': PropertySchema(
id: 19,
id: 20,
name: r'status',
type: IsarType.byte,
enumMap: _MangastatusEnumValueMap,
),
r'updatedAt': PropertySchema(
id: 20,
id: 21,
name: r'updatedAt',
type: IsarType.long,
)
@ -258,9 +263,10 @@ void _mangaSerialize(
writer.writeLong(offsets[15], object.lastUpdate);
writer.writeString(offsets[16], object.link);
writer.writeString(offsets[17], object.name);
writer.writeString(offsets[18], object.source);
writer.writeByte(offsets[19], object.status.index);
writer.writeLong(offsets[20], object.updatedAt);
writer.writeLong(offsets[18], object.smartUpdateDays);
writer.writeString(offsets[19], object.source);
writer.writeByte(offsets[20], object.status.index);
writer.writeLong(offsets[21], object.updatedAt);
}
Manga _mangaDeserialize(
@ -290,10 +296,11 @@ Manga _mangaDeserialize(
lastUpdate: reader.readLongOrNull(offsets[15]),
link: reader.readStringOrNull(offsets[16]),
name: reader.readStringOrNull(offsets[17]),
source: reader.readStringOrNull(offsets[18]),
status: _MangastatusValueEnumMap[reader.readByteOrNull(offsets[19])] ??
smartUpdateDays: reader.readLongOrNull(offsets[18]),
source: reader.readStringOrNull(offsets[19]),
status: _MangastatusValueEnumMap[reader.readByteOrNull(offsets[20])] ??
Status.ongoing,
updatedAt: reader.readLongOrNull(offsets[20]),
updatedAt: reader.readLongOrNull(offsets[21]),
);
return object;
}
@ -343,11 +350,13 @@ P _mangaDeserializeProp<P>(
case 17:
return (reader.readStringOrNull(offset)) as P;
case 18:
return (reader.readStringOrNull(offset)) as P;
return (reader.readLongOrNull(offset)) as P;
case 19:
return (reader.readStringOrNull(offset)) as P;
case 20:
return (_MangastatusValueEnumMap[reader.readByteOrNull(offset)] ??
Status.ongoing) as P;
case 20:
case 21:
return (reader.readLongOrNull(offset)) as P;
default:
throw IsarError('Unknown property with id $propertyId');
@ -2591,6 +2600,75 @@ extension MangaQueryFilter on QueryBuilder<Manga, Manga, QFilterCondition> {
});
}
QueryBuilder<Manga, Manga, QAfterFilterCondition> smartUpdateDaysIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNull(
property: r'smartUpdateDays',
));
});
}
QueryBuilder<Manga, Manga, QAfterFilterCondition> smartUpdateDaysIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNotNull(
property: r'smartUpdateDays',
));
});
}
QueryBuilder<Manga, Manga, QAfterFilterCondition> smartUpdateDaysEqualTo(
int? value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'smartUpdateDays',
value: value,
));
});
}
QueryBuilder<Manga, Manga, QAfterFilterCondition> smartUpdateDaysGreaterThan(
int? value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
include: include,
property: r'smartUpdateDays',
value: value,
));
});
}
QueryBuilder<Manga, Manga, QAfterFilterCondition> smartUpdateDaysLessThan(
int? value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.lessThan(
include: include,
property: r'smartUpdateDays',
value: value,
));
});
}
QueryBuilder<Manga, Manga, QAfterFilterCondition> smartUpdateDaysBetween(
int? lower,
int? upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.between(
property: r'smartUpdateDays',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
));
});
}
QueryBuilder<Manga, Manga, QAfterFilterCondition> sourceIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNull(
@ -3100,6 +3178,18 @@ extension MangaQuerySortBy on QueryBuilder<Manga, Manga, QSortBy> {
});
}
QueryBuilder<Manga, Manga, QAfterSortBy> sortBySmartUpdateDays() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'smartUpdateDays', Sort.asc);
});
}
QueryBuilder<Manga, Manga, QAfterSortBy> sortBySmartUpdateDaysDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'smartUpdateDays', Sort.desc);
});
}
QueryBuilder<Manga, Manga, QAfterSortBy> sortBySource() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'source', Sort.asc);
@ -3330,6 +3420,18 @@ extension MangaQuerySortThenBy on QueryBuilder<Manga, Manga, QSortThenBy> {
});
}
QueryBuilder<Manga, Manga, QAfterSortBy> thenBySmartUpdateDays() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'smartUpdateDays', Sort.asc);
});
}
QueryBuilder<Manga, Manga, QAfterSortBy> thenBySmartUpdateDaysDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'smartUpdateDays', Sort.desc);
});
}
QueryBuilder<Manga, Manga, QAfterSortBy> thenBySource() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'source', Sort.asc);
@ -3485,6 +3587,12 @@ extension MangaQueryWhereDistinct on QueryBuilder<Manga, Manga, QDistinct> {
});
}
QueryBuilder<Manga, Manga, QDistinct> distinctBySmartUpdateDays() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'smartUpdateDays');
});
}
QueryBuilder<Manga, Manga, QDistinct> distinctBySource(
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
@ -3621,6 +3729,12 @@ extension MangaQueryProperty on QueryBuilder<Manga, Manga, QQueryProperty> {
});
}
QueryBuilder<Manga, int?, QQueryOperations> smartUpdateDaysProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'smartUpdateDays');
});
}
QueryBuilder<Manga, String?, QQueryOperations> sourceProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'source');

View file

@ -258,6 +258,24 @@ class Settings {
bool? rpcShowCoverImage;
bool? useMpvConfig;
@enumerated
late DebandingType debandingType;
bool? enableGpuNext;
bool? useYUV420P;
String? audioPreferredLanguages;
bool? enableAudioPitchCorrection;
@enumerated
late AudioChannel audioChannels;
int? volumeBoostCap;
Settings({
this.id = 227,
this.updatedAt = 0,
@ -373,6 +391,14 @@ class Settings {
this.rpcShowReadingWatchingProgress = true,
this.rpcShowTitle = true,
this.rpcShowCoverImage = true,
this.useMpvConfig = true,
this.debandingType = DebandingType.none,
this.enableGpuNext = false,
this.useYUV420P = false,
this.audioPreferredLanguages,
this.enableAudioPitchCorrection,
this.audioChannels = AudioChannel.autoSafe,
this.volumeBoostCap,
});
Settings.fromJson(Map<String, dynamic> json) {
@ -594,6 +620,16 @@ class Settings {
rpcShowReadingWatchingProgress = json['rpcShowReadingWatchingProgress'];
rpcShowTitle = json['rpcShowTitle'];
rpcShowCoverImage = json['rpcShowCoverImage'];
useMpvConfig = json['useMpvConfig'];
debandingType =
DebandingType.values[json['debandingType'] ?? DebandingType.none.index];
enableGpuNext = json['enableGpuNext'];
useYUV420P = json['useYUV420P'];
audioPreferredLanguages = json['audioPreferredLanguages'];
enableAudioPitchCorrection = json['enableAudioPitchCorrection'];
audioChannels = AudioChannel
.values[json['audioChannels'] ?? AudioChannel.autoSafe.index];
volumeBoostCap = json['volumeBoostCap'];
}
Map<String, dynamic> toJson() => {
@ -732,9 +768,31 @@ class Settings {
'rpcShowReadingWatchingProgress': rpcShowReadingWatchingProgress,
'rpcShowTitle': rpcShowTitle,
'rpcShowCoverImage': rpcShowCoverImage,
'useMpvConfig': useMpvConfig,
'debandingType': debandingType.index,
'enableGpuNext': enableGpuNext,
'useYUV420P': useYUV420P,
'audioPreferredLanguages': audioPreferredLanguages,
'enableAudioPitchCorrection': enableAudioPitchCorrection,
'audioChannels': audioChannels.index,
'volumeBoostCap': volumeBoostCap,
};
}
enum DebandingType { none, cpu, gpu }
enum AudioChannel {
auto(mpvName: "auto"),
autoSafe(mpvName: "auto-safe"),
mono(mpvName: "mono"),
stereo(mpvName: "stereo"),
reverseStereo(mpvName: "pan=[stereo|c0=c1|c1=c0]");
final String mpvName;
const AudioChannel({required this.mpvName});
}
enum SectionType { all, anime, manga }
enum DisplayType { compactGrid, comfortableGrid, coverOnlyGrid, list }

File diff suppressed because it is too large Load diff

View file

@ -1,15 +1,23 @@
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';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_qjs/quickjs/ffi.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart' as riv;
import 'package:mangayomi/eval/model/m_bridge.dart';
import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/chapter.dart';
import 'package:mangayomi/models/custom_button.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/models/video.dart' as vid;
import 'package:mangayomi/modules/anime/providers/anime_player_controller_provider.dart';
import 'package:mangayomi/modules/anime/widgets/aniskip_countdown_btn.dart';
@ -20,6 +28,9 @@ import 'package:mangayomi/modules/anime/widgets/mobile.dart';
import 'package:mangayomi/modules/anime/widgets/subtitle_view.dart';
import 'package:mangayomi/modules/anime/widgets/subtitle_setting_widget.dart';
import 'package:mangayomi/modules/manga/reader/providers/push_router.dart';
import 'package:mangayomi/modules/more/settings/player/providers/custom_buttons_provider.dart';
import 'package:mangayomi/modules/more/settings/player/providers/player_audio_state_provider.dart';
import 'package:mangayomi/modules/more/settings/player/providers/player_decoder_state_provider.dart';
import 'package:mangayomi/modules/more/settings/player/providers/player_state_provider.dart';
import 'package:mangayomi/modules/widgets/custom_draggable_tabbar.dart';
import 'package:mangayomi/modules/widgets/progress_center.dart';
@ -31,10 +42,14 @@ import 'package:mangayomi/services/torrent_server.dart';
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
import 'package:mangayomi/utils/language.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit/generated/libmpv/bindings.dart' as generated;
import 'package:media_kit_video/media_kit_video.dart';
import 'package:media_kit_video/media_kit_video_controls/src/controls/extensions/duration.dart';
import 'package:numberpicker/numberpicker.dart';
import 'package:path/path.dart' as p;
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:share_plus/share_plus.dart';
import 'package:super_sliver_list/super_sliver_list.dart';
@ -74,7 +89,7 @@ class _AnimePlayerViewState extends riv.ConsumerState<AnimePlayerView> {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
return serversData.when(
data: (data) {
final (videos, isLocal, infoHashList) = data;
final (videos, isLocal, infoHashList, mpvDirectory) = data;
_infoHashList = infoHashList;
if (videos.isEmpty && !(episode.manga.value!.isLocalArchive ?? false)) {
return Scaffold(
@ -102,6 +117,7 @@ class _AnimePlayerViewState extends riv.ConsumerState<AnimePlayerView> {
desktopFullScreenPlayer: (value) {
desktopFullScreenPlayer = value;
},
mpvDirectory: mpvDirectory,
);
},
error: (error, stackTrace) => Scaffold(
@ -150,6 +166,7 @@ class AnimeStreamPage extends riv.ConsumerStatefulWidget {
final String defaultSubtitle;
final bool isLocal;
final bool isTorrent;
final Directory? mpvDirectory;
final void Function(bool) desktopFullScreenPlayer;
const AnimeStreamPage({
super.key,
@ -159,6 +176,7 @@ class AnimeStreamPage extends riv.ConsumerStatefulWidget {
required this.episode,
required this.isTorrent,
required this.desktopFullScreenPlayer,
required this.mpvDirectory,
});
@override
@ -175,13 +193,76 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
with TickerProviderStateMixin, WidgetsBindingObserver {
late final GlobalKey<VideoState> _key = GlobalKey<VideoState>();
late final useLibass = ref.read(useLibassStateProvider);
late final useMpvConfig = ref.read(useMpvConfigStateProvider);
late final useGpuNext = ref.read(useGpuNextStateProvider);
late final debandingType = ref.read(debandingStateProvider);
late final useYUV420P = ref.read(useYUV420PStateProvider);
late final audioPreferredLang = ref.read(audioPreferredLangStateProvider);
late final enableAudioPitchCorrection = ref.read(
enableAudioPitchCorrectionStateProvider,
);
late final audioChannel = ref.read(audioChannelStateProvider);
late final volumeBoostCap = ref.read(volumeBoostCapStateProvider);
late final Player _player = Player(
configuration: PlayerConfiguration(libass: useLibass),
configuration: PlayerConfiguration(
libass: useLibass,
config: true,
configDir: useMpvConfig ? widget.mpvDirectory?.path ?? "" : "",
options: {
if (debandingType == DebandingType.cpu) "vf": "gradfun=radius=12",
if (debandingType == DebandingType.gpu) "deband": "yes",
if (useYUV420P) "vf": "format=yuv420p",
"alang": audioPreferredLang,
if (enableAudioPitchCorrection) "audio-pitch-correction": "yes",
"volume-max": "${volumeBoostCap + 100}",
if (audioChannel != AudioChannel.reverseStereo)
"audio-channels": audioChannel.mpvName,
if (audioChannel == AudioChannel.reverseStereo)
"af": audioChannel.mpvName,
},
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/mangayomi/chapter_titles":
generated.mpv_format.MPV_FORMAT_NODE,
"user-data/mangayomi/current_chapter":
generated.mpv_format.MPV_FORMAT_INT64,
"user-data/mangayomi/selected_shader":
generated.mpv_format.MPV_FORMAT_NODE,
},
eventHandler: _handleMpvEvents,
),
);
late final hwdecMode = ref.read(hwdecModeStateProvider());
late final VideoController _controller = VideoController(
_player,
configuration: VideoControllerConfiguration(hwdec: hwdecMode),
configuration: VideoControllerConfiguration(
hwdec: hwdecMode,
vo: Platform.isAndroid
? useGpuNext
? "gpu-next"
: "gpu"
: "libmpv",
),
);
late final _streamController = ref.read(
animeStreamControllerProvider(episode: widget.episode).notifier,
@ -207,6 +288,11 @@ 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);
final ValueNotifier<String> _selectedShader = ValueNotifier("");
final ValueNotifier<ActiveCustomButton?> _customButton = ValueNotifier(null);
final ValueNotifier<List<CustomButton>?> _customButtons = ValueNotifier(null);
late final ValueNotifier<_AniSkipPhase> _skipPhase = ValueNotifier(
_AniSkipPhase.none,
);
@ -216,6 +302,10 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
bool _hasEndingSkip = false;
bool _initSubtitleAndAudio = true;
bool _includeSubtitles = false;
int _subDelay = 0;
final _subDelayController = TextEditingController(text: "0");
double _subSpeed = 1;
final _subSpeedController = TextEditingController(text: "1");
int lastRpcTimestampUpdate = DateTime.now().millisecondsSinceEpoch;
late final StreamSubscription<Duration> _currentPositionSub;
@ -249,6 +339,384 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
}
});
Future<void> _handleMpvEvents(Pointer<generated.mpv_event> event) async {
try {
if (event.ref.event_id ==
generated.mpv_event_id.MPV_EVENT_PROPERTY_CHANGE) {
final prop = event.ref.data.cast<generated.mpv_event_property>();
final propName = prop.ref.name.cast<Utf8>().toDartString();
if (kDebugMode) {
if (propName.startsWith("user-data/")) {
print("DEBUG 00: $propName - ${prop.ref.format}");
}
}
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 value = prop.ref.data.cast<Int64>().value;
_handleMpvNumberEvents(propName, value);
}
}
} catch (e) {
if (kDebugMode) {
debugPrint(e.toString());
}
}
}
Future<void> _handleMpvNodeEvents(
String propName,
Pointer<generated.mpv_node> value,
) async {
final nativePlayer = _player.platform as NativePlayer;
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();
if (text.isEmpty) break;
botToast(text);
nativePlayer.setProperty("user-data/aniyomi/show_text", "");
}
break;
case "aniyomi/toggle_ui":
if (value.ref.format == generated.mpv_format.MPV_FORMAT_STRING) {
final text = value.ref.u.string.cast<Utf8>().toDartString();
if (text.isEmpty) break;
switch (text) {
// WIP
case "show":
break;
case "hide":
break;
case "toggle":
break;
}
nativePlayer.setProperty("user-data/aniyomi/toggle_ui", "");
}
break;
case "aniyomi/show_panel":
if (value.ref.format == generated.mpv_format.MPV_FORMAT_STRING) {
final text = value.ref.u.string.cast<Utf8>().toDartString();
if (text.isEmpty) break;
switch (text) {
// WIP
case "subtitle_settings":
break;
case "subtitle_delay":
break;
case "audio_delay":
break;
case "video_filters":
break;
}
nativePlayer.setProperty("user-data/aniyomi/show_panel", "");
}
break;
case "aniyomi/software_keyboard":
if (value.ref.format == generated.mpv_format.MPV_FORMAT_STRING) {
final text = value.ref.u.string.cast<Utf8>().toDartString();
if (text.isEmpty) break;
switch (text) {
// WIP
case "show":
break;
case "hide":
break;
case "toggle":
break;
}
nativePlayer.setProperty("user-data/aniyomi/software_keyboard", "");
}
break;
case "aniyomi/set_button_title":
if (value.ref.format == generated.mpv_format.MPV_FORMAT_STRING) {
final text = value.ref.u.string.cast<Utf8>().toDartString();
if (text.isEmpty) break;
final temp = _customButton.value;
if (temp == null) break;
_customButton.value = temp..currentTitle = text;
nativePlayer.setProperty("user-data/aniyomi/set_button_title", "");
}
break;
case "aniyomi/reset_button_title":
if (value.ref.format == generated.mpv_format.MPV_FORMAT_STRING) {
final text = value.ref.u.string.cast<Utf8>().toDartString();
if (text.isEmpty) break;
final temp = _customButton.value;
if (temp == null) break;
_customButton.value = temp..currentTitle = temp.button.title ?? "";
nativePlayer.setProperty("user-data/aniyomi/reset_button_title", "");
}
break;
case "aniyomi/toggle_button":
if (value.ref.format == generated.mpv_format.MPV_FORMAT_STRING) {
final text = value.ref.u.string.cast<Utf8>().toDartString();
if (text.isEmpty) break;
final temp = _customButton.value;
if (temp == null) break;
switch (text) {
case "show":
_customButton.value = temp..visible = true;
break;
case "hide":
_customButton.value = temp..visible = false;
break;
case "toggle":
_customButton.value = temp..visible = !temp.visible;
break;
}
nativePlayer.setProperty("user-data/aniyomi/toggle_button", "");
}
break;
case "aniyomi/switch_episode":
if (value.ref.format == generated.mpv_format.MPV_FORMAT_STRING) {
final text = value.ref.u.string.cast<Utf8>().toDartString();
if (text.isEmpty) break;
switch (text) {
case "n":
pushToNewEpisode(context, _streamController.getNextEpisode());
break;
case "p":
pushToNewEpisode(context, _streamController.getPrevEpisode());
break;
}
nativePlayer.setProperty("user-data/aniyomi/switch_episode", "");
}
break;
case "aniyomi/pause":
if (value.ref.format == generated.mpv_format.MPV_FORMAT_STRING) {
final text = value.ref.u.string.cast<Utf8>().toDartString();
if (text.isEmpty) break;
switch (text) {
case "pause":
await _player.pause();
break;
case "unpause":
await _player.play();
break;
case "pauseunpause":
await _player.playOrPause();
break;
}
nativePlayer.setProperty("user-data/aniyomi/pause", "");
}
break;
case "aniyomi/seek_by":
if (value.ref.format == generated.mpv_format.MPV_FORMAT_STRING) {
final text = value.ref.u.string.cast<Utf8>().toDartString();
if (text.isEmpty) break;
final data = int.parse(text.replaceAll("\"", ""));
final pos = _currentPosition.value.inSeconds + data;
_tempPosition.value = Duration(seconds: pos);
await _player.seek(Duration(seconds: pos));
_tempPosition.value = null;
nativePlayer.setProperty("user-data/aniyomi/seek_by", "");
}
break;
case "aniyomi/seek_to":
if (value.ref.format == generated.mpv_format.MPV_FORMAT_STRING) {
final text = value.ref.u.string.cast<Utf8>().toDartString();
if (text.isEmpty) break;
final data = int.parse(text.replaceAll("\"", ""));
_tempPosition.value = Duration(seconds: data);
await _player.seek(Duration(seconds: data));
_tempPosition.value = null;
nativePlayer.setProperty("user-data/aniyomi/seek_to", "");
}
break;
case "aniyomi/seek_by_with_text":
if (value.ref.format == generated.mpv_format.MPV_FORMAT_STRING) {
final text = value.ref.u.string.cast<Utf8>().toDartString();
if (text.isEmpty) break;
final data = text.split("|");
final pos =
_currentPosition.value.inSeconds +
int.parse(data[0].replaceAll("\"", ""));
_tempPosition.value = Duration(seconds: pos);
await _player.seek(Duration(seconds: pos));
_tempPosition.value = null;
(_player.platform as NativePlayer).command(["show-text", data[1]]);
nativePlayer.setProperty("user-data/aniyomi/seek_by_with_text", "");
}
break;
case "aniyomi/seek_to_with_text":
if (value.ref.format == generated.mpv_format.MPV_FORMAT_STRING) {
final text = value.ref.u.string.cast<Utf8>().toDartString();
if (text.isEmpty) break;
final data = text.split("|");
final pos = int.parse(data[0].replaceAll("\"", ""));
_tempPosition.value = Duration(seconds: pos);
await _player.seek(Duration(seconds: pos));
_tempPosition.value = null;
(_player.platform as NativePlayer).command(["show-text", data[1]]);
nativePlayer.setProperty("user-data/aniyomi/seek_to_with_text", "");
}
break;
case "aniyomi/launch_int_picker":
if (value.ref.format == generated.mpv_format.MPV_FORMAT_STRING) {
final text = value.ref.u.string.cast<Utf8>().toDartString();
if (text.isEmpty) break;
final data = text.split("|");
final start = int.parse(data[2]);
final stop = int.parse(data[3]);
final step = int.parse(data[4]);
int currentValue = start;
await showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text(data[0]),
content: StatefulBuilder(
builder: (context, setState) => SizedBox(
height: 200,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
NumberPicker(
value: currentValue,
minValue: start,
maxValue: stop,
step: step,
haptics: true,
textMapper: (numberText) =>
data[1].replaceAll("%d", numberText),
onChanged: (value) =>
setState(() => currentValue = value),
),
],
),
),
),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () async {
Navigator.pop(context);
},
child: Text(
context.l10n.cancel,
style: TextStyle(color: context.primaryColor),
),
),
TextButton(
onPressed: () async {
final namePtr = data[5].toNativeUtf8();
final valuePtr = calloc<Int64>(1)
..value = currentValue;
nativePlayer.mpv.mpv_set_property(
nativePlayer.ctx,
namePtr.cast(),
generated.mpv_format.MPV_FORMAT_INT64,
valuePtr.cast(),
);
malloc.free(namePtr);
malloc.free(valuePtr);
Navigator.pop(context);
},
child: Text(
context.l10n.ok,
style: TextStyle(color: context.primaryColor),
),
),
],
),
],
);
},
);
nativePlayer.setProperty("user-data/aniyomi/launch_int_picker", "");
}
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"] is double
? (e["time"] as double).toInt() * 1000
: (e["time"] as int) * 1000,
),
)
.toList();
}
break;
case "mangayomi/selected_shader":
if (value.ref.format == generated.mpv_format.MPV_FORMAT_STRING) {
final text = value.ref.u.string.cast<Utf8>().toDartString();
_selectedShader.value = text;
}
break;
}
}
Future<void> _handleMpvNumberEvents(String propName, int value) async {
switch (propName.substring(10)) {
case "mangayomi/current_chapter":
_currentChapterMark.value = max(value, 0);
break;
}
}
Future<void> _initCustomButton() async {
if (!useMpvConfig) return;
final customButtons = await ref.read(getCustomButtonsStreamProvider.future);
if (customButtons.isEmpty) return;
final primaryButton =
customButtons.firstWhereOrNull((e) => e.isFavourite ?? false) ??
customButtons.first;
var status = await Permission.storage.status;
if (!status.isGranted) {
await Permission.storage.request();
}
final provider = StorageProvider();
final dir = await provider.getMpvDirectory();
String scriptsDir = path.join(dir!.path, 'scripts');
final mpvFile = File('$scriptsDir/init_custom_buttons.js');
final content = StringBuffer();
content.write("var aniyomi = require('./init_aniyomi_functions');");
for (final button in customButtons) {
content.write(
"""
${button.getButtonStartup(primaryButton.id!).trim()}
function button${button.id}() {
${button.getButtonPress(primaryButton.id!).trim()}
}
mp.register_script_message('call_button_${button.id}', button${button.id})
function button${button.id}long() {
${button.getButtonLongPress(primaryButton.id!).trim()}
}
mp.register_script_message('call_button_${button.id}_long', button${button.id}long)""",
);
}
await mpvFile.writeAsString(content.toString());
await (_player.platform as NativePlayer).command([
"load-script",
mpvFile.path,
]);
_customButton.value = ActiveCustomButton(
currentTitle: primaryButton.title!,
visible: true,
button: primaryButton,
onPress: () => (_player.platform as NativePlayer).command([
"script-message",
"call_button_${primaryButton.id}",
]),
onLongPress: () => (_player.platform as NativePlayer).command([
"script-message",
"call_button_${primaryButton.id}_long",
]),
);
_customButtons.value = customButtons;
}
void pushToNewEpisode(BuildContext context, Chapter episode) {
widget.desktopFullScreenPlayer.call(ref.read(fullscreenProvider));
if (context.mounted) {
@ -321,6 +789,47 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
}
}
void _onSubDelayChanged() {
final nativePlayer = (_player.platform as NativePlayer);
final delayMs = int.tryParse(_subDelayController.text);
if (delayMs != null) {
final namePtr = "sub-delay".toNativeUtf8();
final valuePtr = calloc<Double>(1)..value = delayMs / 1000;
nativePlayer.mpv.mpv_set_property(
nativePlayer.ctx,
namePtr.cast(),
generated.mpv_format.MPV_FORMAT_DOUBLE,
valuePtr.cast(),
);
malloc.free(namePtr);
malloc.free(valuePtr);
_subDelay = delayMs;
}
}
void _onSubSpeedChanged() {
final nativePlayer = (_player.platform as NativePlayer);
final speed = double.tryParse(_subSpeedController.text);
if (speed != null) {
final namePtr = "sub-speed".toNativeUtf8();
final valuePtr = calloc<Double>(1)
..value = speed < 0.1
? 0.1
: speed > 10
? 10
: speed;
nativePlayer.mpv.mpv_set_property(
nativePlayer.ctx,
namePtr.cast(),
generated.mpv_format.MPV_FORMAT_DOUBLE,
valuePtr.cast(),
);
malloc.free(namePtr);
malloc.free(valuePtr);
_subSpeed = speed;
}
}
@override
void initState() {
super.initState();
@ -328,6 +837,15 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
// use global "Use Fullscreen" setting.
// Else (if user already watches an episode and just changes it),
// stay in the same mode, the user left it in.
try {
final defaultSkipIntroLength = ref.read(
defaultSkipIntroLengthStateProvider,
);
(_player.platform as NativePlayer).setProperty(
"user-data/current-anime/intro-length",
"$defaultSkipIntroLength",
);
} catch (_) {}
if (_isDesktop && _firstTime) {
final globalFullscreen = ref.read(fullScreenPlayerStateProvider);
setFullScreen(value: globalFullscreen);
@ -354,8 +872,11 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
_setPlaybackSpeed(ref.read(defaultPlayBackSpeedStateProvider));
if (ref.read(enableAniSkipStateProvider)) _initAniSkip();
});
_initCustomButton();
discordRpc?.showChapterDetails(ref, widget.episode);
_currentPosition.addListener(_updateRpcTimestamp);
_subDelayController.addListener(_onSubDelayChanged);
_subSpeedController.addListener(_onSubSpeedChanged);
WidgetsBinding.instance.addObserver(this);
}
@ -444,6 +965,8 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
discordRpc?.showIdleText();
discordRpc?.showOriginalTimestamp();
_currentPosition.dispose();
_subDelayController.dispose();
_subSpeedController.dispose();
super.dispose();
}
@ -675,6 +1198,91 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 12),
child: Column(
children: [
Row(
children: [
Text(context.l10n.subtitle_delay_text),
IconButton(
onPressed: () {
_subDelay = 0;
_subDelayController.value = TextEditingValue(
text: "$_subDelay",
);
_subSpeed = 1;
_subSpeedController.value = TextEditingValue(
text: _subSpeed.toStringAsFixed(2),
);
},
icon: const Icon(Icons.refresh),
),
],
),
const SizedBox(height: 15),
Row(
children: [
IconButton(
onPressed: () {
_subDelay -= 50;
_subDelayController.value = TextEditingValue(
text: "$_subDelay",
);
},
icon: const Icon(Icons.remove_circle),
),
Expanded(
child: TextFormField(
controller: _subDelayController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
isDense: true,
label: Text(context.l10n.subtitle_delay),
),
),
),
IconButton(
onPressed: () {
_subDelay += 50;
_subDelayController.value = TextEditingValue(
text: "$_subDelay",
);
},
icon: const Icon(Icons.add_circle),
),
],
),
const SizedBox(height: 15),
Row(
children: [
IconButton(
onPressed: () {
_subSpeed -= 0.01;
_subSpeedController.value = TextEditingValue(
text: _subSpeed.toStringAsFixed(2),
);
},
icon: const Icon(Icons.remove_circle),
),
Expanded(
child: TextFormField(
controller: _subSpeedController,
keyboardType: TextInputType.numberWithOptions(decimal: true),
decoration: InputDecoration(
isDense: true,
label: Text(context.l10n.subtitle_speed),
),
),
),
IconButton(
onPressed: () {
_subSpeed += 0.01;
_subSpeedController.value = TextEditingValue(
text: _subSpeed.toStringAsFixed(2),
);
},
icon: const Icon(Icons.add_circle),
),
],
),
const SizedBox(height: 15),
...videoSubtitleLast.toSet().toList().map((sub) {
final title =
sub.title ??
@ -834,27 +1442,79 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
padding: const EdgeInsets.symmetric(vertical: 5),
child: SizedBox(
height: 35,
child: ElevatedButton(
onPressed: () async {
_tempPosition.value = Duration(
seconds:
defaultSkipIntroLength + _currentPosition.value.inSeconds,
);
await _player.seek(
Duration(
seconds:
_currentPosition.value.inSeconds + defaultSkipIntroLength,
),
);
_tempPosition.value = null;
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
"+$defaultSkipIntroLength",
style: const TextStyle(fontWeight: FontWeight.w100),
),
),
child: ValueListenableBuilder(
valueListenable: _customButton,
builder: (context, value, child) => (value?.visible ?? true)
? ElevatedButton(
onPressed:
value?.onPress ??
() async {
_tempPosition.value = Duration(
seconds:
defaultSkipIntroLength +
_currentPosition.value.inSeconds,
);
await _player.seek(
Duration(
seconds:
_currentPosition.value.inSeconds +
defaultSkipIntroLength,
),
);
_tempPosition.value = null;
},
onLongPress: value?.onLongPress,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
value != null
? value.currentTitle
: "+$defaultSkipIntroLength",
style: const TextStyle(fontWeight: FontWeight.w100),
),
),
)
: Container(),
),
),
);
}
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,
color: Colors.white,
),
),
),
)
: Container(),
),
),
);
@ -870,7 +1530,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),
],
),
),
],
@ -1014,6 +1678,7 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
controller: _controller,
),
),
_chapterMarkWidget(),
],
),
_buildSettingsButtons(context),
@ -1023,6 +1688,111 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
);
}
List<Widget> _buildMpvSettingsButton(BuildContext context) {
return [
PopupMenuButton<String>(
tooltip: 'Shaders',
icon: const Icon(Icons.high_quality, color: Colors.white),
itemBuilder: (context) =>
[
("Anime4K: Mode A (Fast)", "set_anime_a"),
("Anime4K: Mode B (Fast)", "set_anime_b"),
("Anime4K: Mode C (Fast)", "set_anime_c"),
("Anime4K: Mode A+A (Fast)", "set_anime_aa"),
("Anime4K: Mode B+B (Fast)", "set_anime_bb"),
("Anime4K: Mode C+A (Fast)", "set_anime_ca"),
("Anime4K: Mode A (HQ)", "set_anime_hq_a"),
("Anime4K: Mode B (HQ)", "set_anime_hq_b"),
("Anime4K: Mode C (HQ)", "set_anime_hq_c"),
("Anime4K: Mode A+A (HQ)", "set_anime_hq_aa"),
("Anime4K: Mode B+B (HQ)", "set_anime_hq_bb"),
("Anime4K: Mode C+A (HQ)", "set_anime_hq_ca"),
("AMD FSR", "set_fsr"),
("Luma Upscaling", "set_luma"),
("Qualcomm Snapdragon GSR", "set_snapdragon"),
("NVIDIA Image Scaling", "set_nvidia"),
("Clear GLSL shaders", "clear_anime"),
]
.map(
(mode) => PopupMenuItem<String>(
value: mode.$1,
child: Text(
mode.$1,
style: TextStyle(
fontWeight: _selectedShader.value == mode.$1
? FontWeight.w900
: FontWeight.normal,
),
),
onTap: () {
(_player.platform as NativePlayer).command([
"script-message",
mode.$2,
]);
},
),
)
.toList(),
),
PopupMenuButton<String>(
tooltip: 'Stats',
icon: const Icon(Icons.memory, color: Colors.white),
itemBuilder: (context) =>
[
("Stats Toggle", "stats/display-stats-toggle"),
("Stats Page 1", "stats/display-page-1"),
("Stats Page 2", "stats/display-page-2"),
("Stats Page 3", "stats/display-page-3"),
("Stats Page 4", "stats/display-page-4"),
("Stats Page 5", "stats/display-page-5"),
]
.map(
(mode) => PopupMenuItem<String>(
value: mode.$1,
child: Text(
mode.$1,
style: TextStyle(
fontWeight: _selectedShader.value == mode.$1
? FontWeight.w900
: FontWeight.normal,
),
),
onTap: () {
(_player.platform as NativePlayer).command([
"script-binding",
mode.$2,
]);
},
),
)
.toList(),
),
ValueListenableBuilder(
valueListenable: _customButtons,
builder: (context, value, child) => value != null
? PopupMenuButton<String>(
tooltip: context.l10n.custom_buttons,
icon: const Icon(Icons.terminal, color: Colors.white),
itemBuilder: (context) => value
.map(
(btn) => PopupMenuItem<String>(
value: btn.title!,
child: Text(btn.title!),
onTap: () {
(_player.platform as NativePlayer).command([
"script-message",
"call_button_${btn.id}",
]);
},
),
)
.toList(),
)
: Container(),
),
];
}
/// helper method for _mobileBottomButtonBar() and _desktopBottomButtonBar()
Widget _buildSettingsButtons(BuildContext context) {
final isFullscreen = ref.watch(fullscreenProvider);
@ -1033,6 +1803,7 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
onPressed: () => _videoSettingDraggableMenu(context),
icon: const Icon(Icons.video_settings, color: Colors.white),
),
if (useMpvConfig) ..._buildMpvSettingsButton(context),
PopupMenuButton<double>(
tooltip: '', // Remove default tooltip "Show menu" for consistency
icon: const Icon(Icons.speed, color: Colors.white),
@ -1259,6 +2030,7 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
},
defaultSkipIntroLength: skipIntroLength,
desktopFullScreenPlayer: widget.desktopFullScreenPlayer,
chapterMarks: _chapterMarks,
)
: MobileControllerWidget(
videoController: _controller,
@ -1269,6 +2041,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

@ -9,6 +9,7 @@ import 'package:mangayomi/modules/anime/providers/anime_player_controller_provid
import 'package:mangayomi/modules/anime/widgets/custom_seekbar.dart';
import 'package:mangayomi/modules/anime/widgets/subtitle_view.dart';
import 'package:mangayomi/modules/more/settings/player/providers/player_state_provider.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/media_kit_video.dart';
import 'package:media_kit_video/media_kit_video_controls/src/controls/extensions/duration.dart';
import 'package:window_manager/window_manager.dart';
@ -24,6 +25,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 +38,7 @@ class DesktopControllerWidget extends ConsumerStatefulWidget {
required this.doubleSpeed,
required this.defaultSkipIntroLength,
required this.desktopFullScreenPlayer,
required this.chapterMarks,
});
@override
@ -215,6 +218,48 @@ class _DesktopControllerWidgetState
final desktopFullScreenPlayer = widget.desktopFullScreenPlayer;
await _changeFullScreen(ref, desktopFullScreenPlayer, value: false);
},
const SingleActivator(LogicalKeyboardKey.digit0, control: true): () {
(widget.videoController.player.platform as NativePlayer).command([
"script-message",
"clear_anime",
]);
},
const SingleActivator(LogicalKeyboardKey.digit1, control: true): () {
(widget.videoController.player.platform as NativePlayer).command([
"script-message",
"set_anime_a",
]);
},
const SingleActivator(LogicalKeyboardKey.digit2, control: true): () {
(widget.videoController.player.platform as NativePlayer).command([
"script-message",
"set_anime_b",
]);
},
const SingleActivator(LogicalKeyboardKey.digit3, control: true): () {
(widget.videoController.player.platform as NativePlayer).command([
"script-message",
"set_anime_c",
]);
},
const SingleActivator(LogicalKeyboardKey.digit4, control: true): () {
(widget.videoController.player.platform as NativePlayer).command([
"script-message",
"set_anime_aa",
]);
},
const SingleActivator(LogicalKeyboardKey.digit5, control: true): () {
(widget.videoController.player.platform as NativePlayer).command([
"script-message",
"set_anime_bb",
]);
},
const SingleActivator(LogicalKeyboardKey.digit6, control: true): () {
(widget.videoController.player.platform as NativePlayer).command([
"script-message",
"set_anime_ca",
]);
},
},
child: Stack(
children: [
@ -439,6 +484,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

@ -1833,6 +1833,7 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
child: Row(
children: [
Expanded(child: widget.action!),
Expanded(child: _smartUpdateDays()),
Expanded(
child: widget.itemType == ItemType.novel
? SizedBox.shrink()
@ -1883,6 +1884,35 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
);
}
Widget _smartUpdateDays() {
return SizedBox(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
elevation: 0,
),
onPressed: () {},
child: Column(
children: [
Icon(
Icons.hourglass_empty,
size: 20,
color: context.secondaryColor,
),
const SizedBox(height: 4),
Text(
widget.manga?.smartUpdateDays != null
? context.l10n.n_days(widget.manga!.smartUpdateDays!)
: "N/A",
style: TextStyle(fontSize: 11, color: context.secondaryColor),
textAlign: TextAlign.center,
),
],
),
),
);
}
/// Tracker button
Widget _action() {
return StreamBuilder(

View file

@ -5,6 +5,7 @@ import 'package:mangayomi/models/chapter.dart';
import 'package:mangayomi/models/update.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/services/get_detail.dart';
import 'package:mangayomi/utils/extensions/others.dart';
import 'package:mangayomi/utils/utils.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@ -62,7 +63,7 @@ Future<dynamic> updateMangaDetail(
return;
}
isar.writeTxnSync(() {
isar.mangas.putSync(manga);
final mangaId = isar.mangas.putSync(manga);
manga.lastUpdate = DateTime.now().millisecondsSinceEpoch;
List<Chapter> chapters = [];
@ -81,6 +82,11 @@ Future<dynamic> updateMangaDetail(
scanlator: chaps[i].scanlator ?? '',
mangaId: mangaId,
updatedAt: DateTime.now().millisecondsSinceEpoch,
isFiller: chaps[i].isFiller,
thumbnailUrl: chaps[i].thumbnailUrl,
description: chaps[i].description,
downloadSize: chaps[i].downloadSize,
duration: chaps[i].duration,
)..manga.value = manga;
chapters.add(chapter);
}
@ -115,10 +121,37 @@ Future<dynamic> updateMangaDetail(
oldChap.url = newChap.url;
oldChap.scanlator = newChap.scanlator;
oldChap.updatedAt = DateTime.now().millisecondsSinceEpoch;
oldChap.isFiller = newChap.isFiller;
oldChap.thumbnailUrl = newChap.thumbnailUrl;
oldChap.description = newChap.description;
oldChap.downloadSize = newChap.downloadSize;
oldChap.duration = newChap.duration;
isar.chapters.putSync(oldChap);
oldChap.manga.saveSync();
}
}
final List<int> daysBetweenUploads = [];
for (var i = 0; i + 1 < chaps.length; i++) {
if (chaps[i].dateUpload != null && chaps[i + 1].dateUpload != null) {
final date1 = DateTime.fromMillisecondsSinceEpoch(
int.parse(chaps[i].dateUpload!),
);
final date2 = DateTime.fromMillisecondsSinceEpoch(
int.parse(chaps[i + 1].dateUpload!),
);
daysBetweenUploads.add(date1.difference(date2).abs().inDays);
}
}
if (daysBetweenUploads.isNotEmpty) {
final median = daysBetweenUploads.median();
isar.mangas.putSync(
manga
..id = mangaId
..smartUpdateDays = median != 0
? median
: daysBetweenUploads.arithmeticMean(),
);
}
});
} catch (e, s) {
if (showToast) botToast('$e\n$s');

View file

@ -6,7 +6,7 @@ part of 'update_manga_detail_providers.dart';
// RiverpodGenerator
// **************************************************************************
String _$updateMangaDetailHash() => r'ce51918a48b315c3555b3de4e602bd998e00a992';
String _$updateMangaDetailHash() => r'33c6bd0f1de57e2e839ae695a0301893b9a94624';
/// Copied from Dart SDK
class _SystemHash {

View file

@ -1,5 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mangayomi/modules/widgets/custom_extended_image_provider.dart';
import 'package:mangayomi/modules/widgets/progress_center.dart';
import 'package:mangayomi/utils/constant.dart';
import 'package:marquee/marquee.dart';
import 'package:mangayomi/models/chapter.dart';
import 'package:mangayomi/models/manga.dart';
@ -10,6 +13,8 @@ import 'package:mangayomi/utils/extensions/chapter.dart';
import 'package:mangayomi/utils/extensions/string_extensions.dart';
import 'package:mangayomi/modules/manga/detail/providers/state_providers.dart';
import 'package:mangayomi/modules/manga/download/download_page_widget.dart';
import 'package:photo_view/photo_view.dart';
import 'package:photo_view/photo_view_gallery.dart';
class ChapterListTileWidget extends ConsumerWidget {
final Chapter chapter;
@ -33,6 +38,9 @@ class ChapterListTileWidget extends ConsumerWidget {
onLongPress: () => _handleInteraction(ref),
onSecondaryTap: () => _handleInteraction(ref),
child: ListTile(
tileColor: (chapter.isFiller ?? false)
? context.primaryColor.withValues(alpha: 0.15)
: null,
textColor: chapter.isRead!
? context.isLight
? Colors.black.withValues(alpha: 0.4)
@ -44,14 +52,43 @@ class ChapterListTileWidget extends ConsumerWidget {
onTap: () async => _handleInteraction(ref, context),
title: Row(
children: [
if (chapter.thumbnailUrl != null)
_thumbnailPreview(context, chapter.thumbnailUrl),
chapter.isBookmarked!
? Icon(Icons.bookmark, size: 16, color: context.primaryColor)
: Container(),
Flexible(child: _buildTitle(chapter.name!, context)),
chapter.description != null
? Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTitle(chapter.name!, context),
Text(
chapter.description!,
style: const TextStyle(fontSize: 11),
overflow: TextOverflow.ellipsis,
),
],
),
)
: Flexible(child: _buildTitle(chapter.name!, context)),
],
),
subtitle: Row(
children: [
if (chapter.isFiller ?? false)
Row(
children: [
Icon(Icons.label, size: 16, color: context.primaryColor),
Text(
" Filler ",
style: TextStyle(
fontSize: 11,
color: context.primaryColor,
),
),
],
),
if ((chapter.manga.value!.isLocalArchive ?? false) == false)
Text(
chapter.dateUpload == null || chapter.dateUpload!.isEmpty
@ -172,4 +209,62 @@ class ChapterListTileWidget extends ConsumerWidget {
},
);
}
Widget _thumbnailPreview(BuildContext context, String? imageUrl) {
final imageProvider = CustomExtendedNetworkImageProvider(
toImgUrl(imageUrl ?? ""),
);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 8),
child: GestureDetector(
onTap: () {
_openImage(context, imageProvider);
},
child: SizedBox(
width: 50,
height: 65,
child: Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(5)),
image: DecorationImage(image: imageProvider, fit: BoxFit.cover),
),
),
),
),
);
}
void _openImage(BuildContext context, ImageProvider imageProvider) {
showDialog(
context: context,
builder: (context) {
return Scaffold(
backgroundColor: Colors.transparent,
body: Stack(
children: [
GestureDetector(
onTap: () => Navigator.pop(context),
child: PhotoViewGallery.builder(
backgroundDecoration: const BoxDecoration(
color: Colors.transparent,
),
itemCount: 1,
builder: (context, index) {
return PhotoViewGalleryPageOptions(
imageProvider: imageProvider,
minScale: PhotoViewComputedScale.contained,
maxScale: 2.0,
);
},
loadingBuilder: (context, event) {
return const ProgressCenter();
},
),
),
],
),
);
},
);
}
}

View file

@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/modules/manga/reader/image_view_paged.dart';
import 'package:mangayomi/modules/manga/reader/reader_view.dart';
import 'package:mangayomi/modules/manga/reader/u_chap_data_preload.dart';
import 'package:mangayomi/modules/manga/reader/widgets/circular_progress_indicator_animate_rotate.dart';
import 'package:mangayomi/modules/manga/reader/widgets/transition_view_paged.dart';
import 'package:mangayomi/modules/more/settings/reader/reader_screen.dart';

View file

@ -2,7 +2,7 @@ import 'package:extended_image/extended_image.dart';
import 'package:flutter/material.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/modules/manga/reader/image_view_paged.dart';
import 'package:mangayomi/modules/manga/reader/reader_view.dart';
import 'package:mangayomi/modules/manga/reader/u_chap_data_preload.dart';
import 'package:mangayomi/modules/manga/reader/widgets/circular_progress_indicator_animate_rotate.dart';
import 'package:mangayomi/modules/manga/reader/widgets/transition_view_vertical.dart';
import 'package:mangayomi/modules/more/settings/reader/reader_screen.dart';

View file

@ -2,7 +2,7 @@ import 'package:extended_image/extended_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mangayomi/modules/manga/reader/providers/reader_controller_provider.dart';
import 'package:mangayomi/modules/manga/reader/reader_view.dart';
import 'package:mangayomi/modules/manga/reader/u_chap_data_preload.dart';
import 'package:mangayomi/modules/manga/reader/widgets/color_filter_widget.dart';
import 'package:mangayomi/modules/more/settings/reader/providers/reader_state_provider.dart';
import 'package:mangayomi/utils/extensions/others.dart';

View file

@ -2,7 +2,7 @@ import 'package:extended_image/extended_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mangayomi/modules/manga/reader/providers/reader_controller_provider.dart';
import 'package:mangayomi/modules/manga/reader/reader_view.dart';
import 'package:mangayomi/modules/manga/reader/u_chap_data_preload.dart';
import 'package:mangayomi/modules/manga/reader/widgets/color_filter_widget.dart';
import 'package:mangayomi/modules/more/settings/reader/providers/reader_state_provider.dart';
import 'package:mangayomi/providers/l10n_providers.dart';

View file

@ -1,7 +1,7 @@
import 'dart:async';
import 'dart:isolate';
import 'package:flutter/foundation.dart';
import 'package:mangayomi/modules/manga/reader/reader_view.dart';
import 'package:mangayomi/modules/manga/reader/u_chap_data_preload.dart';
import 'package:mangayomi/src/rust/api/image.dart';
import 'package:mangayomi/src/rust/frb_generated.dart';
import 'package:mangayomi/utils/extensions/others.dart';

View file

@ -12,14 +12,16 @@ import 'package:mangayomi/eval/model/m_bridge.dart';
import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/chapter.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/models/page.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/modules/anime/widgets/desktop.dart';
import 'package:mangayomi/modules/manga/reader/providers/crop_borders_provider.dart';
import 'package:mangayomi/modules/manga/reader/u_chap_data_preload.dart';
import 'package:mangayomi/modules/manga/reader/widgets/btn_chapter_list_dialog.dart';
import 'package:mangayomi/modules/manga/reader/double_columm_view_center.dart';
import 'package:mangayomi/modules/manga/reader/providers/color_filter_provider.dart';
import 'package:mangayomi/modules/manga/reader/widgets/color_filter_widget.dart';
import 'package:mangayomi/modules/manga/reader/widgets/custom_popup_menu_button.dart';
import 'package:mangayomi/modules/manga/reader/widgets/custom_value_indicator_shape.dart';
import 'package:mangayomi/modules/more/settings/reader/providers/reader_state_provider.dart';
import 'package:mangayomi/modules/widgets/custom_draggable_tabbar.dart';
import 'package:mangayomi/providers/l10n_providers.dart';
@ -1633,7 +1635,7 @@ class _MangaChapterPageGalleryState
return SliderTheme(
data: SliderTheme.of(context).copyWith(
valueIndicatorShape:
_CustomValueIndicatorShape(
CustomValueIndicatorShape(
tranform: _isReverseHorizontal,
),
overlayShape:
@ -2504,190 +2506,3 @@ class _MangaChapterPageGalleryState
}
}
}
class UChapDataPreload {
Chapter? chapter;
Directory? directory;
PageUrl? pageUrl;
bool? isLocale;
Uint8List? archiveImage;
int? index;
GetChapterPagesModel? chapterUrlModel;
int? pageIndex;
Uint8List? cropImage;
bool isTransitionPage;
Chapter? nextChapter;
String? mangaName;
bool? isLastChapter;
UChapDataPreload(
this.chapter,
this.directory,
this.pageUrl,
this.isLocale,
this.archiveImage,
this.index,
this.chapterUrlModel,
this.pageIndex, {
this.cropImage,
this.isTransitionPage = false,
this.nextChapter,
this.mangaName,
this.isLastChapter = false,
});
UChapDataPreload.transition({
required Chapter currentChapter,
required this.nextChapter,
required String this.mangaName,
required int this.pageIndex,
this.isLastChapter = false,
}) : chapter = currentChapter,
isTransitionPage = true,
directory = null,
pageUrl = null,
isLocale = null,
archiveImage = null,
index = null,
chapterUrlModel = null,
cropImage = null;
}
class CustomPopupMenuButton<T> extends StatelessWidget {
final String label;
final String title;
final ValueChanged<T> onSelected;
final T value;
final List<T> list;
final String Function(T) itemText;
const CustomPopupMenuButton({
super.key,
required this.label,
required this.title,
required this.onSelected,
required this.value,
required this.list,
required this.itemText,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: PopupMenuButton(
popUpAnimationStyle: popupAnimationStyle,
tooltip: "",
offset: Offset.fromDirection(1),
color: Colors.black,
onSelected: onSelected,
itemBuilder: (context) => [
for (var d in list)
PopupMenuItem(
value: d,
child: Row(
children: [
Icon(
Icons.check,
color: d == value ? Colors.white : Colors.transparent,
),
const SizedBox(width: 7),
Text(
itemText(d),
style: const TextStyle(color: Colors.white),
),
],
),
),
],
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Text(
label,
style: TextStyle(
color: Theme.of(
context,
).textTheme.bodyLarge!.color!.withValues(alpha: 0.9),
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
Row(
children: [
Text(title),
const SizedBox(width: 20),
const Icon(Icons.keyboard_arrow_down_outlined),
],
),
],
),
),
),
);
}
}
class _CustomValueIndicatorShape extends SliderComponentShape {
final _indicatorShape = const PaddleSliderValueIndicatorShape();
final bool tranform;
const _CustomValueIndicatorShape({this.tranform = false});
@override
Size getPreferredSize(bool isEnabled, bool isDiscrete) {
return const Size(40, 40);
}
@override
void paint(
PaintingContext context,
Offset center, {
required Animation<double> activationAnimation,
required Animation<double> enableAnimation,
required bool isDiscrete,
required TextPainter labelPainter,
required RenderBox parentBox,
required SliderThemeData sliderTheme,
required TextDirection textDirection,
required double value,
required double textScaleFactor,
required Size sizeWithOverflow,
}) {
final textSpan = TextSpan(
text: labelPainter.text?.toPlainText(),
style: sliderTheme.valueIndicatorTextStyle,
);
final textPainter = TextPainter(
text: textSpan,
textAlign: labelPainter.textAlign,
textDirection: textDirection,
);
textPainter.layout();
context.canvas.save();
context.canvas.translate(center.dx, center.dy);
context.canvas.scale(tranform ? -1.0 : 1.0, 1.0);
context.canvas.translate(-center.dx, -center.dy);
_indicatorShape.paint(
context,
center,
activationAnimation: activationAnimation,
enableAnimation: enableAnimation,
labelPainter: textPainter,
parentBox: parentBox,
sliderTheme: sliderTheme,
value: value,
textScaleFactor: textScaleFactor,
sizeWithOverflow: sizeWithOverflow,
isDiscrete: isDiscrete,
textDirection: textDirection,
);
context.canvas.restore();
}
}

View file

@ -0,0 +1,54 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:mangayomi/models/chapter.dart';
import 'package:mangayomi/models/page.dart';
import 'package:mangayomi/services/get_chapter_pages.dart';
class UChapDataPreload {
Chapter? chapter;
Directory? directory;
PageUrl? pageUrl;
bool? isLocale;
Uint8List? archiveImage;
int? index;
GetChapterPagesModel? chapterUrlModel;
int? pageIndex;
Uint8List? cropImage;
bool isTransitionPage;
Chapter? nextChapter;
String? mangaName;
bool? isLastChapter;
UChapDataPreload(
this.chapter,
this.directory,
this.pageUrl,
this.isLocale,
this.archiveImage,
this.index,
this.chapterUrlModel,
this.pageIndex, {
this.cropImage,
this.isTransitionPage = false,
this.nextChapter,
this.mangaName,
this.isLastChapter = false,
});
UChapDataPreload.transition({
required Chapter currentChapter,
required this.nextChapter,
required String this.mangaName,
required int this.pageIndex,
this.isLastChapter = false,
}) : chapter = currentChapter,
isTransitionPage = true,
directory = null,
pageUrl = null,
isLocale = null,
archiveImage = null,
index = null,
chapterUrlModel = null,
cropImage = null;
}

View file

@ -1,10 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mangayomi/modules/manga/reader/u_chap_data_preload.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/models/chapter.dart';
import 'package:mangayomi/modules/manga/reader/virtual_scrolling/virtual_page_manager.dart';
import 'package:mangayomi/modules/manga/reader/reader_view.dart' as reader;
import 'package:mangayomi/modules/manga/reader/image_view_vertical.dart';
import 'package:mangayomi/modules/manga/reader/double_columm_view_vertical.dart';
import 'package:mangayomi/modules/manga/reader/widgets/transition_view_vertical.dart';
@ -21,7 +21,7 @@ class VirtualMangaList extends ConsumerStatefulWidget {
final double minCacheExtent;
final int initialScrollIndex;
final ScrollPhysics physics;
final Function(reader.UChapDataPreload data) onLongPressData;
final Function(UChapDataPreload data) onLongPressData;
final Function(bool) onFailedToLoadImage;
final BackgroundColor backgroundColor;
final bool isDoublePageMode;
@ -215,7 +215,7 @@ class _VirtualMangaListState extends ConsumerState<VirtualMangaList> {
final int index1 = index * 2 - 1;
final int index2 = index1 + 1;
final List<reader.UChapDataPreload?> datas = index == 0
final List<UChapDataPreload?> datas = index == 0
? [widget.pageManager.getOriginalPage(0), null]
: [
index1 < widget.pageManager.pageCount

View file

@ -2,7 +2,7 @@ import 'dart:async';
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:mangayomi/modules/manga/reader/reader_view.dart' as reader;
import 'package:mangayomi/modules/manga/reader/u_chap_data_preload.dart';
/// Page loading states for virtual scrolling
enum PageLoadState { notLoaded, loading, loaded, error, cached }
@ -10,7 +10,7 @@ enum PageLoadState { notLoaded, loading, loaded, error, cached }
/// Virtual page information for tracking state
class VirtualPageInfo {
final int index;
final reader.UChapDataPreload originalData;
final UChapDataPreload originalData;
PageLoadState loadState;
DateTime? lastAccessTime;
Object? error;
@ -56,7 +56,7 @@ class VirtualPageConfig {
/// Manages virtual page loading and memory optimization
class VirtualPageManager extends ChangeNotifier {
final List<reader.UChapDataPreload> _originalPages;
final List<UChapDataPreload> _originalPages;
final VirtualPageConfig config;
final Map<int, VirtualPageInfo> _pageInfoMap = {};
final Set<int> _preloadQueue = {};
@ -65,7 +65,7 @@ class VirtualPageManager extends ChangeNotifier {
Timer? _cleanupTimer;
VirtualPageManager({
required List<reader.UChapDataPreload> pages,
required List<UChapDataPreload> pages,
this.config = const VirtualPageConfig(),
}) : _originalPages = List.from(pages) {
_initializePages();
@ -108,7 +108,7 @@ class VirtualPageManager extends ChangeNotifier {
}
/// Get original page data
reader.UChapDataPreload? getOriginalPage(int index) {
UChapDataPreload? getOriginalPage(int index) {
if (index < 0 || index >= _originalPages.length) return null;
return _originalPages[index];
}

View file

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mangayomi/modules/manga/reader/u_chap_data_preload.dart';
import 'package:photo_view/photo_view.dart';
import 'package:photo_view/photo_view_gallery.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
@ -7,11 +8,10 @@ import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/models/chapter.dart';
import 'package:mangayomi/modules/manga/reader/virtual_scrolling/virtual_page_manager.dart';
import 'package:mangayomi/modules/manga/reader/virtual_scrolling/virtual_manga_list.dart';
import 'package:mangayomi/modules/manga/reader/reader_view.dart' as reader;
/// Provides virtual page manager instances
final virtualPageManagerProvider =
Provider.family<VirtualPageManager, List<reader.UChapDataPreload>>((
Provider.family<VirtualPageManager, List<UChapDataPreload>>((
ref,
pages,
) {
@ -20,7 +20,7 @@ final virtualPageManagerProvider =
/// Main widget for virtual reading that replaces ScrollablePositionedList
class VirtualReaderView extends ConsumerStatefulWidget {
final List<reader.UChapDataPreload> pages;
final List<UChapDataPreload> pages;
final ItemScrollController itemScrollController;
final ScrollOffsetController scrollOffsetController;
final ItemPositionsListener itemPositionsListener;
@ -28,7 +28,7 @@ class VirtualReaderView extends ConsumerStatefulWidget {
final double minCacheExtent;
final int initialScrollIndex;
final ScrollPhysics physics;
final Function(reader.UChapDataPreload data) onLongPressData;
final Function(UChapDataPreload data) onLongPressData;
final Function(bool) onFailedToLoadImage;
final BackgroundColor backgroundColor;
final bool isDoublePageMode;
@ -169,10 +169,10 @@ mixin VirtualPageManagerMixin<T extends ConsumerStatefulWidget>
}
/// Override this method to provide the pages list
List<reader.UChapDataPreload> getPages();
List<UChapDataPreload> getPages();
/// Call this when pages change
void updateVirtualPages(List<reader.UChapDataPreload> newPages) {
void updateVirtualPages(List<UChapDataPreload> newPages) {
_virtualPageManager?.dispose();
_virtualPageManager = VirtualPageManager(pages: newPages);
}

View file

@ -0,0 +1,80 @@
import 'package:flutter/material.dart';
import 'package:mangayomi/utils/global_style.dart';
class CustomPopupMenuButton<T> extends StatelessWidget {
final String label;
final String title;
final ValueChanged<T> onSelected;
final T value;
final List<T> list;
final String Function(T) itemText;
const CustomPopupMenuButton({
super.key,
required this.label,
required this.title,
required this.onSelected,
required this.value,
required this.list,
required this.itemText,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: PopupMenuButton(
popUpAnimationStyle: popupAnimationStyle,
tooltip: "",
offset: Offset.fromDirection(1),
color: Colors.black,
onSelected: onSelected,
itemBuilder: (context) => [
for (var d in list)
PopupMenuItem(
value: d,
child: Row(
children: [
Icon(
Icons.check,
color: d == value ? Colors.white : Colors.transparent,
),
const SizedBox(width: 7),
Text(
itemText(d),
style: const TextStyle(color: Colors.white),
),
],
),
),
],
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Text(
label,
style: TextStyle(
color: Theme.of(
context,
).textTheme.bodyLarge!.color!.withValues(alpha: 0.9),
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
Row(
children: [
Text(title),
const SizedBox(width: 20),
const Icon(Icons.keyboard_arrow_down_outlined),
],
),
],
),
),
),
);
}
}

View file

@ -0,0 +1,62 @@
import 'package:flutter/material.dart';
class CustomValueIndicatorShape extends SliderComponentShape {
final _indicatorShape = const PaddleSliderValueIndicatorShape();
final bool tranform;
const CustomValueIndicatorShape({this.tranform = false});
@override
Size getPreferredSize(bool isEnabled, bool isDiscrete) {
return const Size(40, 40);
}
@override
void paint(
PaintingContext context,
Offset center, {
required Animation<double> activationAnimation,
required Animation<double> enableAnimation,
required bool isDiscrete,
required TextPainter labelPainter,
required RenderBox parentBox,
required SliderThemeData sliderTheme,
required TextDirection textDirection,
required double value,
required double textScaleFactor,
required Size sizeWithOverflow,
}) {
final textSpan = TextSpan(
text: labelPainter.text?.toPlainText(),
style: sliderTheme.valueIndicatorTextStyle,
);
final textPainter = TextPainter(
text: textSpan,
textAlign: labelPainter.textAlign,
textDirection: textDirection,
);
textPainter.layout();
context.canvas.save();
context.canvas.translate(center.dx, center.dy);
context.canvas.scale(tranform ? -1.0 : 1.0, 1.0);
context.canvas.translate(-center.dx, -center.dy);
_indicatorShape.paint(
context,
center,
activationAnimation: activationAnimation,
enableAnimation: enableAnimation,
labelPainter: textPainter,
parentBox: parentBox,
sliderTheme: sliderTheme,
value: value,
textScaleFactor: textScaleFactor,
sizeWithOverflow: sizeWithOverflow,
isDiscrete: isDiscrete,
textDirection: textDirection,
);
context.canvas.restore();
}
}

View file

@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mangayomi/modules/manga/reader/reader_view.dart';
import 'package:mangayomi/modules/manga/reader/u_chap_data_preload.dart';
import 'package:mangayomi/modules/manga/reader/widgets/chapter_transition_page.dart';
class TransitionViewPaged extends ConsumerWidget {

View file

@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mangayomi/modules/manga/reader/reader_view.dart';
import 'package:mangayomi/modules/manga/reader/u_chap_data_preload.dart';
import 'package:mangayomi/modules/manga/reader/widgets/chapter_transition_page.dart';
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';

View file

@ -0,0 +1,416 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:isar/isar.dart';
import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/custom_button.dart';
import 'package:mangayomi/modules/more/settings/player/providers/custom_buttons_provider.dart';
import 'package:mangayomi/modules/widgets/progress_center.dart';
import 'package:mangayomi/providers/l10n_providers.dart';
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
class CustomButtonScreen extends ConsumerStatefulWidget {
const CustomButtonScreen({super.key});
@override
ConsumerState<CustomButtonScreen> createState() => _CustomButtonScreenState();
}
class _CustomButtonScreenState extends ConsumerState<CustomButtonScreen> {
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final customButtons = ref.watch(getCustomButtonsStreamProvider);
return Scaffold(
appBar: AppBar(title: Text(l10n.custom_buttons_edit)),
body: customButtons.when(
data: (data) {
if (data.isEmpty) {
return Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
l10n.custom_buttons_edit,
textAlign: TextAlign.center,
),
),
);
}
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: ReorderableListView.builder(
buildDefaultDragHandles: false,
itemCount: data.length,
itemBuilder: (context, index) {
final customButton = data[index];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
key: Key("custom_btn_col_${customButton.id}"),
children: [
Row(
key: Key("custom_btn_row_${customButton.id}"),
children: [
ReorderableDragStartListener(
index: index,
child: const Icon(Icons.drag_handle),
),
Expanded(
child: Row(
key: Key("custom_btn_row1_${customButton.id}"),
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Expanded(
child: ListTile(
key: Key(
"custom_btn_tile_${customButton.id}",
),
dense: true,
title: Text(
customButton.title!,
overflow: TextOverflow.ellipsis,
),
),
),
IconButton(
onPressed: () async {
for (final button in data) {
button.isFavourite =
button.id == customButton.id;
}
await isar.writeTxn(
() async =>
await isar.customButtons.putAll(data),
);
},
icon: Icon(
(customButton.isFavourite ?? false)
? Icons.star
: Icons.star_border,
color: context.primaryColor,
),
),
IconButton(
onPressed: () async {
await _showEditForm(customButton);
},
icon: Icon(Icons.mode_edit_outlined),
),
IconButton(
onPressed: () async {
await _showDeleteButton(customButton);
},
icon: Icon(Icons.delete_outline),
),
],
),
),
],
),
Text(
customButton.codePress ?? "",
overflow: TextOverflow.ellipsis,
),
],
);
},
onReorder: (oldIndex, newIndex) async {
if (oldIndex < newIndex) {
final draggedItemPos = data[oldIndex].pos;
for (var i = oldIndex; i < newIndex - 1; i++) {
data[i].pos = data[i + 1].pos;
}
data[newIndex - 1].pos = draggedItemPos;
} else {
final draggedItemPos = data[oldIndex].pos;
for (var i = oldIndex; i > newIndex; i--) {
data[i].pos = data[i - 1].pos;
}
data[newIndex].pos = draggedItemPos;
}
await isar.writeTxn(
() async => await isar.customButtons.putAll(data),
);
},
),
);
},
error: (Object error, StackTrace stackTrace) {
return Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
l10n.custom_buttons_edit,
textAlign: TextAlign.center,
),
),
);
},
loading: () {
return const ProgressCenter();
},
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () async {
await _showEditForm(null);
},
label: Row(
children: [
const Icon(Icons.add),
const SizedBox(width: 10),
Text(l10n.add),
],
),
),
);
}
Future<void> _showEditForm(CustomButton? customButton) async {
bool isTitleMissing = customButton == null;
bool isCodePressMissing = customButton == null;
final titleController = TextEditingController(
text: customButton?.title ?? "",
);
final codePressController = TextEditingController(
text: customButton?.codePress ?? "",
);
final codeLongPressController = TextEditingController(
text: customButton?.codeLongPress ?? "",
);
final codeStartupController = TextEditingController(
text: customButton?.codeStartup ?? "",
);
await showDialog(
context: context,
builder: (context) {
return SizedBox(
child: StatefulBuilder(
builder: (context, setState) {
return AlertDialog(
title: Text(
"${context.l10n.custom_buttons_add}${customButton != null ? " (ID: ${customButton.id})" : ""}",
),
scrollable: true,
content: Column(
children: [
const SizedBox(height: 20),
CustomTextFormField(
name: context.l10n.custom_buttons_text,
helperText: context.l10n.custom_buttons_text_req,
allowEnterNewLine: false,
controller: titleController,
context: context,
missing: (value) {
setState(() {
isTitleMissing = value;
});
},
isMissing: isTitleMissing,
val: (val) {},
),
const SizedBox(height: 20),
CustomTextFormField(
name: context.l10n.custom_buttons_js_code,
helperText: context.l10n.custom_buttons_js_code_req,
minLines: 4,
controller: codePressController,
context: context,
missing: (value) {
setState(() {
isCodePressMissing = value;
});
},
isMissing: isCodePressMissing,
val: (val) {},
),
const SizedBox(height: 20),
CustomTextFormField(
name: context.l10n.custom_buttons_js_code_long,
minLines: 4,
controller: codeLongPressController,
context: context,
missing: (value) {},
isMissing: false,
val: (val) {},
),
const SizedBox(height: 20),
CustomTextFormField(
name: context.l10n.custom_buttons_startup,
minLines: 4,
controller: codeStartupController,
context: context,
missing: (value) {},
isMissing: false,
val: (val) {},
),
],
),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text(context.l10n.cancel),
),
const SizedBox(width: 15),
TextButton(
onPressed: isTitleMissing || isCodePressMissing
? null
: () async {
final temp = await isar.customButtons
.filter()
.idEqualTo(customButton?.id)
.findFirst();
final button =
temp ??
CustomButton(
title: "",
codePress: "",
codeLongPress: "",
codeStartup: "",
pos: await isar.customButtons.count(),
);
await isar.writeTxn(() async {
await isar.customButtons.put(
button
..title = titleController.text
..codePress = codePressController.text
..codeLongPress =
codeLongPressController.text
..codeStartup =
codeStartupController.text,
);
});
if (context.mounted) {
Navigator.pop(context);
}
},
child: Text(
customButton == null
? context.l10n.add
: context.l10n.edit,
style: TextStyle(
color: isTitleMissing || isCodePressMissing
? Theme.of(
context,
).primaryColor.withValues(alpha: 0.2)
: null,
),
),
),
],
),
],
);
},
),
);
},
);
}
Future<void> _showDeleteButton(CustomButton customButton) async {
await showDialog(
context: context,
builder: (context) {
return AlertDialog(
content: Text(context.l10n.custom_buttons_delete),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(context.l10n.cancel),
),
const SizedBox(width: 15),
ElevatedButton(
onPressed: () async {
await isar.writeTxn(
() async =>
await isar.customButtons.delete(customButton.id!),
);
if (context.mounted) {
Navigator.pop(context, "ok");
}
},
child: Text(context.l10n.ok),
),
],
),
],
);
},
);
}
}
class CustomTextFormField extends StatelessWidget {
final TextEditingController controller;
final BuildContext context;
final Function(bool) missing;
final bool isMissing;
final String name;
final String helperText;
final int minLines;
final bool allowEnterNewLine;
final Function(String) val;
const CustomTextFormField({
super.key,
required this.controller,
required this.context,
required this.missing,
required this.isMissing,
this.name = "",
this.helperText = "",
this.minLines = 1,
this.allowEnterNewLine = true,
required this.val,
});
@override
Widget build(BuildContext context) {
return TextFormField(
minLines: minLines,
maxLines: null,
controller: controller,
keyboardType: allowEnterNewLine
? TextInputType.multiline
: TextInputType.text,
onChanged: (value) {
missing(controller.text.isEmpty);
val(value);
},
onFieldSubmitted: (s) {},
decoration: InputDecoration(
helperText: helperText,
helperStyle: TextStyle(color: isMissing ? Colors.red : null),
isDense: true,
label: Text(
name,
style: TextStyle(color: isMissing ? Colors.red : null),
),
filled: true,
fillColor: Colors.transparent,
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: isMissing ? Colors.red : Theme.of(context).primaryColor,
),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: isMissing ? Colors.red : Theme.of(context).primaryColor,
),
),
border: OutlineInputBorder(
borderSide: BorderSide(
color: isMissing ? Colors.red : Theme.of(context).primaryColor,
),
),
),
);
}
}

View file

@ -0,0 +1,138 @@
import 'dart:io';
import 'package:archive/archive.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mangayomi/modules/more/settings/player/providers/player_state_provider.dart';
import 'package:mangayomi/providers/l10n_providers.dart';
import 'package:mangayomi/providers/storage_provider.dart';
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
import 'package:path/path.dart' as path;
import 'package:permission_handler/permission_handler.dart';
class PlayerAdvancedScreen extends ConsumerStatefulWidget {
const PlayerAdvancedScreen({super.key});
@override
ConsumerState<PlayerAdvancedScreen> createState() =>
_PlayerAdvancedScreenState();
}
class _PlayerAdvancedScreenState extends ConsumerState<PlayerAdvancedScreen> {
@override
Widget build(BuildContext context) {
final useMpvConfig = ref.watch(useMpvConfigStateProvider);
return Scaffold(
appBar: AppBar(title: Text(context.l10n.advanced)),
body: SingleChildScrollView(
child: Column(
children: [
SwitchListTile(
value: useMpvConfig,
title: Text(context.l10n.enable_mpv),
subtitle: Text(
context.l10n.mpv_info,
style: TextStyle(fontSize: 11, color: context.secondaryColor),
),
onChanged: (value) async {
if (value && !(await _checkMpvConfig(context))) {
return;
}
ref.read(useMpvConfigStateProvider.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),
),
),
],
),
),
);
}
Future<bool> _checkMpvConfig(
BuildContext context, {
bool redownload = false,
}) async {
var status = await Permission.storage.status;
if (!status.isGranted) {
await Permission.storage.request();
}
final provider = StorageProvider();
final dir = await provider.getMpvDirectory();
final mpvFile = File('${dir!.path}/mpv.conf');
final inputFile = File('${dir.path}/input.conf');
final filesMissing =
!(await mpvFile.exists()) && !(await inputFile.exists());
if ((redownload || filesMissing) && context.mounted) {
final res = await showDialog(
context: context,
builder: (context) {
return AlertDialog(
content: Text(context.l10n.mpv_download),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(context.l10n.cancel),
),
const SizedBox(width: 15),
ElevatedButton(
onPressed: () async {
final bytes = await rootBundle.load(
"assets/mangayomi_mpv.zip",
);
final archive = ZipDecoder().decodeBytes(
bytes.buffer.asUint8List(),
);
String shadersDir = path.join(dir.path, 'shaders');
await Directory(shadersDir).create(recursive: true);
String scriptsDir = path.join(dir.path, 'scripts');
await Directory(scriptsDir).create(recursive: true);
for (final file in archive.files) {
if (file.name == "mpv.conf") {
await mpvFile.writeAsBytes(file.content);
} else if (file.name == "input.conf") {
await inputFile.writeAsBytes(file.content);
} else if (file.name.startsWith("shaders/") &&
file.name.endsWith(".glsl")) {
final shaderFile = File(
'$shadersDir/${file.name.split("/").last}',
);
await shaderFile.writeAsBytes(file.content);
} else if (file.name.startsWith("scripts/") &&
file.name.endsWith(".js")) {
final scriptFile = File(
'$scriptsDir/${file.name.split("/").last}',
);
await scriptFile.writeAsBytes(file.content);
}
}
if (context.mounted) {
Navigator.pop(context, "ok");
}
},
child: Text(context.l10n.download),
),
],
),
],
);
},
);
return res != null && res == "ok";
}
return context.mounted;
}
}

View file

@ -0,0 +1,236 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mangayomi/l10n/generated/app_localizations.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/modules/more/settings/player/custom_button_screen.dart';
import 'package:mangayomi/modules/more/settings/player/providers/player_audio_state_provider.dart';
import 'package:mangayomi/providers/l10n_providers.dart';
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
import 'package:super_sliver_list/super_sliver_list.dart';
class PlayerAudioScreen extends ConsumerStatefulWidget {
const PlayerAudioScreen({super.key});
@override
ConsumerState<PlayerAudioScreen> createState() => _PlayerAudioScreenState();
}
class _PlayerAudioScreenState extends ConsumerState<PlayerAudioScreen> {
@override
Widget build(BuildContext context) {
final audioPreferredLang = ref.watch(audioPreferredLangStateProvider);
final enableAudioPitchCorrection = ref.watch(
enableAudioPitchCorrectionStateProvider,
);
final audioChannel = ref.watch(audioChannelStateProvider);
final volumeBoostCap = ref.watch(volumeBoostCapStateProvider);
return Scaffold(
appBar: AppBar(title: Text(context.l10n.video_audio)),
body: SingleChildScrollView(
child: Column(
children: [
ListTile(
onTap: () => _showEditController(),
title: Text(context.l10n.audio_preferred_languages),
subtitle: Text(
audioPreferredLang,
style: TextStyle(fontSize: 11, color: context.secondaryColor),
),
),
SwitchListTile(
value: enableAudioPitchCorrection,
title: Text(context.l10n.enable_audio_pitch_correction),
subtitle: Text(
context.l10n.enable_audio_pitch_correction_info,
style: TextStyle(fontSize: 11, color: context.secondaryColor),
),
onChanged: (value) {
ref
.read(enableAudioPitchCorrectionStateProvider.notifier)
.set(value);
},
),
ListTile(
onTap: () {
final values = [
(AudioChannel.auto, "Auto"),
(AudioChannel.autoSafe, "Auto-safe"),
(AudioChannel.mono, "Mono"),
(AudioChannel.stereo, "Stereo"),
(AudioChannel.reverseStereo, "Reverse stereo"),
];
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text(context.l10n.audio_channels),
content: SizedBox(
width: context.width(0.8),
child: SuperListView.builder(
shrinkWrap: true,
itemCount: values.length,
itemBuilder: (context, index) {
return RadioListTile(
dense: true,
contentPadding: const EdgeInsets.all(0),
value: values[index].$1,
groupValue: audioChannel,
onChanged: (value) {
ref
.read(audioChannelStateProvider.notifier)
.set(value!);
Navigator.pop(context);
},
title: Row(children: [Text(values[index].$2)]),
);
},
),
),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () async {
Navigator.pop(context);
},
child: Text(
context.l10n.cancel,
style: TextStyle(color: context.primaryColor),
),
),
],
),
],
);
},
);
},
title: Text(context.l10n.audio_channels),
subtitle: Text(
audioChannel.name,
style: TextStyle(fontSize: 11, color: context.secondaryColor),
),
),
Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(context.l10n.volume_boost_cap),
Text(
"$volumeBoostCap",
style: TextStyle(
fontSize: 11,
color: context.secondaryColor,
),
),
const SizedBox(height: 20),
SliderTheme(
data: SliderTheme.of(context).copyWith(
overlayShape: const RoundSliderOverlayShape(
overlayRadius: 5.0,
),
),
child: Slider.adaptive(
min: 0,
max: 200,
value: volumeBoostCap.toDouble(),
onChanged: (value) {
HapticFeedback.vibrate();
ref
.read(volumeBoostCapStateProvider.notifier)
.set(value.toInt());
},
onChangeEnd: (value) {
ref
.read(volumeBoostCapStateProvider.notifier)
.set(value.toInt());
},
),
),
],
),
),
],
),
),
);
}
void _showEditController() {
final audioPreferredLang = ref.read(audioPreferredLangStateProvider);
final langCodes = AppLocalizations.supportedLocales
.map((e) => e.languageCode)
.toList();
bool isLangCodeError = false;
final textController = TextEditingController(text: audioPreferredLang);
showDialog(
context: context,
builder: (context) {
return StatefulBuilder(
builder: (context, setState) {
return AlertDialog(
title: Column(
children: [
Text(context.l10n.audio_preferred_languages),
Text(
context.l10n.audio_preferred_languages_info,
style: TextStyle(
fontSize: 11,
color: context.secondaryColor,
),
),
],
),
content: SizedBox(
width: context.width(0.8),
child: CustomTextFormField(
controller: textController,
context: context,
isMissing: isLangCodeError,
val: (text) => setState(() {
isLangCodeError = text
.split(",")
.any((e) => !langCodes.contains(e));
}),
missing: (_) {},
),
),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text(
context.l10n.cancel,
style: TextStyle(color: context.primaryColor),
),
),
TextButton(
onPressed: () {
ref
.read(audioPreferredLangStateProvider.notifier)
.set(textController.text);
Navigator.pop(context);
},
child: Text(
context.l10n.ok,
style: TextStyle(color: context.primaryColor),
),
),
],
),
],
);
},
);
},
);
}
}

View file

@ -0,0 +1,195 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/modules/more/settings/player/providers/player_decoder_state_provider.dart';
import 'package:mangayomi/providers/l10n_providers.dart';
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
import 'package:super_sliver_list/super_sliver_list.dart';
class PlayerDecoderScreen extends ConsumerStatefulWidget {
const PlayerDecoderScreen({super.key});
@override
ConsumerState<PlayerDecoderScreen> createState() =>
_PlayerDecoderScreenState();
}
class _PlayerDecoderScreenState extends ConsumerState<PlayerDecoderScreen> {
@override
Widget build(BuildContext context) {
final hwdecMode = ref.watch(hwdecModeStateProvider(rawValue: true));
final useGpuNext = ref.watch(useGpuNextStateProvider);
final debandingType = ref.watch(debandingStateProvider);
final useYUV420P = ref.watch(useYUV420PStateProvider);
return Scaffold(
appBar: AppBar(title: Text(context.l10n.decoder)),
body: SingleChildScrollView(
child: Column(
children: [
ListTile(
onTap: () {
final values = [
("no", ""),
("auto", ""),
("d3d11va", "(Windows 8+)"),
("d3d11va-copy", "(Windows 8+)"),
("videotoolbox", "(iOS 9.0+)"),
("videotoolbox-copy", "(iOS 9.0+)"),
("nvdec", "(CUDA)"),
("nvdec-copy", "(CUDA)"),
("mediacodec", "- HW (Android)"),
("mediacodec-copy", "- HW+ (Android)"),
("crystalhd", ""),
];
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text(context.l10n.hwdec),
content: SizedBox(
width: context.width(0.8),
child: SuperListView.builder(
shrinkWrap: true,
itemCount: values.length,
itemBuilder: (context, index) {
return RadioListTile(
dense: true,
contentPadding: const EdgeInsets.all(0),
value: values[index].$1,
groupValue: hwdecMode,
onChanged: (value) {
ref
.read(
hwdecModeStateProvider(
rawValue: true,
).notifier,
)
.set(value!);
Navigator.pop(context);
},
title: Row(
children: [
Text(
"${values[index].$1} ${values[index].$2}",
),
],
),
);
},
),
),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () async {
Navigator.pop(context);
},
child: Text(
context.l10n.cancel,
style: TextStyle(color: context.primaryColor),
),
),
],
),
],
);
},
);
},
title: Text(context.l10n.hwdec),
subtitle: Text(
hwdecMode,
style: TextStyle(fontSize: 11, color: context.secondaryColor),
),
),
SwitchListTile(
value: useGpuNext,
title: Text(context.l10n.enable_gpu_next),
subtitle: Text(
context.l10n.enable_gpu_next_info,
style: TextStyle(fontSize: 11, color: context.secondaryColor),
),
onChanged: (value) {
ref.read(useGpuNextStateProvider.notifier).set(value);
},
),
ListTile(
onTap: () {
final values = [
(DebandingType.none, "None"),
(DebandingType.cpu, "CPU"),
(DebandingType.gpu, "GPU"),
];
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text(context.l10n.debanding),
content: SizedBox(
width: context.width(0.8),
child: SuperListView.builder(
shrinkWrap: true,
itemCount: values.length,
itemBuilder: (context, index) {
return RadioListTile(
dense: true,
contentPadding: const EdgeInsets.all(0),
value: values[index].$1,
groupValue: debandingType,
onChanged: (value) {
ref
.read(debandingStateProvider.notifier)
.set(value!);
Navigator.pop(context);
},
title: Row(children: [Text(values[index].$2)]),
);
},
),
),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () async {
Navigator.pop(context);
},
child: Text(
context.l10n.cancel,
style: TextStyle(color: context.primaryColor),
),
),
],
),
],
);
},
);
},
title: Text(context.l10n.debanding),
subtitle: Text(
debandingType.name,
style: TextStyle(fontSize: 11, color: context.secondaryColor),
),
),
SwitchListTile(
value: useYUV420P,
title: Text(context.l10n.use_yuv420p),
subtitle: Text(
context.l10n.use_yuv420p_info,
style: TextStyle(fontSize: 11, color: context.secondaryColor),
),
onChanged: (value) {
ref.read(useYUV420PStateProvider.notifier).set(value);
},
),
],
),
),
);
}
}

View file

@ -0,0 +1,52 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:mangayomi/modules/more/widgets/list_tile_widget.dart';
import 'package:mangayomi/providers/l10n_providers.dart';
class PlayerOverviewScreen extends StatelessWidget {
const PlayerOverviewScreen({super.key});
@override
Widget build(BuildContext context) {
final l10n = l10nLocalizations(context);
return Scaffold(
appBar: AppBar(title: Text(l10n!.player)),
body: SingleChildScrollView(
child: Column(
children: [
ListTileWidget(
title: l10n.internal_player,
subtitle: l10n.internal_player_info,
icon: Icons.play_circle_outline_outlined,
onTap: () => context.push('/playerMode'),
),
ListTileWidget(
title: l10n.decoder,
subtitle: l10n.decoder_info,
icon: Icons.memory_outlined,
onTap: () => context.push('/playerDecoderScreen'),
),
ListTileWidget(
title: l10n.video_audio,
subtitle: l10n.video_audio_info,
icon: Icons.audiotrack_outlined,
onTap: () => context.push('/playerAudioScreen'),
),
ListTileWidget(
title: l10n.custom_buttons,
subtitle: l10n.custom_buttons_info,
icon: Icons.terminal_outlined,
onTap: () => context.push('/customButtonScreen'),
),
ListTileWidget(
title: l10n.advanced,
subtitle: l10n.advanced_info,
icon: Icons.code_outlined,
onTap: () => context.push('/playerAdvancedScreen'),
),
],
),
),
);
}
}

View file

@ -8,11 +8,16 @@ import 'package:numberpicker/numberpicker.dart';
import 'package:super_sliver_list/super_sliver_list.dart';
import 'package:mangayomi/l10n/generated/app_localizations.dart';
class PlayerScreen extends ConsumerWidget {
class PlayerScreen extends ConsumerStatefulWidget {
const PlayerScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
ConsumerState<PlayerScreen> createState() => _PlayerScreenState();
}
class _PlayerScreenState extends ConsumerState<PlayerScreen> {
@override
Widget build(BuildContext context) {
final defaultSubtitleLang = ref.watch(defaultSubtitleLangStateProvider);
final markEpisodeAsSeenType = ref.watch(markEpisodeAsSeenTypeStateProvider);
final defaultSkipIntroLength = ref.watch(
@ -26,11 +31,10 @@ class PlayerScreen extends ConsumerWidget {
final enableAutoSkip = ref.watch(enableAutoSkipStateProvider);
final aniSkipTimeoutLength = ref.watch(aniSkipTimeoutLengthStateProvider);
final useLibass = ref.watch(useLibassStateProvider);
final hwdecMode = ref.watch(hwdecModeStateProvider(rawValue: true));
final fullScreenPlayer = ref.watch(fullScreenPlayerStateProvider);
return Scaffold(
appBar: AppBar(title: Text(context.l10n.player)),
appBar: AppBar(title: Text(context.l10n.internal_player)),
body: SingleChildScrollView(
child: Column(
children: [
@ -343,23 +347,6 @@ class PlayerScreen extends ConsumerWidget {
style: TextStyle(fontSize: 11, color: context.secondaryColor),
),
),
ListTile(
title: Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
children: [
Icon(
Icons.info_outline_rounded,
color: context.secondaryColor,
),
],
),
),
subtitle: Text(
context.l10n.aniskip_requires_info,
style: TextStyle(fontSize: 11, color: context.secondaryColor),
),
),
SwitchListTile(
value: useLibass,
title: Text(context.l10n.use_libass),
@ -380,6 +367,26 @@ class PlayerScreen extends ConsumerWidget {
onExpansionChanged: (value) =>
ref.read(enableAniSkipStateProvider.notifier).set(value),
children: [
ListTile(
title: Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
children: [
Icon(
Icons.info_outline_rounded,
color: context.secondaryColor,
),
],
),
),
subtitle: Text(
context.l10n.aniskip_requires_info,
style: TextStyle(
fontSize: 11,
color: context.secondaryColor,
),
),
),
SwitchListTile(
value: enableAutoSkip,
title: Text(context.l10n.enable_auto_skip),
@ -468,80 +475,6 @@ class PlayerScreen extends ConsumerWidget {
ref.read(fullScreenPlayerStateProvider.notifier).set(value);
},
),
ListTile(
onTap: () {
final values = [
("no", ""),
("auto", ""),
("d3d11va", "(Windows 8+)"),
("d3d11va-copy", "(Windows 8+)"),
("videotoolbox", "(iOS 9.0+)"),
("videotoolbox-copy", "(iOS 9.0+)"),
("nvdec", "(CUDA)"),
("nvdec-copy", "(CUDA)"),
("mediacodec", "- HW (Android)"),
("mediacodec-copy", "- HW+ (Android)"),
("crystalhd", ""),
];
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text(context.l10n.hwdec),
content: SizedBox(
width: context.width(0.8),
child: SuperListView.builder(
shrinkWrap: true,
itemCount: values.length,
itemBuilder: (context, index) {
return RadioListTile(
dense: true,
contentPadding: const EdgeInsets.all(0),
value: values[index].$1,
groupValue: hwdecMode,
onChanged: (value) {
ref
.read(hwdecModeStateProvider(rawValue: true).notifier)
.set(value!);
Navigator.pop(context);
},
title: Row(
children: [
Text(
"${values[index].$1} ${values[index].$2}",
),
],
),
);
},
),
),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () async {
Navigator.pop(context);
},
child: Text(
context.l10n.cancel,
style: TextStyle(color: context.primaryColor),
),
),
],
),
],
);
},
);
},
title: Text(context.l10n.hwdec),
subtitle: Text(
hwdecMode,
style: TextStyle(fontSize: 11, color: context.secondaryColor),
),
),
],
),
),

View file

@ -0,0 +1,13 @@
import 'package:isar/isar.dart';
import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/custom_button.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
part 'custom_buttons_provider.g.dart';
@riverpod
Stream<List<CustomButton>> getCustomButtonsStream(Ref ref) async* {
yield* isar.customButtons.filter().idIsNotNull().sortByPos().watch(
fireImmediately: true,
);
}

View file

@ -0,0 +1,30 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'custom_buttons_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$getCustomButtonsStreamHash() =>
r'476c26eb3d20e9e9eed2e1d8bb15fa74ce357ba3';
/// See also [getCustomButtonsStream].
@ProviderFor(getCustomButtonsStream)
final getCustomButtonsStreamProvider =
AutoDisposeStreamProvider<List<CustomButton>>.internal(
getCustomButtonsStream,
name: r'getCustomButtonsStreamProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$getCustomButtonsStreamHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef GetCustomButtonsStreamRef
= AutoDisposeStreamProviderRef<List<CustomButton>>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View file

@ -0,0 +1,86 @@
import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'player_audio_state_provider.g.dart';
@riverpod
class AudioPreferredLangState extends _$AudioPreferredLangState {
@override
String build() {
return isar.settings.getSync(227)!.audioPreferredLanguages ?? "";
}
void set(String value) {
final settings = isar.settings.getSync(227);
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(
settings!
..audioPreferredLanguages = value
..updatedAt = DateTime.now().millisecondsSinceEpoch,
),
);
}
}
@riverpod
class EnableAudioPitchCorrectionState
extends _$EnableAudioPitchCorrectionState {
@override
bool build() {
return isar.settings.getSync(227)!.enableAudioPitchCorrection ?? true;
}
void set(bool value) {
final settings = isar.settings.getSync(227);
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(
settings!
..enableAudioPitchCorrection = value
..updatedAt = DateTime.now().millisecondsSinceEpoch,
),
);
}
}
@riverpod
class AudioChannelState extends _$AudioChannelState {
@override
AudioChannel build() {
return isar.settings.getSync(227)!.audioChannels;
}
void set(AudioChannel value) {
final settings = isar.settings.getSync(227);
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(
settings!
..audioChannels = value
..updatedAt = DateTime.now().millisecondsSinceEpoch,
),
);
}
}
@riverpod
class VolumeBoostCapState extends _$VolumeBoostCapState {
@override
int build() {
return isar.settings.getSync(227)!.volumeBoostCap ?? 30;
}
void set(int value) {
final settings = isar.settings.getSync(227);
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(
settings!
..volumeBoostCap = value
..updatedAt = DateTime.now().millisecondsSinceEpoch,
),
);
}
}

View file

@ -0,0 +1,77 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'player_audio_state_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$audioPreferredLangStateHash() =>
r'9d70ec2677efb51b8e0c174b55114865853f12ea';
/// See also [AudioPreferredLangState].
@ProviderFor(AudioPreferredLangState)
final audioPreferredLangStateProvider =
AutoDisposeNotifierProvider<AudioPreferredLangState, String>.internal(
AudioPreferredLangState.new,
name: r'audioPreferredLangStateProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$audioPreferredLangStateHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$AudioPreferredLangState = AutoDisposeNotifier<String>;
String _$enableAudioPitchCorrectionStateHash() =>
r'6614f4b04ff8fe8ef57c9a6f160646d3d25e2f4d';
/// See also [EnableAudioPitchCorrectionState].
@ProviderFor(EnableAudioPitchCorrectionState)
final enableAudioPitchCorrectionStateProvider =
AutoDisposeNotifierProvider<EnableAudioPitchCorrectionState, bool>.internal(
EnableAudioPitchCorrectionState.new,
name: r'enableAudioPitchCorrectionStateProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$enableAudioPitchCorrectionStateHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$EnableAudioPitchCorrectionState = AutoDisposeNotifier<bool>;
String _$audioChannelStateHash() => r'e71ffa85c37d545fb7b22e9539241b4926a2d384';
/// See also [AudioChannelState].
@ProviderFor(AudioChannelState)
final audioChannelStateProvider =
AutoDisposeNotifierProvider<AudioChannelState, AudioChannel>.internal(
AudioChannelState.new,
name: r'audioChannelStateProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$audioChannelStateHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$AudioChannelState = AutoDisposeNotifier<AudioChannel>;
String _$volumeBoostCapStateHash() =>
r'b0f5ad3bbb0e1a798ce229572b363465ad606a06';
/// See also [VolumeBoostCapState].
@ProviderFor(VolumeBoostCapState)
final volumeBoostCapStateProvider =
AutoDisposeNotifierProvider<VolumeBoostCapState, int>.internal(
VolumeBoostCapState.new,
name: r'volumeBoostCapStateProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$volumeBoostCapStateHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$VolumeBoostCapState = AutoDisposeNotifier<int>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View file

@ -0,0 +1,110 @@
import 'dart:io';
import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'player_decoder_state_provider.g.dart';
final hwdecs = {
"no": ["all"],
"auto": ["all"],
"d3d11va": ["windows"],
"d3d11va-copy": ["windows"],
"videotoolbox": ["ios"],
"videotoolbox-copy": ["ios"],
"nvdec": ["all"],
"nvdec-copy": ["all"],
"mediacodec": ["android"],
"mediacodec-copy": ["android"],
"crystalhd": ["all"],
};
@riverpod
class HwdecModeState extends _$HwdecModeState {
@override
String build({bool rawValue = false}) {
final hwdecMode = isar.settings.getSync(227)!.hwdecMode ?? "auto";
if (rawValue) {
return hwdecMode;
}
final hwdecSupport = hwdecs[hwdecMode] ?? [];
if (!hwdecSupport.contains("all") &&
!hwdecSupport.contains(Platform.operatingSystem)) {
return Platform.isAndroid ? "auto-safe" : "auto";
}
return hwdecMode;
}
void set(String value) {
final settings = isar.settings.getSync(227);
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(
settings!
..hwdecMode = value
..updatedAt = DateTime.now().millisecondsSinceEpoch,
),
);
}
}
@riverpod
class DebandingState extends _$DebandingState {
@override
DebandingType build() {
return isar.settings.getSync(227)!.debandingType;
}
void set(DebandingType value) {
final settings = isar.settings.getSync(227);
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(
settings!
..debandingType = value
..updatedAt = DateTime.now().millisecondsSinceEpoch,
),
);
}
}
@riverpod
class UseGpuNextState extends _$UseGpuNextState {
@override
bool build() {
return isar.settings.getSync(227)!.enableGpuNext ?? false;
}
void set(bool value) {
final settings = isar.settings.getSync(227);
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(
settings!
..enableGpuNext = value
..updatedAt = DateTime.now().millisecondsSinceEpoch,
),
);
}
}
@riverpod
class UseYUV420PState extends _$UseYUV420PState {
@override
bool build() {
return isar.settings.getSync(227)!.useYUV420P ?? false;
}
void set(bool value) {
final settings = isar.settings.getSync(227);
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(
settings!
..useYUV420P = value
..updatedAt = DateTime.now().millisecondsSinceEpoch,
),
);
}
}

View file

@ -0,0 +1,223 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'player_decoder_state_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$hwdecModeStateHash() => r'8186e3c5f3db0e952f629d56b2e580e546aed65e';
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
abstract class _$HwdecModeState extends BuildlessAutoDisposeNotifier<String> {
late final bool rawValue;
String build({
bool rawValue = false,
});
}
/// See also [HwdecModeState].
@ProviderFor(HwdecModeState)
const hwdecModeStateProvider = HwdecModeStateFamily();
/// See also [HwdecModeState].
class HwdecModeStateFamily extends Family<String> {
/// See also [HwdecModeState].
const HwdecModeStateFamily();
/// See also [HwdecModeState].
HwdecModeStateProvider call({
bool rawValue = false,
}) {
return HwdecModeStateProvider(
rawValue: rawValue,
);
}
@override
HwdecModeStateProvider getProviderOverride(
covariant HwdecModeStateProvider provider,
) {
return call(
rawValue: provider.rawValue,
);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'hwdecModeStateProvider';
}
/// See also [HwdecModeState].
class HwdecModeStateProvider
extends AutoDisposeNotifierProviderImpl<HwdecModeState, String> {
/// See also [HwdecModeState].
HwdecModeStateProvider({
bool rawValue = false,
}) : this._internal(
() => HwdecModeState()..rawValue = rawValue,
from: hwdecModeStateProvider,
name: r'hwdecModeStateProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$hwdecModeStateHash,
dependencies: HwdecModeStateFamily._dependencies,
allTransitiveDependencies:
HwdecModeStateFamily._allTransitiveDependencies,
rawValue: rawValue,
);
HwdecModeStateProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.rawValue,
}) : super.internal();
final bool rawValue;
@override
String runNotifierBuild(
covariant HwdecModeState notifier,
) {
return notifier.build(
rawValue: rawValue,
);
}
@override
Override overrideWith(HwdecModeState Function() create) {
return ProviderOverride(
origin: this,
override: HwdecModeStateProvider._internal(
() => create()..rawValue = rawValue,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
rawValue: rawValue,
),
);
}
@override
AutoDisposeNotifierProviderElement<HwdecModeState, String> createElement() {
return _HwdecModeStateProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is HwdecModeStateProvider && other.rawValue == rawValue;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, rawValue.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin HwdecModeStateRef on AutoDisposeNotifierProviderRef<String> {
/// The parameter `rawValue` of this provider.
bool get rawValue;
}
class _HwdecModeStateProviderElement
extends AutoDisposeNotifierProviderElement<HwdecModeState, String>
with HwdecModeStateRef {
_HwdecModeStateProviderElement(super.provider);
@override
bool get rawValue => (origin as HwdecModeStateProvider).rawValue;
}
String _$debandingStateHash() => r'b93e2fc826d98cc8bce1aab9a92900353e4d3958';
/// See also [DebandingState].
@ProviderFor(DebandingState)
final debandingStateProvider =
AutoDisposeNotifierProvider<DebandingState, DebandingType>.internal(
DebandingState.new,
name: r'debandingStateProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$debandingStateHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$DebandingState = AutoDisposeNotifier<DebandingType>;
String _$useGpuNextStateHash() => r'cfc109cd7db66e359e9523102a84aa8cf37bf243';
/// See also [UseGpuNextState].
@ProviderFor(UseGpuNextState)
final useGpuNextStateProvider =
AutoDisposeNotifierProvider<UseGpuNextState, bool>.internal(
UseGpuNextState.new,
name: r'useGpuNextStateProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$useGpuNextStateHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$UseGpuNextState = AutoDisposeNotifier<bool>;
String _$useYUV420PStateHash() => r'c600001eff34b2b8df31ba604413b8b20edc3044';
/// See also [UseYUV420PState].
@ProviderFor(UseYUV420PState)
final useYUV420PStateProvider =
AutoDisposeNotifierProvider<UseYUV420PState, bool>.internal(
UseYUV420PState.new,
name: r'useYUV420PStateProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$useYUV420PStateHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$UseYUV420PState = AutoDisposeNotifier<bool>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View file

@ -218,45 +218,18 @@ class UseLibassState extends _$UseLibassState {
}
}
final hwdecs = {
"no": ["all"],
"auto": ["all"],
"d3d11va": ["windows"],
"d3d11va-copy": ["windows"],
"videotoolbox": ["ios"],
"videotoolbox-copy": ["ios"],
"nvdec": ["all"],
"nvdec-copy": ["all"],
"mediacodec": ["android"],
"mediacodec-copy": ["android"],
"crystalhd": ["all"],
};
@riverpod
class HwdecModeState extends _$HwdecModeState {
class UseMpvConfigState extends _$UseMpvConfigState {
@override
String build({bool rawValue = false}) {
final hwdecMode = isar.settings.getSync(227)!.hwdecMode ?? "auto";
if (rawValue) {
return hwdecMode;
}
final hwdecSupport = hwdecs[hwdecMode] ?? [];
if (!hwdecSupport.contains("all") &&
!hwdecSupport.contains(Platform.operatingSystem)) {
return Platform.isAndroid ? "auto-safe" : "auto";
}
return hwdecMode;
bool build() {
return isar.settings.getSync(227)!.useMpvConfig ?? false;
}
void set(String value) {
void set(bool value) {
final settings = isar.settings.getSync(227);
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(
settings!
..hwdecMode = value
..updatedAt = DateTime.now().millisecondsSinceEpoch,
),
() => isar.settings.putSync(settings!..useMpvConfig = value),
);
}
}

View file

@ -175,169 +175,21 @@ final useLibassStateProvider =
);
typedef _$UseLibassState = AutoDisposeNotifier<bool>;
String _$hwdecModeStateHash() => r'8186e3c5f3db0e952f629d56b2e580e546aed65e';
String _$useMpvConfigStateHash() => r'f91e6a7dbd3c5f7674ba74842521ecfca01c78b0';
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
/// See also [UseMpvConfigState].
@ProviderFor(UseMpvConfigState)
final useMpvConfigStateProvider =
AutoDisposeNotifierProvider<UseMpvConfigState, bool>.internal(
UseMpvConfigState.new,
name: r'useMpvConfigStateProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$useMpvConfigStateHash,
dependencies: null,
allTransitiveDependencies: null,
);
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
abstract class _$HwdecModeState extends BuildlessAutoDisposeNotifier<String> {
late final bool rawValue;
String build({
bool rawValue = false,
});
}
/// See also [HwdecModeState].
@ProviderFor(HwdecModeState)
const hwdecModeStateProvider = HwdecModeStateFamily();
/// See also [HwdecModeState].
class HwdecModeStateFamily extends Family<String> {
/// See also [HwdecModeState].
const HwdecModeStateFamily();
/// See also [HwdecModeState].
HwdecModeStateProvider call({
bool rawValue = false,
}) {
return HwdecModeStateProvider(
rawValue: rawValue,
);
}
@override
HwdecModeStateProvider getProviderOverride(
covariant HwdecModeStateProvider provider,
) {
return call(
rawValue: provider.rawValue,
);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'hwdecModeStateProvider';
}
/// See also [HwdecModeState].
class HwdecModeStateProvider
extends AutoDisposeNotifierProviderImpl<HwdecModeState, String> {
/// See also [HwdecModeState].
HwdecModeStateProvider({
bool rawValue = false,
}) : this._internal(
() => HwdecModeState()..rawValue = rawValue,
from: hwdecModeStateProvider,
name: r'hwdecModeStateProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$hwdecModeStateHash,
dependencies: HwdecModeStateFamily._dependencies,
allTransitiveDependencies:
HwdecModeStateFamily._allTransitiveDependencies,
rawValue: rawValue,
);
HwdecModeStateProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.rawValue,
}) : super.internal();
final bool rawValue;
@override
String runNotifierBuild(
covariant HwdecModeState notifier,
) {
return notifier.build(
rawValue: rawValue,
);
}
@override
Override overrideWith(HwdecModeState Function() create) {
return ProviderOverride(
origin: this,
override: HwdecModeStateProvider._internal(
() => create()..rawValue = rawValue,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
rawValue: rawValue,
),
);
}
@override
AutoDisposeNotifierProviderElement<HwdecModeState, String> createElement() {
return _HwdecModeStateProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is HwdecModeStateProvider && other.rawValue == rawValue;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, rawValue.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin HwdecModeStateRef on AutoDisposeNotifierProviderRef<String> {
/// The parameter `rawValue` of this provider.
bool get rawValue;
}
class _HwdecModeStateProviderElement
extends AutoDisposeNotifierProviderElement<HwdecModeState, String>
with HwdecModeStateRef {
_HwdecModeStateProviderElement(super.provider);
@override
bool get rawValue => (origin as HwdecModeStateProvider).rawValue;
}
typedef _$UseMpvConfigState = AutoDisposeNotifier<bool>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View file

@ -21,52 +21,43 @@ class SettingsScreen extends StatelessWidget {
),
ListTileWidget(
title: l10n.appearance,
subtitle: l10n.appearance_subtitle,
icon: Icons.color_lens_rounded,
onTap: () => context.push('/appearance'),
),
ListTileWidget(
title: l10n.reader,
subtitle: l10n.reader_subtitle,
icon: Icons.chrome_reader_mode_rounded,
onTap: () => context.push('/readerMode'),
),
ListTileWidget(
title: l10n.player,
subtitle: l10n.reader_subtitle,
icon: Icons.play_circle_outline_outlined,
onTap: () => context.push('/playerMode'),
onTap: () => context.push('/playerOverview'),
),
ListTileWidget(
title: l10n.downloads,
subtitle: l10n.downloads_subtitle,
icon: Icons.download_outlined,
onTap: () => context.push('/downloads'),
),
ListTileWidget(
title: l10n.tracking,
subtitle: "",
icon: Icons.sync_outlined,
onTap: () => context.push('/track'),
),
ListTileWidget(
title: l10n.syncing,
subtitle: l10n.syncing_subtitle,
icon: Icons.cloud_sync_outlined,
onTap: () => context.push('/sync'),
),
ListTileWidget(
title: l10n.browse,
subtitle: l10n.browse_subtitle,
icon: Icons.explore_rounded,
onTap: () => context.push('/browseS'),
),
ListTileWidget(
onTap: () {
context.push('/about');
},
icon: Icons.info_outline,
title: l10n.about,
icon: Icons.info_outline,
onTap: () => context.push('/about'),
),
],
),

View file

@ -31,6 +31,12 @@ class ListTileWidget extends StatelessWidget {
child: Icon(icon, color: context.primaryColor),
),
title: Text(title),
subtitle: subtitle != null
? Text(
subtitle!,
style: TextStyle(fontSize: 11, color: context.secondaryColor),
)
: null,
trailing: trailing,
);
}

View file

@ -11,7 +11,6 @@ import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/chapter.dart';
import 'package:mangayomi/models/page.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/modules/anime/widgets/desktop.dart';
import 'package:mangayomi/modules/manga/reader/widgets/btn_chapter_list_dialog.dart';
@ -22,9 +21,7 @@ import 'package:mangayomi/services/get_html_content.dart';
import 'package:mangayomi/utils/extensions/dom_extensions.dart';
import 'package:mangayomi/utils/utils.dart';
import 'package:mangayomi/modules/manga/reader/providers/push_router.dart';
import 'package:mangayomi/services/get_chapter_pages.dart';
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
import 'package:mangayomi/utils/global_style.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:window_manager/window_manager.dart';
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
@ -759,104 +756,3 @@ class _NovelWebViewState extends ConsumerState<NovelWebView>
return null;
}
}
class UChapDataPreload {
Chapter? chapter;
Directory? directory;
PageUrl? pageUrl;
bool? isLocale;
Uint8List? archiveImage;
int? index;
GetChapterPagesModel? chapterUrlModel;
int? pageIndex;
Uint8List? cropImage;
UChapDataPreload(
this.chapter,
this.directory,
this.pageUrl,
this.isLocale,
this.archiveImage,
this.index,
this.chapterUrlModel,
this.pageIndex, {
this.cropImage,
});
}
class CustomPopupMenuButton<T> extends StatelessWidget {
final String label;
final String title;
final ValueChanged<T> onSelected;
final T value;
final List<T> list;
final String Function(T) itemText;
const CustomPopupMenuButton({
super.key,
required this.label,
required this.title,
required this.onSelected,
required this.value,
required this.list,
required this.itemText,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: PopupMenuButton(
popUpAnimationStyle: popupAnimationStyle,
tooltip: "",
offset: Offset.fromDirection(1),
color: Colors.black,
onSelected: onSelected,
itemBuilder: (context) => [
for (var d in list)
PopupMenuItem(
value: d,
child: Row(
children: [
Icon(
Icons.check,
color: d == value ? Colors.white : Colors.transparent,
),
const SizedBox(width: 7),
Text(
itemText(d),
style: const TextStyle(color: Colors.white),
),
],
),
),
],
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Text(
label,
style: TextStyle(
color: Theme.of(
context,
).textTheme.bodyLarge!.color!.withValues(alpha: 0.9),
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
Row(
children: [
Text(title),
const SizedBox(width: 20),
const Icon(Icons.keyboard_arrow_down_outlined),
],
),
],
),
),
),
);
}
}

View file

@ -15,11 +15,13 @@ import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
class TrackerItemCard extends StatelessWidget {
final TrackSearch track;
final ItemType itemType;
final int? mangaId;
const TrackerItemCard({
super.key,
required this.track,
required this.itemType,
this.mangaId,
});
@override
@ -140,6 +142,12 @@ class TrackerItemCard extends StatelessWidget {
initExpanded: false,
onChanged: (value) {},
),
if (mangaId != null)
TextButton.icon(
onPressed: () => _pushLocalLibrary(context),
label: Text(l10n.track_library_navigate),
icon: Icon(Icons.north_east),
),
TextButton.icon(
onPressed: () => _pushMigrationScreen(context),
label: Text(l10n.track_library_add),
@ -182,6 +190,12 @@ class TrackerItemCard extends StatelessWidget {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
if (mangaId != null)
TextButton.icon(
onPressed: () => _pushLocalLibrary(context),
label: Text(l10n.track_library_navigate),
icon: Icon(Icons.north_east),
),
TextButton.icon(
onPressed: () => _pushMigrationScreen(context),
label: Text(l10n.track_library_add),
@ -250,6 +264,10 @@ class TrackerItemCard extends StatelessWidget {
);
}
void _pushLocalLibrary(BuildContext context) {
context.push('/manga-reader/detail', extra: mangaId);
}
void _pushMigrationScreen(BuildContext context) {
context.push(
"/migrate/tracker",

View file

@ -33,18 +33,18 @@ class _TrackerLibraryImageCardState
Widget build(BuildContext context) {
super.build(context);
final trackData = widget.track;
return GestureDetector(
onTap: () => _showCard(context),
child: StreamBuilder(
stream: isar.tracks
.filter()
.mangaIdIsNotNull()
.mediaIdEqualTo(trackData.mediaId)
.itemTypeEqualTo(widget.itemType)
.watch(fireImmediately: true),
builder: (context, snapshot) {
final hasData = snapshot.hasData && snapshot.data!.isNotEmpty;
return Padding(
return StreamBuilder(
stream: isar.tracks
.filter()
.mangaIdIsNotNull()
.mediaIdEqualTo(trackData.mediaId)
.itemTypeEqualTo(widget.itemType)
.watch(fireImmediately: true),
builder: (context, snapshot) {
final hasData = snapshot.hasData && snapshot.data!.isNotEmpty;
return GestureDetector(
onTap: () => _showCard(context, snapshot.data?.firstOrNull?.mangaId),
child: Padding(
padding: const EdgeInsets.only(left: 10),
child: Stack(
children: [
@ -129,17 +129,20 @@ class _TrackerLibraryImageCardState
),
],
),
);
},
),
),
);
},
);
}
void _showCard(BuildContext context) {
void _showCard(BuildContext context, int? mangaId) {
showDialog(
context: context,
builder: (context) =>
TrackerItemCard(track: widget.track, itemType: widget.itemType),
builder: (context) => TrackerItemCard(
track: widget.track,
itemType: widget.itemType,
mangaId: mangaId,
),
);
}

View file

@ -6,6 +6,7 @@ import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/category.dart';
import 'package:mangayomi/models/changed.dart';
import 'package:mangayomi/models/chapter.dart';
import 'package:mangayomi/models/custom_button.dart';
import 'package:mangayomi/models/download.dart';
import 'package:mangayomi/models/update.dart';
import 'package:mangayomi/models/history.dart';
@ -61,6 +62,13 @@ class StorageProvider {
return directory;
}
Future<Directory?> getMpvDirectory() async {
final defaultDirectory = await getDefaultDirectory();
String dbDir = path.join(defaultDirectory!.path, 'mpv');
await Directory(dbDir).create(recursive: true);
return Directory(dbDir);
}
Future<Directory?> getBtDirectory() async {
final gefaultDirectory = await getDefaultDirectory();
String dbDir = path.join(gefaultDirectory!.path, 'torrents');
@ -169,6 +177,7 @@ class StorageProvider {
ChangedPartSchema,
ChapterSchema,
CategorySchema,
CustomButtonSchema,
UpdateSchema,
HistorySchema,
DownloadSchema,
@ -192,6 +201,38 @@ class StorageProvider {
});
}
final customButton = await isar.customButtons
.filter()
.idIsNotNull()
.findFirst();
if (customButton == null) {
await isar.writeTxn(() async {
await isar.customButtons.put(
CustomButton(
title: "+85 s",
codePress:
"""var intro_length = mp.get_property_number("user-data/current-anime/intro-length")
aniyomi.right_seek_by(intro_length)""",
codeLongPress:
"""aniyomi.int_picker("Change intro length", "%ds", 0, 255, 1, "user-data/current-anime/intro-length")""",
codeStartup: """function update_button(_, length) {
if (length && length == 0) {
aniyomi.hide_button()
} else {
aniyomi.show_button()
}
aniyomi.set_button_title("+" + length + " s")
if (\$isPrimary) {
mp.observe_property("user-data/current-anime/intro-length", "number", update_button)
}""",
isFavourite: true,
pos: 0,
updatedAt: DateTime.now().millisecondsSinceEpoch,
),
);
});
}
return isar;
}
}

View file

@ -16,6 +16,11 @@ import 'package:mangayomi/modules/more/data_and_storage/create_backup.dart';
import 'package:mangayomi/modules/more/data_and_storage/data_and_storage.dart';
import 'package:mangayomi/modules/more/settings/appearance/custom_navigation_settings.dart';
import 'package:mangayomi/modules/more/settings/browse/source_repositories.dart';
import 'package:mangayomi/modules/more/settings/player/custom_button_screen.dart';
import 'package:mangayomi/modules/more/settings/player/player_advanced_screen.dart';
import 'package:mangayomi/modules/more/settings/player/player_audio_screen.dart';
import 'package:mangayomi/modules/more/settings/player/player_decoder_screen.dart';
import 'package:mangayomi/modules/more/settings/player/player_overview_screen.dart';
import 'package:mangayomi/modules/more/settings/reader/providers/reader_state_provider.dart';
import 'package:mangayomi/modules/more/statistics/statistics_screen.dart';
import 'package:mangayomi/modules/novel/novel_reader_view.dart';
@ -203,6 +208,7 @@ class RouterNotifier extends ChangeNotifier {
name: "trackingDetail",
builder: (trackerPref) => TrackingDetail(trackerPref: trackerPref),
),
_genericRoute(name: "playerOverview", child: const PlayerOverviewScreen()),
_genericRoute(name: "playerMode", child: const PlayerScreen()),
_genericRoute<int>(
name: "codeEditor",
@ -214,6 +220,19 @@ class RouterNotifier extends ChangeNotifier {
name: "customNavigationSettings",
child: const CustomNavigationSettings(),
),
_genericRoute(
name: "customButtonScreen",
child: const CustomButtonScreen(),
),
_genericRoute(
name: "playerDecoderScreen",
child: const PlayerDecoderScreen(),
),
_genericRoute(name: "playerAudioScreen", child: const PlayerAudioScreen()),
_genericRoute(
name: "playerAdvancedScreen",
child: const PlayerAdvancedScreen(),
),
_genericRoute<Manga>(
name: "migrate",
builder: (manga) => MigrationScreen(manga: manga),

View file

@ -1,6 +1,7 @@
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:mangayomi/modules/manga/reader/u_chap_data_preload.dart';
import 'package:path/path.dart' as p;
import 'package:mangayomi/eval/lib.dart';
import 'package:mangayomi/eval/javascript/http.dart';
@ -9,7 +10,6 @@ import 'package:mangayomi/models/chapter.dart';
import 'package:mangayomi/models/page.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/modules/manga/archive_reader/providers/archive_reader_providers.dart';
import 'package:mangayomi/modules/manga/reader/reader_view.dart';
import 'package:mangayomi/providers/storage_provider.dart';
import 'package:mangayomi/utils/utils.dart';
import 'package:mangayomi/utils/reg_exp_matcher.dart';

View file

@ -13,11 +13,12 @@ import 'package:path/path.dart' as p;
part 'get_video_list.g.dart';
@riverpod
Future<(List<Video>, bool, List<String>)> getVideoList(
Future<(List<Video>, bool, List<String>, Directory?)> getVideoList(
Ref ref, {
required Chapter episode,
}) async {
final storageProvider = StorageProvider();
final mpvDirectory = await storageProvider.getMpvDirectory();
final mangaDirectory = await storageProvider.getMangaMainDirectory(episode);
final isLocalArchive =
episode.manga.value!.isLocalArchive! &&
@ -52,6 +53,7 @@ Future<(List<Video>, bool, List<String>)> getVideoList(
[Video(path!, episode.name!, path, subtitles: subtitles)],
true,
infoHashes,
mpvDirectory
);
}
final source = getSource(
@ -68,7 +70,7 @@ Future<(List<Video>, bool, List<String>)> getVideoList(
episode.url,
episode.archivePath,
);
return (videos, false, [infohash ?? ""]);
return (videos, false, [infohash ?? ""], mpvDirectory);
}
try {
@ -91,7 +93,7 @@ Future<(List<Video>, bool, List<String>)> getVideoList(
}
}
}
return (torrentList, false, infoHashes);
return (torrentList, false, infoHashes, mpvDirectory);
}
List<Video> list = await getExtensionService(
@ -105,5 +107,5 @@ Future<(List<Video>, bool, List<String>)> getVideoList(
}
}
return (videos, false, infoHashes);
return (videos, false, infoHashes, mpvDirectory);
}

View file

@ -6,7 +6,7 @@ part of 'get_video_list.dart';
// RiverpodGenerator
// **************************************************************************
String _$getVideoListHash() => r'aeed8a24962e960a374d6bc7294e798ad3d0c05e';
String _$getVideoListHash() => r'140ac1ca572d6220b7791c4350a0b32e275535a4';
/// Copied from Dart SDK
class _SystemHash {
@ -35,7 +35,7 @@ const getVideoListProvider = GetVideoListFamily();
/// See also [getVideoList].
class GetVideoListFamily
extends Family<AsyncValue<(List<Video>, bool, List<String>)>> {
extends Family<AsyncValue<(List<Video>, bool, List<String>, Directory?)>> {
/// See also [getVideoList].
const GetVideoListFamily();
@ -73,8 +73,8 @@ class GetVideoListFamily
}
/// See also [getVideoList].
class GetVideoListProvider
extends AutoDisposeFutureProvider<(List<Video>, bool, List<String>)> {
class GetVideoListProvider extends AutoDisposeFutureProvider<
(List<Video>, bool, List<String>, Directory?)> {
/// See also [getVideoList].
GetVideoListProvider({
required Chapter episode,
@ -109,7 +109,7 @@ class GetVideoListProvider
@override
Override overrideWith(
FutureOr<(List<Video>, bool, List<String>)> Function(
FutureOr<(List<Video>, bool, List<String>, Directory?)> Function(
GetVideoListRef provider)
create,
) {
@ -128,8 +128,8 @@ class GetVideoListProvider
}
@override
AutoDisposeFutureProviderElement<(List<Video>, bool, List<String>)>
createElement() {
AutoDisposeFutureProviderElement<
(List<Video>, bool, List<String>, Directory?)> createElement() {
return _GetVideoListProviderElement(this);
}
@ -149,15 +149,14 @@ class GetVideoListProvider
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin GetVideoListRef
on AutoDisposeFutureProviderRef<(List<Video>, bool, List<String>)> {
mixin GetVideoListRef on AutoDisposeFutureProviderRef<
(List<Video>, bool, List<String>, Directory?)> {
/// The parameter `episode` of this provider.
Chapter get episode;
}
class _GetVideoListProviderElement
extends AutoDisposeFutureProviderElement<(List<Video>, bool, List<String>)>
with GetVideoListRef {
class _GetVideoListProviderElement extends AutoDisposeFutureProviderElement<
(List<Video>, bool, List<String>, Directory?)> with GetVideoListRef {
_GetVideoListProviderElement(super.provider);
@override

View file

@ -6,7 +6,7 @@ part of 'sync_server.dart';
// RiverpodGenerator
// **************************************************************************
String _$syncServerHash() => r'97a778696e0cc8b8e4c706de50d60464bb7b2f03';
String _$syncServerHash() => r'141ba3be28182e05480e06fbf3f1de68f868cb8e';
/// Copied from Dart SDK
class _SystemHash {

View file

@ -91,6 +91,7 @@ class DiscordRPC {
: "Reading";
final title = chapter.manga.value!.name;
final chapterTitle = chapter.name;
final imageUrl = chapter.manga.value!.imageUrl;
final rpcShowTitle = ref.read(rpcShowTitleStateProvider);
final rpcShowCoverImage = ref.read(rpcShowCoverImageStateProvider);
await updateActivity(
@ -98,9 +99,10 @@ class DiscordRPC {
state: rpcShowTitle && rpcShowReadingWatchingProgress
? chapterTitle
: "-----",
assets: rpcShowCoverImage
assets:
rpcShowCoverImage && imageUrl != null && imageUrl.startsWith("http")
? RPCAssets(
largeImage: chapter.manga.value!.imageUrl,
largeImage: imageUrl,
largeText: rpcShowTitle ? chapter.manga.value!.name : "-----",
smallImage: "app-icon",
smallText: "Mangayomi",

View file

@ -5,7 +5,7 @@ import 'dart:ui';
import 'package:extended_image/extended_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mangayomi/modules/manga/reader/reader_view.dart';
import 'package:mangayomi/modules/manga/reader/u_chap_data_preload.dart';
import 'package:mangayomi/modules/more/settings/reader/providers/reader_state_provider.dart';
import 'package:mangayomi/modules/widgets/custom_extended_image_provider.dart';
import 'package:mangayomi/utils/headers.dart';
@ -20,6 +20,21 @@ extension LetExtension<T> on T {
}
}
extension MedianExtension on List<int> {
int median() {
var middle = length ~/ 2;
if (length % 2 == 1) {
return this[middle];
} else {
return ((this[middle - 1] + this[middle]) / 2).round();
}
}
int arithmeticMean() {
return isNotEmpty ? (reduce((e1, e2) => e1 + e2) / length).round() : 0;
}
}
extension ImageProviderExtension on ImageProvider {
Future<Uint8List?> getBytes(
BuildContext context, {

View file

@ -1,3 +1,6 @@
import 'dart:ffi';
import 'package:ffi/ffi.dart';
extension StringExtensions on String {
String substringAfter(String pattern) {
final startIndex = indexOf(pattern);
@ -73,3 +76,22 @@ extension StringExtensions on String {
].any((extension) => toLowerCase().endsWith(extension));
}
}
extension NativeStringExtensions on List<String> {
Pointer<Pointer<Int8>> strListToPointer() {
final strings = this;
List<Pointer<Int8>> int8PointerList = strings
.map((str) => str.toNativeUtf8().cast<Int8>())
.toList();
final Pointer<Pointer<Int8>> pointerPointer = malloc.allocate(
int8PointerList.length,
);
strings.asMap().forEach((index, utf) {
pointerPointer[index] = int8PointerList[index];
});
return pointerPointer;
}
}

View file

@ -14,6 +14,7 @@
#include <media_kit_video/media_kit_video_plugin.h>
#include <screen_retriever_linux/screen_retriever_linux_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
#include <volume_controller/volume_controller_plugin.h>
#include <window_manager/window_manager_plugin.h>
#include <window_to_front/window_to_front_plugin.h>
@ -42,6 +43,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
g_autoptr(FlPluginRegistrar) volume_controller_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "VolumeControllerPlugin");
volume_controller_plugin_register_with_registrar(volume_controller_registrar);
g_autoptr(FlPluginRegistrar) window_manager_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin");
window_manager_plugin_register_with_registrar(window_manager_registrar);

View file

@ -11,6 +11,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
media_kit_video
screen_retriever_linux
url_launcher_linux
volume_controller
window_manager
window_to_front
)

View file

@ -1162,10 +1162,11 @@ packages:
media_kit:
dependency: "direct main"
description:
name: media_kit
sha256: "48c10c3785df5d88f0eef970743f8c99b2e5da2b34b9d8f9876e598f62d9e776"
url: "https://pub.dev"
source: hosted
path: media_kit
ref: HEAD
resolved-ref: a02ac2f7e6118cdab855ecacd194fa9ee6961a18
url: "https://github.com/Schnitzel5/media-kit.git"
source: git
version: "1.2.0"
media_kit_libs_android_video:
dependency: transitive
@ -1219,9 +1220,9 @@ packages:
dependency: "direct main"
description:
path: media_kit_video
ref: aeb29faa8ea93a386ad1185b69fd6225fa331c74
resolved-ref: aeb29faa8ea93a386ad1185b69fd6225fa331c74
url: "https://github.com/media-kit/media-kit.git"
ref: HEAD
resolved-ref: a02ac2f7e6118cdab855ecacd194fa9ee6961a18
url: "https://github.com/Schnitzel5/media-kit.git"
source: git
version: "1.3.0"
meta:
@ -2081,10 +2082,10 @@ packages:
dependency: transitive
description:
name: volume_controller
sha256: e82fd689bb8e1fe8e64be3fa5946ff8699058f8cf9f4c1679acdba20cda7f5bd
sha256: d75039e69c0d90e7810bfd47e3eedf29ff8543ea7a10392792e81f9bded7edf5
url: "https://pub.dev"
source: hosted
version: "3.3.3"
version: "3.4.0"
wakelock_plus:
dependency: transitive
description:

View file

@ -41,12 +41,14 @@ dependencies:
flutter_web_auth_2: ^3.1.2
numberpicker: ^2.1.2
encrypt: ^5.0.3
media_kit: ^1.2.0
media_kit:
git:
url: https://github.com/Schnitzel5/media-kit.git
path: media_kit
media_kit_video:
git:
url: https://github.com/media-kit/media-kit.git
url: https://github.com/Schnitzel5/media-kit.git
path: media_kit_video
ref: aeb29faa8ea93a386ad1185b69fd6225fa331c74
media_kit_libs_video: ^1.0.6
crypto: ^3.0.6
cupertino_icons: ^1.0.8
@ -157,4 +159,3 @@ inno_bundle:
- german
admin: false
version: 0.6.3