feat: Implement app lock feature with biometric authentication

- Added AppLockScreen for biometric authentication to unlock the app.
- Introduced security settings screen to enable/disable app lock.
- Integrated local_auth package for biometric authentication support.
- Created security state providers to manage app lock state.
- Updated chapter list tile widget to support dismiss actions for bookmarking and marking chapters as read.
- Enhanced CBZ conversion process to include ComicInfo.xml metadata.
- Added conditional UI elements based on platform capabilities.
- Added Completed & Tracked filter in library
This commit is contained in:
Moustapha Kodjo Amadou 2026-03-04 11:56:49 +01:00
parent a5fd40fdfb
commit f729380223
47 changed files with 3473 additions and 739 deletions

View file

@ -1,7 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.kodjodevf.mangayomi"> package="com.kodjodevf.mangayomi">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.USE_BIOMETRIC"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />

View file

@ -5,14 +5,14 @@ import libmtorrentserver.Libmtorrentserver
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.StandardMethodCodec import io.flutter.plugin.common.StandardMethodCodec
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterFragmentActivity
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.net.Uri import android.net.Uri
import java.io.File import java.io.File
class MainActivity: FlutterActivity() { class MainActivity: FlutterFragmentActivity() {
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine) super.configureFlutterEngine(flutterEngine)

View file

@ -41,6 +41,8 @@
<array> <array>
<string>fetch</string> <string>fetch</string>
</array> </array>
<key>NSFaceIDUsageDescription</key>
<string>Mangayomi needs to authenticate using Face ID.</string>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>UIMainStoryboardFile</key> <key>UIMainStoryboardFile</key>

View file

@ -565,5 +565,17 @@
"forceLandscapeMode": "Force landscape mode", "forceLandscapeMode": "Force landscape mode",
"forceLandscapeModeSubtitle": "Force the player to use landscape orientation.", "forceLandscapeModeSubtitle": "Force the player to use landscape orientation.",
"dns_over_https": "DNS-over-HTTPS (DoH)", "dns_over_https": "DNS-over-HTTPS (DoH)",
"dns_provider": "DNS Provider" "dns_provider": "DNS Provider",
"tracked": "Tracked",
"auth_unlock_msg": "Authenticate to unlock Mangayomi",
"app_locked": "Mangayomi is locked",
"auth_to_continue": "Authenticate to continue",
"authenticating": "Authenticating...",
"unlock": "Unlock",
"security": "Security",
"auth_to_change_security_setting": "Authenticate to change security settings",
"app_lock": "App lock",
"require_biometric_or_device_credential": "Require biometric or device credential to open the app",
"biometric_or_device_credential_not_available": "Biometric authentication not available on this device",
"app_lock_description": "When app lock is enabled, you will be asked to authenticate \nevery time you open the app or switch back to it from the background."
} }

View file

@ -3466,6 +3466,78 @@ abstract class AppLocalizations {
/// In en, this message translates to: /// In en, this message translates to:
/// **'DNS Provider'** /// **'DNS Provider'**
String get dns_provider; String get dns_provider;
/// No description provided for @tracked.
///
/// In en, this message translates to:
/// **'Tracked'**
String get tracked;
/// No description provided for @auth_unlock_msg.
///
/// In en, this message translates to:
/// **'Authenticate to unlock Mangayomi'**
String get auth_unlock_msg;
/// No description provided for @app_locked.
///
/// In en, this message translates to:
/// **'Mangayomi is locked'**
String get app_locked;
/// No description provided for @auth_to_continue.
///
/// In en, this message translates to:
/// **'Authenticate to continue'**
String get auth_to_continue;
/// No description provided for @authenticating.
///
/// In en, this message translates to:
/// **'Authenticating...'**
String get authenticating;
/// No description provided for @unlock.
///
/// In en, this message translates to:
/// **'Unlock'**
String get unlock;
/// No description provided for @security.
///
/// In en, this message translates to:
/// **'Security'**
String get security;
/// No description provided for @auth_to_change_security_setting.
///
/// In en, this message translates to:
/// **'Authenticate to change security settings'**
String get auth_to_change_security_setting;
/// No description provided for @app_lock.
///
/// In en, this message translates to:
/// **'App lock'**
String get app_lock;
/// No description provided for @require_biometric_or_device_credential.
///
/// In en, this message translates to:
/// **'Require biometric or device credential to open the app'**
String get require_biometric_or_device_credential;
/// No description provided for @biometric_or_device_credential_not_available.
///
/// In en, this message translates to:
/// **'Biometric authentication not available on this device'**
String get biometric_or_device_credential_not_available;
/// No description provided for @app_lock_description.
///
/// In en, this message translates to:
/// **'When app lock is enabled, you will be asked to authenticate \nevery time you open the app or switch back to it from the background.'**
String get app_lock_description;
} }
class _AppLocalizationsDelegate class _AppLocalizationsDelegate

View file

@ -1794,4 +1794,44 @@ class AppLocalizationsAr extends AppLocalizations {
@override @override
String get dns_provider => 'DNS Provider'; String get dns_provider => 'DNS Provider';
@override
String get tracked => 'Tracked';
@override
String get auth_unlock_msg => 'Authenticate to unlock Mangayomi';
@override
String get app_locked => 'Mangayomi is locked';
@override
String get auth_to_continue => 'Authenticate to continue';
@override
String get authenticating => 'Authenticating...';
@override
String get unlock => 'Unlock';
@override
String get security => 'Security';
@override
String get auth_to_change_security_setting =>
'Authenticate to change security settings';
@override
String get app_lock => 'App lock';
@override
String get require_biometric_or_device_credential =>
'Require biometric or device credential to open the app';
@override
String get biometric_or_device_credential_not_available =>
'Biometric authentication not available on this device';
@override
String get app_lock_description =>
'When app lock is enabled, you will be asked to authenticate \nevery time you open the app or switch back to it from the background.';
} }

View file

@ -1800,4 +1800,44 @@ class AppLocalizationsAs extends AppLocalizations {
@override @override
String get dns_provider => 'DNS Provider'; String get dns_provider => 'DNS Provider';
@override
String get tracked => 'Tracked';
@override
String get auth_unlock_msg => 'Authenticate to unlock Mangayomi';
@override
String get app_locked => 'Mangayomi is locked';
@override
String get auth_to_continue => 'Authenticate to continue';
@override
String get authenticating => 'Authenticating...';
@override
String get unlock => 'Unlock';
@override
String get security => 'Security';
@override
String get auth_to_change_security_setting =>
'Authenticate to change security settings';
@override
String get app_lock => 'App lock';
@override
String get require_biometric_or_device_credential =>
'Require biometric or device credential to open the app';
@override
String get biometric_or_device_credential_not_available =>
'Biometric authentication not available on this device';
@override
String get app_lock_description =>
'When app lock is enabled, you will be asked to authenticate \nevery time you open the app or switch back to it from the background.';
} }

View file

@ -1815,4 +1815,44 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get dns_provider => 'DNS Provider'; String get dns_provider => 'DNS Provider';
@override
String get tracked => 'Tracked';
@override
String get auth_unlock_msg => 'Authenticate to unlock Mangayomi';
@override
String get app_locked => 'Mangayomi is locked';
@override
String get auth_to_continue => 'Authenticate to continue';
@override
String get authenticating => 'Authenticating...';
@override
String get unlock => 'Unlock';
@override
String get security => 'Security';
@override
String get auth_to_change_security_setting =>
'Authenticate to change security settings';
@override
String get app_lock => 'App lock';
@override
String get require_biometric_or_device_credential =>
'Require biometric or device credential to open the app';
@override
String get biometric_or_device_credential_not_available =>
'Biometric authentication not available on this device';
@override
String get app_lock_description =>
'When app lock is enabled, you will be asked to authenticate \nevery time you open the app or switch back to it from the background.';
} }

View file

@ -1794,4 +1794,44 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get dns_provider => 'DNS Provider'; String get dns_provider => 'DNS Provider';
@override
String get tracked => 'Tracked';
@override
String get auth_unlock_msg => 'Authenticate to unlock Mangayomi';
@override
String get app_locked => 'Mangayomi is locked';
@override
String get auth_to_continue => 'Authenticate to continue';
@override
String get authenticating => 'Authenticating...';
@override
String get unlock => 'Unlock';
@override
String get security => 'Security';
@override
String get auth_to_change_security_setting =>
'Authenticate to change security settings';
@override
String get app_lock => 'App lock';
@override
String get require_biometric_or_device_credential =>
'Require biometric or device credential to open the app';
@override
String get biometric_or_device_credential_not_available =>
'Biometric authentication not available on this device';
@override
String get app_lock_description =>
'When app lock is enabled, you will be asked to authenticate \nevery time you open the app or switch back to it from the background.';
} }

View file

@ -1823,6 +1823,46 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get dns_provider => 'DNS Provider'; String get dns_provider => 'DNS Provider';
@override
String get tracked => 'Tracked';
@override
String get auth_unlock_msg => 'Authenticate to unlock Mangayomi';
@override
String get app_locked => 'Mangayomi is locked';
@override
String get auth_to_continue => 'Authenticate to continue';
@override
String get authenticating => 'Authenticating...';
@override
String get unlock => 'Unlock';
@override
String get security => 'Security';
@override
String get auth_to_change_security_setting =>
'Authenticate to change security settings';
@override
String get app_lock => 'App lock';
@override
String get require_biometric_or_device_credential =>
'Require biometric or device credential to open the app';
@override
String get biometric_or_device_credential_not_available =>
'Biometric authentication not available on this device';
@override
String get app_lock_description =>
'When app lock is enabled, you will be asked to authenticate \nevery time you open the app or switch back to it from the background.';
} }
/// The translations for Spanish Castilian, as used in Latin America and the Caribbean (`es_419`). /// The translations for Spanish Castilian, as used in Latin America and the Caribbean (`es_419`).

View file

@ -1823,4 +1823,44 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get dns_provider => 'DNS Provider'; String get dns_provider => 'DNS Provider';
@override
String get tracked => 'Tracked';
@override
String get auth_unlock_msg => 'Authenticate to unlock Mangayomi';
@override
String get app_locked => 'Mangayomi is locked';
@override
String get auth_to_continue => 'Authenticate to continue';
@override
String get authenticating => 'Authenticating...';
@override
String get unlock => 'Unlock';
@override
String get security => 'Security';
@override
String get auth_to_change_security_setting =>
'Authenticate to change security settings';
@override
String get app_lock => 'App lock';
@override
String get require_biometric_or_device_credential =>
'Require biometric or device credential to open the app';
@override
String get biometric_or_device_credential_not_available =>
'Biometric authentication not available on this device';
@override
String get app_lock_description =>
'When app lock is enabled, you will be asked to authenticate \nevery time you open the app or switch back to it from the background.';
} }

View file

@ -1800,4 +1800,44 @@ class AppLocalizationsHi extends AppLocalizations {
@override @override
String get dns_provider => 'DNS Provider'; String get dns_provider => 'DNS Provider';
@override
String get tracked => 'Tracked';
@override
String get auth_unlock_msg => 'Authenticate to unlock Mangayomi';
@override
String get app_locked => 'Mangayomi is locked';
@override
String get auth_to_continue => 'Authenticate to continue';
@override
String get authenticating => 'Authenticating...';
@override
String get unlock => 'Unlock';
@override
String get security => 'Security';
@override
String get auth_to_change_security_setting =>
'Authenticate to change security settings';
@override
String get app_lock => 'App lock';
@override
String get require_biometric_or_device_credential =>
'Require biometric or device credential to open the app';
@override
String get biometric_or_device_credential_not_available =>
'Biometric authentication not available on this device';
@override
String get app_lock_description =>
'When app lock is enabled, you will be asked to authenticate \nevery time you open the app or switch back to it from the background.';
} }

View file

@ -1806,4 +1806,44 @@ class AppLocalizationsId extends AppLocalizations {
@override @override
String get dns_provider => 'DNS Provider'; String get dns_provider => 'DNS Provider';
@override
String get tracked => 'Tracked';
@override
String get auth_unlock_msg => 'Authenticate to unlock Mangayomi';
@override
String get app_locked => 'Mangayomi is locked';
@override
String get auth_to_continue => 'Authenticate to continue';
@override
String get authenticating => 'Authenticating...';
@override
String get unlock => 'Unlock';
@override
String get security => 'Security';
@override
String get auth_to_change_security_setting =>
'Authenticate to change security settings';
@override
String get app_lock => 'App lock';
@override
String get require_biometric_or_device_credential =>
'Require biometric or device credential to open the app';
@override
String get biometric_or_device_credential_not_available =>
'Biometric authentication not available on this device';
@override
String get app_lock_description =>
'When app lock is enabled, you will be asked to authenticate \nevery time you open the app or switch back to it from the background.';
} }

View file

@ -1820,4 +1820,44 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get dns_provider => 'DNS Provider'; String get dns_provider => 'DNS Provider';
@override
String get tracked => 'Tracked';
@override
String get auth_unlock_msg => 'Authenticate to unlock Mangayomi';
@override
String get app_locked => 'Mangayomi is locked';
@override
String get auth_to_continue => 'Authenticate to continue';
@override
String get authenticating => 'Authenticating...';
@override
String get unlock => 'Unlock';
@override
String get security => 'Security';
@override
String get auth_to_change_security_setting =>
'Authenticate to change security settings';
@override
String get app_lock => 'App lock';
@override
String get require_biometric_or_device_credential =>
'Require biometric or device credential to open the app';
@override
String get biometric_or_device_credential_not_available =>
'Biometric authentication not available on this device';
@override
String get app_lock_description =>
'When app lock is enabled, you will be asked to authenticate \nevery time you open the app or switch back to it from the background.';
} }

View file

@ -1771,4 +1771,44 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get dns_provider => 'DNS Provider'; String get dns_provider => 'DNS Provider';
@override
String get tracked => 'Tracked';
@override
String get auth_unlock_msg => 'Authenticate to unlock Mangayomi';
@override
String get app_locked => 'Mangayomi is locked';
@override
String get auth_to_continue => 'Authenticate to continue';
@override
String get authenticating => 'Authenticating...';
@override
String get unlock => 'Unlock';
@override
String get security => 'Security';
@override
String get auth_to_change_security_setting =>
'Authenticate to change security settings';
@override
String get app_lock => 'App lock';
@override
String get require_biometric_or_device_credential =>
'Require biometric or device credential to open the app';
@override
String get biometric_or_device_credential_not_available =>
'Biometric authentication not available on this device';
@override
String get app_lock_description =>
'When app lock is enabled, you will be asked to authenticate \nevery time you open the app or switch back to it from the background.';
} }

View file

@ -1818,6 +1818,46 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get dns_provider => 'DNS Provider'; String get dns_provider => 'DNS Provider';
@override
String get tracked => 'Tracked';
@override
String get auth_unlock_msg => 'Authenticate to unlock Mangayomi';
@override
String get app_locked => 'Mangayomi is locked';
@override
String get auth_to_continue => 'Authenticate to continue';
@override
String get authenticating => 'Authenticating...';
@override
String get unlock => 'Unlock';
@override
String get security => 'Security';
@override
String get auth_to_change_security_setting =>
'Authenticate to change security settings';
@override
String get app_lock => 'App lock';
@override
String get require_biometric_or_device_credential =>
'Require biometric or device credential to open the app';
@override
String get biometric_or_device_credential_not_available =>
'Biometric authentication not available on this device';
@override
String get app_lock_description =>
'When app lock is enabled, you will be asked to authenticate \nevery time you open the app or switch back to it from the background.';
} }
/// The translations for Portuguese, as used in Brazil (`pt_BR`). /// The translations for Portuguese, as used in Brazil (`pt_BR`).

View file

@ -1823,4 +1823,44 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get dns_provider => 'DNS Provider'; String get dns_provider => 'DNS Provider';
@override
String get tracked => 'Tracked';
@override
String get auth_unlock_msg => 'Authenticate to unlock Mangayomi';
@override
String get app_locked => 'Mangayomi is locked';
@override
String get auth_to_continue => 'Authenticate to continue';
@override
String get authenticating => 'Authenticating...';
@override
String get unlock => 'Unlock';
@override
String get security => 'Security';
@override
String get auth_to_change_security_setting =>
'Authenticate to change security settings';
@override
String get app_lock => 'App lock';
@override
String get require_biometric_or_device_credential =>
'Require biometric or device credential to open the app';
@override
String get biometric_or_device_credential_not_available =>
'Biometric authentication not available on this device';
@override
String get app_lock_description =>
'When app lock is enabled, you will be asked to authenticate \nevery time you open the app or switch back to it from the background.';
} }

View file

@ -1794,4 +1794,44 @@ class AppLocalizationsTh extends AppLocalizations {
@override @override
String get dns_provider => 'DNS Provider'; String get dns_provider => 'DNS Provider';
@override
String get tracked => 'Tracked';
@override
String get auth_unlock_msg => 'Authenticate to unlock Mangayomi';
@override
String get app_locked => 'Mangayomi is locked';
@override
String get auth_to_continue => 'Authenticate to continue';
@override
String get authenticating => 'Authenticating...';
@override
String get unlock => 'Unlock';
@override
String get security => 'Security';
@override
String get auth_to_change_security_setting =>
'Authenticate to change security settings';
@override
String get app_lock => 'App lock';
@override
String get require_biometric_or_device_credential =>
'Require biometric or device credential to open the app';
@override
String get biometric_or_device_credential_not_available =>
'Biometric authentication not available on this device';
@override
String get app_lock_description =>
'When app lock is enabled, you will be asked to authenticate \nevery time you open the app or switch back to it from the background.';
} }

View file

@ -1806,4 +1806,44 @@ class AppLocalizationsTr extends AppLocalizations {
@override @override
String get dns_provider => 'DNS Provider'; String get dns_provider => 'DNS Provider';
@override
String get tracked => 'Tracked';
@override
String get auth_unlock_msg => 'Authenticate to unlock Mangayomi';
@override
String get app_locked => 'Mangayomi is locked';
@override
String get auth_to_continue => 'Authenticate to continue';
@override
String get authenticating => 'Authenticating...';
@override
String get unlock => 'Unlock';
@override
String get security => 'Security';
@override
String get auth_to_change_security_setting =>
'Authenticate to change security settings';
@override
String get app_lock => 'App lock';
@override
String get require_biometric_or_device_credential =>
'Require biometric or device credential to open the app';
@override
String get biometric_or_device_credential_not_available =>
'Biometric authentication not available on this device';
@override
String get app_lock_description =>
'When app lock is enabled, you will be asked to authenticate \nevery time you open the app or switch back to it from the background.';
} }

View file

@ -1751,4 +1751,44 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get dns_provider => 'DNS Provider'; String get dns_provider => 'DNS Provider';
@override
String get tracked => 'Tracked';
@override
String get auth_unlock_msg => 'Authenticate to unlock Mangayomi';
@override
String get app_locked => 'Mangayomi is locked';
@override
String get auth_to_continue => 'Authenticate to continue';
@override
String get authenticating => 'Authenticating...';
@override
String get unlock => 'Unlock';
@override
String get security => 'Security';
@override
String get auth_to_change_security_setting =>
'Authenticate to change security settings';
@override
String get app_lock => 'App lock';
@override
String get require_biometric_or_device_credential =>
'Require biometric or device credential to open the app';
@override
String get biometric_or_device_credential_not_available =>
'Biometric authentication not available on this device';
@override
String get app_lock_description =>
'When app lock is enabled, you will be asked to authenticate \nevery time you open the app or switch back to it from the background.';
} }

View file

@ -41,6 +41,8 @@ import 'package:mangayomi/utils/log/logger.dart';
import 'package:mangayomi/utils/url_protocol/api.dart'; import 'package:mangayomi/utils/url_protocol/api.dart';
import 'package:mangayomi/modules/more/settings/appearance/providers/theme_provider.dart'; import 'package:mangayomi/modules/more/settings/appearance/providers/theme_provider.dart';
import 'package:mangayomi/modules/library/providers/file_scanner.dart'; import 'package:mangayomi/modules/library/providers/file_scanner.dart';
import 'package:mangayomi/modules/more/settings/security/providers/security_state_provider.dart';
import 'package:mangayomi/modules/more/settings/security/app_lock_screen.dart';
import 'package:media_kit/media_kit.dart'; import 'package:media_kit/media_kit.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
@ -135,7 +137,7 @@ class MyApp extends ConsumerStatefulWidget {
ConsumerState<MyApp> createState() => _MyAppState(); ConsumerState<MyApp> createState() => _MyAppState();
} }
class _MyAppState extends ConsumerState<MyApp> { class _MyAppState extends ConsumerState<MyApp> with WidgetsBindingObserver {
late AppLinks _appLinks; late AppLinks _appLinks;
StreamSubscription<Uri>? _linkSubscription; StreamSubscription<Uri>? _linkSubscription;
Uri? lastUri; Uri? lastUri;
@ -143,6 +145,7 @@ class _MyAppState extends ConsumerState<MyApp> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addObserver(this);
initializeDateFormatting(); initializeDateFormatting();
customDns = ref.read(customDnsStateProvider); customDns = ref.read(customDnsStateProvider);
_checkTrackerRefresh(); _checkTrackerRefresh();
@ -162,6 +165,22 @@ class _MyAppState extends ConsumerState<MyApp> {
}); });
} }
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.paused ||
state == AppLifecycleState.hidden) {
if (Platform.isLinux) {
return;
}
// Lock the app when going to background (if lock is enabled)
final lockEnabled = isar.settings.getSync(227)!.appLockEnabled ?? false;
if (lockEnabled) {
ref.read(appUnlockedStateProvider.notifier).lock();
}
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final followSystem = ref.watch(followSystemThemeStateProvider); final followSystem = ref.watch(followSystemThemeStateProvider);
@ -180,7 +199,17 @@ class _MyAppState extends ConsumerState<MyApp> {
locale: locale, locale: locale,
localizationsDelegates: AppLocalizations.localizationsDelegates, localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales, supportedLocales: AppLocalizations.supportedLocales,
builder: BotToastInit(), builder: Platform.isLinux
? null
: (context, child) {
child = BotToastInit()(context, child);
final isUnlocked = ref.watch(appUnlockedStateProvider);
final lockEnabled = ref.watch(appLockEnabledStateProvider);
if (lockEnabled && !isUnlocked) {
return const AppLockScreen();
}
return child;
},
routeInformationParser: router.routeInformationParser, routeInformationParser: router.routeInformationParser,
routerDelegate: router.routerDelegate, routerDelegate: router.routerDelegate,
routeInformationProvider: router.routeInformationProvider, routeInformationProvider: router.routeInformationProvider,
@ -191,6 +220,7 @@ class _MyAppState extends ConsumerState<MyApp> {
@override @override
void dispose() { void dispose() {
WidgetsBinding.instance.removeObserver(this);
MExtensionServerPlatform(ref).stopServer(); MExtensionServerPlatform(ref).stopServer();
_linkSubscription?.cancel(); _linkSubscription?.cancel();
discordRpc?.destroy(); discordRpc?.destroy();

View file

@ -310,6 +310,20 @@ class Settings {
List<String>? localFolders; List<String>? localFolders;
bool? appLockEnabled;
int? libraryFilterMangasCompletedType;
int? libraryFilterAnimeCompletedType;
int? libraryFilterNovelCompletedType;
int? libraryFilterMangasTrackingType;
int? libraryFilterAnimeTrackingType;
int? libraryFilterNovelTrackingType;
Settings({ Settings({
this.id = 227, this.id = 227,
this.updatedAt = 0, this.updatedAt = 0,
@ -450,6 +464,13 @@ class Settings {
this.downloadedOnlyMode = false, this.downloadedOnlyMode = false,
this.algorithmWeights, this.algorithmWeights,
this.localFolders, this.localFolders,
this.appLockEnabled = false,
this.libraryFilterMangasCompletedType = 0,
this.libraryFilterAnimeCompletedType = 0,
this.libraryFilterNovelCompletedType = 0,
this.libraryFilterMangasTrackingType = 0,
this.libraryFilterAnimeTrackingType = 0,
this.libraryFilterNovelTrackingType = 0,
}); });
Settings.fromJson(Map<String, dynamic> json) { Settings.fromJson(Map<String, dynamic> json) {
@ -708,6 +729,13 @@ class Settings {
? AlgorithmWeights.fromJson(json['algorithmWeights']) ? AlgorithmWeights.fromJson(json['algorithmWeights'])
: null; : null;
localFolders = json['localFolders']; localFolders = json['localFolders'];
appLockEnabled = json['appLockEnabled'];
libraryFilterMangasCompletedType = json['libraryFilterMangasCompletedType'];
libraryFilterAnimeCompletedType = json['libraryFilterAnimeCompletedType'];
libraryFilterNovelCompletedType = json['libraryFilterNovelCompletedType'];
libraryFilterMangasTrackingType = json['libraryFilterMangasTrackingType'];
libraryFilterAnimeTrackingType = json['libraryFilterAnimeTrackingType'];
libraryFilterNovelTrackingType = json['libraryFilterNovelTrackingType'];
} }
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
@ -872,6 +900,13 @@ class Settings {
if (algorithmWeights != null) if (algorithmWeights != null)
'algorithmWeights': algorithmWeights!.toJson(), 'algorithmWeights': algorithmWeights!.toJson(),
'localFolders': localFolders, 'localFolders': localFolders,
'appLockEnabled': appLockEnabled,
'libraryFilterMangasCompletedType': libraryFilterMangasCompletedType,
'libraryFilterAnimeCompletedType': libraryFilterAnimeCompletedType,
'libraryFilterNovelCompletedType': libraryFilterNovelCompletedType,
'libraryFilterMangasTrackingType': libraryFilterMangasTrackingType,
'libraryFilterAnimeTrackingType': libraryFilterAnimeTrackingType,
'libraryFilterNovelTrackingType': libraryFilterNovelTrackingType,
}; };
} }

File diff suppressed because it is too large Load diff

View file

@ -159,6 +159,12 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen>
final bookmarkedFilterType = watchWithSettingsAndManga( final bookmarkedFilterType = watchWithSettingsAndManga(
mangaFilterBookmarkedStateProvider.call, mangaFilterBookmarkedStateProvider.call,
); );
final completedFilterType = watchWithSettingsAndManga(
mangaFilterCompletedStateProvider.call,
);
final trackingFilterType = watchWithSettingsAndManga(
mangaFilterTrackingStateProvider.call,
);
final sortType = final sortType =
watchWithSettings(sortLibraryMangaStateProvider.call).index as int; watchWithSettings(sortLibraryMangaStateProvider.call).index as int;
@ -174,6 +180,8 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen>
unreadFilterType: unreadFilterType, unreadFilterType: unreadFilterType,
startedFilterType: startedFilterType, startedFilterType: startedFilterType,
bookmarkedFilterType: bookmarkedFilterType, bookmarkedFilterType: bookmarkedFilterType,
completedFilterType: completedFilterType,
trackingFilterType: trackingFilterType,
reverse: reverse, reverse: reverse,
downloadedChapter: downloadedChapter, downloadedChapter: downloadedChapter,
continueReaderBtn: continueReaderBtn, continueReaderBtn: continueReaderBtn,
@ -195,6 +203,8 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen>
unreadFilterType: unreadFilterType, unreadFilterType: unreadFilterType,
startedFilterType: startedFilterType, startedFilterType: startedFilterType,
bookmarkedFilterType: bookmarkedFilterType, bookmarkedFilterType: bookmarkedFilterType,
completedFilterType: completedFilterType,
trackingFilterType: trackingFilterType,
settings: settings, settings: settings,
downloadedOnly: downloadedOnly, downloadedOnly: downloadedOnly,
searchQuery: searchQuery, searchQuery: searchQuery,
@ -217,6 +227,8 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen>
unreadFilterType: unreadFilterType, unreadFilterType: unreadFilterType,
startedFilterType: startedFilterType, startedFilterType: startedFilterType,
bookmarkedFilterType: bookmarkedFilterType, bookmarkedFilterType: bookmarkedFilterType,
completedFilterType: completedFilterType,
trackingFilterType: trackingFilterType,
sortType: sortType, sortType: sortType,
downloadedOnly: downloadedOnly, downloadedOnly: downloadedOnly,
searchQuery: searchQuery, searchQuery: searchQuery,

View file

@ -2,6 +2,7 @@ import 'package:isar_community/isar.dart';
import 'package:mangayomi/main.dart'; import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/download.dart'; import 'package:mangayomi/models/download.dart';
import 'package:mangayomi/models/manga.dart'; import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/models/track.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'library_filter_provider.g.dart'; part 'library_filter_provider.g.dart';
@ -17,10 +18,14 @@ Set<int> downloadedChapterIds(Ref ref) {
return downloads.whereType<int>().toSet(); return downloads.whereType<int>().toSet();
} }
/// Pre-fetches all manga IDs that have at least one tracking entry.
@riverpod
Set<int> trackedMangaIds(Ref ref) {
final tracks = isar.tracks.where().findAllSync();
return tracks.map((t) => t.mangaId).whereType<int>().toSet();
}
/// Filters and sorts a list of [Manga] based on library filter/sort settings. /// Filters and sorts a list of [Manga] based on library filter/sort settings.
///
/// Uses [downloadedChapterIds] for O(1) download lookups instead of
/// per-chapter Isar queries (previous behavior was O(chapters × manga)).
@riverpod @riverpod
List<Manga> filteredLibraryManga( List<Manga> filteredLibraryManga(
Ref ref, { Ref ref, {
@ -29,12 +34,15 @@ List<Manga> filteredLibraryManga(
required int unreadFilterType, required int unreadFilterType,
required int startedFilterType, required int startedFilterType,
required int bookmarkedFilterType, required int bookmarkedFilterType,
required int completedFilterType,
required int trackingFilterType,
required int sortType, required int sortType,
required bool downloadedOnly, required bool downloadedOnly,
required String searchQuery, required String searchQuery,
required bool ignoreFiltersOnSearch, required bool ignoreFiltersOnSearch,
}) { }) {
final downloadedIds = ref.watch(downloadedChapterIdsProvider); final downloadedIds = ref.watch(downloadedChapterIdsProvider);
final trackedIds = ref.watch(trackedMangaIdsProvider);
return _filterAndSortManga( return _filterAndSortManga(
data: data, data: data,
@ -42,11 +50,14 @@ List<Manga> filteredLibraryManga(
unreadFilterType: unreadFilterType, unreadFilterType: unreadFilterType,
startedFilterType: startedFilterType, startedFilterType: startedFilterType,
bookmarkedFilterType: bookmarkedFilterType, bookmarkedFilterType: bookmarkedFilterType,
completedFilterType: completedFilterType,
trackingFilterType: trackingFilterType,
sortType: sortType, sortType: sortType,
downloadedOnly: downloadedOnly, downloadedOnly: downloadedOnly,
searchQuery: searchQuery, searchQuery: searchQuery,
ignoreFiltersOnSearch: ignoreFiltersOnSearch, ignoreFiltersOnSearch: ignoreFiltersOnSearch,
downloadedIds: downloadedIds, downloadedIds: downloadedIds,
trackedIds: trackedIds,
); );
} }
@ -71,11 +82,14 @@ List<Manga> _filterAndSortManga({
required int unreadFilterType, required int unreadFilterType,
required int startedFilterType, required int startedFilterType,
required int bookmarkedFilterType, required int bookmarkedFilterType,
required int completedFilterType,
required int trackingFilterType,
required int sortType, required int sortType,
required bool downloadedOnly, required bool downloadedOnly,
required String searchQuery, required String searchQuery,
required bool ignoreFiltersOnSearch, required bool ignoreFiltersOnSearch,
required Set<int> downloadedIds, required Set<int> downloadedIds,
required Set<int> trackedIds,
}) { }) {
List<Manga> mangas; List<Manga> mangas;
@ -121,6 +135,24 @@ List<Manga> _filterAndSortManga({
if (!allNotBookmarked) return false; if (!allNotBookmarked) return false;
} }
// Filter by completed status
if (completedFilterType == 1) {
if (element.status != Status.completed) return false;
} else if (completedFilterType == 2) {
if (element.status == Status.completed) return false;
}
// Filter by tracking
if (trackingFilterType == 1) {
if (element.id == null || !trackedIds.contains(element.id)) {
return false;
}
} else if (trackingFilterType == 2) {
if (element.id != null && trackedIds.contains(element.id)) {
return false;
}
}
// Search filter // Search filter
if (searchQuery.isNotEmpty) { if (searchQuery.isNotEmpty) {
if (!_matchesSearchQuery(element, searchQuery)) return false; if (!_matchesSearchQuery(element, searchQuery)) return false;

View file

@ -58,26 +58,63 @@ final class DownloadedChapterIdsProvider
String _$downloadedChapterIdsHash() => String _$downloadedChapterIdsHash() =>
r'a51ff78fb0ad2548c719d1ca400ae474fc01e683'; r'a51ff78fb0ad2548c719d1ca400ae474fc01e683';
/// Pre-fetches all manga IDs that have at least one tracking entry.
@ProviderFor(trackedMangaIds)
final trackedMangaIdsProvider = TrackedMangaIdsProvider._();
/// Pre-fetches all manga IDs that have at least one tracking entry.
final class TrackedMangaIdsProvider
extends $FunctionalProvider<Set<int>, Set<int>, Set<int>>
with $Provider<Set<int>> {
/// Pre-fetches all manga IDs that have at least one tracking entry.
TrackedMangaIdsProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'trackedMangaIdsProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$trackedMangaIdsHash();
@$internal
@override
$ProviderElement<Set<int>> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
Set<int> create(Ref ref) {
return trackedMangaIds(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(Set<int> value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<Set<int>>(value),
);
}
}
String _$trackedMangaIdsHash() => r'8fd052ae3ff4e9fe47e66d5e24cd57233aa03d0a';
/// Filters and sorts a list of [Manga] based on library filter/sort settings. /// Filters and sorts a list of [Manga] based on library filter/sort settings.
///
/// Uses [downloadedChapterIds] for O(1) download lookups instead of
/// per-chapter Isar queries (previous behavior was O(chapters × manga)).
@ProviderFor(filteredLibraryManga) @ProviderFor(filteredLibraryManga)
final filteredLibraryMangaProvider = FilteredLibraryMangaFamily._(); final filteredLibraryMangaProvider = FilteredLibraryMangaFamily._();
/// Filters and sorts a list of [Manga] based on library filter/sort settings. /// Filters and sorts a list of [Manga] based on library filter/sort settings.
///
/// Uses [downloadedChapterIds] for O(1) download lookups instead of
/// per-chapter Isar queries (previous behavior was O(chapters × manga)).
final class FilteredLibraryMangaProvider final class FilteredLibraryMangaProvider
extends $FunctionalProvider<List<Manga>, List<Manga>, List<Manga>> extends $FunctionalProvider<List<Manga>, List<Manga>, List<Manga>>
with $Provider<List<Manga>> { with $Provider<List<Manga>> {
/// Filters and sorts a list of [Manga] based on library filter/sort settings. /// Filters and sorts a list of [Manga] based on library filter/sort settings.
///
/// Uses [downloadedChapterIds] for O(1) download lookups instead of
/// per-chapter Isar queries (previous behavior was O(chapters × manga)).
FilteredLibraryMangaProvider._({ FilteredLibraryMangaProvider._({
required FilteredLibraryMangaFamily super.from, required FilteredLibraryMangaFamily super.from,
required ({ required ({
@ -86,6 +123,8 @@ final class FilteredLibraryMangaProvider
int unreadFilterType, int unreadFilterType,
int startedFilterType, int startedFilterType,
int bookmarkedFilterType, int bookmarkedFilterType,
int completedFilterType,
int trackingFilterType,
int sortType, int sortType,
bool downloadedOnly, bool downloadedOnly,
String searchQuery, String searchQuery,
@ -125,6 +164,8 @@ final class FilteredLibraryMangaProvider
int unreadFilterType, int unreadFilterType,
int startedFilterType, int startedFilterType,
int bookmarkedFilterType, int bookmarkedFilterType,
int completedFilterType,
int trackingFilterType,
int sortType, int sortType,
bool downloadedOnly, bool downloadedOnly,
String searchQuery, String searchQuery,
@ -137,6 +178,8 @@ final class FilteredLibraryMangaProvider
unreadFilterType: argument.unreadFilterType, unreadFilterType: argument.unreadFilterType,
startedFilterType: argument.startedFilterType, startedFilterType: argument.startedFilterType,
bookmarkedFilterType: argument.bookmarkedFilterType, bookmarkedFilterType: argument.bookmarkedFilterType,
completedFilterType: argument.completedFilterType,
trackingFilterType: argument.trackingFilterType,
sortType: argument.sortType, sortType: argument.sortType,
downloadedOnly: argument.downloadedOnly, downloadedOnly: argument.downloadedOnly,
searchQuery: argument.searchQuery, searchQuery: argument.searchQuery,
@ -164,12 +207,9 @@ final class FilteredLibraryMangaProvider
} }
String _$filteredLibraryMangaHash() => String _$filteredLibraryMangaHash() =>
r'34cd87ea154cc617e85572ede503b81fb36f2a97'; r'afecb3de71f1f8c1682a0bfd9949f8a372c7d1b6';
/// Filters and sorts a list of [Manga] based on library filter/sort settings. /// Filters and sorts a list of [Manga] based on library filter/sort settings.
///
/// Uses [downloadedChapterIds] for O(1) download lookups instead of
/// per-chapter Isar queries (previous behavior was O(chapters × manga)).
final class FilteredLibraryMangaFamily extends $Family final class FilteredLibraryMangaFamily extends $Family
with with
@ -181,6 +221,8 @@ final class FilteredLibraryMangaFamily extends $Family
int unreadFilterType, int unreadFilterType,
int startedFilterType, int startedFilterType,
int bookmarkedFilterType, int bookmarkedFilterType,
int completedFilterType,
int trackingFilterType,
int sortType, int sortType,
bool downloadedOnly, bool downloadedOnly,
String searchQuery, String searchQuery,
@ -197,9 +239,6 @@ final class FilteredLibraryMangaFamily extends $Family
); );
/// Filters and sorts a list of [Manga] based on library filter/sort settings. /// Filters and sorts a list of [Manga] based on library filter/sort settings.
///
/// Uses [downloadedChapterIds] for O(1) download lookups instead of
/// per-chapter Isar queries (previous behavior was O(chapters × manga)).
FilteredLibraryMangaProvider call({ FilteredLibraryMangaProvider call({
required List<Manga> data, required List<Manga> data,
@ -207,6 +246,8 @@ final class FilteredLibraryMangaFamily extends $Family
required int unreadFilterType, required int unreadFilterType,
required int startedFilterType, required int startedFilterType,
required int bookmarkedFilterType, required int bookmarkedFilterType,
required int completedFilterType,
required int trackingFilterType,
required int sortType, required int sortType,
required bool downloadedOnly, required bool downloadedOnly,
required String searchQuery, required String searchQuery,
@ -218,6 +259,8 @@ final class FilteredLibraryMangaFamily extends $Family
unreadFilterType: unreadFilterType, unreadFilterType: unreadFilterType,
startedFilterType: startedFilterType, startedFilterType: startedFilterType,
bookmarkedFilterType: bookmarkedFilterType, bookmarkedFilterType: bookmarkedFilterType,
completedFilterType: completedFilterType,
trackingFilterType: trackingFilterType,
sortType: sortType, sortType: sortType,
downloadedOnly: downloadedOnly, downloadedOnly: downloadedOnly,
searchQuery: searchQuery, searchQuery: searchQuery,

View file

@ -466,6 +466,118 @@ class MangaFilterBookmarkedState extends _$MangaFilterBookmarkedState {
} }
} }
// Completed filter
@riverpod
class MangaFilterCompletedState extends _$MangaFilterCompletedState {
@override
int build({
required List<Manga> mangaList,
required ItemType itemType,
required Settings settings,
}) {
state = getType();
return getType();
}
int getType() {
switch (itemType) {
case ItemType.manga:
return settings.libraryFilterMangasCompletedType ?? 0;
case ItemType.anime:
return settings.libraryFilterAnimeCompletedType ?? 0;
default:
return settings.libraryFilterNovelCompletedType ?? 0;
}
}
void setType(int type) {
Settings appSettings = Settings();
switch (itemType) {
case ItemType.manga:
appSettings = settings..libraryFilterMangasCompletedType = type;
break;
case ItemType.anime:
appSettings = settings..libraryFilterAnimeCompletedType = type;
break;
default:
appSettings = settings..libraryFilterNovelCompletedType = type;
}
isar.writeTxnSync(() {
isar.settings.putSync(
appSettings..updatedAt = DateTime.now().millisecondsSinceEpoch,
);
});
state = type;
}
void update() {
if (state == 0) {
setType(1);
} else if (state == 1) {
setType(2);
} else {
setType(0);
}
}
}
// Tracking filter
@riverpod
class MangaFilterTrackingState extends _$MangaFilterTrackingState {
@override
int build({
required List<Manga> mangaList,
required ItemType itemType,
required Settings settings,
}) {
state = getType();
return getType();
}
int getType() {
switch (itemType) {
case ItemType.manga:
return settings.libraryFilterMangasTrackingType ?? 0;
case ItemType.anime:
return settings.libraryFilterAnimeTrackingType ?? 0;
default:
return settings.libraryFilterNovelTrackingType ?? 0;
}
}
void setType(int type) {
Settings appSettings = Settings();
switch (itemType) {
case ItemType.manga:
appSettings = settings..libraryFilterMangasTrackingType = type;
break;
case ItemType.anime:
appSettings = settings..libraryFilterAnimeTrackingType = type;
break;
default:
appSettings = settings..libraryFilterNovelTrackingType = type;
}
isar.writeTxnSync(() {
isar.settings.putSync(
appSettings..updatedAt = DateTime.now().millisecondsSinceEpoch,
);
});
state = type;
}
void update() {
if (state == 0) {
setType(1);
} else if (state == 1) {
setType(2);
} else {
setType(0);
}
}
}
@riverpod @riverpod
class MangasFilterResultState extends _$MangasFilterResultState { class MangasFilterResultState extends _$MangasFilterResultState {
@override @override
@ -502,10 +614,26 @@ class MangasFilterResultState extends _$MangasFilterResultState {
settings: settings, settings: settings,
), ),
); );
final completedFilterType = ref.watch(
mangaFilterCompletedStateProvider(
mangaList: mangaList,
itemType: itemType,
settings: settings,
),
);
final trackingFilterType = ref.watch(
mangaFilterTrackingStateProvider(
mangaList: mangaList,
itemType: itemType,
settings: settings,
),
);
return downloadFilterType == 0 && return downloadFilterType == 0 &&
unreadFilterType == 0 && unreadFilterType == 0 &&
startedFilterType == 0 && startedFilterType == 0 &&
bookmarkedFilterType == 0; bookmarkedFilterType == 0 &&
completedFilterType == 0 &&
trackingFilterType == 0;
} }
} }

View file

@ -693,6 +693,248 @@ abstract class _$MangaFilterBookmarkedState extends $Notifier<int> {
} }
} }
@ProviderFor(MangaFilterCompletedState)
final mangaFilterCompletedStateProvider = MangaFilterCompletedStateFamily._();
final class MangaFilterCompletedStateProvider
extends $NotifierProvider<MangaFilterCompletedState, int> {
MangaFilterCompletedStateProvider._({
required MangaFilterCompletedStateFamily super.from,
required ({List<Manga> mangaList, ItemType itemType, Settings settings})
super.argument,
}) : super(
retry: null,
name: r'mangaFilterCompletedStateProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$mangaFilterCompletedStateHash();
@override
String toString() {
return r'mangaFilterCompletedStateProvider'
''
'$argument';
}
@$internal
@override
MangaFilterCompletedState create() => MangaFilterCompletedState();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(int value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<int>(value),
);
}
@override
bool operator ==(Object other) {
return other is MangaFilterCompletedStateProvider &&
other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$mangaFilterCompletedStateHash() =>
r'8a9f60b94db16d65d29caa8598443c070f7c26e6';
final class MangaFilterCompletedStateFamily extends $Family
with
$ClassFamilyOverride<
MangaFilterCompletedState,
int,
int,
int,
({List<Manga> mangaList, ItemType itemType, Settings settings})
> {
MangaFilterCompletedStateFamily._()
: super(
retry: null,
name: r'mangaFilterCompletedStateProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
MangaFilterCompletedStateProvider call({
required List<Manga> mangaList,
required ItemType itemType,
required Settings settings,
}) => MangaFilterCompletedStateProvider._(
argument: (mangaList: mangaList, itemType: itemType, settings: settings),
from: this,
);
@override
String toString() => r'mangaFilterCompletedStateProvider';
}
abstract class _$MangaFilterCompletedState extends $Notifier<int> {
late final _$args =
ref.$arg
as ({List<Manga> mangaList, ItemType itemType, Settings settings});
List<Manga> get mangaList => _$args.mangaList;
ItemType get itemType => _$args.itemType;
Settings get settings => _$args.settings;
int build({
required List<Manga> mangaList,
required ItemType itemType,
required Settings settings,
});
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<int, int>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<int, int>,
int,
Object?,
Object?
>;
element.handleCreate(
ref,
() => build(
mangaList: _$args.mangaList,
itemType: _$args.itemType,
settings: _$args.settings,
),
);
}
}
@ProviderFor(MangaFilterTrackingState)
final mangaFilterTrackingStateProvider = MangaFilterTrackingStateFamily._();
final class MangaFilterTrackingStateProvider
extends $NotifierProvider<MangaFilterTrackingState, int> {
MangaFilterTrackingStateProvider._({
required MangaFilterTrackingStateFamily super.from,
required ({List<Manga> mangaList, ItemType itemType, Settings settings})
super.argument,
}) : super(
retry: null,
name: r'mangaFilterTrackingStateProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$mangaFilterTrackingStateHash();
@override
String toString() {
return r'mangaFilterTrackingStateProvider'
''
'$argument';
}
@$internal
@override
MangaFilterTrackingState create() => MangaFilterTrackingState();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(int value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<int>(value),
);
}
@override
bool operator ==(Object other) {
return other is MangaFilterTrackingStateProvider &&
other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$mangaFilterTrackingStateHash() =>
r'fe79a139011725cf0a3d735930a41e1f593f0b70';
final class MangaFilterTrackingStateFamily extends $Family
with
$ClassFamilyOverride<
MangaFilterTrackingState,
int,
int,
int,
({List<Manga> mangaList, ItemType itemType, Settings settings})
> {
MangaFilterTrackingStateFamily._()
: super(
retry: null,
name: r'mangaFilterTrackingStateProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
MangaFilterTrackingStateProvider call({
required List<Manga> mangaList,
required ItemType itemType,
required Settings settings,
}) => MangaFilterTrackingStateProvider._(
argument: (mangaList: mangaList, itemType: itemType, settings: settings),
from: this,
);
@override
String toString() => r'mangaFilterTrackingStateProvider';
}
abstract class _$MangaFilterTrackingState extends $Notifier<int> {
late final _$args =
ref.$arg
as ({List<Manga> mangaList, ItemType itemType, Settings settings});
List<Manga> get mangaList => _$args.mangaList;
ItemType get itemType => _$args.itemType;
Settings get settings => _$args.settings;
int build({
required List<Manga> mangaList,
required ItemType itemType,
required Settings settings,
});
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<int, int>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<int, int>,
int,
Object?,
Object?
>;
element.handleCreate(
ref,
() => build(
mangaList: _$args.mangaList,
itemType: _$args.itemType,
settings: _$args.settings,
),
);
}
}
@ProviderFor(MangasFilterResultState) @ProviderFor(MangasFilterResultState)
final mangasFilterResultStateProvider = MangasFilterResultStateFamily._(); final mangasFilterResultStateProvider = MangasFilterResultStateFamily._();
@ -745,7 +987,7 @@ final class MangasFilterResultStateProvider
} }
String _$mangasFilterResultStateHash() => String _$mangasFilterResultStateHash() =>
r'c6f916c35e9b7125ba073d09aa6838605b933b20'; r'6fbbc29f7e71e5d929f49fdaecd69a665bd034fb';
final class MangasFilterResultStateFamily extends $Family final class MangasFilterResultStateFamily extends $Family
with with

View file

@ -25,6 +25,8 @@ class LibraryBody extends ConsumerWidget {
final int unreadFilterType; final int unreadFilterType;
final int startedFilterType; final int startedFilterType;
final int bookmarkedFilterType; final int bookmarkedFilterType;
final int completedFilterType;
final int trackingFilterType;
final bool reverse; final bool reverse;
final bool downloadedChapter; final bool downloadedChapter;
final bool continueReaderBtn; final bool continueReaderBtn;
@ -45,6 +47,8 @@ class LibraryBody extends ConsumerWidget {
required this.unreadFilterType, required this.unreadFilterType,
required this.startedFilterType, required this.startedFilterType,
required this.bookmarkedFilterType, required this.bookmarkedFilterType,
required this.completedFilterType,
required this.trackingFilterType,
required this.reverse, required this.reverse,
required this.downloadedChapter, required this.downloadedChapter,
required this.continueReaderBtn, required this.continueReaderBtn,
@ -89,6 +93,8 @@ class LibraryBody extends ConsumerWidget {
unreadFilterType: unreadFilterType, unreadFilterType: unreadFilterType,
startedFilterType: startedFilterType, startedFilterType: startedFilterType,
bookmarkedFilterType: bookmarkedFilterType, bookmarkedFilterType: bookmarkedFilterType,
completedFilterType: completedFilterType,
trackingFilterType: trackingFilterType,
sortType: sortType ?? 0, sortType: sortType ?? 0,
downloadedOnly: downloadedOnly, downloadedOnly: downloadedOnly,
searchQuery: searchQuery, searchQuery: searchQuery,
@ -149,6 +155,8 @@ class CategoryBadge extends ConsumerWidget {
final int unreadFilterType; final int unreadFilterType;
final int startedFilterType; final int startedFilterType;
final int bookmarkedFilterType; final int bookmarkedFilterType;
final int completedFilterType;
final int trackingFilterType;
final Settings settings; final Settings settings;
final bool downloadedOnly; final bool downloadedOnly;
final String searchQuery; final String searchQuery;
@ -162,6 +170,8 @@ class CategoryBadge extends ConsumerWidget {
required this.unreadFilterType, required this.unreadFilterType,
required this.startedFilterType, required this.startedFilterType,
required this.bookmarkedFilterType, required this.bookmarkedFilterType,
required this.completedFilterType,
required this.trackingFilterType,
required this.settings, required this.settings,
required this.downloadedOnly, required this.downloadedOnly,
required this.searchQuery, required this.searchQuery,
@ -188,6 +198,8 @@ class CategoryBadge extends ConsumerWidget {
unreadFilterType: unreadFilterType, unreadFilterType: unreadFilterType,
startedFilterType: startedFilterType, startedFilterType: startedFilterType,
bookmarkedFilterType: bookmarkedFilterType, bookmarkedFilterType: bookmarkedFilterType,
completedFilterType: completedFilterType,
trackingFilterType: trackingFilterType,
sortType: sortType ?? 0, sortType: sortType ?? 0,
downloadedOnly: downloadedOnly, downloadedOnly: downloadedOnly,
searchQuery: searchQuery, searchQuery: searchQuery,

View file

@ -137,6 +137,48 @@ class _FilterTab extends ConsumerWidget {
.update(); .update();
}, },
), ),
ListTileChapterFilter(
label: l10n.completed,
type: ref.watch(
mangaFilterCompletedStateProvider(
itemType: itemType,
mangaList: entries,
settings: settings,
),
),
onTap: () {
ref
.read(
mangaFilterCompletedStateProvider(
itemType: itemType,
mangaList: entries,
settings: settings,
).notifier,
)
.update();
},
),
ListTileChapterFilter(
label: l10n.tracked,
type: ref.watch(
mangaFilterTrackingStateProvider(
itemType: itemType,
mangaList: entries,
settings: settings,
),
),
onTap: () {
ref
.read(
mangaFilterTrackingStateProvider(
itemType: itemType,
mangaList: entries,
settings: settings,
).notifier,
)
.update();
},
),
], ],
); );
} }

View file

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mangayomi/main.dart';
import 'package:mangayomi/modules/widgets/custom_extended_image_provider.dart'; import 'package:mangayomi/modules/widgets/custom_extended_image_provider.dart';
import 'package:mangayomi/modules/widgets/progress_center.dart'; import 'package:mangayomi/modules/widgets/progress_center.dart';
import 'package:mangayomi/utils/constant.dart'; import 'package:mangayomi/utils/constant.dart';
@ -30,110 +31,194 @@ class ChapterListTileWidget extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final l10n = l10nLocalizations(context)!; final l10n = l10nLocalizations(context)!;
return Container( final isLongPressed = ref.watch(isLongPressedStateProvider);
color: chapterList.contains(chapter) return Dismissible(
? context.primaryColor.withValues(alpha: 0.4) key: ValueKey('chapter_swipe_${chapter.id}'),
: null, direction: isLongPressed
child: GestureDetector( ? DismissDirection.none
onLongPress: () => _handleInteraction(ref), : DismissDirection.horizontal,
onSecondaryTap: () => _handleInteraction(ref), confirmDismiss: (direction) async {
child: ListTile( if (direction == DismissDirection.startToEnd) {
contentPadding: EdgeInsets.symmetric(horizontal: 15), // Swipe right toggle bookmark
minLeadingWidth: 0, final chap = chapter;
horizontalTitleGap: 13, isar.writeTxnSync(() {
leading: Container( chap.isBookmarked = !chap.isBookmarked!;
width: 2, chap.updatedAt = DateTime.now().millisecondsSinceEpoch;
height: 40, isar.chapters.putSync(chap);
decoration: BoxDecoration( });
color: chapter.isRead! } else if (direction == DismissDirection.endToStart) {
? Colors.grey.withValues(alpha: 0.3) // Swipe left toggle read
: context.primaryColor, final chap = chapter;
borderRadius: BorderRadius.circular(10), isar.writeTxnSync(() {
chap.isRead = !chap.isRead!;
if (!chap.isRead!) {
chap.lastPageRead = "1";
}
chap.updatedAt = DateTime.now().millisecondsSinceEpoch;
isar.chapters.putSync(chap);
});
}
return false; // Don't dismiss, snap back
},
background: Container(
color: context.primaryColor,
alignment: Alignment.centerLeft,
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Icon(
chapter.isBookmarked! ? Icons.bookmark_remove : Icons.bookmark_add,
color: Colors.white,
),
),
secondaryBackground: Container(
color: chapter.isRead! ? Colors.grey : Colors.green,
alignment: Alignment.centerRight,
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Icon(
chapter.isRead! ? Icons.visibility_off : Icons.done_all,
color: Colors.white,
),
),
child: Container(
color: chapterList.contains(chapter)
? context.primaryColor.withValues(alpha: 0.4)
: null,
child: GestureDetector(
onLongPress: () => _handleInteraction(ref),
onSecondaryTap: () => _handleInteraction(ref),
child: ListTile(
contentPadding: EdgeInsets.symmetric(horizontal: 15),
minLeadingWidth: 0,
horizontalTitleGap: 13,
leading: Container(
width: 2,
height: 40,
decoration: BoxDecoration(
color: chapter.isRead!
? Colors.grey.withValues(alpha: 0.3)
: context.primaryColor,
borderRadius: BorderRadius.circular(10),
),
), ),
), tileColor: (chapter.isFiller ?? false)
tileColor: (chapter.isFiller ?? false) ? context.primaryColor.withValues(alpha: 0.15)
? context.primaryColor.withValues(alpha: 0.15) : null,
: null, textColor: chapter.isRead!
textColor: chapter.isRead! ? context.isLight
? context.isLight ? Colors.black.withValues(alpha: 0.4)
? Colors.black.withValues(alpha: 0.4) : Colors.white.withValues(alpha: 0.3)
: Colors.white.withValues(alpha: 0.3) : null,
: null, selectedColor: chapter.isRead!
selectedColor: chapter.isRead! ? Colors.white.withValues(alpha: 0.3)
? Colors.white.withValues(alpha: 0.3) : Colors.white,
: Colors.white, onTap: () async => _handleInteraction(ref, context),
onTap: () async => _handleInteraction(ref, context), title: Row(
title: Row( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ if (chapter.thumbnailUrl != null)
if (chapter.thumbnailUrl != null) _thumbnailPreview(context, chapter.thumbnailUrl),
_thumbnailPreview(context, chapter.thumbnailUrl), chapter.isBookmarked!
chapter.isBookmarked! ? Icon(
? Icon(Icons.bookmark, size: 16, color: context.primaryColor) Icons.bookmark,
: SizedBox.shrink(), size: 16,
chapter.description != null
? Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTitle(chapter.name!, context),
Text(
chapter.description!,
style: const TextStyle(fontSize: 11),
overflow: TextOverflow.ellipsis,
),
],
),
)
: Flexible(child: _buildTitle(chapter.name!, context)),
],
),
subtitle: Row(
children: [
if (chapter.isFiller ?? false)
Row(
children: [
Icon(Icons.label, size: 16, color: context.primaryColor),
Text(
" Filler ",
style: TextStyle(
fontSize: 11,
color: context.primaryColor, color: context.primaryColor,
), )
), : SizedBox.shrink(),
], chapter.description != null
), ? Flexible(
if ((chapter.manga.value!.isLocalArchive ?? false) == false) child: Column(
Text( crossAxisAlignment: CrossAxisAlignment.start,
chapter.dateUpload == null || chapter.dateUpload!.isEmpty children: [
? "" _buildTitle(chapter.name!, context),
: dateFormat( Text(
chapter.dateUpload!, chapter.description!,
ref: ref, style: const TextStyle(fontSize: 11),
context: context, overflow: TextOverflow.ellipsis,
),
],
), ),
style: const TextStyle(fontSize: 11), )
), : Flexible(child: _buildTitle(chapter.name!, context)),
if (!chapter.isRead!) ],
if (chapter.lastPageRead!.isNotEmpty && ),
chapter.lastPageRead != "1") subtitle: Row(
children: [
if (chapter.isFiller ?? false)
Row(
children: [
Icon(Icons.label, size: 16, color: context.primaryColor),
Text(
" Filler ",
style: TextStyle(
fontSize: 11,
color: context.primaryColor,
),
),
],
),
if ((chapter.manga.value!.isLocalArchive ?? false) == false)
Text(
chapter.dateUpload == null || chapter.dateUpload!.isEmpty
? ""
: dateFormat(
chapter.dateUpload!,
ref: ref,
context: context,
),
style: const TextStyle(fontSize: 11),
),
if (!chapter.isRead!)
if (chapter.lastPageRead!.isNotEmpty &&
chapter.lastPageRead != "1")
Row(
children: [
const Text(''),
Text(
chapter.manga.value!.itemType == ItemType.anime
? l10n.episode_progress(
Duration(
milliseconds: int.parse(
chapter.lastPageRead!,
),
).toString().substringBefore("."),
)
: l10n.page(
chapter.manga.value!.itemType ==
ItemType.manga
? chapter.lastPageRead!
: "${((double.tryParse(chapter.lastPageRead!) ?? 0) * 100).toStringAsFixed(0)} %",
),
style: TextStyle(
fontSize: 11,
color: context.isLight
? Colors.black.withValues(alpha: 0.4)
: Colors.white.withValues(alpha: 0.3),
),
),
],
),
if (chapter.scanlator?.isNotEmpty ?? false)
Row( Row(
children: [ children: [
const Text(''), const Text(''),
Text( Text(
chapter.manga.value!.itemType == ItemType.anime chapter.scanlator!,
? l10n.episode_progress( style: TextStyle(
Duration( fontSize: 11,
milliseconds: int.parse( color: chapter.isRead!
chapter.lastPageRead!, ? context.isLight
), ? Colors.black.withValues(alpha: 0.4)
).toString().substringBefore("."), : Colors.white.withValues(alpha: 0.3)
) : null,
: l10n.page( ),
chapter.manga.value!.itemType == ItemType.manga ),
? chapter.lastPageRead! ],
: "${((double.tryParse(chapter.lastPageRead!) ?? 0) * 100).toStringAsFixed(0)} %", ),
), if (chapter.downloadSize != null)
Row(
children: [
const Text(''),
Text(
chapter.downloadSize!,
style: TextStyle( style: TextStyle(
fontSize: 11, fontSize: 11,
color: context.isLight color: context.isLight
@ -143,44 +228,13 @@ class ChapterListTileWidget extends ConsumerWidget {
), ),
], ],
), ),
if (chapter.scanlator?.isNotEmpty ?? false) ],
Row( ),
children: [ trailing:
const Text(''), !sourceExist || (chapter.manga.value!.isLocalArchive ?? false)
Text( ? null
chapter.scanlator!, : ChapterPageDownload(chapter: chapter),
style: TextStyle(
fontSize: 11,
color: chapter.isRead!
? context.isLight
? Colors.black.withValues(alpha: 0.4)
: Colors.white.withValues(alpha: 0.3)
: null,
),
),
],
),
if (chapter.downloadSize != null)
Row(
children: [
const Text(''),
Text(
chapter.downloadSize!,
style: TextStyle(
fontSize: 11,
color: context.isLight
? Colors.black.withValues(alpha: 0.4)
: Colors.white.withValues(alpha: 0.3),
),
),
],
),
],
), ),
trailing:
!sourceExist || (chapter.manga.value!.isLocalArchive ?? false)
? null
: ChapterPageDownload(chapter: chapter),
), ),
), ),
); );

View file

@ -1,3 +1,4 @@
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:archive/archive_io.dart'; import 'package:archive/archive_io.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@ -5,19 +6,96 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
part 'convert_to_cbz.g.dart'; part 'convert_to_cbz.g.dart';
/// Metadata for ComicInfo.xml generation (serializable for isolate).
class ComicInfoData {
final String? title;
final String? series;
final String? number;
final String? writer;
final String? penciller;
final String? summary;
final String? genre;
final String? translator;
final String? publishingStatusStr;
final int pageCount;
const ComicInfoData({
this.title,
this.series,
this.number,
this.writer,
this.penciller,
this.summary,
this.genre,
this.translator,
this.publishingStatusStr,
this.pageCount = 0,
});
}
@riverpod @riverpod
Future<List<String>> convertToCBZ( Future<List<String>> convertToCBZ(
Ref ref, Ref ref,
String chapterDir, String chapterDir,
String mangaDir, String mangaDir,
String chapterName, String chapterName,
List<String> pageList, List<String> pageList, {
) async { ComicInfoData? comicInfo,
return compute(_convertToCBZ, (chapterDir, mangaDir, chapterName, pageList)); }) async {
return compute(_convertToCBZ, (
chapterDir,
mangaDir,
chapterName,
pageList,
comicInfo,
));
} }
List<String> _convertToCBZ((String, String, String, List<String>) datas) { String _buildComicInfoXml(ComicInfoData info, int pageCount) {
final (chapterDir, mangaDir, chapterName, pageList) = datas; final sb = StringBuffer();
sb.writeln('<?xml version="1.0" encoding="utf-8"?>');
sb.writeln(
'<ComicInfo xmlns:xsd="http://www.w3.org/2001/XMLSchema" '
'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">',
);
void addTag(String tag, String? value) {
if (value != null && value.isNotEmpty) {
final escaped = _xmlEscape(value);
sb.writeln(' <$tag>$escaped</$tag>');
}
}
addTag('Title', info.title);
addTag('Series', info.series);
addTag('Number', info.number);
addTag('Writer', info.writer);
addTag('Penciller', info.penciller);
addTag('Summary', info.summary);
addTag('Genre', info.genre);
addTag('Translator', info.translator);
if (pageCount > 0) {
sb.writeln(' <PageCount>$pageCount</PageCount>');
}
addTag('PublishingStatusTachiyomi', info.publishingStatusStr);
sb.writeln('</ComicInfo>');
return sb.toString();
}
String _xmlEscape(String value) {
return value
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&apos;');
}
List<String> _convertToCBZ(
(String, String, String, List<String>, ComicInfoData?) datas,
) {
final (chapterDir, mangaDir, chapterName, pageList, comicInfo) = datas;
final imagesPaths = pageList.where((path) => path.endsWith('.jpg')).toList() final imagesPaths = pageList.where((path) => path.endsWith('.jpg')).toList()
..sort(); ..sort();
@ -39,6 +117,13 @@ List<String> _convertToCBZ((String, String, String, List<String>) datas) {
archive.add(ArchiveFile.bytes(fileName, bytes)); archive.add(ArchiveFile.bytes(fileName, bytes));
includedFiles.add(imagePath); includedFiles.add(imagePath);
} }
// Add ComicInfo.xml if metadata is provided
if (comicInfo != null) {
final xml = _buildComicInfoXml(comicInfo, includedFiles.length);
archive.add(ArchiveFile.bytes('ComicInfo.xml', utf8.encode(xml)));
}
try { try {
final cbzData = ZipEncoder().encode(archive); final cbzData = ZipEncoder().encode(archive);
File(cbzPath).writeAsBytesSync(cbzData); File(cbzPath).writeAsBytesSync(cbzData);

View file

@ -22,7 +22,8 @@ final class ConvertToCBZProvider
with $FutureModifier<List<String>>, $FutureProvider<List<String>> { with $FutureModifier<List<String>>, $FutureProvider<List<String>> {
ConvertToCBZProvider._({ ConvertToCBZProvider._({
required ConvertToCBZFamily super.from, required ConvertToCBZFamily super.from,
required (String, String, String, List<String>) super.argument, required (String, String, String, List<String>, {ComicInfoData? comicInfo})
super.argument,
}) : super( }) : super(
retry: null, retry: null,
name: r'convertToCBZProvider', name: r'convertToCBZProvider',
@ -49,13 +50,22 @@ final class ConvertToCBZProvider
@override @override
FutureOr<List<String>> create(Ref ref) { FutureOr<List<String>> create(Ref ref) {
final argument = this.argument as (String, String, String, List<String>); final argument =
this.argument
as (
String,
String,
String,
List<String>, {
ComicInfoData? comicInfo,
});
return convertToCBZ( return convertToCBZ(
ref, ref,
argument.$1, argument.$1,
argument.$2, argument.$2,
argument.$3, argument.$3,
argument.$4, argument.$4,
comicInfo: argument.comicInfo,
); );
} }
@ -70,13 +80,13 @@ final class ConvertToCBZProvider
} }
} }
String _$convertToCBZHash() => r'56f4320034ec2420c8c2c2b22a2522721181ab54'; String _$convertToCBZHash() => r'0f75969b8eccb5932089e5e269a5bba4012842b8';
final class ConvertToCBZFamily extends $Family final class ConvertToCBZFamily extends $Family
with with
$FunctionalFamilyOverride< $FunctionalFamilyOverride<
FutureOr<List<String>>, FutureOr<List<String>>,
(String, String, String, List<String>) (String, String, String, List<String>, {ComicInfoData? comicInfo})
> { > {
ConvertToCBZFamily._() ConvertToCBZFamily._()
: super( : super(
@ -91,9 +101,16 @@ final class ConvertToCBZFamily extends $Family
String chapterDir, String chapterDir,
String mangaDir, String mangaDir,
String chapterName, String chapterName,
List<String> pageList, List<String> pageList, {
) => ConvertToCBZProvider._( ComicInfoData? comicInfo,
argument: (chapterDir, mangaDir, chapterName, pageList), }) => ConvertToCBZProvider._(
argument: (
chapterDir,
mangaDir,
chapterName,
pageList,
comicInfo: comicInfo,
),
from: this, from: this,
); );

View file

@ -25,6 +25,7 @@ import 'package:mangayomi/services/get_chapter_pages.dart';
import 'package:mangayomi/services/http/m_client.dart'; import 'package:mangayomi/services/http/m_client.dart';
import 'package:mangayomi/services/download_manager/m3u8/m3u8_downloader.dart'; import 'package:mangayomi/services/download_manager/m3u8/m3u8_downloader.dart';
import 'package:mangayomi/services/download_manager/m3u8/models/download.dart'; import 'package:mangayomi/services/download_manager/m3u8/models/download.dart';
import 'package:mangayomi/utils/chapter_recognition.dart';
import 'package:mangayomi/utils/extensions/chapter.dart'; import 'package:mangayomi/utils/extensions/chapter.dart';
import 'package:mangayomi/utils/extensions/string_extensions.dart'; import 'package:mangayomi/utils/extensions/string_extensions.dart';
import 'package:mangayomi/utils/headers.dart'; import 'package:mangayomi/utils/headers.dart';
@ -106,12 +107,31 @@ Future<void> downloadChapter(
Future<void> processConvert() async { Future<void> processConvert() async {
if (!ref.read(saveAsCBZArchiveStateProvider)) return; if (!ref.read(saveAsCBZArchiveStateProvider)) return;
try { try {
// Extract chapter number from name (e.g., "Chapter 5" "5")
final chapterNumber = ChapterRecognition().parseChapterNumber(
chapter.manga.value!.name!,
chapter.name!,
);
final comicInfo = ComicInfoData(
title: chapter.name,
series: manga.name,
number: chapterNumber.toString(),
writer: manga.author,
penciller: manga.artist,
summary: manga.description,
genre: manga.genre?.join(', '),
translator: chapter.scanlator,
publishingStatusStr: manga.status.name,
);
await ref.read( await ref.read(
convertToCBZProvider( convertToCBZProvider(
chapterDirectory.path, chapterDirectory.path,
mangaMainDirectory!.path, mangaMainDirectory!.path,
chapter.name!, chapter.name!,
pages.map((e) => e.fileName!).toList(), pages.map((e) => e.fileName!).toList(),
comicInfo: comicInfo,
).future, ).future,
); );
} catch (error) { } catch (error) {

View file

@ -136,7 +136,7 @@ final class DownloadChapterProvider
} }
} }
String _$downloadChapterHash() => r'db235f856cf106c89d6124c361a51f2e312e9aa3'; String _$downloadChapterHash() => r'34ecaeac678ca578ce785b8e43d089e95cba89d0';
final class DownloadChapterFamily extends $Family final class DownloadChapterFamily extends $Family
with with

View file

@ -0,0 +1,86 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:local_auth/local_auth.dart';
import 'package:mangayomi/modules/more/settings/security/providers/security_state_provider.dart';
import 'package:mangayomi/providers/l10n_providers.dart';
class AppLockScreen extends ConsumerStatefulWidget {
const AppLockScreen({super.key});
@override
ConsumerState<AppLockScreen> createState() => _AppLockScreenState();
}
class _AppLockScreenState extends ConsumerState<AppLockScreen> {
final LocalAuthentication _localAuth = LocalAuthentication();
bool _isAuthenticating = false;
late final l10n = context.l10n;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => _authenticate());
}
Future<void> _authenticate() async {
if (_isAuthenticating) return;
setState(() => _isAuthenticating = true);
try {
final didAuthenticate = await _localAuth.authenticate(
localizedReason: l10n.auth_unlock_msg,
biometricOnly: false,
persistAcrossBackgrounding: true,
);
if (didAuthenticate && mounted) {
ref.read(appUnlockedStateProvider.notifier).unlock();
}
} catch (_) {
} finally {
if (mounted) {
setState(() => _isAuthenticating = false);
}
}
}
@override
Widget build(BuildContext context) {
return PopScope(
canPop: false,
child: Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.lock_outline,
size: 80,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(height: 24),
Text(
l10n.app_locked,
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 8),
Text(
l10n.auth_unlock_msg,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).hintColor,
),
),
const SizedBox(height: 32),
FilledButton.icon(
onPressed: _isAuthenticating ? null : _authenticate,
icon: const Icon(Icons.fingerprint),
label: Text(
_isAuthenticating ? l10n.authenticating : l10n.unlock,
),
),
],
),
),
),
);
}
}

View file

@ -0,0 +1,40 @@
import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'security_state_provider.g.dart';
@riverpod
class AppLockEnabledState extends _$AppLockEnabledState {
@override
bool build() {
return isar.settings.getSync(227)!.appLockEnabled ?? false;
}
void set(bool value) {
final settings = isar.settings.getSync(227)!;
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(
settings
..appLockEnabled = value
..updatedAt = DateTime.now().millisecondsSinceEpoch,
),
);
}
}
/// Tracks whether the app is currently unlocked.
/// Resets to false when app goes to background (if lock is enabled).
@riverpod
class AppUnlockedState extends _$AppUnlockedState {
@override
bool build() {
// If app lock is not enabled, always unlocked
final lockEnabled = isar.settings.getSync(227)!.appLockEnabled ?? false;
return !lockEnabled;
}
void unlock() => state = true;
void lock() => state = false;
}

View file

@ -0,0 +1,125 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'security_state_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(AppLockEnabledState)
final appLockEnabledStateProvider = AppLockEnabledStateProvider._();
final class AppLockEnabledStateProvider
extends $NotifierProvider<AppLockEnabledState, bool> {
AppLockEnabledStateProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'appLockEnabledStateProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$appLockEnabledStateHash();
@$internal
@override
AppLockEnabledState create() => AppLockEnabledState();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(bool value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<bool>(value),
);
}
}
String _$appLockEnabledStateHash() =>
r'cdd466aee9037e776f5adf992e11ccedb8c58e74';
abstract class _$AppLockEnabledState extends $Notifier<bool> {
bool build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<bool, bool>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<bool, bool>,
bool,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
/// Tracks whether the app is currently unlocked.
/// Resets to false when app goes to background (if lock is enabled).
@ProviderFor(AppUnlockedState)
final appUnlockedStateProvider = AppUnlockedStateProvider._();
/// Tracks whether the app is currently unlocked.
/// Resets to false when app goes to background (if lock is enabled).
final class AppUnlockedStateProvider
extends $NotifierProvider<AppUnlockedState, bool> {
/// Tracks whether the app is currently unlocked.
/// Resets to false when app goes to background (if lock is enabled).
AppUnlockedStateProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'appUnlockedStateProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$appUnlockedStateHash();
@$internal
@override
AppUnlockedState create() => AppUnlockedState();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(bool value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<bool>(value),
);
}
}
String _$appUnlockedStateHash() => r'e5dd0982d0fc0b51cb3db8e6be04d11490d46b9b';
/// Tracks whether the app is currently unlocked.
/// Resets to false when app goes to background (if lock is enabled).
abstract class _$AppUnlockedState extends $Notifier<bool> {
bool build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<bool, bool>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<bool, bool>,
bool,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}

View file

@ -0,0 +1,113 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:local_auth/local_auth.dart';
import 'package:mangayomi/modules/more/settings/security/providers/security_state_provider.dart';
import 'package:mangayomi/providers/l10n_providers.dart';
class SecurityScreen extends ConsumerStatefulWidget {
const SecurityScreen({super.key});
@override
ConsumerState<SecurityScreen> createState() => _SecurityScreenState();
}
class _SecurityScreenState extends ConsumerState<SecurityScreen> {
final LocalAuthentication _localAuth = LocalAuthentication();
bool _canCheckBiometrics = false;
late final l10n = context.l10n;
@override
void initState() {
super.initState();
_checkBiometricAvailability();
}
Future<void> _checkBiometricAvailability() async {
try {
final canAuth = await _localAuth.canCheckBiometrics;
final isDeviceSupported = await _localAuth.isDeviceSupported();
if (mounted) {
setState(() {
_canCheckBiometrics = canAuth || isDeviceSupported;
});
}
} catch (_) {
if (mounted) {
setState(() => _canCheckBiometrics = false);
}
}
}
Future<bool> _authenticate() async {
try {
return await _localAuth.authenticate(
localizedReason: l10n.auth_to_change_security_setting,
biometricOnly: false,
persistAcrossBackgrounding: true,
);
} catch (_) {
return false;
}
}
@override
Widget build(BuildContext context) {
final appLockEnabled = ref.watch(appLockEnabledStateProvider);
return Scaffold(
appBar: AppBar(title: Text(l10n.security)),
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SwitchListTile(
value: appLockEnabled,
title: Text(l10n.app_lock),
subtitle: Text(
_canCheckBiometrics
? l10n.require_biometric_or_device_credential
: l10n.biometric_or_device_credential_not_available,
style: TextStyle(
fontSize: 12,
color: Theme.of(context).hintColor,
),
),
onChanged: _canCheckBiometrics
? (value) async {
if (value) {
final authenticated = await _authenticate();
if (authenticated) {
ref
.read(appLockEnabledStateProvider.notifier)
.set(true);
}
} else {
final authenticated = await _authenticate();
if (authenticated) {
ref
.read(appLockEnabledStateProvider.notifier)
.set(false);
ref.read(appUnlockedStateProvider.notifier).unlock();
}
}
}
: null,
),
const Divider(),
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
l10n.app_lock_description,
style: TextStyle(
fontSize: 12,
color: Theme.of(context).hintColor,
),
),
),
],
),
),
);
}
}

