Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions lib/config_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ class ConfigKey {
static const String requirePassword = "requirePassword";
static const String biometricUnlock = "biometricUnlock";
static const String passwordHash = "passwordHash";
static const String passwordIsPin = "passwordIsPin";
// DEPRECATED
static const String imageQuality = "imageQuality";
static const String homePageViewMode = "homePageViewMode";
Expand Down Expand Up @@ -157,7 +158,8 @@ class ConfigProvider with ChangeNotifier {
final Map<String, dynamic> _secureConfig = {
ConfigKey.requirePassword: false,
ConfigKey.biometricUnlock: false,
ConfigKey.passwordHash: ""
ConfigKey.passwordHash: "",
ConfigKey.passwordIsPin: false,
};

bool _isSecureKey(String key) => _secureConfig.containsKey(key);
Expand Down Expand Up @@ -237,7 +239,8 @@ class ConfigProvider with ChangeNotifier {
// Never let a migration failure abort startup; settings fall back to
// defaults via readConfig() if the config can't be moved.
try {
_logger.info('Config migration started: ${oldFile.path} -> ${newFile.path}');
_logger
.info('Config migration started: ${oldFile.path} -> ${newFile.path}');
await oldFile.copy(newFile.path);
if (newFile.existsSync() && newFile.lengthSync() > 0) {
await oldFile.delete();
Expand Down
45 changes: 41 additions & 4 deletions lib/widgets/auth_popup.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,20 +39,50 @@ class _AuthPopupState extends State<AuthPopup> {
String? _error;
bool _showPassword = false;
bool _biometricsPrompted = false;
bool _isPin = false;
bool _passwordFocusRequested = false;
Animation<double>? _routeAnimation;

@override
void initState() {
super.initState();
// Auto-focus password field in unlock mode
if (widget.mode == AuthPopupMode.unlock) {
_isPin = ConfigProvider.instance.get(ConfigKey.passwordIsPin) ?? false;
}
}

@override
void didChangeDependencies() {
super.didChangeDependencies();
// Request keyboard focus after dialog animation completes
if (widget.mode == AuthPopupMode.unlock && !widget.showBiometrics) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_passwordFocusNode.requestFocus();
});
final animation = ModalRoute.of(context)?.animation;
if (animation != _routeAnimation) {
_routeAnimation?.removeStatusListener(_handleRouteAnimationStatus);
_routeAnimation = animation;
_routeAnimation?.addStatusListener(_handleRouteAnimationStatus);
if (_routeAnimation == null || _routeAnimation!.isCompleted) {
_requestPasswordFocus();
}
}
}
}

void _handleRouteAnimationStatus(AnimationStatus status) {
if (status == AnimationStatus.completed) {
_requestPasswordFocus();
}
}

void _requestPasswordFocus() {
if (_passwordFocusRequested) return;
_passwordFocusRequested = true;
_passwordFocusNode.requestFocus();
}

@override
void dispose() {
_routeAnimation?.removeStatusListener(_handleRouteAnimationStatus);
_oldController.dispose();
_passwordController.dispose();
_confirmController.dispose();
Expand All @@ -64,9 +94,13 @@ class _AuthPopupState extends State<AuthPopup> {
return sha256.convert(utf8.encode(password)).toString();
}

bool _isNumericOnly(String value) => RegExp(r'^\d+$').hasMatch(value);

Future<void> savePassword(String password) async {
await ConfigProvider.instance
.set(ConfigKey.passwordHash, await _hashPassword(password));
await ConfigProvider.instance
.set(ConfigKey.passwordIsPin, _isNumericOnly(password));
}

Future<bool> validatePassword(String password) async {
Expand Down Expand Up @@ -205,6 +239,9 @@ class _AuthPopupState extends State<AuthPopup> {
focusNode: _passwordFocusNode,
obscureText: !_showPassword,
autocorrect: false,
keyboardType: widget.mode == AuthPopupMode.unlock && _isPin
? TextInputType.number
: TextInputType.text,
decoration: InputDecoration(
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12.0)),
Expand Down
Loading