From e2beaeab22d3ce5b9fb70472425f5a4859c7f6e4 Mon Sep 17 00:00:00 2001 From: Demizo Date: Sun, 16 Aug 2026 11:39:40 -0500 Subject: [PATCH 1/2] feat: show numeric keyboard when the password is a pin --- lib/config_provider.dart | 7 +++++-- lib/widgets/auth_popup.dart | 11 +++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/lib/config_provider.dart b/lib/config_provider.dart index f4c5bb92..7cdfcd24 100644 --- a/lib/config_provider.dart +++ b/lib/config_provider.dart @@ -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"; @@ -157,7 +158,8 @@ class ConfigProvider with ChangeNotifier { final Map _secureConfig = { ConfigKey.requirePassword: false, ConfigKey.biometricUnlock: false, - ConfigKey.passwordHash: "" + ConfigKey.passwordHash: "", + ConfigKey.passwordIsPin: false, }; bool _isSecureKey(String key) => _secureConfig.containsKey(key); @@ -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(); diff --git a/lib/widgets/auth_popup.dart b/lib/widgets/auth_popup.dart index 91a781c3..500675ef 100644 --- a/lib/widgets/auth_popup.dart +++ b/lib/widgets/auth_popup.dart @@ -39,6 +39,7 @@ class _AuthPopupState extends State { String? _error; bool _showPassword = false; bool _biometricsPrompted = false; + bool _isPin = false; @override void initState() { @@ -49,6 +50,9 @@ class _AuthPopupState extends State { _passwordFocusNode.requestFocus(); }); } + if (widget.mode == AuthPopupMode.unlock) { + _isPin = ConfigProvider.instance.get(ConfigKey.passwordIsPin) ?? false; + } } @override @@ -64,9 +68,13 @@ class _AuthPopupState extends State { return sha256.convert(utf8.encode(password)).toString(); } + bool _isNumericOnly(String value) => RegExp(r'^\d+$').hasMatch(value); + Future savePassword(String password) async { await ConfigProvider.instance .set(ConfigKey.passwordHash, await _hashPassword(password)); + await ConfigProvider.instance + .set(ConfigKey.passwordIsPin, _isNumericOnly(password)); } Future validatePassword(String password) async { @@ -205,6 +213,9 @@ class _AuthPopupState extends State { 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)), From 549746050af15509416bf8f00ee81292fbc59f07 Mon Sep 17 00:00:00 2001 From: Demizo Date: Sun, 16 Aug 2026 14:21:39 -0500 Subject: [PATCH 2/2] fix: open keyboard after dialog animation While the password input was focused upon launch, requesting the focus prior to the animation completing resulted in the keyboard not popping up on all devices. --- lib/widgets/auth_popup.dart | 38 +++++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/lib/widgets/auth_popup.dart b/lib/widgets/auth_popup.dart index 500675ef..38147aa3 100644 --- a/lib/widgets/auth_popup.dart +++ b/lib/widgets/auth_popup.dart @@ -40,23 +40,49 @@ class _AuthPopupState extends State { bool _showPassword = false; bool _biometricsPrompted = false; bool _isPin = false; + bool _passwordFocusRequested = false; + Animation? _routeAnimation; @override void initState() { super.initState(); - // Auto-focus password field in unlock mode - if (widget.mode == AuthPopupMode.unlock && !widget.showBiometrics) { - WidgetsBinding.instance.addPostFrameCallback((_) { - _passwordFocusNode.requestFocus(); - }); - } 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) { + 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();