View file

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:mangayomi/modules/more/widgets/list_tile_widget.dart'; import 'package:mangayomi/modules/more/widgets/list_tile_widget.dart';
@ -54,6 +56,12 @@ class SettingsScreen extends StatelessWidget {
icon: Icons.explore_rounded, icon: Icons.explore_rounded,
onTap: () => context.push('/browseS'), onTap: () => context.push('/browseS'),
), ),
if (!Platform.isLinux)
ListTileWidget(
title: l10n.security,
icon: Icons.security_rounded,
onTap: () => context.push('/security'),
),
ListTileWidget( ListTileWidget(
title: l10n.about, title: l10n.about,
icon: Icons.info_outline, icon: Icons.info_outline,

View file

@ -1,6 +1,7 @@
import 'dart:io'; import 'dart:io';
import 'package:bot_toast/bot_toast.dart'; import 'package:bot_toast/bot_toast.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:mangayomi/models/manga.dart'; import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/models/settings.dart'; import 'package:mangayomi/models/settings.dart';
@ -56,6 +57,7 @@ import 'package:mangayomi/modules/more/settings/browse/browse_screen.dart';
import 'package:mangayomi/modules/more/settings/general/general_screen.dart'; import 'package:mangayomi/modules/more/settings/general/general_screen.dart';
import 'package:mangayomi/modules/more/settings/reader/reader_screen.dart'; import 'package:mangayomi/modules/more/settings/reader/reader_screen.dart';
import 'package:mangayomi/modules/more/settings/settings_screen.dart'; import 'package:mangayomi/modules/more/settings/settings_screen.dart';
import 'package:mangayomi/modules/more/settings/security/security_screen.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
part 'router.g.dart'; part 'router.g.dart';
@ -86,6 +88,7 @@ class RouterCurrentLocationState extends _$RouterCurrentLocationState {
bool _didSubscribe = false; bool _didSubscribe = false;
@override @override
String? build() { String? build() {
ref.keepAlive();
// Delay listenerregistration until after the first frame. // Delay listenerregistration until after the first frame.
if (!_didSubscribe) { if (!_didSubscribe) {
_didSubscribe = true; _didSubscribe = true;
@ -207,6 +210,7 @@ class RouterNotifier extends ChangeNotifier {
), ),
_genericRoute(name: "downloads", child: const DownloadsScreen()), _genericRoute(name: "downloads", child: const DownloadsScreen()),
_genericRoute(name: "dataAndStorage", child: const DataAndStorage()), _genericRoute(name: "dataAndStorage", child: const DataAndStorage()),
_genericRoute(name: "security", child: const SecurityScreen()),
_genericRoute(name: "manageTrackers", child: const ManageTrackersScreen()), _genericRoute(name: "manageTrackers", child: const ManageTrackersScreen()),
_genericRoute<TrackPreference>( _genericRoute<TrackPreference>(
name: "trackingDetail", name: "trackingDetail",
@ -280,36 +284,24 @@ class RouterNotifier extends ChangeNotifier {
return child!; return child!;
} }
}, },
pageBuilder: (context, state) { pageBuilder: (Platform.isIOS || Platform.isMacOS)
final pageChild = builder != null ? builder(state.extra as T) : child!; ? (context, state) {
return transitionPage(key: state.pageKey, child: pageChild); final pageChild = builder != null
}, ? builder(state.extra as T)
: child!;
return transitionPage(key: state.pageKey, child: pageChild);
}
: null,
); );
} }
} }
Page transitionPage({required LocalKey key, required child}) { Page transitionPage({required LocalKey key, required child}) {
return Platform.isIOS return CupertinoPage(key: key, child: child);
? CupertinoPage(key: key, child: child)
: CustomTransition(child: child, key: key);
}
class CustomTransition extends CustomTransitionPage {
CustomTransition({required LocalKey super.key, required super.child})
: super(
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(opacity: animation, child: child);
},
);
} }
Route createRoute({required Widget page}) { Route createRoute({required Widget page}) {
return Platform.isIOS return (Platform.isIOS || Platform.isMacOS)
? CupertinoPageRoute(builder: (context) => page) ? CupertinoPageRoute(builder: (context) => page)
: PageRouteBuilder( : MaterialPageRoute(builder: (context) => page);
pageBuilder: (context, animation, secondaryAnimation) => page,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(opacity: animation, child: child);
},
);
} }

