mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-01-22 08:46:18 +01:00
Add app lock
This commit is contained in:
parent
fd8251b86c
commit
58171f14c9
28 changed files with 1206 additions and 33 deletions
|
@ -8,14 +8,14 @@ import com.nkming.nc_photos.np_android_core.UriUtil
|
|||
import com.nkming.nc_photos.np_android_core.logE
|
||||
import com.nkming.nc_photos.np_android_core.logI
|
||||
import com.nkming.nc_photos.np_platform_image_processor.NpPlatformImageProcessorPlugin
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.android.FlutterFragmentActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import java.net.URLEncoder
|
||||
|
||||
class MainActivity : FlutterActivity(), MethodChannel.MethodCallHandler {
|
||||
class MainActivity : FlutterFragmentActivity(), MethodChannel.MethodCallHandler {
|
||||
companion object {
|
||||
private const val METHOD_CHANNEL = "com.nkming.nc_photos/activity"
|
||||
|
||||
|
|
18
app/lib/animation_util.dart
Normal file
18
app/lib/animation_util.dart
Normal file
|
@ -0,0 +1,18 @@
|
|||
double tremblingTransform(int count, double t) {
|
||||
final tt = (t * count) % 1;
|
||||
return _tremblingTransformT(tt);
|
||||
}
|
||||
|
||||
double _tremblingTransformT(double t) {
|
||||
if (t <= 0 || t >= 1) {
|
||||
return 0;
|
||||
}
|
||||
final x = 4 * t;
|
||||
if (x < 1) {
|
||||
return -x;
|
||||
} else if (x < 3) {
|
||||
return x - 1;
|
||||
} else {
|
||||
return 4 - x;
|
||||
}
|
||||
}
|
|
@ -12,6 +12,7 @@ class BlocListenerT<B extends StateStreamable<S>, S, T>
|
|||
extends SingleChildStatelessWidget {
|
||||
const BlocListenerT({
|
||||
super.key,
|
||||
super.child,
|
||||
required this.selector,
|
||||
required this.listener,
|
||||
});
|
||||
|
|
|
@ -239,9 +239,36 @@ class PrefController {
|
|||
_c.pref.getSecondarySeedColor()?.run(Color.new));
|
||||
}
|
||||
|
||||
@npSubjectAccessor
|
||||
class SecurePrefController {
|
||||
SecurePrefController(this._c);
|
||||
|
||||
Future<void> setProtectedPageAuthType(ProtectedPageAuthType? value) =>
|
||||
_setOrRemove<ProtectedPageAuthType>(
|
||||
controller: _protectedPageAuthTypeController,
|
||||
setter: (pref, value) => pref.setProtectedPageAuthType(value.index),
|
||||
remover: (pref) => pref.setProtectedPageAuthType(null),
|
||||
value: value,
|
||||
);
|
||||
|
||||
Future<void> setProtectedPageAuthPin(CiString? value) =>
|
||||
_setOrRemove<CiString>(
|
||||
controller: _protectedPageAuthPinController,
|
||||
setter: (pref, value) =>
|
||||
pref.setProtectedPageAuthPin(value.toCaseInsensitiveString()),
|
||||
remover: (pref) => pref.setProtectedPageAuthPin(null),
|
||||
value: value,
|
||||
);
|
||||
|
||||
Future<void> setProtectedPageAuthPassword(CiString? value) =>
|
||||
_setOrRemove<CiString>(
|
||||
controller: _protectedPageAuthPasswordController,
|
||||
setter: (pref, value) =>
|
||||
pref.setProtectedPageAuthPassword(value.toCaseInsensitiveString()),
|
||||
remover: (pref) => pref.setProtectedPageAuthPassword(null),
|
||||
value: value,
|
||||
);
|
||||
|
||||
// ignore: unused_element
|
||||
Future<void> _set<T>({
|
||||
required BehaviorSubject<T> controller,
|
||||
|
@ -273,6 +300,17 @@ class SecurePrefController {
|
|||
);
|
||||
|
||||
final DiContainer _c;
|
||||
@npSubjectAccessor
|
||||
late final _protectedPageAuthTypeController = BehaviorSubject.seeded(_c
|
||||
.securePref
|
||||
.getProtectedPageAuthType()
|
||||
?.let((e) => ProtectedPageAuthType.values[e]));
|
||||
@npSubjectAccessor
|
||||
late final _protectedPageAuthPinController =
|
||||
BehaviorSubject.seeded(_c.securePref.getProtectedPageAuthPin()?.toCi());
|
||||
@npSubjectAccessor
|
||||
late final _protectedPageAuthPasswordController = BehaviorSubject.seeded(
|
||||
_c.securePref.getProtectedPageAuthPassword()?.toCi());
|
||||
}
|
||||
|
||||
Future<void> _doSet<T>({
|
||||
|
|
|
@ -142,3 +142,32 @@ extension $PrefControllerNpSubjectAccessor on PrefController {
|
|||
secondarySeedColor.distinct().skip(1);
|
||||
Color? get secondarySeedColorValue => _secondarySeedColorController.value;
|
||||
}
|
||||
|
||||
extension $SecurePrefControllerNpSubjectAccessor on SecurePrefController {
|
||||
// _protectedPageAuthTypeController
|
||||
ValueStream<ProtectedPageAuthType?> get protectedPageAuthType =>
|
||||
_protectedPageAuthTypeController.stream;
|
||||
Stream<ProtectedPageAuthType?> get protectedPageAuthTypeNew =>
|
||||
protectedPageAuthType.skip(1);
|
||||
Stream<ProtectedPageAuthType?> get protectedPageAuthTypeChange =>
|
||||
protectedPageAuthType.distinct().skip(1);
|
||||
ProtectedPageAuthType? get protectedPageAuthTypeValue =>
|
||||
_protectedPageAuthTypeController.value;
|
||||
// _protectedPageAuthPinController
|
||||
ValueStream<CiString?> get protectedPageAuthPin =>
|
||||
_protectedPageAuthPinController.stream;
|
||||
Stream<CiString?> get protectedPageAuthPinNew => protectedPageAuthPin.skip(1);
|
||||
Stream<CiString?> get protectedPageAuthPinChange =>
|
||||
protectedPageAuthPin.distinct().skip(1);
|
||||
CiString? get protectedPageAuthPinValue =>
|
||||
_protectedPageAuthPinController.value;
|
||||
// _protectedPageAuthPasswordController
|
||||
ValueStream<CiString?> get protectedPageAuthPassword =>
|
||||
_protectedPageAuthPasswordController.stream;
|
||||
Stream<CiString?> get protectedPageAuthPasswordNew =>
|
||||
protectedPageAuthPassword.skip(1);
|
||||
Stream<CiString?> get protectedPageAuthPasswordChange =>
|
||||
protectedPageAuthPassword.distinct().skip(1);
|
||||
CiString? get protectedPageAuthPasswordValue =>
|
||||
_protectedPageAuthPasswordController.value;
|
||||
}
|
||||
|
|
|
@ -109,6 +109,9 @@ enum PrefKey implements PrefKeyInterface {
|
|||
isVideoPlayerMute,
|
||||
isVideoPlayerLoop,
|
||||
secondarySeedColor,
|
||||
protectedPageAuthType,
|
||||
protectedPageAuthPin,
|
||||
protectedPageAuthPassword,
|
||||
;
|
||||
|
||||
@override
|
||||
|
@ -187,6 +190,12 @@ enum PrefKey implements PrefKeyInterface {
|
|||
return "isVideoPlayerLoop";
|
||||
case PrefKey.secondarySeedColor:
|
||||
return "secondarySeedColor";
|
||||
case PrefKey.protectedPageAuthType:
|
||||
return "protectedPageAuthType";
|
||||
case PrefKey.protectedPageAuthPin:
|
||||
return "protectedPageAuthPin";
|
||||
case PrefKey.protectedPageAuthPassword:
|
||||
return "protectedPageAuthPassword";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -290,6 +290,44 @@ extension PrefExtension on Pref {
|
|||
(key, value) => provider.setInt(key, value));
|
||||
}
|
||||
}
|
||||
|
||||
int? getProtectedPageAuthType() =>
|
||||
provider.getInt(PrefKey.protectedPageAuthType);
|
||||
int getProtectedPageAuthTypeOr(int def) => getProtectedPageAuthType() ?? def;
|
||||
Future<bool> setProtectedPageAuthType(int? value) {
|
||||
if (value == null) {
|
||||
return _remove(PrefKey.protectedPageAuthType);
|
||||
} else {
|
||||
return _set<int>(PrefKey.protectedPageAuthType, value,
|
||||
(key, value) => provider.setInt(key, value));
|
||||
}
|
||||
}
|
||||
|
||||
String? getProtectedPageAuthPin() =>
|
||||
provider.getString(PrefKey.protectedPageAuthPin);
|
||||
String getProtectedPageAuthPinOr(String def) =>
|
||||
getProtectedPageAuthPin() ?? def;
|
||||
Future<bool> setProtectedPageAuthPin(String? value) {
|
||||
if (value == null) {
|
||||
return _remove(PrefKey.protectedPageAuthPin);
|
||||
} else {
|
||||
return _set<String>(PrefKey.protectedPageAuthPin, value,
|
||||
(key, value) => provider.setString(key, value));
|
||||
}
|
||||
}
|
||||
|
||||
String? getProtectedPageAuthPassword() =>
|
||||
provider.getString(PrefKey.protectedPageAuthPassword);
|
||||
String getProtectedPageAuthPasswordOr(String def) =>
|
||||
getProtectedPageAuthPassword() ?? def;
|
||||
Future<bool> setProtectedPageAuthPassword(String? value) {
|
||||
if (value == null) {
|
||||
return _remove(PrefKey.protectedPageAuthPassword);
|
||||
} else {
|
||||
return _set<String>(PrefKey.protectedPageAuthPassword, value,
|
||||
(key, value) => provider.setString(key, value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AccountPrefExtension on AccountPref {
|
||||
|
|
|
@ -1435,6 +1435,14 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"appLockUnlockHint": "Unlock the app",
|
||||
"@appLockUnlockHint": {
|
||||
"description": "Unlock app via selected means (e.g., password) in case app lock is enabled by user"
|
||||
},
|
||||
"appLockUnlockWrongPassword": "Incorrect password",
|
||||
"@appLockUnlockWrongPassword": {
|
||||
"description": "Unlock app via selected means (e.g., password) in case app lock is enabled by user"
|
||||
},
|
||||
|
||||
"errorUnauthenticated": "Unauthenticated access. Please sign-in again if the problem continues",
|
||||
"@errorUnauthenticated": {
|
||||
|
|
|
@ -227,6 +227,8 @@
|
|||
"contributorsTooltip",
|
||||
"setAsTooltip",
|
||||
"deleteAccountConfirmDialogText",
|
||||
"appLockUnlockHint",
|
||||
"appLockUnlockWrongPassword",
|
||||
"errorUnauthenticated",
|
||||
"errorDisconnected",
|
||||
"errorLocked",
|
||||
|
@ -240,7 +242,9 @@
|
|||
"cs": [
|
||||
"settingsThemePrimaryColor",
|
||||
"settingsThemeSecondaryColor",
|
||||
"settingsThemePresets"
|
||||
"settingsThemePresets",
|
||||
"appLockUnlockHint",
|
||||
"appLockUnlockWrongPassword"
|
||||
],
|
||||
|
||||
"de": [
|
||||
|
@ -272,7 +276,9 @@
|
|||
"searchFilterBubbleTypeImageText",
|
||||
"imageSaveOptionDialogTitle",
|
||||
"imageSaveOptionDialogContent",
|
||||
"loopTooltip"
|
||||
"loopTooltip",
|
||||
"appLockUnlockHint",
|
||||
"appLockUnlockWrongPassword"
|
||||
],
|
||||
|
||||
"el": [
|
||||
|
@ -387,25 +393,33 @@
|
|||
"accountSettingsTooltip",
|
||||
"contributorsTooltip",
|
||||
"setAsTooltip",
|
||||
"deleteAccountConfirmDialogText"
|
||||
"deleteAccountConfirmDialogText",
|
||||
"appLockUnlockHint",
|
||||
"appLockUnlockWrongPassword"
|
||||
],
|
||||
|
||||
"es": [
|
||||
"settingsThemePrimaryColor",
|
||||
"settingsThemeSecondaryColor",
|
||||
"settingsThemePresets"
|
||||
"settingsThemePresets",
|
||||
"appLockUnlockHint",
|
||||
"appLockUnlockWrongPassword"
|
||||
],
|
||||
|
||||
"fi": [
|
||||
"settingsThemePrimaryColor",
|
||||
"settingsThemeSecondaryColor",
|
||||
"settingsThemePresets"
|
||||
"settingsThemePresets",
|
||||
"appLockUnlockHint",
|
||||
"appLockUnlockWrongPassword"
|
||||
],
|
||||
|
||||
"fr": [
|
||||
"settingsThemePrimaryColor",
|
||||
"settingsThemeSecondaryColor",
|
||||
"settingsThemePresets"
|
||||
"settingsThemePresets",
|
||||
"appLockUnlockHint",
|
||||
"appLockUnlockWrongPassword"
|
||||
],
|
||||
|
||||
"it": [
|
||||
|
@ -416,7 +430,9 @@
|
|||
"settingsThemePresets",
|
||||
"unmuteTooltip",
|
||||
"slideshowTooltip",
|
||||
"enhanceColorPopTitle"
|
||||
"enhanceColorPopTitle",
|
||||
"appLockUnlockHint",
|
||||
"appLockUnlockWrongPassword"
|
||||
],
|
||||
|
||||
"nl": [
|
||||
|
@ -764,6 +780,8 @@
|
|||
"contributorsTooltip",
|
||||
"setAsTooltip",
|
||||
"deleteAccountConfirmDialogText",
|
||||
"appLockUnlockHint",
|
||||
"appLockUnlockWrongPassword",
|
||||
"errorUnauthenticated",
|
||||
"errorDisconnected",
|
||||
"errorLocked",
|
||||
|
@ -781,7 +799,9 @@
|
|||
"settingsThemePresets",
|
||||
"enhanceColorPopTitle",
|
||||
"imageEditTransformOrientationClockwise",
|
||||
"imageEditTransformOrientationCounterclockwise"
|
||||
"imageEditTransformOrientationCounterclockwise",
|
||||
"appLockUnlockHint",
|
||||
"appLockUnlockWrongPassword"
|
||||
],
|
||||
|
||||
"pt": [
|
||||
|
@ -807,19 +827,25 @@
|
|||
"accountSettingsTooltip",
|
||||
"contributorsTooltip",
|
||||
"setAsTooltip",
|
||||
"deleteAccountConfirmDialogText"
|
||||
"deleteAccountConfirmDialogText",
|
||||
"appLockUnlockHint",
|
||||
"appLockUnlockWrongPassword"
|
||||
],
|
||||
|
||||
"ru": [
|
||||
"settingsThemePrimaryColor",
|
||||
"settingsThemeSecondaryColor",
|
||||
"settingsThemePresets"
|
||||
"settingsThemePresets",
|
||||
"appLockUnlockHint",
|
||||
"appLockUnlockWrongPassword"
|
||||
],
|
||||
|
||||
"tr": [
|
||||
"settingsThemePrimaryColor",
|
||||
"settingsThemeSecondaryColor",
|
||||
"settingsThemePresets"
|
||||
"settingsThemePresets",
|
||||
"appLockUnlockHint",
|
||||
"appLockUnlockWrongPassword"
|
||||
],
|
||||
|
||||
"zh": [
|
||||
|
@ -950,7 +976,9 @@
|
|||
"accountSettingsTooltip",
|
||||
"contributorsTooltip",
|
||||
"setAsTooltip",
|
||||
"deleteAccountConfirmDialogText"
|
||||
"deleteAccountConfirmDialogText",
|
||||
"appLockUnlockHint",
|
||||
"appLockUnlockWrongPassword"
|
||||
],
|
||||
|
||||
"zh_Hant": [
|
||||
|
@ -1081,6 +1109,8 @@
|
|||
"accountSettingsTooltip",
|
||||
"contributorsTooltip",
|
||||
"setAsTooltip",
|
||||
"deleteAccountConfirmDialogText"
|
||||
"deleteAccountConfirmDialogText",
|
||||
"appLockUnlockHint",
|
||||
"appLockUnlockWrongPassword"
|
||||
]
|
||||
}
|
||||
|
|
149
app/lib/protected_page_handler.dart
Normal file
149
app/lib/protected_page_handler.dart
Normal file
|
@ -0,0 +1,149 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:local_auth/local_auth.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/app_localizations.dart';
|
||||
import 'package:nc_photos/controller/pref_controller.dart';
|
||||
import 'package:nc_photos/widget/protected_page_password_auth_dialog.dart';
|
||||
import 'package:nc_photos/widget/protected_page_pin_auth_dialog.dart';
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
import 'package:np_string/np_string.dart';
|
||||
|
||||
part 'protected_page_handler.g.dart';
|
||||
|
||||
enum ProtectedPageAuthType {
|
||||
biometric,
|
||||
pin,
|
||||
password,
|
||||
;
|
||||
}
|
||||
|
||||
class ProtectedPageAuthException implements Exception {
|
||||
const ProtectedPageAuthException([this.message]);
|
||||
|
||||
@override
|
||||
String toString() => "ProtectedPageAuthException: $message";
|
||||
|
||||
final dynamic message;
|
||||
}
|
||||
|
||||
extension ProtectedPageBuildContextExtension on NavigatorState {
|
||||
Future<T?> pushReplacementProtected<T extends Object?, U extends Object?>(
|
||||
String routeName, {
|
||||
U? result,
|
||||
Object? arguments,
|
||||
}) async {
|
||||
if (await _auth()) {
|
||||
return pushReplacementNamed(routeName,
|
||||
arguments: arguments, result: result);
|
||||
} else {
|
||||
throw const ProtectedPageAuthException();
|
||||
}
|
||||
}
|
||||
|
||||
Future<T?> pushProtected<T extends Object?>(
|
||||
String routeName, {
|
||||
Object? arguments,
|
||||
}) async {
|
||||
if (await _auth()) {
|
||||
return pushNamed(routeName, arguments: arguments);
|
||||
} else {
|
||||
throw const ProtectedPageAuthException();
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _auth() async {
|
||||
final securePrefController = context.read<SecurePrefController>();
|
||||
switch (securePrefController.protectedPageAuthTypeValue) {
|
||||
case null:
|
||||
// unprotected
|
||||
return true;
|
||||
case ProtectedPageAuthType.biometric:
|
||||
return _authBiometric(securePrefController);
|
||||
case ProtectedPageAuthType.pin:
|
||||
return _authPin(securePrefController);
|
||||
case ProtectedPageAuthType.password:
|
||||
return _authPassword(securePrefController);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _authBiometric(SecurePrefController securePrefController) async {
|
||||
if (await _BiometricAuthHandler().auth()) {
|
||||
return true;
|
||||
} else {
|
||||
if (securePrefController.protectedPageAuthPasswordValue != null) {
|
||||
return _authPassword(securePrefController);
|
||||
} else {
|
||||
return _authPin(securePrefController);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _authPin(SecurePrefController securePrefController) =>
|
||||
_PinAuthHandler(context, securePrefController.protectedPageAuthPinValue!)
|
||||
.auth();
|
||||
|
||||
Future<bool> _authPassword(SecurePrefController securePrefController) =>
|
||||
_PasswordAuthHandler(
|
||||
context, securePrefController.protectedPageAuthPasswordValue!)
|
||||
.auth();
|
||||
}
|
||||
|
||||
abstract class _AuthHandler {
|
||||
Future<bool> auth();
|
||||
}
|
||||
|
||||
@npLog
|
||||
class _BiometricAuthHandler implements _AuthHandler {
|
||||
@override
|
||||
Future<bool> auth() async {
|
||||
try {
|
||||
final localAuth = LocalAuthentication();
|
||||
final available = await localAuth.getAvailableBiometrics();
|
||||
if (available.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
return await localAuth.authenticate(
|
||||
localizedReason: L10n.global().appLockUnlockHint,
|
||||
options: const AuthenticationOptions(
|
||||
biometricOnly: true,
|
||||
),
|
||||
);
|
||||
} catch (e, stackTrace) {
|
||||
_log.severe("[auth] Exception", e, stackTrace);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _PinAuthHandler implements _AuthHandler {
|
||||
const _PinAuthHandler(this.context, this.pin);
|
||||
|
||||
@override
|
||||
Future<bool> auth() async {
|
||||
final result = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => ProtectedPagePinAuthDialog(pin: pin),
|
||||
);
|
||||
return result == true;
|
||||
}
|
||||
|
||||
final BuildContext context;
|
||||
final CiString pin;
|
||||
}
|
||||
|
||||
class _PasswordAuthHandler implements _AuthHandler {
|
||||
const _PasswordAuthHandler(this.context, this.password);
|
||||
|
||||
@override
|
||||
Future<bool> auth() async {
|
||||
final result = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => ProtectedPagePasswordAuthDialog(password: password),
|
||||
);
|
||||
return result == true;
|
||||
}
|
||||
|
||||
final BuildContext context;
|
||||
final CiString password;
|
||||
}
|
14
app/lib/protected_page_handler.g.dart
Normal file
14
app/lib/protected_page_handler.g.dart
Normal file
|
@ -0,0 +1,14 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'protected_page_handler.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// NpLogGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$_BiometricAuthHandlerNpLog on _BiometricAuthHandler {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log = Logger("protected_page_handler._BiometricAuthHandler");
|
||||
}
|
100
app/lib/widget/protected_page_password_auth_dialog.dart
Normal file
100
app/lib/widget/protected_page_password_auth_dialog.dart
Normal file
|
@ -0,0 +1,100 @@
|
|||
import 'package:copy_with/copy_with.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/animation_util.dart';
|
||||
import 'package:nc_photos/app_localizations.dart';
|
||||
import 'package:nc_photos/bloc_util.dart';
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
import 'package:np_common/unique.dart';
|
||||
import 'package:np_string/np_string.dart';
|
||||
import 'package:to_string/to_string.dart';
|
||||
|
||||
part 'protected_page_password_auth_dialog.g.dart';
|
||||
part 'protected_page_password_auth_dialog/bloc.dart';
|
||||
part 'protected_page_password_auth_dialog/state_event.dart';
|
||||
part 'protected_page_password_auth_dialog/view.dart';
|
||||
|
||||
class ProtectedPagePasswordAuthDialog extends StatelessWidget {
|
||||
const ProtectedPagePasswordAuthDialog({
|
||||
super.key,
|
||||
required this.password,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => _Bloc(
|
||||
password: password,
|
||||
),
|
||||
child: _WrappedProtectedPagePasswordAuthDialog(),
|
||||
);
|
||||
}
|
||||
|
||||
final CiString password;
|
||||
}
|
||||
|
||||
class _WrappedProtectedPagePasswordAuthDialog extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocListener(
|
||||
listeners: [
|
||||
_BlocListenerT(
|
||||
selector: (state) => state.isAuthorized,
|
||||
listener: (context, isAuthorized) {
|
||||
if (isAuthorized.value == true) {
|
||||
Navigator.of(context).pop(true);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
child: AlertDialog(
|
||||
title: Text(L10n.global().appLockUnlockHint),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextField(
|
||||
keyboardType: TextInputType.text,
|
||||
obscureText: true,
|
||||
enableSuggestions: false,
|
||||
autocorrect: false,
|
||||
decoration: InputDecoration(
|
||||
hintText: L10n.global().passwordInputHint,
|
||||
),
|
||||
onSubmitted: (value) {
|
||||
context.addEvent(_Submit(value));
|
||||
},
|
||||
),
|
||||
_BlocSelector(
|
||||
selector: (state) => state.isAuthorized,
|
||||
builder: (context, isAuthorized) {
|
||||
if (isAuthorized.value == false) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.only(top: 8, bottom: 4),
|
||||
child: _ErrorNotice(),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// typedef _BlocBuilder = BlocBuilder<_Bloc, _State>;
|
||||
// typedef _BlocListener = BlocListener<_Bloc, _State>;
|
||||
typedef _BlocListenerT<T> = BlocListenerT<_Bloc, _State, T>;
|
||||
typedef _BlocSelector<T> = BlocSelector<_Bloc, _State, T>;
|
||||
|
||||
extension on BuildContext {
|
||||
_Bloc get bloc => read<_Bloc>();
|
||||
// _State get state => bloc.state;
|
||||
void addEvent(_Event event) => bloc.add(event);
|
||||
}
|
63
app/lib/widget/protected_page_password_auth_dialog.g.dart
Normal file
63
app/lib/widget/protected_page_password_auth_dialog.g.dart
Normal file
|
@ -0,0 +1,63 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'protected_page_password_auth_dialog.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// CopyWithLintRuleGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// ignore_for_file: library_private_types_in_public_api, duplicate_ignore
|
||||
|
||||
// **************************************************************************
|
||||
// CopyWithGenerator
|
||||
// **************************************************************************
|
||||
|
||||
abstract class $_StateCopyWithWorker {
|
||||
_State call({Unique<bool?>? isAuthorized});
|
||||
}
|
||||
|
||||
class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker {
|
||||
_$_StateCopyWithWorkerImpl(this.that);
|
||||
|
||||
@override
|
||||
_State call({dynamic isAuthorized}) {
|
||||
return _State(
|
||||
isAuthorized: isAuthorized as Unique<bool?>? ?? that.isAuthorized);
|
||||
}
|
||||
|
||||
final _State that;
|
||||
}
|
||||
|
||||
extension $_StateCopyWith on _State {
|
||||
$_StateCopyWithWorker get copyWith => _$copyWith;
|
||||
$_StateCopyWithWorker get _$copyWith => _$_StateCopyWithWorkerImpl(this);
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// NpLogGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$_BlocNpLog on _Bloc {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log = Logger("widget.protected_page_password_auth_dialog._Bloc");
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// ToStringGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$_StateToString on _State {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_State {isAuthorized: $isAuthorized}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$_SubmitToString on _Submit {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_Submit {value: $value}";
|
||||
}
|
||||
}
|
22
app/lib/widget/protected_page_password_auth_dialog/bloc.dart
Normal file
22
app/lib/widget/protected_page_password_auth_dialog/bloc.dart
Normal file
|
@ -0,0 +1,22 @@
|
|||
part of '../protected_page_password_auth_dialog.dart';
|
||||
|
||||
@npLog
|
||||
class _Bloc extends Bloc<_Event, _State> with BlocLogger {
|
||||
_Bloc({
|
||||
required this.password,
|
||||
}) : _hasher = sha256,
|
||||
super(_State.init()) {
|
||||
on<_Submit>(_onSubmit);
|
||||
}
|
||||
|
||||
void _onSubmit(_Submit ev, Emitter<_State> emit) {
|
||||
_log.info(ev);
|
||||
final hash = _hasher.convert(ev.value.codeUnits);
|
||||
final isAuth = hash.toString().toCi() == password;
|
||||
emit(state.copyWith(isAuthorized: Unique(isAuth)));
|
||||
}
|
||||
|
||||
final CiString password;
|
||||
|
||||
final Hash _hasher;
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
part of '../protected_page_password_auth_dialog.dart';
|
||||
|
||||
@genCopyWith
|
||||
@toString
|
||||
class _State {
|
||||
const _State({
|
||||
required this.isAuthorized,
|
||||
});
|
||||
|
||||
factory _State.init() => _State(
|
||||
isAuthorized: Unique(null),
|
||||
);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final Unique<bool?> isAuthorized;
|
||||
}
|
||||
|
||||
abstract class _Event {
|
||||
const _Event();
|
||||
}
|
||||
|
||||
@toString
|
||||
class _Submit implements _Event {
|
||||
const _Submit(this.value);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final String value;
|
||||
}
|
44
app/lib/widget/protected_page_password_auth_dialog/view.dart
Normal file
44
app/lib/widget/protected_page_password_auth_dialog/view.dart
Normal file
|
@ -0,0 +1,44 @@
|
|||
part of '../protected_page_password_auth_dialog.dart';
|
||||
|
||||
class _ErrorNotice extends StatefulWidget {
|
||||
const _ErrorNotice();
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _ErrorNoticeState();
|
||||
}
|
||||
|
||||
class _ErrorNoticeState extends State<_ErrorNotice>
|
||||
with TickerProviderStateMixin {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_controller.forward(from: 0);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BlocListenerT(
|
||||
selector: (state) => state.isAuthorized,
|
||||
listener: (context, isAuthorized) {
|
||||
if (isAuthorized.value == false) {
|
||||
_controller.forward(from: 0);
|
||||
}
|
||||
},
|
||||
child: SlideTransition(
|
||||
position: _controller.drive(Animatable<Offset>.fromCallback(
|
||||
(t) => Offset(tremblingTransform(3, t) * .05, 0))),
|
||||
child: Text(
|
||||
L10n.global().appLockUnlockWrongPassword,
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
late final _controller = AnimationController(vsync: this)
|
||||
..duration = k.animationDurationLong;
|
||||
}
|
154
app/lib/widget/protected_page_pin_auth_dialog.dart
Normal file
154
app/lib/widget/protected_page_pin_auth_dialog.dart
Normal file
|
@ -0,0 +1,154 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:copy_with/copy_with.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/animation_util.dart';
|
||||
import 'package:nc_photos/app_localizations.dart';
|
||||
import 'package:nc_photos/bloc_util.dart';
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
import 'package:np_collection/np_collection.dart';
|
||||
import 'package:np_common/unique.dart';
|
||||
import 'package:np_string/np_string.dart';
|
||||
import 'package:to_string/to_string.dart';
|
||||
|
||||
part 'protected_page_pin_auth_dialog.g.dart';
|
||||
part 'protected_page_pin_auth_dialog/bloc.dart';
|
||||
part 'protected_page_pin_auth_dialog/state_event.dart';
|
||||
part 'protected_page_pin_auth_dialog/view.dart';
|
||||
|
||||
class ProtectedPagePinAuthDialog extends StatelessWidget {
|
||||
const ProtectedPagePinAuthDialog({
|
||||
super.key,
|
||||
required this.pin,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => _Bloc(
|
||||
pin: pin,
|
||||
removeItemBuilder: (_, animation, value) => ScaleTransition(
|
||||
scale: animation.drive(CurveTween(curve: Curves.linear)),
|
||||
child: _ObsecuredDigitDisplay(randomInt: value),
|
||||
),
|
||||
),
|
||||
child: _WrappedProtectedPagePinAuthDialog(),
|
||||
);
|
||||
}
|
||||
|
||||
final CiString pin;
|
||||
}
|
||||
|
||||
class _WrappedProtectedPagePinAuthDialog extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocListener(
|
||||
listeners: [
|
||||
_BlocListenerT(
|
||||
selector: (state) => state.isAuthorized,
|
||||
listener: (context, isAuthorized) {
|
||||
if (isAuthorized) {
|
||||
Navigator.of(context).pop(true);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
child: AlertDialog(
|
||||
title: Text(L10n.global().appLockUnlockHint),
|
||||
scrollable: true,
|
||||
content: SizedBox(
|
||||
width: 280,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 64,
|
||||
child: Align(
|
||||
alignment: Alignment(0, -0.5),
|
||||
child: _ObsecuredInputView(),
|
||||
),
|
||||
),
|
||||
Table(
|
||||
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
|
||||
children: [
|
||||
TableRow(
|
||||
children: [1, 2, 3]
|
||||
.map(
|
||||
(e) => _DigitButton(
|
||||
child: Text(e.toString()),
|
||||
onTap: () {
|
||||
context.addEvent(_PushDigit(e));
|
||||
},
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
TableRow(
|
||||
children: [4, 5, 6]
|
||||
.map(
|
||||
(e) => _DigitButton(
|
||||
child: Text(e.toString()),
|
||||
onTap: () {
|
||||
context.addEvent(_PushDigit(e));
|
||||
},
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
TableRow(
|
||||
children: [7, 8, 9]
|
||||
.map(
|
||||
(e) => _DigitButton(
|
||||
child: Text(e.toString()),
|
||||
onTap: () {
|
||||
context.addEvent(_PushDigit(e));
|
||||
},
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
TableRow(
|
||||
children: [
|
||||
const SizedBox.shrink(),
|
||||
_DigitButton(
|
||||
child: const Text("0"),
|
||||
onTap: () {
|
||||
context.addEvent(const _PushDigit(0));
|
||||
},
|
||||
),
|
||||
_BlocSelector(
|
||||
selector: (state) => state.obsecuredInput.isEmpty,
|
||||
builder: (context, isEmpty) => _BackspaceButton(
|
||||
onTap: isEmpty
|
||||
? null
|
||||
: () {
|
||||
context.addEvent(const _PopDigit());
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// typedef _BlocBuilder = BlocBuilder<_Bloc, _State>;
|
||||
// typedef _BlocListener = BlocListener<_Bloc, _State>;
|
||||
typedef _BlocListenerT<T> = BlocListenerT<_Bloc, _State, T>;
|
||||
typedef _BlocSelector<T> = BlocSelector<_Bloc, _State, T>;
|
||||
|
||||
extension on BuildContext {
|
||||
_Bloc get bloc => read<_Bloc>();
|
||||
_State get state => bloc.state;
|
||||
void addEvent(_Event event) => bloc.add(event);
|
||||
}
|
81
app/lib/widget/protected_page_pin_auth_dialog.g.dart
Normal file
81
app/lib/widget/protected_page_pin_auth_dialog.g.dart
Normal file
|
@ -0,0 +1,81 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'protected_page_pin_auth_dialog.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// CopyWithLintRuleGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// ignore_for_file: library_private_types_in_public_api, duplicate_ignore
|
||||
|
||||
// **************************************************************************
|
||||
// CopyWithGenerator
|
||||
// **************************************************************************
|
||||
|
||||
abstract class $_StateCopyWithWorker {
|
||||
_State call(
|
||||
{String? input,
|
||||
List<int>? obsecuredInput,
|
||||
bool? isAuthorized,
|
||||
Unique<bool?>? isPinError});
|
||||
}
|
||||
|
||||
class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker {
|
||||
_$_StateCopyWithWorkerImpl(this.that);
|
||||
|
||||
@override
|
||||
_State call(
|
||||
{dynamic input,
|
||||
dynamic obsecuredInput,
|
||||
dynamic isAuthorized,
|
||||
dynamic isPinError}) {
|
||||
return _State(
|
||||
input: input as String? ?? that.input,
|
||||
obsecuredInput: obsecuredInput as List<int>? ?? that.obsecuredInput,
|
||||
isAuthorized: isAuthorized as bool? ?? that.isAuthorized,
|
||||
isPinError: isPinError as Unique<bool?>? ?? that.isPinError);
|
||||
}
|
||||
|
||||
final _State that;
|
||||
}
|
||||
|
||||
extension $_StateCopyWith on _State {
|
||||
$_StateCopyWithWorker get copyWith => _$copyWith;
|
||||
$_StateCopyWithWorker get _$copyWith => _$_StateCopyWithWorkerImpl(this);
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// NpLogGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$_BlocNpLog on _Bloc {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log = Logger("widget.protected_page_pin_auth_dialog._Bloc");
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// ToStringGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$_StateToString on _State {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_State {input: $input, obsecuredInput: [length: ${obsecuredInput.length}], isAuthorized: $isAuthorized, isPinError: $isPinError}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$_PushDigitToString on _PushDigit {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_PushDigit {digit: $digit}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$_PopDigitToString on _PopDigit {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_PopDigit {}";
|
||||
}
|
||||
}
|
68
app/lib/widget/protected_page_pin_auth_dialog/bloc.dart
Normal file
68
app/lib/widget/protected_page_pin_auth_dialog/bloc.dart
Normal file
|
@ -0,0 +1,68 @@
|
|||
part of '../protected_page_pin_auth_dialog.dart';
|
||||
|
||||
@npLog
|
||||
class _Bloc extends Bloc<_Event, _State> with BlocLogger {
|
||||
_Bloc({
|
||||
required this.pin,
|
||||
required this.removeItemBuilder,
|
||||
}) : _rand = Random(),
|
||||
_hasher = sha256,
|
||||
super(_State.init()) {
|
||||
on<_PushDigit>(_onPushDigit);
|
||||
on<_PopDigit>(_onPopDigit);
|
||||
}
|
||||
|
||||
void _onPushDigit(_PushDigit ev, Emitter<_State> emit) {
|
||||
_log.info(ev);
|
||||
if (state.input.length >= 6) {
|
||||
// max length of pin is 6
|
||||
emit(state.copyWith(isPinError: Unique(true)));
|
||||
return;
|
||||
}
|
||||
final index = state.input.length;
|
||||
emit(state.copyWith(
|
||||
input: "${state.input}${ev.digit}",
|
||||
obsecuredInput: state.obsecuredInput.added(_rand.nextInt(65536)),
|
||||
));
|
||||
listKey.currentState?.insertItem(
|
||||
index,
|
||||
duration: k.animationDurationLong,
|
||||
);
|
||||
|
||||
if (state.input.length >= 4) {
|
||||
// valid pin must contain at least 4 digits
|
||||
final hash = _hasher.convert(state.input.codeUnits);
|
||||
if (hash.toString().toCi() == pin) {
|
||||
emit(state.copyWith(isAuthorized: true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _onPopDigit(_PopDigit ev, Emitter<_State> emit) {
|
||||
_log.info(ev);
|
||||
if (state.input.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final index = state.input.length - 1;
|
||||
final item = state.obsecuredInput.last;
|
||||
emit(state.copyWith(
|
||||
input: state.input.slice(0, -1),
|
||||
obsecuredInput: state.obsecuredInput.slice(0, -1),
|
||||
));
|
||||
listKey.currentState?.removeItem(
|
||||
index,
|
||||
(context, animation) => removeItemBuilder(context, animation, item),
|
||||
duration: k.animationDurationNormal,
|
||||
);
|
||||
}
|
||||
|
||||
final CiString pin;
|
||||
final Widget Function(
|
||||
BuildContext context, Animation<double> animation, int value)
|
||||
removeItemBuilder;
|
||||
|
||||
final listKey = GlobalKey<AnimatedListState>();
|
||||
|
||||
final Random _rand;
|
||||
final Hash _hasher;
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
part of '../protected_page_pin_auth_dialog.dart';
|
||||
|
||||
@genCopyWith
|
||||
@toString
|
||||
class _State {
|
||||
const _State({
|
||||
required this.input,
|
||||
required this.obsecuredInput,
|
||||
required this.isAuthorized,
|
||||
required this.isPinError,
|
||||
});
|
||||
|
||||
factory _State.init() => _State(
|
||||
input: "",
|
||||
obsecuredInput: const [],
|
||||
isAuthorized: false,
|
||||
isPinError: Unique(null),
|
||||
);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final String input;
|
||||
final List<int> obsecuredInput;
|
||||
final bool isAuthorized;
|
||||
final Unique<bool?> isPinError;
|
||||
}
|
||||
|
||||
abstract class _Event {
|
||||
const _Event();
|
||||
}
|
||||
|
||||
@toString
|
||||
class _PushDigit implements _Event {
|
||||
const _PushDigit(this.digit);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final int digit;
|
||||
}
|
||||
|
||||
@toString
|
||||
class _PopDigit implements _Event {
|
||||
const _PopDigit();
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
}
|
130
app/lib/widget/protected_page_pin_auth_dialog/view.dart
Normal file
130
app/lib/widget/protected_page_pin_auth_dialog/view.dart
Normal file
|
@ -0,0 +1,130 @@
|
|||
part of '../protected_page_pin_auth_dialog.dart';
|
||||
|
||||
class _ObsecuredInputView extends StatefulWidget {
|
||||
const _ObsecuredInputView();
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _ObsecuredInputViewState();
|
||||
}
|
||||
|
||||
class _ObsecuredInputViewState extends State<_ObsecuredInputView>
|
||||
with TickerProviderStateMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BlocListenerT(
|
||||
selector: (state) => state.isPinError,
|
||||
listener: (context, isPinError) {
|
||||
if (isPinError.value == true) {
|
||||
_controller.forward(from: 0);
|
||||
}
|
||||
},
|
||||
child: SlideTransition(
|
||||
position: _controller.drive(Animatable<Offset>.fromCallback(
|
||||
(t) => Offset(tremblingTransform(3, t) * .05, 0))),
|
||||
child: AnimatedList(
|
||||
key: context.bloc.listKey,
|
||||
scrollDirection: Axis.horizontal,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
initialItemCount: context.state.obsecuredInput.length,
|
||||
itemBuilder: (context, index, animation) => ScaleTransition(
|
||||
scale: animation.drive(CurveTween(curve: Curves.elasticOut)),
|
||||
child: _ObsecuredDigitDisplay(
|
||||
randomInt: context.state.obsecuredInput[index],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
late final _controller = AnimationController(vsync: this)
|
||||
..duration = k.animationDurationLong;
|
||||
}
|
||||
|
||||
class _ObsecuredDigitDisplay extends StatelessWidget {
|
||||
_ObsecuredDigitDisplay({
|
||||
required int randomInt,
|
||||
}) : text = String.fromCharCode(0x1f600 + (randomInt % 0x30));
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 2),
|
||||
child: Text(
|
||||
text,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final String text;
|
||||
}
|
||||
|
||||
class _DigitButton extends StatelessWidget {
|
||||
const _DigitButton({
|
||||
required this.child,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(2),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.all(8),
|
||||
height: 56,
|
||||
child: DefaultTextStyle(
|
||||
style: Theme.of(context).textTheme.headlineMedium!,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final Widget child;
|
||||
final VoidCallback? onTap;
|
||||
}
|
||||
|
||||
class _BackspaceButton extends StatelessWidget {
|
||||
const _BackspaceButton({
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.all(8),
|
||||
height: 56,
|
||||
child: Icon(
|
||||
Icons.backspace_outlined,
|
||||
color: onTap == null ? Theme.of(context).disabledColor : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final VoidCallback? onTap;
|
||||
}
|
|
@ -12,6 +12,7 @@ import 'package:nc_photos/entity/pref.dart';
|
|||
import 'package:nc_photos/k.dart' as k;
|
||||
import 'package:nc_photos/mobile/android/activity.dart';
|
||||
import 'package:nc_photos/mobile/android/permission_util.dart';
|
||||
import 'package:nc_photos/protected_page_handler.dart';
|
||||
import 'package:nc_photos/use_case/compat/v29.dart';
|
||||
import 'package:nc_photos/use_case/compat/v46.dart';
|
||||
import 'package:nc_photos/use_case/compat/v55.dart';
|
||||
|
@ -64,7 +65,7 @@ class _SplashState extends State<Splash> {
|
|||
_isUpgrading = false;
|
||||
});
|
||||
}
|
||||
unawaited(_exit());
|
||||
_exit();
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -135,24 +136,30 @@ class _SplashState extends State<Splash> {
|
|||
);
|
||||
}
|
||||
|
||||
Future<void> _exit() async {
|
||||
void _exit() {
|
||||
_log.info("[_exit]");
|
||||
final account = Pref().getCurrentAccount();
|
||||
if (isNeedSetup()) {
|
||||
unawaited(Navigator.pushReplacementNamed(context, Setup.routeName));
|
||||
Navigator.pushReplacementNamed(context, Setup.routeName);
|
||||
} else if (account == null) {
|
||||
unawaited(Navigator.pushReplacementNamed(context, SignIn.routeName));
|
||||
Navigator.pushReplacementNamed(context, SignIn.routeName);
|
||||
} else {
|
||||
unawaited(
|
||||
Navigator.pushReplacementNamed(context, Home.routeName,
|
||||
arguments: HomeArguments(account)),
|
||||
);
|
||||
if (getRawPlatform() == NpPlatform.android) {
|
||||
final initialRoute = await Activity.consumeInitialRoute();
|
||||
if (initialRoute != null) {
|
||||
unawaited(Navigator.pushNamed(context, initialRoute));
|
||||
Navigator.of(context)
|
||||
.pushReplacementProtected(Home.routeName,
|
||||
arguments: HomeArguments(account))
|
||||
.then((value) async {
|
||||
if (getRawPlatform() == NpPlatform.android) {
|
||||
final initialRoute = await Activity.consumeInitialRoute();
|
||||
if (initialRoute != null) {
|
||||
unawaited(Navigator.pushNamed(context, initialRoute));
|
||||
}
|
||||
}
|
||||
}
|
||||
}).onError<ProtectedPageAuthException>((_, __) async {
|
||||
_log.warning("[_exit] Auth failed");
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
_exit();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -277,7 +277,7 @@ packages:
|
|||
source: hosted
|
||||
version: "1.6.3"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: crypto
|
||||
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
|
||||
|
@ -814,6 +814,46 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
local_auth:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: local_auth
|
||||
sha256: "280421b416b32de31405b0a25c3bd42dfcef2538dfbb20c03019e02a5ed55ed0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
local_auth_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: local_auth_android
|
||||
sha256: e0e5b1ea247c5a0951c13a7ee13dc1beae69750e6a2e1910d1ed6a3cd4d56943
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.38"
|
||||
local_auth_darwin:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: local_auth_darwin
|
||||
sha256: e424ebf90d5233452be146d4a7da4bcd7a70278b67791592f3fde1bda8eef9e2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.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: "505ba3367ca781efb1c50d3132e44a2446bccc4163427bc203b9b4d8994d97ea"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.10"
|
||||
logger:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1842,18 +1882,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: "5a751eddf9db89b3e5f9d50c20ab8612296e4e8db69009788d6c8b060a84191c"
|
||||
sha256: f2add6fa510d3ae152903412227bda57d0d5a8da61d2c39c1fb022c9429a41c0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.4"
|
||||
version: "5.0.6"
|
||||
win32_registry:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32_registry
|
||||
sha256: "1c52f994bdccb77103a6231ad4ea331a244dbcef5d1f37d8462f713143b0bfae"
|
||||
sha256: e4506d60b7244251bc59df15656a3093501c37fb5af02105a944d73eb95be4c9
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
version: "1.1.1"
|
||||
wkt_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -46,6 +46,7 @@ dependencies:
|
|||
url: https://gitlab.com/nkming2/dart-copy-with
|
||||
path: copy_with
|
||||
ref: copy_with-1.3.0
|
||||
crypto: ^3.0.3
|
||||
devicelocale: ^0.6.0
|
||||
device_info_plus: ^9.0.1
|
||||
draggable_scrollbar:
|
||||
|
@ -83,6 +84,7 @@ dependencies:
|
|||
path: library
|
||||
intl: 0.18.1
|
||||
kiwi: ^4.1.0
|
||||
local_auth: ^2.2.0
|
||||
logging: ^1.2.0
|
||||
memory_info: ^0.0.4
|
||||
mime: ^1.0.5
|
||||
|
|
|
@ -82,4 +82,6 @@ extension ListExtension<T> on List<T> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
List<T> added(T value) => toList()..add(value);
|
||||
}
|
||||
|
|
16
np_common/lib/unique.dart
Normal file
16
np_common/lib/unique.dart
Normal file
|
@ -0,0 +1,16 @@
|
|||
import 'package:to_string/to_string.dart';
|
||||
|
||||
part 'unique.g.dart';
|
||||
|
||||
/// An unique value does not compare equal with others having the same value,
|
||||
/// instead only the same instances are considered equal
|
||||
@toString
|
||||
class Unique<T> {
|
||||
// no const!
|
||||
Unique(this.value);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final T value;
|
||||
}
|
14
np_common/lib/unique.g.dart
Normal file
14
np_common/lib/unique.g.dart
Normal file
|
@ -0,0 +1,14 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'unique.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// ToStringGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$UniqueToString on Unique {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "Unique {value: $value}";
|
||||
}
|
||||
}
|
15
np_common/test/unique_test.dart
Normal file
15
np_common/test/unique_test.dart
Normal file
|
@ -0,0 +1,15 @@
|
|||
import 'package:np_common/unique.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group("Unique", () {
|
||||
test("same value", () {
|
||||
expect(Unique(1) == Unique(1), false);
|
||||
});
|
||||
|
||||
test("same instance", () {
|
||||
final a = Unique(1);
|
||||
expect(a == a, true);
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue