diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 4319b96..2d833ce 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -3,6 +3,7 @@
+
Unit)? = null
- private val selectFileCode = 0x11
-
- override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
- super.onActivityResult(requestCode, resultCode, data)
- if (requestCode == pickDirectoryCode) {
- if(resultCode != Activity.RESULT_OK) {
- result.success(null)
- return
- }
- val pickedDirectoryUri = data?.data
- if (pickedDirectoryUri == null) {
- result.success(null)
- return
- }
- Thread {
- try {
- result.success(onPickedDirectory(pickedDirectoryUri))
- }
- catch (e: Exception) {
- result.error("Failed to Copy Files", e.toString(), null)
- }
- }.start()
- } else if (requestCode == storageRequestCode) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- storagePermissionRequest?.invoke(Environment.isExternalStorageManager())
- }
- storagePermissionRequest = null
- } else if (requestCode == selectFileCode) {
- if (resultCode != Activity.RESULT_OK) {
- result.success(null)
- return
- }
- val uri = data?.data
- if (uri == null) {
- result.success(null)
- return
- }
- val contentResolver = context.contentResolver
- val file = DocumentFile.fromSingleUri(context, uri)
- if (file == null) {
- result.success(null)
- return
- }
- val fileName = file.name
- if (fileName == null) {
- result.success(null)
- return
- }
- // copy file to cache directory
- val cacheDir = context.cacheDir
- val newFile = File(cacheDir, fileName)
- val inputStream = contentResolver.openInputStream(uri)
- if (inputStream == null) {
- result.success(null)
- return
- }
- val outputStream = FileOutputStream(newFile)
- inputStream.copyTo(outputStream)
- inputStream.close()
- outputStream.close()
- // send file path to flutter
- result.success(newFile.absolutePath)
- }
- }
-
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine)
MethodChannel(
@@ -115,12 +49,30 @@ class MainActivity : FlutterActivity() {
}
res.success(null)
}
+
"getDirectoryPath" -> {
- this.result = res
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
- startActivityForResult(intent, pickDirectoryCode)
+ val launcher =
+ registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult ->
+ if (activityResult.resultCode != Activity.RESULT_OK) {
+ result.success(null)
+ }
+ val pickedDirectoryUri = activityResult.data?.data
+ if (pickedDirectoryUri == null)
+ result.success(null)
+ else
+ Thread {
+ try {
+ result.success(onPickedDirectory(pickedDirectoryUri))
+ } catch (e: Exception) {
+ result.error("Failed to Copy Files", e.toString(), null)
+ }
+ }.start()
+ }
+ launcher.launch(intent)
}
+
else -> res.notImplemented()
}
}
@@ -137,6 +89,7 @@ class MainActivity : FlutterActivity() {
events.success(2)
}
}
+
override fun onCancel(arguments: Any?) {
listening = false
}
@@ -144,7 +97,7 @@ class MainActivity : FlutterActivity() {
val storageChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "venera/storage")
storageChannel.setMethodCallHandler { _, res ->
- requestStoragePermission {result ->
+ requestStoragePermission { result ->
res.success(result)
}
}
@@ -167,12 +120,13 @@ class MainActivity : FlutterActivity() {
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
- if(listening){
+ if (listening) {
when (keyCode) {
KeyEvent.KEYCODE_VOLUME_DOWN -> {
volumeListen.down()
return true
}
+
KeyEvent.KEYCODE_VOLUME_UP -> {
volumeListen.up()
return true
@@ -184,8 +138,8 @@ class MainActivity : FlutterActivity() {
/// copy the directory to tmp directory, return copied directory
private fun onPickedDirectory(uri: Uri): String {
- val contentResolver = context.contentResolver
- var tmp = context.cacheDir
+ val contentResolver = contentResolver
+ var tmp = cacheDir
tmp = File(tmp, "getDirectoryPathTemp")
tmp.mkdir()
copyDirectory(contentResolver, uri, tmp)
@@ -194,9 +148,9 @@ class MainActivity : FlutterActivity() {
}
private fun copyDirectory(resolver: ContentResolver, srcUri: Uri, destDir: File) {
- val src = DocumentFile.fromTreeUri(context, srcUri) ?: return
+ val src = DocumentFile.fromTreeUri(this, srcUri) ?: return
for (file in src.listFiles()) {
- if(file.isDirectory) {
+ if (file.isDirectory) {
val newDir = File(destDir, file.name!!)
newDir.mkdir()
copyDirectory(resolver, file.uri, newDir)
@@ -212,7 +166,7 @@ class MainActivity : FlutterActivity() {
}
private fun requestStoragePermission(result: (Boolean) -> Unit) {
- if(Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
val readPermission = ContextCompat.checkSelfPermission(
this,
Manifest.permission.READ_EXTERNAL_STORAGE
@@ -241,8 +195,11 @@ class MainActivity : FlutterActivity() {
try {
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
intent.addCategory("android.intent.category.DEFAULT")
- intent.data = Uri.parse("package:" + context.packageName)
- startActivityForResult(intent, storageRequestCode)
+ intent.data = Uri.parse("package:$packageName")
+ val launcher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { _ ->
+ result(Environment.isExternalStorageManager())
+ }
+ launcher.launch(intent)
} catch (e: Exception) {
result(false)
}
@@ -258,7 +215,7 @@ class MainActivity : FlutterActivity() {
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
- if(requestCode == storageRequestCode) {
+ if (requestCode == storageRequestCode) {
storagePermissionRequest?.invoke(grantResults.all {
it == PackageManager.PERMISSION_GRANTED
})
@@ -266,21 +223,57 @@ class MainActivity : FlutterActivity() {
}
}
- fun openFile() {
+ private fun openFile() {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "*/*"
- startActivityForResult(intent, selectFileCode)
+ val launcher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult ->
+ if (activityResult.resultCode != Activity.RESULT_OK) {
+ result.success(null)
+ }
+ val uri = activityResult.data?.data
+ if (uri == null) {
+ result.success(null)
+ return@registerForActivityResult
+ }
+ val contentResolver = contentResolver
+ val file = DocumentFile.fromSingleUri(this, uri)
+ if (file == null) {
+ result.success(null)
+ return@registerForActivityResult
+ }
+ val fileName = file.name
+ if (fileName == null) {
+ result.success(null)
+ return@registerForActivityResult
+ }
+ // copy file to cache directory
+ val cacheDir = cacheDir
+ val newFile = File(cacheDir, fileName)
+ val inputStream = contentResolver.openInputStream(uri)
+ if (inputStream == null) {
+ result.success(null)
+ return@registerForActivityResult
+ }
+ val outputStream = FileOutputStream(newFile)
+ inputStream.copyTo(outputStream)
+ inputStream.close()
+ outputStream.close()
+ // send file path to flutter
+ result.success(newFile.absolutePath)
+ }
+ launcher.launch(intent)
}
}
-class VolumeListen{
+class VolumeListen {
var onUp = fun() {}
var onDown = fun() {}
- fun up(){
+ fun up() {
onUp()
}
- fun down(){
+
+ fun down() {
onDown()
}
}
diff --git a/assets/translation.json b/assets/translation.json
index 14604bb..96579de 100644
--- a/assets/translation.json
+++ b/assets/translation.json
@@ -210,7 +210,8 @@
"Create Folder": "新建文件夹",
"Select an image on screen": "选择屏幕上的图片",
"Added @count comics to download queue.": "已添加 @count 本漫画到下载队列",
- "Ignore Certificate Errors": "忽略证书错误"
+ "Ignore Certificate Errors": "忽略证书错误",
+ "Authorization Required": "需要身份验证"
},
"zh_TW": {
"Home": "首頁",
@@ -423,6 +424,7 @@
"Create Folder": "新建文件夾",
"Select an image on screen": "選擇屏幕上的圖片",
"Added @count comics to download queue.": "已添加 @count 本漫畫到下載隊列",
- "Ignore Certificate Errors": "忽略證書錯誤"
+ "Ignore Certificate Errors": "忽略證書錯誤",
+ "Authorization Required": "需要身份驗證"
}
}
\ No newline at end of file
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 93a0af2..be8d5d6 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -51,5 +51,7 @@
LSSupportsOpeningDocumentsInPlace
+ NSFaceIDUsageDescription
+ Ensure that the operation is being performed by the user themselves.
diff --git a/lib/foundation/appdata.dart b/lib/foundation/appdata.dart
index 1cc7ed3..8c5d347 100644
--- a/lib/foundation/appdata.dart
+++ b/lib/foundation/appdata.dart
@@ -120,6 +120,7 @@ class _Settings with ChangeNotifier {
'enableTurnPageByVolumeKey': true,
'enableClockAndBatteryInfoInReader': true,
'ignoreCertificateErrors': false,
+ 'authorizationRequired': false,
};
operator [](String key) {
diff --git a/lib/main.dart b/lib/main.dart
index 7351392..2000267 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -5,7 +5,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:rhttp/rhttp.dart';
import 'package:venera/foundation/log.dart';
-import 'package:venera/network/app_dio.dart';
+import 'package:venera/pages/auth_page.dart';
import 'package:venera/pages/comic_source_page.dart';
import 'package:venera/pages/main_page.dart';
import 'package:venera/pages/settings/settings_page.dart';
@@ -65,15 +65,59 @@ class MyApp extends StatefulWidget {
State createState() => _MyAppState();
}
-class _MyAppState extends State {
+class _MyAppState extends State with WidgetsBindingObserver {
@override
void initState() {
checkUpdates();
App.registerForceRebuild(forceRebuild);
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
+ WidgetsBinding.instance.addObserver(this);
super.initState();
}
+ bool isAuthPageActive = false;
+
+ OverlayEntry? hideContentOverlay;
+
+ @override
+ void didChangeAppLifecycleState(AppLifecycleState state) {
+ if(!App.isMobile) {
+ return;
+ }
+ if (state == AppLifecycleState.inactive && hideContentOverlay == null) {
+ hideContentOverlay = OverlayEntry(
+ builder: (context) {
+ return Positioned.fill(
+ child: Container(
+ width: double.infinity,
+ height: double.infinity,
+ color: App.rootContext.colorScheme.surface,
+ ),
+ );
+ },
+ );
+ Overlay.of(App.rootContext).insert(hideContentOverlay!);
+ } else if (hideContentOverlay != null &&
+ state == AppLifecycleState.resumed) {
+ hideContentOverlay!.remove();
+ hideContentOverlay = null;
+ }
+ if (state == AppLifecycleState.hidden &&
+ appdata.settings['authorizationRequired'] &&
+ !isAuthPageActive) {
+ isAuthPageActive = true;
+ App.rootContext.to(
+ () => AuthPage(
+ onSuccessfulAuth: () {
+ App.rootContext.pop();
+ isAuthPageActive = false;
+ },
+ ),
+ );
+ }
+ super.didChangeAppLifecycleState(state);
+ }
+
void forceRebuild() {
void rebuild(Element el) {
el.markNeedsBuild();
@@ -86,14 +130,25 @@ class _MyAppState extends State {
@override
Widget build(BuildContext context) {
+ Widget home;
+ if (appdata.settings['authorizationRequired']) {
+ home = AuthPage(
+ onSuccessfulAuth: () {
+ App.rootContext.toReplacement(() => const MainPage());
+ },
+ );
+ } else {
+ home = const MainPage();
+ }
return MaterialApp(
- home: const MainPage(),
+ home: home,
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: App.mainColor,
surface: Colors.white,
primary: App.mainColor.shade600,
+ // ignore: deprecated_member_use
background: Colors.white,
),
fontFamily: App.isWindows ? "Microsoft YaHei" : null,
@@ -105,6 +160,7 @@ class _MyAppState extends State {
brightness: Brightness.dark,
surface: Colors.black,
primary: App.mainColor.shade400,
+ // ignore: deprecated_member_use
background: Colors.black,
),
fontFamily: App.isWindows ? "Microsoft YaHei" : null,
@@ -171,12 +227,12 @@ class _MyAppState extends State {
}
void checkUpdates() async {
- if(!appdata.settings['checkUpdateOnStart']) {
+ if (!appdata.settings['checkUpdateOnStart']) {
return;
}
var lastCheck = appdata.implicitData['lastCheckUpdate'] ?? 0;
var now = DateTime.now().millisecondsSinceEpoch;
- if(now - lastCheck < 24 * 60 * 60 * 1000) {
+ if (now - lastCheck < 24 * 60 * 60 * 1000) {
return;
}
appdata.implicitData['lastCheckUpdate'] = now;
diff --git a/lib/pages/auth_page.dart b/lib/pages/auth_page.dart
new file mode 100644
index 0000000..67b0d29
--- /dev/null
+++ b/lib/pages/auth_page.dart
@@ -0,0 +1,60 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:local_auth/local_auth.dart';
+import 'package:venera/utils/translations.dart';
+
+class AuthPage extends StatefulWidget {
+ const AuthPage({super.key, this.onSuccessfulAuth});
+
+ final void Function()? onSuccessfulAuth;
+
+ @override
+ State createState() => _AuthPageState();
+}
+
+class _AuthPageState extends State {
+
+ @override
+ Widget build(BuildContext context) {
+ return PopScope(
+ canPop: false,
+ onPopInvokedWithResult: (didPop, result) {
+ if (!didPop) {
+ SystemNavigator.pop();
+ }
+ },
+ child: Material(
+ child: Center(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ const Icon(Icons.security, size: 36),
+ const SizedBox(height: 16),
+ Text("Authentication Required".tl),
+ const SizedBox(height: 16),
+ FilledButton(
+ onPressed: auth,
+ child: Text("Continue".tl),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
+ void auth() async {
+ var localAuth = LocalAuthentication();
+ var canCheckBiometrics = await localAuth.canCheckBiometrics;
+ if (!canCheckBiometrics && !await localAuth.isDeviceSupported()) {
+ widget.onSuccessfulAuth?.call();
+ return;
+ }
+ var isAuthorized = await localAuth.authenticate(
+ localizedReason: "Please authenticate to continue".tl,
+ );
+ if (isAuthorized) {
+ widget.onSuccessfulAuth?.call();
+ }
+ }
+}
diff --git a/lib/pages/settings/app.dart b/lib/pages/settings/app.dart
index c491f51..b35301f 100644
--- a/lib/pages/settings/app.dart
+++ b/lib/pages/settings/app.dart
@@ -36,12 +36,12 @@ class _AppSettingsState extends State {
if (App.isAndroid) {
var channel = const MethodChannel("venera/storage");
var permission = await channel.invokeMethod('');
- if(permission != true) {
+ if (permission != true) {
context.showMessage(message: "Permission denied".tl);
return;
}
var path = await selectDirectory();
- if(path != null) {
+ if (path != null) {
// check if the path is writable
var testFile = File(FilePath.join(path, "test"));
try {
@@ -177,6 +177,29 @@ class _AppSettingsState extends State {
App.forceRebuild();
},
).toSliver(),
+ if (!App.isLinux)
+ _SwitchSetting(
+ title: "Authorization Required".tl,
+ settingKey: "authorizationRequired",
+ onChanged: () async {
+ var current = appdata.settings['authorizationRequired'];
+ if (current) {
+ final auth = LocalAuthentication();
+ final bool canAuthenticateWithBiometrics =
+ await auth.canCheckBiometrics;
+ final bool canAuthenticate = canAuthenticateWithBiometrics ||
+ await auth.isDeviceSupported();
+ if (!canAuthenticate) {
+ context.showMessage(message: "Biometrics not supported".tl);
+ setState(() {
+ appdata.settings['authorizationRequired'] = false;
+ });
+ appdata.saveData();
+ return;
+ }
+ }
+ },
+ ).toSliver(),
],
);
}
diff --git a/lib/pages/settings/setting_components.dart b/lib/pages/settings/setting_components.dart
index ee14ddc..6472ca8 100644
--- a/lib/pages/settings/setting_components.dart
+++ b/lib/pages/settings/setting_components.dart
@@ -33,9 +33,10 @@ class _SwitchSettingState extends State<_SwitchSetting> {
onChanged: (value) {
setState(() {
appdata.settings[widget.settingKey] = value;
- appdata.saveData();
});
- widget.onChanged?.call();
+ appdata.saveData().then((_) {
+ widget.onChanged?.call();
+ });
},
),
);
diff --git a/lib/pages/settings/settings_page.dart b/lib/pages/settings/settings_page.dart
index 889a7c6..04a0215 100644
--- a/lib/pages/settings/settings_page.dart
+++ b/lib/pages/settings/settings_page.dart
@@ -4,6 +4,7 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_reorderable_grid_view/widgets/reorderable_builder.dart';
+import 'package:local_auth/local_auth.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:venera/components/components.dart';
import 'package:venera/foundation/app.dart';
diff --git a/pubspec.lock b/pubspec.lock
index 88ac6d3..22bde61 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -356,6 +356,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
+ flutter_plugin_android_lifecycle:
+ dependency: transitive
+ description:
+ name: flutter_plugin_android_lifecycle
+ sha256: "9b78450b89f059e96c9ebb355fa6b3df1d6b330436e0b885fb49594c41721398"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.23"
flutter_qjs:
dependency: "direct main"
description:
@@ -512,6 +520,46 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.0"
+ local_auth:
+ dependency: "direct main"
+ description:
+ name: local_auth
+ sha256: "434d854cf478f17f12ab29a76a02b3067f86a63a6d6c4eb8fbfdcfe4879c1b7b"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.3.0"
+ local_auth_android:
+ dependency: transitive
+ description:
+ name: local_auth_android
+ sha256: "6763aaf8965f21822624cb2fd3c03d2a8b3791037b5efb0fe4b13e110f5afc92"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.46"
+ local_auth_darwin:
+ dependency: transitive
+ description:
+ name: local_auth_darwin
+ sha256: "6d2950da311d26d492a89aeb247c72b4653ddc93601ea36a84924a396806d49c"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.4.1"
+ local_auth_platform_interface:
+ dependency: transitive
+ description:
+ name: local_auth_platform_interface
+ sha256: "1b842ff177a7068442eae093b64abe3592f816afd2a533c0ebcdbe40f9d2075a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.10"
+ local_auth_windows:
+ dependency: transitive
+ description:
+ name: local_auth_windows
+ sha256: bc4e66a29b0fdf751aafbec923b5bed7ad6ed3614875d8151afe2578520b2ab5
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.11"
lodepng_flutter:
dependency: "direct main"
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index 89e929b..19e08db 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -64,6 +64,7 @@ dependencies:
url: https://github.com/wgh136/webdav_client
ref: 285f87f15bccd2d5d5ff443761348c6ee47b98d1
battery_plus: ^6.2.0
+ local_auth: ^2.3.0
dev_dependencies:
flutter_test:
diff --git a/windows/build.iss b/windows/build.iss
index 01d6c2b..1e4ec0d 100644
--- a/windows/build.iss
+++ b/windows/build.iss
@@ -54,6 +54,7 @@ Source: "{#RootPath}\build\windows\x64\runner\Release\url_launcher_windows_plugi
Source: "{#RootPath}\build\windows\x64\runner\Release\battery_plus_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#RootPath}\build\windows\x64\runner\Release\screen_retriever_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#RootPath}\build\windows\x64\runner\Release\window_manager_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "{#RootPath}\build\windows\x64\runner\Release\local_auth_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#RootPath}\build\windows\x64\runner\Release\zip_flutter.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#RootPath}\build\windows\x64\runner\Release\rhttp.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#RootPath}\build\windows\x64\runner\Release\lodepng_flutter.dll"; DestDir: "{app}"; Flags: ignoreversion