added mpv config for Anime4K

This commit is contained in:
Schnitzel5 2025-07-24 22:25:14 +02:00
parent 5b56578029
commit 1450641f16
36 changed files with 1571 additions and 77 deletions

View file

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

View file

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

View file

@ -468,5 +468,8 @@
"hide_discord_rpc_incognito": "Hide Discord RPC while in Incognito",
"rpc_show_reading_watching_progress": "Show current chapter in Discord (requires a restart)",
"rpc_show_title": "Show current title in Discord",
"rpc_show_cover_image": "Show current cover image in Discord"
"rpc_show_cover_image": "Show current cover image in Discord",
"anime4K": "Enable Anime4K",
"anime4K_info": "Supports .js scripts under /mpv/scripts/",
"anime4K_download": "MPV config files are required!\nDownload now?"
}

View file

@ -2884,6 +2884,24 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Show current cover image in Discord'**
String get rpc_show_cover_image;
/// No description provided for @anime4K.
///
/// In en, this message translates to:
/// **'Enable Anime4K'**
String get anime4K;
/// No description provided for @anime4K_info.
///
/// In en, this message translates to:
/// **'Supports .js scripts under /mpv/scripts/'**
String get anime4K_info;
/// No description provided for @anime4K_download.
///
/// In en, this message translates to:
/// **'MPV config files are required!\nDownload now?'**
String get anime4K_download;
}
class _AppLocalizationsDelegate

View file

@ -1485,4 +1485,14 @@ class AppLocalizationsAr extends AppLocalizations {
@override
String get rpc_show_cover_image => 'Show current cover image in Discord';
@override
String get anime4K => 'Enable Anime4K';
@override
String get anime4K_info => 'Supports .js scripts under /mpv/scripts/';
@override
String get anime4K_download =>
'MPV config files are required!\nDownload now?';
}

View file

@ -1498,4 +1498,14 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get rpc_show_cover_image => 'Show current cover image in Discord';
@override
String get anime4K => 'Enable Anime4K';
@override
String get anime4K_info => 'Supports .js scripts under /mpv/scripts/';
@override
String get anime4K_download =>
'MPV config files are required!\nDownload now?';
}

View file

@ -1486,4 +1486,14 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get rpc_show_cover_image => 'Show current cover image in Discord';
@override
String get anime4K => 'Enable Anime4K';
@override
String get anime4K_info => 'Supports .js scripts under /mpv/scripts/';
@override
String get anime4K_download =>
'MPV config files are required!\nDownload now?';
}

View file

@ -1503,6 +1503,16 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get rpc_show_cover_image => 'Show current cover image in Discord';
@override
String get anime4K => 'Enable Anime4K';
@override
String get anime4K_info => 'Supports .js scripts under /mpv/scripts/';
@override
String get anime4K_download =>
'MPV config files are required!\nDownload now?';
}
/// The translations for Spanish Castilian, as used in Latin America and the Caribbean (`es_419`).

View file

@ -1504,4 +1504,14 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get rpc_show_cover_image => 'Show current cover image in Discord';
@override
String get anime4K => 'Enable Anime4K';
@override
String get anime4K_info => 'Supports .js scripts under /mpv/scripts/';
@override
String get anime4K_download =>
'MPV config files are required!\nDownload now?';
}

View file

@ -1492,4 +1492,14 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get rpc_show_cover_image => 'Show current cover image in Discord';
@override
String get anime4K => 'Enable Anime4K';
@override
String get anime4K_info => 'Supports .js scripts under /mpv/scripts/';
@override
String get anime4K_download =>
'MPV config files are required!\nDownload now?';
}

View file

@ -1501,4 +1501,14 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get rpc_show_cover_image => 'Show current cover image in Discord';
@override
String get anime4K => 'Enable Anime4K';
@override
String get anime4K_info => 'Supports .js scripts under /mpv/scripts/';
@override
String get anime4K_download =>
'MPV config files are required!\nDownload now?';
}

View file

@ -1500,6 +1500,16 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get rpc_show_cover_image => 'Show current cover image in Discord';
@override
String get anime4K => 'Enable Anime4K';
@override
String get anime4K_info => 'Supports .js scripts under /mpv/scripts/';
@override
String get anime4K_download =>
'MPV config files are required!\nDownload now?';
}
/// The translations for Portuguese, as used in Brazil (`pt_BR`).

View file

@ -1502,4 +1502,14 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get rpc_show_cover_image => 'Show current cover image in Discord';
@override
String get anime4K => 'Enable Anime4K';
@override
String get anime4K_info => 'Supports .js scripts under /mpv/scripts/';
@override
String get anime4K_download =>
'MPV config files are required!\nDownload now?';
}

View file

@ -1486,4 +1486,14 @@ class AppLocalizationsTh extends AppLocalizations {
@override
String get rpc_show_cover_image => 'Show current cover image in Discord';
@override
String get anime4K => 'Enable Anime4K';
@override
String get anime4K_info => 'Supports .js scripts under /mpv/scripts/';
@override
String get anime4K_download =>
'MPV config files are required!\nDownload now?';
}

View file

@ -1492,4 +1492,14 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get rpc_show_cover_image => 'Show current cover image in Discord';
@override
String get anime4K => 'Enable Anime4K';
@override
String get anime4K_info => 'Supports .js scripts under /mpv/scripts/';
@override
String get anime4K_download =>
'MPV config files are required!\nDownload now?';
}

View file

@ -1457,4 +1457,14 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get rpc_show_cover_image => 'Show current cover image in Discord';
@override
String get anime4K => 'Enable Anime4K';
@override
String get anime4K_info => 'Supports .js scripts under /mpv/scripts/';
@override
String get anime4K_download =>
'MPV config files are required!\nDownload now?';
}

View file

@ -30,6 +30,7 @@ import 'package:mangayomi/utils/discord_rpc.dart';
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';
// ignore: depend_on_referenced_packages
import 'package:media_kit/media_kit.dart';
import 'package:path_provider/path_provider.dart';
import 'package:window_manager/window_manager.dart';

View file

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

File diff suppressed because it is too large Load diff

View file

