added custom buttons

This commit is contained in:
Schnitzel5 2025-07-28 23:42:55 +02:00
parent 1f0938fab9
commit 4682ab3577
26 changed files with 926 additions and 349 deletions

BIN
assets/mangayomi_mpv.zip Normal file

Binary file not shown.

View file

@ -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"
}

View file

@ -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.

View file

@ -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) {

View file

@ -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) {

View file

@ -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) {

View file

@ -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) {

View file

@ -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) {

View file

@ -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) {

View file

@ -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) {

View file

@ -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) {

View file

@ -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) {

View file

@ -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) {

View file

@ -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) {

View file

@ -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) {

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

@ -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<String, dynamic> 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,
});
}

View file

@ -375,7 +375,7 @@ class Settings {
this.rpcShowReadingWatchingProgress = true,
this.rpcShowTitle = true,
this.rpcShowCoverImage = true,
this.useMpvConfig = false,
this.useMpvConfig = true,
});
Settings.fromJson(Map<String, dynamic> json) {

View file

@ -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<AnimeStreamPage>
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<AnimeStreamPage>
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);
late final ValueNotifier<_AniSkipPhase> _skipPhase = ValueNotifier(
_AniSkipPhase.none,
);
@ -301,6 +309,9 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
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 (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>();
@ -322,76 +333,269 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
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 "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<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 "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<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 "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<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 "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<Utf8>().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<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 "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<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 "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<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 "user-data/aniyomi/pause":
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 "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<Utf8>().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<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 "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<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 "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<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 "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<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":
@ -410,6 +614,12 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
.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;
}
}
@ -421,6 +631,57 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
}
}
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",
]),
);
}
void pushToNewEpisode(BuildContext context, Chapter episode) {
widget.desktopFullScreenPlayer.call(ref.read(fullscreenProvider));
if (context.mounted) {
@ -526,6 +787,22 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
_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<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(),
),
),
);
@ -1061,6 +1350,7 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
@ -1276,9 +1566,16 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
.map(
(mode) => PopupMenuItem<String>(
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,
]);

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';
@ -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",
]);

View file

@ -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<CustomButtonScreen> {
List<CustomButton> _entries = [];
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
@ -23,7 +25,6 @@ class _CustomButtonScreenState extends ConsumerState<CustomButtonScreen> {
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<CustomButtonScreen> {
),
);
}
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<CustomButtonScreen> {
},
),
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<CustomButtonScreen> {
),
);
}
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 List<CustomButton> 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,
),
),
),

View file

@ -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<PlayerScreen> {
int _total = 0;
int _received = 0;
http.StreamedResponse? _response;
final List<int> _bytes = [];
StreamSubscription<List<int>>? _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<PlayerScreen> {
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),
),
],

View file

@ -7,5 +7,7 @@ part 'custom_buttons_provider.g.dart';
@riverpod
Stream<List<CustomButton>> getCustomButtonsStream(Ref ref) async* {
yield* isar.customButtons.filter().idIsNotNull().watch(fireImmediately: true);
yield* isar.customButtons.filter().idIsNotNull().sortByPos().watch(
fireImmediately: true,
);
}

View file

@ -7,7 +7,7 @@ part of 'custom_buttons_provider.dart';
// **************************************************************************
String _$getCustomButtonsStreamHash() =>
r'463d2142793ffb5a905f6f90c3a756445be8b133';
r'476c26eb3d20e9e9eed2e1d8bb15fa74ce357ba3';
/// See also [getCustomButtonsStream].
@ProviderFor(getCustomButtonsStream)

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

@ -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,