View file

@ -13,6 +13,7 @@ import flutter_inappwebview_macos
import flutter_qjs import flutter_qjs
import flutter_web_auth_2 import flutter_web_auth_2
import isar_community_flutter_libs import isar_community_flutter_libs
import local_auth_darwin
import m_extension_server import m_extension_server
import media_kit_libs_macos_video import media_kit_libs_macos_video
import media_kit_video import media_kit_video
@ -35,6 +36,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterQjsPlugin.register(with: registry.registrar(forPlugin: "FlutterQjsPlugin")) FlutterQjsPlugin.register(with: registry.registrar(forPlugin: "FlutterQjsPlugin"))
FlutterWebAuth2Plugin.register(with: registry.registrar(forPlugin: "FlutterWebAuth2Plugin")) FlutterWebAuth2Plugin.register(with: registry.registrar(forPlugin: "FlutterWebAuth2Plugin"))
IsarFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "IsarFlutterLibsPlugin")) IsarFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "IsarFlutterLibsPlugin"))
LocalAuthPlugin.register(with: registry.registrar(forPlugin: "LocalAuthPlugin"))
MExtensionServerPlugin.register(with: registry.registrar(forPlugin: "MExtensionServerPlugin")) MExtensionServerPlugin.register(with: registry.registrar(forPlugin: "MExtensionServerPlugin"))
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin")) MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin")) MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin"))