@ -256,6 +256,8 @@ class Settings {
bool? rpcShowCoverImage;
bool? useAnime4K;
Settings({
this.id = 227,
this.displayType = DisplayType.compactGrid,
@ -370,6 +372,7 @@ class Settings {
this.rpcShowReadingWatchingProgress = true,
this.rpcShowTitle = true,
this.rpcShowCoverImage = true,
this.useAnime4K = false,
});
Settings.fromJson(Map<String, dynamic> json) {
@ -590,6 +593,7 @@ class Settings {
rpcShowReadingWatchingProgress = json['rpcShowReadingWatchingProgress'];
rpcShowTitle = json['rpcShowTitle'];
rpcShowCoverImage = json['rpcShowCoverImage'];
useAnime4K = json['useAnime4K'];
}
Map<String, dynamic> toJson() => {
@ -727,6 +731,7 @@ class Settings {
'rpcShowReadingWatchingProgress': rpcShowReadingWatchingProgress,
'rpcShowTitle': rpcShowTitle,
'rpcShowCoverImage': rpcShowCoverImage,
'useAnime4K': useAnime4K,
};
}

View file

@ -623,18 +623,23 @@ const SettingsSchema = CollectionSchema(
name: r'updateProgressAfterReading',
type: IsarType.bool,
),
r'useLibass': PropertySchema(
r'useAnime4K': PropertySchema(
id: 115,
name: r'useAnime4K',
type: IsarType.bool,
),
r'useLibass': PropertySchema(
id: 116,
name: r'useLibass',
type: IsarType.bool,
),
r'usePageTapZones': PropertySchema(
id: 116,
id: 117,
name: r'usePageTapZones',
type: IsarType.bool,
),
r'userAgent': PropertySchema(
id: 117,
id: 118,
name: r'userAgent',
type: IsarType.string,
)
@ -1238,9 +1243,10 @@ void _settingsSerialize(
writer.writeLong(offsets[112], object.startDatebackup);
writer.writeBool(offsets[113], object.themeIsDark);
writer.writeBool(offsets[114], object.updateProgressAfterReading);
writer.writeBool(offsets[115], object.useLibass);
writer.writeBool(offsets[116], object.usePageTapZones);
writer.writeString(offsets[117], object.userAgent);
writer.writeBool(offsets[115], object.useAnime4K);
writer.writeBool(offsets[116], object.useLibass);
writer.writeBool(offsets[117], object.usePageTapZones);
writer.writeString(offsets[118], object.userAgent);
}
Settings _settingsDeserialize(
@ -1455,9 +1461,10 @@ Settings _settingsDeserialize(
startDatebackup: reader.readLongOrNull(offsets[112]),
themeIsDark: reader.readBoolOrNull(offsets[113]),
updateProgressAfterReading: reader.readBoolOrNull(offsets[114]),
useLibass: reader.readBoolOrNull(offsets[115]),
usePageTapZones: reader.readBoolOrNull(offsets[116]),
userAgent: reader.readStringOrNull(offsets[117]),
useAnime4K: reader.readBoolOrNull(offsets[115]),
useLibass: reader.readBoolOrNull(offsets[116]),
usePageTapZones: reader.readBoolOrNull(offsets[117]),
userAgent: reader.readStringOrNull(offsets[118]),
);
object.chapterFilterBookmarkedList =
reader.readObjectList<ChapterFilterBookmarked>(
@ -1850,6 +1857,8 @@ P _settingsDeserializeProp<P>(
case 116:
return (reader.readBoolOrNull(offset)) as P;
case 117:
return (reader.readBoolOrNull(offset)) as P;
case 118:
return (reader.readStringOrNull(offset)) as P;
default:
throw IsarError('Unknown property with id $propertyId');
@ -9504,6 +9513,33 @@ extension SettingsQueryFilter
});
}
QueryBuilder<Settings, Settings, QAfterFilterCondition> useAnime4KIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNull(
property: r'useAnime4K',
));
});
}
QueryBuilder<Settings, Settings, QAfterFilterCondition>
useAnime4KIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNotNull(
property: r'useAnime4K',
));
});
}
QueryBuilder<Settings, Settings, QAfterFilterCondition> useAnime4KEqualTo(
bool? value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'useAnime4K',
value: value,
));
});
}
QueryBuilder<Settings, Settings, QAfterFilterCondition> useLibassIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNull(
@ -11113,6 +11149,18 @@ extension SettingsQuerySortBy on QueryBuilder<Settings, Settings, QSortBy> {
});
}
QueryBuilder<Settings, Settings, QAfterSortBy> sortByUseAnime4K() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'useAnime4K', Sort.asc);
});
}
QueryBuilder<Settings, Settings, QAfterSortBy> sortByUseAnime4KDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'useAnime4K', Sort.desc);
});
}
QueryBuilder<Settings, Settings, QAfterSortBy> sortByUseLibass() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'useLibass', Sort.asc);
@ -12358,6 +12406,18 @@ extension SettingsQuerySortThenBy
});
}
QueryBuilder<Settings, Settings, QAfterSortBy> thenByUseAnime4K() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'useAnime4K', Sort.asc);
});
}
QueryBuilder<Settings, Settings, QAfterSortBy> thenByUseAnime4KDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'useAnime4K', Sort.desc);
});
}
QueryBuilder<Settings, Settings, QAfterSortBy> thenByUseLibass() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'useLibass', Sort.asc);
@ -13016,6 +13076,12 @@ extension SettingsQueryWhereDistinct
});
}
QueryBuilder<Settings, Settings, QDistinct> distinctByUseAnime4K() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'useAnime4K');
});
}
QueryBuilder<Settings, Settings, QDistinct> distinctByUseLibass() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'useLibass');
@ -13813,6 +13879,12 @@ extension SettingsQueryProperty
});
}
QueryBuilder<Settings, bool?, QQueryOperations> useAnime4KProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'useAnime4K');
});
}
QueryBuilder<Settings, bool?, QQueryOperations> useLibassProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'useLibass');

View file

@ -1,7 +1,10 @@
import 'dart:async';
import 'dart:ffi';
import 'dart:io';
import 'package:bot_toast/bot_toast.dart';
import 'package:ffi/ffi.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@ -31,6 +34,7 @@ import 'package:mangayomi/services/torrent_server.dart';
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
import 'package:mangayomi/utils/language.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit/generated/libmpv/bindings.dart' as generated;
import 'package:media_kit_video/media_kit_video.dart';
import 'package:media_kit_video/media_kit_video_controls/src/controls/extensions/duration.dart';
import 'package:path/path.dart' as p;
@ -71,7 +75,7 @@ class _AnimePlayerViewState extends riv.ConsumerState<AnimePlayerView> {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
return serversData.when(
data: (data) {
final (videos, isLocal, infoHashList) = data;
final (videos, isLocal, infoHashList, mpvDirectory) = data;
_infoHashList = infoHashList;
if (videos.isEmpty && !(episode.manga.value!.isLocalArchive ?? false)) {
return Scaffold(
@ -99,6 +103,7 @@ class _AnimePlayerViewState extends riv.ConsumerState<AnimePlayerView> {
desktopFullScreenPlayer: (value) {
desktopFullScreenPlayer = value;
},
mpvDirectory: mpvDirectory,
);
},
error: (error, stackTrace) => Scaffold(
@ -147,6 +152,7 @@ class AnimeStreamPage extends riv.ConsumerStatefulWidget {
final String defaultSubtitle;
final bool isLocal;
final bool isTorrent;
final Directory? mpvDirectory;
final void Function(bool) desktopFullScreenPlayer;
const AnimeStreamPage({
super.key,
@ -156,6 +162,7 @@ class AnimeStreamPage extends riv.ConsumerStatefulWidget {
required this.episode,
required this.isTorrent,
required this.desktopFullScreenPlayer,
required this.mpvDirectory,
});
@override
@ -172,8 +179,15 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
with TickerProviderStateMixin, WidgetsBindingObserver {
late final GlobalKey<VideoState> _key = GlobalKey<VideoState>();
late final useLibass = ref.read(useLibassStateProvider);
late final useAnime4K = ref.read(useAnime4KStateProvider);
late final Player _player = Player(
configuration: PlayerConfiguration(libass: useLibass),
configuration: PlayerConfiguration(
libass: useLibass,
config: true,
configDir: useAnime4K ? widget.mpvDirectory?.path ?? "" : "",
observeProperties: {},
eventHandler: _handleMpvEvents,
),
);
late final hwdecMode = ref.read(hwdecModeStateProvider());
late final VideoController _controller = VideoController(
@ -246,6 +260,25 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
}
});
Future<void> _handleMpvEvents(Pointer<generated.mpv_event> event) async {
try {
if (event.ref.event_id ==
generated.mpv_event_id.MPV_EVENT_PROPERTY_CHANGE) {
final prop = event.ref.data.cast<generated.mpv_event_property>();
if (prop.ref.name.cast<Utf8>().toDartString() ==
"user-data/aniyomi/dummy_number" &&
prop.ref.format == generated.mpv_format.MPV_FORMAT_INT64) {
final number = prop.ref.data.cast<Int64>().value;
botToast("Dummy number: $number");
}
}
} catch (e) {
if (kDebugMode) {
debugPrint(e.toString());
}
}
}
void pushToNewEpisode(BuildContext context, Chapter episode) {
widget.desktopFullScreenPlayer.call(ref.read(fullscreenProvider));
if (context.mounted) {
@ -1030,6 +1063,34 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
onPressed: () => _videoSettingDraggableMenu(context),
icon: const Icon(Icons.video_settings, color: Colors.white),
),
if (useAnime4K)
PopupMenuButton<String>(
tooltip: '', // Remove default tooltip "Show menu" for consistency
icon: const Icon(Icons.high_quality, color: Colors.white),
itemBuilder: (context) =>
[
("Anime4K: Mode A (Fast)", "CTRL+1"),
("Anime4K: Mode B (Fast)", "CTRL+2"),
("Anime4K: Mode C (Fast)", "CTRL+3"),
("Anime4K: Mode A+A (Fast)", "CTRL+4"),
("Anime4K: Mode B+B (Fast)", "CTRL+5"),
("Anime4K: Mode C+A (Fast)", "CTRL+6"),
("Clear GLSL shaders", "CTRL+0"),
]
.map(
(mode) => PopupMenuItem<String>(
value: mode.$1,
child: Text(mode.$1),
onTap: () {
(_player.platform as dynamic).command([
"keydown",
mode.$2,
]);
},
),
)
.toList(),
),
PopupMenuButton<double>(
tooltip: '', // Remove default tooltip "Show menu" for consistency
icon: const Icon(Icons.speed, color: Colors.white),

View file

@ -215,6 +215,27 @@ class _DesktopControllerWidgetState
final desktopFullScreenPlayer = widget.desktopFullScreenPlayer;
await _changeFullScreen(ref, desktopFullScreenPlayer, value: false);
},
const SingleActivator(LogicalKeyboardKey.digit0, control: true): () {
(widget.videoController.player.platform as dynamic).command(["keydown", "CTRL+0"]);
},
const SingleActivator(LogicalKeyboardKey.digit1, control: true): () {
(widget.videoController.player.platform as dynamic).command(["keydown", "CTRL+1"]);
},
const SingleActivator(LogicalKeyboardKey.digit2, control: true): () {
(widget.videoController.player.platform as dynamic).command(["keydown", "CTRL+2"]);
},
const SingleActivator(LogicalKeyboardKey.digit3, control: true): () {
(widget.videoController.player.platform as dynamic).command(["keydown", "CTRL+3"]);
},
const SingleActivator(LogicalKeyboardKey.digit4, control: true): () {
(widget.videoController.player.platform as dynamic).command(["keydown", "CTRL+4"]);
},
const SingleActivator(LogicalKeyboardKey.digit5, control: true): () {
(widget.videoController.player.platform as dynamic).command(["keydown", "CTRL+5"]);
},
const SingleActivator(LogicalKeyboardKey.digit6, control: true): () {
(widget.videoController.player.platform as dynamic).command(["keydown", "CTRL+6"]);
},
},
child: Stack(
children: [

View file

@ -81,6 +81,11 @@ Future<dynamic> updateMangaDetail(
scanlator: chaps[i].scanlator ?? '',
mangaId: mangaId,
updatedAt: DateTime.now().millisecondsSinceEpoch,
isFiller: chaps[i].isFiller,
thumbnailUrl: chaps[i].thumbnailUrl,
description: chaps[i].description,
downloadSize: chaps[i].downloadSize,
duration: chaps[i].duration,
)..manga.value = manga;
chapters.add(chapter);
}
@ -115,6 +120,11 @@ Future<dynamic> updateMangaDetail(
oldChap.url = newChap.url;
oldChap.scanlator = newChap.scanlator;
oldChap.updatedAt = DateTime.now().millisecondsSinceEpoch;
oldChap.isFiller = newChap.isFiller;
oldChap.thumbnailUrl = newChap.thumbnailUrl;
oldChap.description = newChap.description;
oldChap.downloadSize = newChap.downloadSize;
oldChap.duration = newChap.duration;
isar.chapters.putSync(oldChap);
oldChap.manga.saveSync();
}

View file

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

View file

@ -1,18 +1,43 @@
import 'dart:async';
import 'dart:io';
import 'package:archive/archive.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http;
import 'package:mangayomi/modules/more/settings/player/providers/player_state_provider.dart';
import 'package:mangayomi/providers/l10n_providers.dart';
import 'package:mangayomi/providers/storage_provider.dart';
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
import 'package:mangayomi/utils/language.dart';
import 'package:numberpicker/numberpicker.dart';
import 'package:path/path.dart' as path;
import 'package:permission_handler/permission_handler.dart';
import 'package:super_sliver_list/super_sliver_list.dart';
import 'package:mangayomi/l10n/generated/app_localizations.dart';
class PlayerScreen extends ConsumerWidget {
class PlayerScreen extends ConsumerStatefulWidget {
const PlayerScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
ConsumerState<PlayerScreen> createState() => _PlayerScreenState();
}
class _PlayerScreenState extends ConsumerState<PlayerScreen> {
int _total = 0;
int _received = 0;
late http.StreamedResponse _response;
final List<int> _bytes = [];
late StreamSubscription<List<int>>? _subscription;
@override
void dispose() {
_subscription?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final defaultSubtitleLang = ref.watch(defaultSubtitleLangStateProvider);
final markEpisodeAsSeenType = ref.watch(markEpisodeAsSeenTypeStateProvider);
final defaultSkipIntroLength = ref.watch(
@ -26,6 +51,7 @@ class PlayerScreen extends ConsumerWidget {
final enableAutoSkip = ref.watch(enableAutoSkipStateProvider);
final aniSkipTimeoutLength = ref.watch(aniSkipTimeoutLengthStateProvider);
final useLibass = ref.watch(useLibassStateProvider);
final useAnime4K = ref.watch(useAnime4KStateProvider);
final hwdecMode = ref.watch(hwdecModeStateProvider(rawValue: true));
final fullScreenPlayer = ref.watch(fullScreenPlayerStateProvider);
@ -457,6 +483,20 @@ class PlayerScreen extends ConsumerWidget {
),
],
),
SwitchListTile(
value: useAnime4K,
title: Text(context.l10n.anime4K),
subtitle: Text(
context.l10n.anime4K_info,
style: TextStyle(fontSize: 11, color: context.secondaryColor),
),
onChanged: (value) async {
if (value && !(await _checkAnime4K(context))) {
return;
}
ref.read(useAnime4KStateProvider.notifier).set(value);
},
),
SwitchListTile(
value: fullScreenPlayer,
title: Text(context.l10n.full_screen_player),
@ -501,7 +541,11 @@ class PlayerScreen extends ConsumerWidget {
groupValue: hwdecMode,
onChanged: (value) {
ref
.read(hwdecModeStateProvider(rawValue: true).notifier)
.read(
hwdecModeStateProvider(
rawValue: true,
).notifier,
)
.set(value!);
Navigator.pop(context);
},
@ -547,4 +591,130 @@ class PlayerScreen extends ConsumerWidget {
),
);
}
Future<bool> _checkAnime4K(BuildContext context) async {
var status = await Permission.storage.status;
if (!status.isGranted) {
await Permission.storage.request();
}
final provider = StorageProvider();
final dir = await provider.getMpvDirectory();
final mpvFile = File('${dir!.path}/mpv.conf');
final inputFile = File('${dir.path}/input.conf');
if (!(await mpvFile.exists()) &&
!(await inputFile.exists()) &&
context.mounted) {
final res = await showDialog(
context: context,
builder: (context) {
return AlertDialog(
content: SingleChildScrollView(
child: Column(
children: [
Text(context.l10n.anime4K_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(),
],
),
),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () async {
try {
await _subscription?.cancel();
} catch (_) {}
if (context.mounted) {
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/Tama47/Anime4K/releases/download/v4.0.1/GLSL_Windows_Low-end.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.endsWith(".glsl")) {
final shaderFile = File(
'$shadersDir/${file.name.split("/").last}',
);
await shaderFile.writeAsBytes(file.content);
}
}
_total = 0;
_received = 0;
_bytes.clear();
if (context.mounted) {
Navigator.pop(context, "ok");
}
});
}
: null,
child: Text(context.l10n.download),
),
],
),
],
);
},
);
return res != null && res == "ok";
}
return context.mounted;
}
}

View file

@ -183,6 +183,22 @@ class UseLibassState extends _$UseLibassState {
}
}
@riverpod
class UseAnime4KState extends _$UseAnime4KState {
@override
bool build() {
return isar.settings.getSync(227)!.useAnime4K ?? false;
}
void set(bool value) {
final settings = isar.settings.getSync(227);
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(settings!..useAnime4K = value),
);
}
}
final hwdecs = {
"no": ["all"],
"auto": ["all"],

View file

@ -175,6 +175,22 @@ final useLibassStateProvider =
);
typedef _$UseLibassState = AutoDisposeNotifier<bool>;
String _$useAnime4KStateHash() => r'3902552d399794bf7c78d5f18adcf59f267b3cf6';
/// See also [UseAnime4KState].
@ProviderFor(UseAnime4KState)
final useAnime4KStateProvider =
AutoDisposeNotifierProvider<UseAnime4KState, bool>.internal(
UseAnime4KState.new,
name: r'useAnime4KStateProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$useAnime4KStateHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$UseAnime4KState = AutoDisposeNotifier<bool>;
String _$hwdecModeStateHash() => r'e8b8e2b378eb9ac687bd8dad5637a816ad33fedb';
/// Copied from Dart SDK

View file

@ -61,6 +61,13 @@ class StorageProvider {
return directory;
}
Future<Directory?> getMpvDirectory() async {
final defaultDirectory = await getDefaultDirectory();
String dbDir = path.join(defaultDirectory!.path, 'mpv');
await Directory(dbDir).create(recursive: true);
return Directory(dbDir);
}
Future<Directory?> getBtDirectory() async {
final gefaultDirectory = await getDefaultDirectory();
String dbDir = path.join(gefaultDirectory!.path, 'torrents');

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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