diff --git a/assets/mangayomi_mpv.zip b/assets/mangayomi_mpv.zip new file mode 100644 index 00000000..40513093 Binary files /dev/null and b/assets/mangayomi_mpv.zip differ diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 87a19002..7049d912 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -481,10 +481,12 @@ "custom_buttons_info": "Execute Javascript code with custom buttons", "custom_buttons_edit": "Edit custom buttons", "custom_buttons_add": "Add custom button", - "custom_buttons_edit": "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": "On startup", + "custom_buttons_startup": "Javascript code (on startup)", "n_days": "{n} days" } diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 0fc30cbd..73ef3982 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -2948,7 +2948,7 @@ abstract class AppLocalizations { /// No description provided for @custom_buttons_edit. /// /// In en, this message translates to: - /// **'Add custom button'** + /// **'Edit custom buttons'** String get custom_buttons_edit; /// No description provided for @custom_buttons_add. @@ -2957,18 +2957,36 @@ abstract class AppLocalizations { /// **'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: @@ -2978,7 +2996,7 @@ abstract class AppLocalizations { /// No description provided for @custom_buttons_startup. /// /// In en, this message translates to: - /// **'On startup'** + /// **'Javascript code (on startup)'** String get custom_buttons_startup; /// No description provided for @n_days. diff --git a/lib/l10n/generated/app_localizations_ar.dart b/lib/l10n/generated/app_localizations_ar.dart index 0ee8e394..a3fb79f5 100644 --- a/lib/l10n/generated/app_localizations_ar.dart +++ b/lib/l10n/generated/app_localizations_ar.dart @@ -1518,22 +1518,31 @@ class AppLocalizationsAr extends AppLocalizations { 'Execute Javascript code with custom buttons'; @override - String get custom_buttons_edit => 'Add custom button'; + 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 => 'On startup'; + String get custom_buttons_startup => 'Javascript code (on startup)'; @override String n_days(Object n) { diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index ca9fbf35..ccc8ce7f 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -1531,22 +1531,31 @@ class AppLocalizationsDe extends AppLocalizations { 'Execute Javascript code with custom buttons'; @override - String get custom_buttons_edit => 'Add custom button'; + 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 => 'On startup'; + String get custom_buttons_startup => 'Javascript code (on startup)'; @override String n_days(Object n) { diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 49995069..19050456 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -1519,22 +1519,31 @@ class AppLocalizationsEn extends AppLocalizations { 'Execute Javascript code with custom buttons'; @override - String get custom_buttons_edit => 'Add custom button'; + 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 => 'On startup'; + String get custom_buttons_startup => 'Javascript code (on startup)'; @override String n_days(Object n) { diff --git a/lib/l10n/generated/app_localizations_es.dart b/lib/l10n/generated/app_localizations_es.dart index d01ce663..8ad3b531 100644 --- a/lib/l10n/generated/app_localizations_es.dart +++ b/lib/l10n/generated/app_localizations_es.dart @@ -1536,22 +1536,31 @@ class AppLocalizationsEs extends AppLocalizations { 'Execute Javascript code with custom buttons'; @override - String get custom_buttons_edit => 'Add custom button'; + 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 => 'On startup'; + String get custom_buttons_startup => 'Javascript code (on startup)'; @override String n_days(Object n) { diff --git a/lib/l10n/generated/app_localizations_fr.dart b/lib/l10n/generated/app_localizations_fr.dart index f86ffbaf..f1dee22e 100644 --- a/lib/l10n/generated/app_localizations_fr.dart +++ b/lib/l10n/generated/app_localizations_fr.dart @@ -1537,22 +1537,31 @@ class AppLocalizationsFr extends AppLocalizations { 'Execute Javascript code with custom buttons'; @override - String get custom_buttons_edit => 'Add custom button'; + 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 => 'On startup'; + String get custom_buttons_startup => 'Javascript code (on startup)'; @override String n_days(Object n) { diff --git a/lib/l10n/generated/app_localizations_id.dart b/lib/l10n/generated/app_localizations_id.dart index 9f0a2b3b..c2c87a01 100644 --- a/lib/l10n/generated/app_localizations_id.dart +++ b/lib/l10n/generated/app_localizations_id.dart @@ -1525,22 +1525,31 @@ class AppLocalizationsId extends AppLocalizations { 'Execute Javascript code with custom buttons'; @override - String get custom_buttons_edit => 'Add custom button'; + 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 => 'On startup'; + String get custom_buttons_startup => 'Javascript code (on startup)'; @override String n_days(Object n) { diff --git a/lib/l10n/generated/app_localizations_it.dart b/lib/l10n/generated/app_localizations_it.dart index b4049fcb..7411c45b 100644 --- a/lib/l10n/generated/app_localizations_it.dart +++ b/lib/l10n/generated/app_localizations_it.dart @@ -1534,22 +1534,31 @@ class AppLocalizationsIt extends AppLocalizations { 'Execute Javascript code with custom buttons'; @override - String get custom_buttons_edit => 'Add custom button'; + 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 => 'On startup'; + String get custom_buttons_startup => 'Javascript code (on startup)'; @override String n_days(Object n) { diff --git a/lib/l10n/generated/app_localizations_pt.dart b/lib/l10n/generated/app_localizations_pt.dart index 1ffda025..c1885463 100644 --- a/lib/l10n/generated/app_localizations_pt.dart +++ b/lib/l10n/generated/app_localizations_pt.dart @@ -1533,22 +1533,31 @@ class AppLocalizationsPt extends AppLocalizations { 'Execute Javascript code with custom buttons'; @override - String get custom_buttons_edit => 'Add custom button'; + 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 => 'On startup'; + String get custom_buttons_startup => 'Javascript code (on startup)'; @override String n_days(Object n) { diff --git a/lib/l10n/generated/app_localizations_ru.dart b/lib/l10n/generated/app_localizations_ru.dart index 89cc8dc5..55c58f7b 100644 --- a/lib/l10n/generated/app_localizations_ru.dart +++ b/lib/l10n/generated/app_localizations_ru.dart @@ -1535,22 +1535,31 @@ class AppLocalizationsRu extends AppLocalizations { 'Execute Javascript code with custom buttons'; @override - String get custom_buttons_edit => 'Add custom button'; + 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 => 'On startup'; + String get custom_buttons_startup => 'Javascript code (on startup)'; @override String n_days(Object n) { diff --git a/lib/l10n/generated/app_localizations_th.dart b/lib/l10n/generated/app_localizations_th.dart index 0d60364b..b9cbb00c 100644 --- a/lib/l10n/generated/app_localizations_th.dart +++ b/lib/l10n/generated/app_localizations_th.dart @@ -1519,22 +1519,31 @@ class AppLocalizationsTh extends AppLocalizations { 'Execute Javascript code with custom buttons'; @override - String get custom_buttons_edit => 'Add custom button'; + 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 => 'On startup'; + String get custom_buttons_startup => 'Javascript code (on startup)'; @override String n_days(Object n) { diff --git a/lib/l10n/generated/app_localizations_tr.dart b/lib/l10n/generated/app_localizations_tr.dart index dd1ec81b..382f9f66 100644 --- a/lib/l10n/generated/app_localizations_tr.dart +++ b/lib/l10n/generated/app_localizations_tr.dart @@ -1525,22 +1525,31 @@ class AppLocalizationsTr extends AppLocalizations { 'Execute Javascript code with custom buttons'; @override - String get custom_buttons_edit => 'Add custom button'; + 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 => 'On startup'; + String get custom_buttons_startup => 'Javascript code (on startup)'; @override String n_days(Object n) { diff --git a/lib/l10n/generated/app_localizations_zh.dart b/lib/l10n/generated/app_localizations_zh.dart index 060b17ad..b1eca29e 100644 --- a/lib/l10n/generated/app_localizations_zh.dart +++ b/lib/l10n/generated/app_localizations_zh.dart @@ -1490,22 +1490,31 @@ class AppLocalizationsZh extends AppLocalizations { 'Execute Javascript code with custom buttons'; @override - String get custom_buttons_edit => 'Add custom button'; + 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 => 'On startup'; + String get custom_buttons_startup => 'Javascript code (on startup)'; @override String n_days(Object n) { diff --git a/lib/main.dart b/lib/main.dart index 32a5f03f..ed50af13 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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 { super.initState(); initializeDateFormatting(); _initDeepLinks(); + _setupMpvConfig(); unawaited(ref.read(scanLocalLibraryProvider.future)); WidgetsBinding.instance.addPostFrameCallback((_) { @@ -242,6 +246,38 @@ class _MyAppState extends ConsumerState { } return true; } + + Future _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 { diff --git a/lib/models/custom_button.dart b/lib/models/custom_button.dart index 0ea64789..b0bd3873 100644 --- a/lib/models/custom_button.dart +++ b/lib/models/custom_button.dart @@ -31,6 +31,30 @@ class CustomButton { 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 json) { id = json['id']; title = json['title']; @@ -53,3 +77,19 @@ class CustomButton { '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, + }); +} diff --git a/lib/models/settings.dart b/lib/models/settings.dart index 61ebad75..e4655ed0 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -375,7 +375,7 @@ class Settings { this.rpcShowReadingWatchingProgress = true, this.rpcShowTitle = true, this.rpcShowCoverImage = true, - this.useMpvConfig = false, + this.useMpvConfig = true, }); Settings.fromJson(Map json) { diff --git a/lib/modules/anime/anime_player_view.dart b/lib/modules/anime/anime_player_view.dart index f923007c..0de42202 100644 --- a/lib/modules/anime/anime_player_view.dart +++ b/lib/modules/anime/anime_player_view.dart @@ -9,11 +9,13 @@ 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/video.dart' as vid; import 'package:mangayomi/modules/anime/providers/anime_player_controller_provider.dart'; @@ -25,6 +27,7 @@ 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_state_provider.dart'; import 'package:mangayomi/modules/widgets/custom_draggable_tabbar.dart'; import 'package:mangayomi/modules/widgets/progress_center.dart'; @@ -39,8 +42,11 @@ 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'; @@ -212,12 +218,12 @@ class _AnimeStreamPageState extends riv.ConsumerState generated.mpv_format.MPV_FORMAT_NODE, "user-data/aniyomi/launch_int_picker": generated.mpv_format.MPV_FORMAT_NODE, - "user-data/current-anime/intro-length": - generated.mpv_format.MPV_FORMAT_INT64, "user-data/mangayomi/chapter_titles": generated.mpv_format.MPV_FORMAT_NODE, "user-data/mangayomi/current_chapter": generated.mpv_format.MPV_FORMAT_INT64, + "user-data/mangayomi/selected_shader": + generated.mpv_format.MPV_FORMAT_NODE, }, eventHandler: _handleMpvEvents, ), @@ -253,6 +259,8 @@ class _AnimeStreamPageState extends riv.ConsumerState final ValueNotifier _fit = ValueNotifier(BoxFit.contain); final ValueNotifier> _chapterMarks = ValueNotifier([]); final ValueNotifier _currentChapterMark = ValueNotifier(null); + final ValueNotifier _selectedShader = ValueNotifier(""); + final ValueNotifier _customButton = ValueNotifier(null); late final ValueNotifier<_AniSkipPhase> _skipPhase = ValueNotifier( _AniSkipPhase.none, ); @@ -301,6 +309,9 @@ class _AnimeStreamPageState extends riv.ConsumerState generated.mpv_event_id.MPV_EVENT_PROPERTY_CHANGE) { final prop = event.ref.data.cast(); final propName = prop.ref.name.cast().toDartString(); + 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(); @@ -322,76 +333,269 @@ class _AnimeStreamPageState extends riv.ConsumerState String propName, Pointer 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().toDartString(); + if (text.isEmpty) break; botToast(text); + nativePlayer.setProperty("user-data/aniyomi/show_text", ""); } break; - case "user-data/aniyomi/toggle_ui": + case "aniyomi/toggle_ui": if (value.ref.format == generated.mpv_format.MPV_FORMAT_STRING) { final text = value.ref.u.string.cast().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 "user-data/aniyomi/show_panel": + case "aniyomi/show_panel": if (value.ref.format == generated.mpv_format.MPV_FORMAT_STRING) { final text = value.ref.u.string.cast().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 "user-data/aniyomi/software_keyboard": + case "aniyomi/software_keyboard": if (value.ref.format == generated.mpv_format.MPV_FORMAT_STRING) { final text = value.ref.u.string.cast().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 "user-data/aniyomi/set_button_title": + case "aniyomi/set_button_title": if (value.ref.format == generated.mpv_format.MPV_FORMAT_STRING) { final text = value.ref.u.string.cast().toDartString(); + print("DEBUG SET BUTTON TITLE: $text"); + 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 "user-data/aniyomi/reset_button_title": + case "aniyomi/reset_button_title": if (value.ref.format == generated.mpv_format.MPV_FORMAT_STRING) { final text = value.ref.u.string.cast().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 "user-data/aniyomi/toggle_button": + case "aniyomi/toggle_button": if (value.ref.format == generated.mpv_format.MPV_FORMAT_STRING) { final text = value.ref.u.string.cast().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 "user-data/aniyomi/switch_episode": + case "aniyomi/switch_episode": if (value.ref.format == generated.mpv_format.MPV_FORMAT_STRING) { final text = value.ref.u.string.cast().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 "user-data/aniyomi/pause": + case "aniyomi/pause": if (value.ref.format == generated.mpv_format.MPV_FORMAT_STRING) { final text = value.ref.u.string.cast().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 "user-data/aniyomi/seek_by": + case "aniyomi/seek_by": if (value.ref.format == generated.mpv_format.MPV_FORMAT_STRING) { final text = value.ref.u.string.cast().toDartString(); + final tt = await nativePlayer.getProperty( + "user-data/current-anime/intro-length", + ); + 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 "user-data/aniyomi/seek_to": + case "aniyomi/seek_to": if (value.ref.format == generated.mpv_format.MPV_FORMAT_STRING) { final text = value.ref.u.string.cast().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 "user-data/aniyomi/seek_by_with_text": + case "aniyomi/seek_by_with_text": if (value.ref.format == generated.mpv_format.MPV_FORMAT_STRING) { final text = value.ref.u.string.cast().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 "user-data/aniyomi/seek_to_with_text": + case "aniyomi/seek_to_with_text": if (value.ref.format == generated.mpv_format.MPV_FORMAT_STRING) { final text = value.ref.u.string.cast().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 "user-data/aniyomi/launch_int_picker": + case "aniyomi/launch_int_picker": if (value.ref.format == generated.mpv_format.MPV_FORMAT_STRING) { final text = value.ref.u.string.cast().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(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": @@ -410,6 +614,12 @@ class _AnimeStreamPageState extends riv.ConsumerState .toList(); } break; + case "mangayomi/selected_shader": + if (value.ref.format == generated.mpv_format.MPV_FORMAT_STRING) { + final text = value.ref.u.string.cast().toDartString(); + _selectedShader.value = text; + } + break; } } @@ -421,6 +631,57 @@ class _AnimeStreamPageState extends riv.ConsumerState } } + Future _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", + ]), + ); + } + void pushToNewEpisode(BuildContext context, Chapter episode) { widget.desktopFullScreenPlayer.call(ref.read(fullscreenProvider)); if (context.mounted) { @@ -526,6 +787,22 @@ class _AnimeStreamPageState extends riv.ConsumerState _setPlaybackSpeed(ref.read(defaultPlayBackSpeedStateProvider)); if (ref.read(enableAniSkipStateProvider)) _initAniSkip(); }); + final defaultSkipIntroLength = ref.read( + defaultSkipIntroLengthStateProvider, + ); + (_player.platform as NativePlayer).setProperty( + "user-data/current-anime/intro-length", + "$defaultSkipIntroLength", + ); + (_player.platform as NativePlayer).command([ + "script-binding", + "stats/display-stats-toggle", + ]); + (_player.platform as NativePlayer).command([ + "script-binding", + "stats/display-page-1", + ]); + _initCustomButton(); discordRpc?.showChapterDetails(ref, widget.episode); _currentPosition.addListener(_updateRpcTimestamp); WidgetsBinding.instance.addObserver(this); @@ -1006,27 +1283,39 @@ class _AnimeStreamPageState extends riv.ConsumerState 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(), ), ), ); @@ -1061,6 +1350,7 @@ class _AnimeStreamPageState extends riv.ConsumerState style: const TextStyle( fontSize: 12, fontWeight: FontWeight.bold, + color: Colors.white, ), ), ), @@ -1276,9 +1566,16 @@ class _AnimeStreamPageState extends riv.ConsumerState .map( (mode) => PopupMenuItem( value: mode.$1, - child: Text(mode.$1), + child: Text( + mode.$1, + style: TextStyle( + fontWeight: _selectedShader.value == mode.$1 + ? FontWeight.w900 + : FontWeight.normal, + ), + ), onTap: () { - (_player.platform as dynamic).command([ + (_player.platform as NativePlayer).command([ "script-message", mode.$2, ]); diff --git a/lib/modules/anime/widgets/desktop.dart b/lib/modules/anime/widgets/desktop.dart index 8c1932d0..d3754a9c 100644 --- a/lib/modules/anime/widgets/desktop.dart +++ b/lib/modules/anime/widgets/desktop.dart @@ -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'; @@ -218,43 +219,43 @@ class _DesktopControllerWidgetState await _changeFullScreen(ref, desktopFullScreenPlayer, value: false); }, const SingleActivator(LogicalKeyboardKey.digit0, control: true): () { - (widget.videoController.player.platform as dynamic).command([ + (widget.videoController.player.platform as NativePlayer).command([ "script-message", "clear_anime", ]); }, const SingleActivator(LogicalKeyboardKey.digit1, control: true): () { - (widget.videoController.player.platform as dynamic).command([ + (widget.videoController.player.platform as NativePlayer).command([ "script-message", "set_anime_a", ]); }, const SingleActivator(LogicalKeyboardKey.digit2, control: true): () { - (widget.videoController.player.platform as dynamic).command([ + (widget.videoController.player.platform as NativePlayer).command([ "script-message", "set_anime_b", ]); }, const SingleActivator(LogicalKeyboardKey.digit3, control: true): () { - (widget.videoController.player.platform as dynamic).command([ + (widget.videoController.player.platform as NativePlayer).command([ "script-message", "set_anime_c", ]); }, const SingleActivator(LogicalKeyboardKey.digit4, control: true): () { - (widget.videoController.player.platform as dynamic).command([ + (widget.videoController.player.platform as NativePlayer).command([ "script-message", "set_anime_aa", ]); }, const SingleActivator(LogicalKeyboardKey.digit5, control: true): () { - (widget.videoController.player.platform as dynamic).command([ + (widget.videoController.player.platform as NativePlayer).command([ "script-message", "set_anime_bb", ]); }, const SingleActivator(LogicalKeyboardKey.digit6, control: true): () { - (widget.videoController.player.platform as dynamic).command([ + (widget.videoController.player.platform as NativePlayer).command([ "script-message", "set_anime_ca", ]); diff --git a/lib/modules/more/settings/player/custom_button_screen.dart b/lib/modules/more/settings/player/custom_button_screen.dart index 6aced89e..785034a6 100644 --- a/lib/modules/more/settings/player/custom_button_screen.dart +++ b/lib/modules/more/settings/player/custom_button_screen.dart @@ -1,9 +1,12 @@ 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}); @@ -13,7 +16,6 @@ class CustomButtonScreen extends ConsumerStatefulWidget { } class _CustomButtonScreenState extends ConsumerState { - List _entries = []; @override Widget build(BuildContext context) { final l10n = context.l10n; @@ -23,7 +25,6 @@ class _CustomButtonScreenState extends ConsumerState { body: customButtons.when( data: (data) { if (data.isEmpty) { - _entries = []; return Center( child: Padding( padding: const EdgeInsets.all(8.0), @@ -34,71 +35,107 @@ class _CustomButtonScreenState extends ConsumerState { ), ); } - data.sort((a, b) => (a.pos ?? 0).compareTo(b.pos ?? 0)); - _entries = data; return Padding( padding: const EdgeInsets.symmetric(horizontal: 15), child: ReorderableListView.builder( buildDefaultDragHandles: false, - itemCount: _entries.length, + itemCount: data.length, itemBuilder: (context, index) { - final customButton = _entries[index]; - return Row( - key: Key('custom_btn_${customButton.id}'), + final customButton = data[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + key: Key("custom_btn_col_${customButton.id}"), children: [ - ReorderableDragStartListener( - index: index, - child: const Icon(Icons.drag_handle), + 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), + ), + ], + ), + ), + ], ), - Expanded( - child: Row( - children: [ - IconButton( - onPressed: () {}, - icon: Icon( - (customButton.isFavourite ?? false) - ? Icons.star - : Icons.star_border, - ), - ), - IconButton( - onPressed: () {}, - icon: Icon(Icons.mode_edit_outlined), - ), - IconButton( - onPressed: () {}, - icon: Icon(Icons.delete_outline), - ), - ], - ), + Text( + customButton.codePress ?? "", + overflow: TextOverflow.ellipsis, ), ], ); }, - onReorder: (oldIndex, newIndex) { - /*if (oldIndex < newIndex) { - final draggedItem = navigationOrder[oldIndex]; + onReorder: (oldIndex, newIndex) async { + if (oldIndex < newIndex) { + final draggedItemPos = data[oldIndex].pos; for (var i = oldIndex; i < newIndex - 1; i++) { - navigationOrder[i] = navigationOrder[i + 1]; + data[i].pos = data[i + 1].pos; } - navigationOrder[newIndex - 1] = draggedItem; + data[newIndex - 1].pos = draggedItemPos; } else { - final draggedItem = navigationOrder[oldIndex]; + final draggedItemPos = data[oldIndex].pos; for (var i = oldIndex; i > newIndex; i--) { - navigationOrder[i] = navigationOrder[i - 1]; + data[i].pos = data[i - 1].pos; } - navigationOrder[newIndex] = draggedItem; + data[newIndex].pos = draggedItemPos; } - ref - .read(navigationOrderStateProvider.notifier) - .set(navigationOrder);*/ + await isar.writeTxn( + () async => await isar.customButtons.putAll(data), + ); }, ), ); }, error: (Object error, StackTrace stackTrace) { - _entries = []; return Center( child: Padding( padding: const EdgeInsets.all(8.0), @@ -114,89 +151,8 @@ class _CustomButtonScreenState extends ConsumerState { }, ), floatingActionButton: FloatingActionButton.extended( - onPressed: () { - bool isExist = false; - final controller = TextEditingController(); - showDialog( - context: context, - builder: (context) { - return SizedBox( - child: StatefulBuilder( - builder: (context, setState) { - return AlertDialog( - title: Text(l10n.add_category), - content: CustomTextFormField( - controller: controller, - entries: _entries, - context: context, - exist: (value) { - setState(() { - isExist = value; - }); - }, - isExist: isExist, - val: (val) {}, - ), - actions: [ - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () { - Navigator.pop(context); - }, - child: Text(l10n.cancel), - ), - const SizedBox(width: 15), - TextButton( - onPressed: controller.text.isEmpty || isExist - ? null - : () async { - /*final category = Category( - forItemType: widget.itemType, - name: controller.text, - updatedAt: DateTime.now() - .millisecondsSinceEpoch, - ); - isar.writeTxnSync(() { - isar.categorys.putSync( - category..pos = category.id, - ); - final categories = isar.categorys - .filter() - .posIsNull() - .findAllSync(); - for (var category in categories) { - isar.categorys.putSync( - category..pos = category.id, - ); - } - });*/ - - if (context.mounted) { - Navigator.pop(context); - } - }, - child: Text( - l10n.add, - style: TextStyle( - color: controller.text.isEmpty || isExist - ? Theme.of( - context, - ).primaryColor.withValues(alpha: 0.2) - : null, - ), - ), - ), - ], - ), - ], - ); - }, - ), - ); - }, - ); + onPressed: () async { + await _showEditForm(null); }, label: Row( children: [ @@ -208,77 +164,250 @@ class _CustomButtonScreenState extends ConsumerState { ), ); } + + Future _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 _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 List entries; final BuildContext context; - final Function(bool) exist; - final bool isExist; + 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.entries, required this.context, - required this.exist, - required this.isExist, + required this.missing, + required this.isMissing, this.name = "", + this.helperText = "", + this.minLines = 1, + this.allowEnterNewLine = true, required this.val, }); @override Widget build(BuildContext context) { - final l10n = l10nLocalizations(context); return TextFormField( - autofocus: true, + minLines: minLines, + maxLines: null, controller: controller, - keyboardType: TextInputType.text, + keyboardType: allowEnterNewLine + ? TextInputType.multiline + : TextInputType.text, onChanged: (value) { - if (name != controller.text) { - exist( - entries - .where((element) => element.title == controller.text) - .toList() - .isNotEmpty, - ); - } + missing(controller.text.isEmpty); val(value); }, onFieldSubmitted: (s) {}, decoration: InputDecoration( - helperText: isExist == true - ? l10n!.add_category_error_exist - : l10n!.category_name_required, - helperStyle: TextStyle(color: isExist == true ? Colors.red : null), + helperText: helperText, + helperStyle: TextStyle(color: isMissing ? Colors.red : null), isDense: true, label: Text( - l10n.name, - style: TextStyle(color: isExist == true ? Colors.red : null), + name, + style: TextStyle(color: isMissing ? Colors.red : null), ), filled: true, fillColor: Colors.transparent, enabledBorder: OutlineInputBorder( borderSide: BorderSide( - color: isExist == true - ? Colors.red - : Theme.of(context).primaryColor, + color: isMissing ? Colors.red : Theme.of(context).primaryColor, ), ), focusedBorder: OutlineInputBorder( borderSide: BorderSide( - color: isExist == true - ? Colors.red - : Theme.of(context).primaryColor, + color: isMissing ? Colors.red : Theme.of(context).primaryColor, ), ), border: OutlineInputBorder( borderSide: BorderSide( - color: isExist == true - ? Colors.red - : Theme.of(context).primaryColor, + color: isMissing ? Colors.red : Theme.of(context).primaryColor, ), ), ), diff --git a/lib/modules/more/settings/player/player_screen.dart b/lib/modules/more/settings/player/player_screen.dart index 203882cb..0647c5e6 100644 --- a/lib/modules/more/settings/player/player_screen.dart +++ b/lib/modules/more/settings/player/player_screen.dart @@ -3,9 +3,9 @@ import 'dart:io'; import 'package:archive/archive.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' show rootBundle; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; -import 'package:http/http.dart' as http; import 'package:mangayomi/modules/more/settings/player/providers/player_state_provider.dart'; import 'package:mangayomi/modules/more/widgets/list_tile_widget.dart'; import 'package:mangayomi/providers/l10n_providers.dart'; @@ -26,18 +26,6 @@ class PlayerScreen extends ConsumerStatefulWidget { } class _PlayerScreenState extends ConsumerState { - int _total = 0; - int _received = 0; - http.StreamedResponse? _response; - final List _bytes = []; - StreamSubscription>? _subscription; - - @override - void dispose() { - _subscription?.cancel(); - super.dispose(); - } - @override Widget build(BuildContext context) { final defaultSubtitleLang = ref.watch(defaultSubtitleLangStateProvider); @@ -631,110 +619,51 @@ class _PlayerScreenState extends ConsumerState { context: context, builder: (context) { return AlertDialog( - content: SingleChildScrollView( - child: Column( - children: [ - Text(context.l10n.mpv_download), - _total > 0 - ? Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Flexible( - child: LinearProgressIndicator( - value: _total > 0 - ? (_received * 1.0) / _total - : 0.0, - ), - ), - Flexible( - child: Text( - '${(_received / 1048576.0).toStringAsFixed(2)}/${(_total / 1048576.0).toStringAsFixed(2)} MB', - ), - ), - ], - ) - : SizedBox.shrink(), - ], - ), - ), + content: Text(context.l10n.mpv_download), actions: [ Row( mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton( - onPressed: () async { - try { - await _subscription?.cancel(); - } catch (_) {} - if (context.mounted) { - Navigator.pop(context); - } - }, + onPressed: () => Navigator.pop(context), child: Text(context.l10n.cancel), ), const SizedBox(width: 15), ElevatedButton( - onPressed: _total == 0 - ? () async { - _response = await http.Client().send( - http.Request( - 'GET', - Uri.parse( - "https://github.com/Schnitzel5/mangayomi/releases/download/v0.6.3-anime4k/mangayomi_mpv.zip", - ), - ), - ); - _total = _response?.contentLength ?? 0; - _subscription = _response?.stream.listen((value) { - setState(() { - _bytes.addAll(value); - _received += value.length; - }); - }); - _subscription?.onDone(() async { - final archive = ZipDecoder().decodeBytes(_bytes); - 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); - } - } - _total = 0; - _received = 0; - _bytes.clear(); - if (context.mounted) { - Navigator.pop(context, "ok"); - } - }); - } - : null, + 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), ), ], diff --git a/lib/modules/more/settings/player/providers/custom_buttons_provider.dart b/lib/modules/more/settings/player/providers/custom_buttons_provider.dart index f931e92d..40e7c12d 100644 --- a/lib/modules/more/settings/player/providers/custom_buttons_provider.dart +++ b/lib/modules/more/settings/player/providers/custom_buttons_provider.dart @@ -7,5 +7,7 @@ part 'custom_buttons_provider.g.dart'; @riverpod Stream> getCustomButtonsStream(Ref ref) async* { - yield* isar.customButtons.filter().idIsNotNull().watch(fireImmediately: true); + yield* isar.customButtons.filter().idIsNotNull().sortByPos().watch( + fireImmediately: true, + ); } diff --git a/lib/modules/more/settings/player/providers/custom_buttons_provider.g.dart b/lib/modules/more/settings/player/providers/custom_buttons_provider.g.dart index 276a045a..c8157b2f 100644 --- a/lib/modules/more/settings/player/providers/custom_buttons_provider.g.dart +++ b/lib/modules/more/settings/player/providers/custom_buttons_provider.g.dart @@ -7,7 +7,7 @@ part of 'custom_buttons_provider.dart'; // ************************************************************************** String _$getCustomButtonsStreamHash() => - r'463d2142793ffb5a905f6f90c3a756445be8b133'; + r'476c26eb3d20e9e9eed2e1d8bb15fa74ce357ba3'; /// See also [getCustomButtonsStream]. @ProviderFor(getCustomButtonsStream) diff --git a/lib/modules/more/widgets/list_tile_widget.dart b/lib/modules/more/widgets/list_tile_widget.dart index 4952b4de..31717dd1 100644 --- a/lib/modules/more/widgets/list_tile_widget.dart +++ b/lib/modules/more/widgets/list_tile_widget.dart @@ -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, ); } diff --git a/lib/providers/storage_provider.dart b/lib/providers/storage_provider.dart index 3747ba35..e7313c23 100644 --- a/lib/providers/storage_provider.dart +++ b/lib/providers/storage_provider.dart @@ -203,19 +203,28 @@ class StorageProvider { final customButton = await isar.customButtons .filter() - .idEqualTo(1) + .idIsNotNull() .findFirst(); if (customButton == null) { await isar.writeTxn(() async { - isar.customButtons.put( + await isar.customButtons.put( CustomButton( title: "+85 s", codePress: - "var intro_length = mp.get_property_number(\"user-data/current-anime/intro-length\")\naniyomi.right_seek_by(intro_length)", + """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) {\n if (!length || length == 0) {\n aniyomi.hide_button()\n } else {\n aniyomi.show_button()\n }\n aniyomi.set_button_title(\"+\" + length + \" s\")", + """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,