View file

@ -19,6 +19,9 @@ PODS:
- FlutterMacOS (1.0.0) - FlutterMacOS (1.0.0)
- isar_community_flutter_libs (1.0.0): - isar_community_flutter_libs (1.0.0):
- FlutterMacOS - FlutterMacOS
- local_auth_darwin (0.0.1):
- Flutter
- FlutterMacOS
- m_extension_server (0.0.1): - m_extension_server (0.0.1):
- FlutterMacOS - FlutterMacOS
- media_kit_libs_macos_video (1.0.4): - media_kit_libs_macos_video (1.0.4):
@ -58,6 +61,7 @@ DEPENDENCIES:
- flutter_web_auth_2 (from `Flutter/ephemeral/.symlinks/plugins/flutter_web_auth_2/macos`) - flutter_web_auth_2 (from `Flutter/ephemeral/.symlinks/plugins/flutter_web_auth_2/macos`)
- FlutterMacOS (from `Flutter/ephemeral`) - FlutterMacOS (from `Flutter/ephemeral`)
- isar_community_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/isar_community_flutter_libs/macos`) - isar_community_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/isar_community_flutter_libs/macos`)
- local_auth_darwin (from `Flutter/ephemeral/.symlinks/plugins/local_auth_darwin/darwin`)
- m_extension_server (from `Flutter/ephemeral/.symlinks/plugins/m_extension_server/macos`) - m_extension_server (from `Flutter/ephemeral/.symlinks/plugins/m_extension_server/macos`)
- media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`) - media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`)
- media_kit_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos`) - media_kit_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos`)
@ -97,6 +101,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral :path: Flutter/ephemeral
isar_community_flutter_libs: isar_community_flutter_libs:
:path: Flutter/ephemeral/.symlinks/plugins/isar_community_flutter_libs/macos :path: Flutter/ephemeral/.symlinks/plugins/isar_community_flutter_libs/macos
local_auth_darwin:
:path: Flutter/ephemeral/.symlinks/plugins/local_auth_darwin/darwin
m_extension_server: m_extension_server:
:path: Flutter/ephemeral/.symlinks/plugins/m_extension_server/macos :path: Flutter/ephemeral/.symlinks/plugins/m_extension_server/macos
media_kit_libs_macos_video: media_kit_libs_macos_video:
@ -135,6 +141,7 @@ SPEC CHECKSUMS:
flutter_web_auth_2: 62b08da29f15a20fa63f144234622a1488d45b65 flutter_web_auth_2: 62b08da29f15a20fa63f144234622a1488d45b65
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
isar_community_flutter_libs: a631ceb5622413b56bcd0a8bf49cb55bf3d8bb2b isar_community_flutter_libs: a631ceb5622413b56bcd0a8bf49cb55bf3d8bb2b
local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb
m_extension_server: 50e95a61bbf93c9a33ddc812d0753bddf1c01456 m_extension_server: 50e95a61bbf93c9a33ddc812d0753bddf1c01456
media_kit_libs_macos_video: 85a23e549b5f480e72cae3e5634b5514bc692f65 media_kit_libs_macos_video: 85a23e549b5f480e72cae3e5634b5514bc692f65
media_kit_video: fa6564e3799a0a28bff39442334817088b7ca758 media_kit_video: fa6564e3799a0a28bff39442334817088b7ca758

View file

@ -946,6 +946,46 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.2" version: "1.0.2"
local_auth:
dependency: "direct main"
description:
name: local_auth
sha256: ae6f382f638108c6becd134318d7c3f0a93875383a54010f61d7c97ac05d5137
url: "https://pub.dev"
source: hosted
version: "3.0.1"
local_auth_android:
dependency: transitive
description:
name: local_auth_android
sha256: dc9663a7bc8ac33d7d988e63901974f63d527ebef260eabd19c479447cc9c911
url: "https://pub.dev"
source: hosted
version: "2.0.5"
local_auth_darwin:
dependency: transitive
description:
name: local_auth_darwin
sha256: a8c3d4e17454111f7fd31ff72a31222359f6059f7fe956c2dcfe0f88f49826d4
url: "https://pub.dev"
source: hosted
version: "2.0.3"
local_auth_platform_interface:
dependency: transitive
description:
name: local_auth_platform_interface
sha256: f98b8e388588583d3f781f6806e4f4c9f9e189d898d27f0c249b93a1973dd122
url: "https://pub.dev"
source: hosted
version: "1.1.0"
local_auth_windows:
dependency: transitive
description:
name: local_auth_windows
sha256: be12c5b8ba5e64896983123655c5f67d2484ecfcc95e367952ad6e3bff94cb16
url: "https://pub.dev"
source: hosted
version: "2.0.1"
logging: logging:
dependency: transitive dependency: transitive
description: description:

