mirror of
https://github.com/kodjodevf/mangayomi.git
synced 2026-03-11 13:15:36 +00:00
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:
parent
a5fd40fdfb
commit
f729380223
47 changed files with 3473 additions and 739 deletions
|
|
@ -1,7 +1,8 @@
|
|||
<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.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.MANAGE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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`).
|
||||
|
|
|
|||
|
|
@ -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.';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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`).
|
||||
|
|
|
|||
|
|
@ -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.';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,110 +31,194 @@ class ChapterListTileWidget extends ConsumerWidget {
|
|||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = l10nLocalizations(context)!;
|
||||
return 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),
|
||||
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,
|
||||
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)
|
||||
? context.primaryColor.withValues(alpha: 0.15)
|
||||
: null,
|
||||
textColor: chapter.isRead!
|
||||
? context.isLight
|
||||
? Colors.black.withValues(alpha: 0.4)
|
||||
: Colors.white.withValues(alpha: 0.3)
|
||||
: null,
|
||||
selectedColor: chapter.isRead!
|
||||
? Colors.white.withValues(alpha: 0.3)
|
||||
: Colors.white,
|
||||
onTap: () async => _handleInteraction(ref, context),
|
||||
title: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (chapter.thumbnailUrl != null)
|
||||
_thumbnailPreview(context, chapter.thumbnailUrl),
|
||||
chapter.isBookmarked!
|
||||
? Icon(Icons.bookmark, size: 16, color: context.primaryColor)
|
||||
: SizedBox.shrink(),
|
||||
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,
|
||||
tileColor: (chapter.isFiller ?? false)
|
||||
? context.primaryColor.withValues(alpha: 0.15)
|
||||
: null,
|
||||
textColor: chapter.isRead!
|
||||
? context.isLight
|
||||
? Colors.black.withValues(alpha: 0.4)
|
||||
: Colors.white.withValues(alpha: 0.3)
|
||||
: null,
|
||||
selectedColor: chapter.isRead!
|
||||
? Colors.white.withValues(alpha: 0.3)
|
||||
: Colors.white,
|
||||
onTap: () async => _handleInteraction(ref, context),
|
||||
title: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (chapter.thumbnailUrl != null)
|
||||
_thumbnailPreview(context, chapter.thumbnailUrl),
|
||||
chapter.isBookmarked!
|
||||
? Icon(
|
||||
Icons.bookmark,
|
||||
size: 16,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if ((chapter.manga.value!.isLocalArchive ?? false) == false)
|
||||
Text(
|
||||
chapter.dateUpload == null || chapter.dateUpload!.isEmpty
|
||||
? ""
|
||||
: dateFormat(
|
||||
chapter.dateUpload!,
|
||||
ref: ref,
|
||||
context: context,
|
||||
)
|
||||
: SizedBox.shrink(),
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
style: const TextStyle(fontSize: 11),
|
||||
),
|
||||
if (!chapter.isRead!)
|
||||
if (chapter.lastPageRead!.isNotEmpty &&
|
||||
chapter.lastPageRead != "1")
|
||||
)
|
||||
: 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
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(
|
||||
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)} %",
|
||||
),
|
||||
chapter.scanlator!,
|
||||
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
|
||||
|
|
@ -143,44 +228,13 @@ class ChapterListTileWidget extends ConsumerWidget {
|
|||
),
|
||||
],
|
||||
),
|
||||
if (chapter.scanlator?.isNotEmpty ?? false)
|
||||
Row(
|
||||
children: [
|
||||
const Text(' • '),
|
||||
Text(
|
||||
chapter.scanlator!,
|
||||
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),
|
||||
),
|
||||
trailing:
|
||||
!sourceExist || (chapter.manga.value!.isLocalArchive ?? false)
|
||||
? null
|
||||
: ChapterPageDownload(chapter: chapter),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''');
|
||||
}
|
||||
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ final class DownloadChapterProvider
|
|||
}
|
||||
}
|
||||
|
||||
String _$downloadChapterHash() => r'db235f856cf106c89d6124c361a51f2e312e9aa3';
|
||||
String _$downloadChapterHash() => r'34ecaeac678ca578ce785b8e43d089e95cba89d0';
|
||||
|
||||
final class DownloadChapterFamily extends $Family
|
||||
with
|
||||
|
|
|
|||
86
lib/modules/more/settings/security/app_lock_screen.dart
Normal file
86
lib/modules/more/settings/security/app_lock_screen.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
113
lib/modules/more/settings/security/security_screen.dart
Normal file
113
lib/modules/more/settings/security/security_screen.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 listener‐registration 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!;
|
||||
return transitionPage(key: state.pageKey, child: pageChild);
|
||||
},
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
40
pubspec.lock
40
pubspec.lock
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue