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,6 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.kodjodevf.mangayomi">
<uses-permission android:name="android.permission.INTERNET" />
<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.MANAGE_EXTERNAL_STORAGE"/>

View file

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

View file

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

View file

@ -565,5 +565,17 @@
"forceLandscapeMode": "Force landscape mode",
"forceLandscapeModeSubtitle": "Force the player to use landscape orientation.",
"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:
/// **'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

View file

@ -1794,4 +1794,44 @@ class AppLocalizationsAr extends AppLocalizations {
@override
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
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
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
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
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`).

View file

@ -1823,4 +1823,44 @@ class AppLocalizationsFr extends AppLocalizations {
@override
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
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
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
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
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
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`).

View file

@ -1823,4 +1823,44 @@ class AppLocalizationsRu extends AppLocalizations {
@override
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
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
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
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/modules/more/settings/appearance/providers/theme_provider.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:path_provider/path_provider.dart';
import 'package:window_manager/window_manager.dart';
@ -135,7 +137,7 @@ class MyApp extends ConsumerStatefulWidget {
ConsumerState<MyApp> createState() => _MyAppState();
}
class _MyAppState extends ConsumerState<MyApp> {
class _MyAppState extends ConsumerState<MyApp> with WidgetsBindingObserver {
late AppLinks _appLinks;
StreamSubscription<Uri>? _linkSubscription;
Uri? lastUri;
@ -143,6 +145,7 @@ class _MyAppState extends ConsumerState<MyApp> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
initializeDateFormatting();
customDns = ref.read(customDnsStateProvider);
_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
Widget build(BuildContext context) {
final followSystem = ref.watch(followSystemThemeStateProvider);
@ -180,7 +199,17 @@ class _MyAppState extends ConsumerState<MyApp> {
locale: locale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
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,
routerDelegate: router.routerDelegate,
routeInformationProvider: router.routeInformationProvider,
@ -191,6 +220,7 @@ class _MyAppState extends ConsumerState<MyApp> {
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
MExtensionServerPlatform(ref).stopServer();
_linkSubscription?.cancel();
discordRpc?.destroy();

View file

@ -310,6 +310,20 @@ class Settings {
List<String>? localFolders;
bool? appLockEnabled;
int? libraryFilterMangasCompletedType;
int? libraryFilterAnimeCompletedType;
int? libraryFilterNovelCompletedType;
int? libraryFilterMangasTrackingType;
int? libraryFilterAnimeTrackingType;
int? libraryFilterNovelTrackingType;
Settings({
this.id = 227,
this.updatedAt = 0,
@ -450,6 +464,13 @@ class Settings {
this.downloadedOnlyMode = false,
this.algorithmWeights,
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) {
@ -708,6 +729,13 @@ class Settings {
? AlgorithmWeights.fromJson(json['algorithmWeights'])
: null;
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() => {
@ -872,6 +900,13 @@ class Settings {
if (algorithmWeights != null)
'algorithmWeights': algorithmWeights!.toJson(),
'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(
mangaFilterBookmarkedStateProvider.call,
);
final completedFilterType = watchWithSettingsAndManga(
mangaFilterCompletedStateProvider.call,
);
final trackingFilterType = watchWithSettingsAndManga(
mangaFilterTrackingStateProvider.call,
);
final sortType =
watchWithSettings(sortLibraryMangaStateProvider.call).index as int;
@ -174,6 +180,8 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen>
unreadFilterType: unreadFilterType,
startedFilterType: startedFilterType,
bookmarkedFilterType: bookmarkedFilterType,
completedFilterType: completedFilterType,
trackingFilterType: trackingFilterType,
reverse: reverse,
downloadedChapter: downloadedChapter,
continueReaderBtn: continueReaderBtn,
@ -195,6 +203,8 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen>
unreadFilterType: unreadFilterType,
startedFilterType: startedFilterType,
bookmarkedFilterType: bookmarkedFilterType,
completedFilterType: completedFilterType,
trackingFilterType: trackingFilterType,
settings: settings,
downloadedOnly: downloadedOnly,
searchQuery: searchQuery,
@ -217,6 +227,8 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen>
unreadFilterType: unreadFilterType,
startedFilterType: startedFilterType,
bookmarkedFilterType: bookmarkedFilterType,
completedFilterType: completedFilterType,
trackingFilterType: trackingFilterType,
sortType: sortType,
downloadedOnly: downloadedOnly,
searchQuery: searchQuery,

View file

@ -2,6 +2,7 @@ import 'package:isar_community/isar.dart';
import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/download.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/models/track.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'library_filter_provider.g.dart';
@ -17,10 +18,14 @@ Set<int> downloadedChapterIds(Ref ref) {
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.
///
/// Uses [downloadedChapterIds] for O(1) download lookups instead of
/// per-chapter Isar queries (previous behavior was O(chapters × manga)).
@riverpod
List<Manga> filteredLibraryManga(
Ref ref, {
@ -29,12 +34,15 @@ List<Manga> filteredLibraryManga(
required int unreadFilterType,
required int startedFilterType,
required int bookmarkedFilterType,
required int completedFilterType,
required int trackingFilterType,
required int sortType,
required bool downloadedOnly,
required String searchQuery,
required bool ignoreFiltersOnSearch,
}) {
final downloadedIds = ref.watch(downloadedChapterIdsProvider);
final trackedIds = ref.watch(trackedMangaIdsProvider);
return _filterAndSortManga(
data: data,
@ -42,11 +50,14 @@ List<Manga> filteredLibraryManga(
unreadFilterType: unreadFilterType,
startedFilterType: startedFilterType,
bookmarkedFilterType: bookmarkedFilterType,
completedFilterType: completedFilterType,
trackingFilterType: trackingFilterType,
sortType: sortType,
downloadedOnly: downloadedOnly,
searchQuery: searchQuery,
ignoreFiltersOnSearch: ignoreFiltersOnSearch,
downloadedIds: downloadedIds,
trackedIds: trackedIds,
);
}
@ -71,11 +82,14 @@ List<Manga> _filterAndSortManga({
required int unreadFilterType,
required int startedFilterType,
required int bookmarkedFilterType,
required int completedFilterType,
required int trackingFilterType,
required int sortType,
required bool downloadedOnly,
required String searchQuery,
required bool ignoreFiltersOnSearch,
required Set<int> downloadedIds,
required Set<int> trackedIds,
}) {
List<Manga> mangas;
@ -121,6 +135,24 @@ List<Manga> _filterAndSortManga({
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
if (searchQuery.isNotEmpty) {
if (!_matchesSearchQuery(element, searchQuery)) return false;

View file

@ -58,26 +58,63 @@ final class DownloadedChapterIdsProvider
String _$downloadedChapterIdsHash() =>
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.
///
/// Uses [downloadedChapterIds] for O(1) download lookups instead of
/// per-chapter Isar queries (previous behavior was O(chapters × manga)).
@ProviderFor(filteredLibraryManga)
final filteredLibraryMangaProvider = FilteredLibraryMangaFamily._();
/// 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
extends $FunctionalProvider<List<Manga>, List<Manga>, List<Manga>>
with $Provider<List<Manga>> {
/// 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._({
required FilteredLibraryMangaFamily super.from,
required ({
@ -86,6 +123,8 @@ final class FilteredLibraryMangaProvider
int unreadFilterType,
int startedFilterType,
int bookmarkedFilterType,
int completedFilterType,
int trackingFilterType,
int sortType,
bool downloadedOnly,
String searchQuery,
@ -125,6 +164,8 @@ final class FilteredLibraryMangaProvider
int unreadFilterType,
int startedFilterType,
int bookmarkedFilterType,
int completedFilterType,
int trackingFilterType,
int sortType,
bool downloadedOnly,
String searchQuery,
@ -137,6 +178,8 @@ final class FilteredLibraryMangaProvider
unreadFilterType: argument.unreadFilterType,
startedFilterType: argument.startedFilterType,
bookmarkedFilterType: argument.bookmarkedFilterType,
completedFilterType: argument.completedFilterType,
trackingFilterType: argument.trackingFilterType,
sortType: argument.sortType,
downloadedOnly: argument.downloadedOnly,
searchQuery: argument.searchQuery,
@ -164,12 +207,9 @@ final class FilteredLibraryMangaProvider
}
String _$filteredLibraryMangaHash() =>
r'34cd87ea154cc617e85572ede503b81fb36f2a97';
r'afecb3de71f1f8c1682a0bfd9949f8a372c7d1b6';
/// 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
with
@ -181,6 +221,8 @@ final class FilteredLibraryMangaFamily extends $Family
int unreadFilterType,
int startedFilterType,
int bookmarkedFilterType,
int completedFilterType,
int trackingFilterType,
int sortType,
bool downloadedOnly,
String searchQuery,
@ -197,9 +239,6 @@ final class FilteredLibraryMangaFamily extends $Family
);
/// 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({
required List<Manga> data,
@ -207,6 +246,8 @@ final class FilteredLibraryMangaFamily extends $Family
required int unreadFilterType,
required int startedFilterType,
required int bookmarkedFilterType,
required int completedFilterType,
required int trackingFilterType,
required int sortType,
required bool downloadedOnly,
required String searchQuery,
@ -218,6 +259,8 @@ final class FilteredLibraryMangaFamily extends $Family
unreadFilterType: unreadFilterType,
startedFilterType: startedFilterType,
bookmarkedFilterType: bookmarkedFilterType,
completedFilterType: completedFilterType,
trackingFilterType: trackingFilterType,
sortType: sortType,
downloadedOnly: downloadedOnly,
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
class MangasFilterResultState extends _$MangasFilterResultState {
@override
@ -502,10 +614,26 @@ class MangasFilterResultState extends _$MangasFilterResultState {
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 &&
unreadFilterType == 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)
final mangasFilterResultStateProvider = MangasFilterResultStateFamily._();
@ -745,7 +987,7 @@ final class MangasFilterResultStateProvider
}
String _$mangasFilterResultStateHash() =>
r'c6f916c35e9b7125ba073d09aa6838605b933b20';
r'6fbbc29f7e71e5d929f49fdaecd69a665bd034fb';
final class MangasFilterResultStateFamily extends $Family
with

View file

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

View file

@ -137,6 +137,48 @@ class _FilterTab extends ConsumerWidget {
.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_riverpod/flutter_riverpod.dart';
import 'package:mangayomi/main.dart';
import 'package:mangayomi/modules/widgets/custom_extended_image_provider.dart';
import 'package:mangayomi/modules/widgets/progress_center.dart';
import 'package:mangayomi/utils/constant.dart';
@ -30,7 +31,54 @@ class ChapterListTileWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = l10nLocalizations(context)!;
return Container(
final isLongPressed = ref.watch(isLongPressedStateProvider);
return Dismissible(
key: ValueKey('chapter_swipe_${chapter.id}'),
direction: isLongPressed
? DismissDirection.none
: DismissDirection.horizontal,
confirmDismiss: (direction) async {
if (direction == DismissDirection.startToEnd) {
// Swipe right toggle bookmark
final chap = chapter;
isar.writeTxnSync(() {
chap.isBookmarked = !chap.isBookmarked!;
chap.updatedAt = DateTime.now().millisecondsSinceEpoch;
isar.chapters.putSync(chap);
});
} else if (direction == DismissDirection.endToStart) {
// Swipe left toggle read
final chap = chapter;
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,
@ -69,7 +117,11 @@ class ChapterListTileWidget extends ConsumerWidget {
if (chapter.thumbnailUrl != null)
_thumbnailPreview(context, chapter.thumbnailUrl),
chapter.isBookmarked!
? Icon(Icons.bookmark, size: 16, color: context.primaryColor)
? Icon(
Icons.bookmark,
size: 16,
color: context.primaryColor,
)
: SizedBox.shrink(),
chapter.description != null
? Flexible(
@ -130,7 +182,8 @@ class ChapterListTileWidget extends ConsumerWidget {
).toString().substringBefore("."),
)
: l10n.page(
chapter.manga.value!.itemType == ItemType.manga
chapter.manga.value!.itemType ==
ItemType.manga
? chapter.lastPageRead!
: "${((double.tryParse(chapter.lastPageRead!) ?? 0) * 100).toStringAsFixed(0)} %",
),
@ -183,6 +236,7 @@ class ChapterListTileWidget extends ConsumerWidget {
: ChapterPageDownload(chapter: chapter),
),
),
),
);
}

View file

@ -1,3 +1,4 @@
import 'dart:convert';
import 'dart:io';
import 'package:archive/archive_io.dart';
import 'package:flutter/foundation.dart';
@ -5,19 +6,96 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:path/path.dart' as path;
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
Future<List<String>> convertToCBZ(
Ref ref,
String chapterDir,
String mangaDir,
String chapterName,
List<String> pageList,
) async {
return compute(_convertToCBZ, (chapterDir, mangaDir, chapterName, pageList));
List<String> pageList, {
ComicInfoData? comicInfo,
}) async {
return compute(_convertToCBZ, (
chapterDir,
mangaDir,
chapterName,
pageList,
comicInfo,
));
}
List<String> _convertToCBZ((String, String, String, List<String>) datas) {
final (chapterDir, mangaDir, chapterName, pageList) = datas;
String _buildComicInfoXml(ComicInfoData info, int pageCount) {
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()
..sort();
@ -39,6 +117,13 @@ List<String> _convertToCBZ((String, String, String, List<String>) datas) {
archive.add(ArchiveFile.bytes(fileName, bytes));
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 {
final cbzData = ZipEncoder().encode(archive);
File(cbzPath).writeAsBytesSync(cbzData);

View file

@ -22,7 +22,8 @@ final class ConvertToCBZProvider
with $FutureModifier<List<String>>, $FutureProvider<List<String>> {
ConvertToCBZProvider._({
required ConvertToCBZFamily super.from,
required (String, String, String, List<String>) super.argument,
required (String, String, String, List<String>, {ComicInfoData? comicInfo})
super.argument,
}) : super(
retry: null,
name: r'convertToCBZProvider',
@ -49,13 +50,22 @@ final class ConvertToCBZProvider
@override
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(
ref,
argument.$1,
argument.$2,
argument.$3,
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
with
$FunctionalFamilyOverride<
FutureOr<List<String>>,
(String, String, String, List<String>)
(String, String, String, List<String>, {ComicInfoData? comicInfo})
> {
ConvertToCBZFamily._()
: super(
@ -91,9 +101,16 @@ final class ConvertToCBZFamily extends $Family
String chapterDir,
String mangaDir,
String chapterName,
List<String> pageList,
) => ConvertToCBZProvider._(
argument: (chapterDir, mangaDir, chapterName, pageList),
List<String> pageList, {
ComicInfoData? comicInfo,
}) => ConvertToCBZProvider._(
argument: (
chapterDir,
mangaDir,
chapterName,
pageList,
comicInfo: comicInfo,
),
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/download_manager/m3u8/m3u8_downloader.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/string_extensions.dart';
import 'package:mangayomi/utils/headers.dart';
@ -106,12 +107,31 @@ Future<void> downloadChapter(
Future<void> processConvert() async {
if (!ref.read(saveAsCBZArchiveStateProvider)) return;
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(
convertToCBZProvider(
chapterDirectory.path,
mangaMainDirectory!.path,
chapter.name!,
pages.map((e) => e.fileName!).toList(),
comicInfo: comicInfo,
).future,
);
} catch (error) {

View file

@ -136,7 +136,7 @@ final class DownloadChapterProvider
}
}
String _$downloadChapterHash() => r'db235f856cf106c89d6124c361a51f2e312e9aa3';
String _$downloadChapterHash() => r'34ecaeac678ca578ce785b8e43d089e95cba89d0';
final class DownloadChapterFamily extends $Family
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:go_router/go_router.dart';
import 'package:mangayomi/modules/more/widgets/list_tile_widget.dart';
@ -54,6 +56,12 @@ class SettingsScreen extends StatelessWidget {
icon: Icons.explore_rounded,
onTap: () => context.push('/browseS'),
),
if (!Platform.isLinux)
ListTileWidget(
title: l10n.security,
icon: Icons.security_rounded,
onTap: () => context.push('/security'),
),
ListTileWidget(
title: l10n.about,
icon: Icons.info_outline,

View file

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

View file

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

View file

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

View file

@ -946,6 +946,46 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:

View file

@ -95,6 +95,7 @@ dependencies:
url: https://github.com/Schnitzel5/flutter-discord-rpc.git
ref: main
table_calendar: ^3.2.0
local_auth: ^3.0.1
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_qjs/flutter_qjs_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 <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>
@ -34,6 +35,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("FlutterQjsPlugin"));
IsarFlutterLibsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("IsarFlutterLibsPlugin"));
LocalAuthPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("LocalAuthPlugin"));
MExtensionServerPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("MExtensionServerPluginCApi"));
MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar(

View file

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