View file

@ -95,6 +95,7 @@ dependencies:
url: https://github.com/Schnitzel5/flutter-discord-rpc.git url: https://github.com/Schnitzel5/flutter-discord-rpc.git
ref: main ref: main
table_calendar: ^3.2.0 table_calendar: ^3.2.0
local_auth: ^3.0.1
m_extension_server: m_extension_server:
git: git:
url: https://github.com/kodjodevf/m_extension_server.git url: https://github.com/kodjodevf/m_extension_server.git

View file

@ -11,6 +11,7 @@
#include <flutter_inappwebview_windows/flutter_inappwebview_windows_plugin_c_api.h> #include <flutter_inappwebview_windows/flutter_inappwebview_windows_plugin_c_api.h>
#include <flutter_qjs/flutter_qjs_plugin.h> #include <flutter_qjs/flutter_qjs_plugin.h>
#include <isar_community_flutter_libs/isar_flutter_libs_plugin.h> #include <isar_community_flutter_libs/isar_flutter_libs_plugin.h>
#include <local_auth_windows/local_auth_plugin.h>
#include <m_extension_server/m_extension_server_plugin_c_api.h> #include <m_extension_server/m_extension_server_plugin_c_api.h>
#include <media_kit_libs_windows_video/media_kit_libs_windows_video_plugin_c_api.h> #include <media_kit_libs_windows_video/media_kit_libs_windows_video_plugin_c_api.h>
#include <media_kit_video/media_kit_video_plugin_c_api.h> #include <media_kit_video/media_kit_video_plugin_c_api.h>
@ -34,6 +35,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("FlutterQjsPlugin")); registry->GetRegistrarForPlugin("FlutterQjsPlugin"));
IsarFlutterLibsPluginRegisterWithRegistrar( IsarFlutterLibsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("IsarFlutterLibsPlugin")); registry->GetRegistrarForPlugin("IsarFlutterLibsPlugin"));
LocalAuthPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("LocalAuthPlugin"));
MExtensionServerPluginCApiRegisterWithRegistrar( MExtensionServerPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("MExtensionServerPluginCApi")); registry->GetRegistrarForPlugin("MExtensionServerPluginCApi"));
MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar( MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar(

View file

@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
flutter_inappwebview_windows flutter_inappwebview_windows
flutter_qjs flutter_qjs
isar_community_flutter_libs isar_community_flutter_libs
local_auth_windows
m_extension_server m_extension_server
media_kit_libs_windows_video media_kit_libs_windows_video
media_kit_video media_kit_video