import 'package:flutter/material.dart'; import '../l10n/strings.dart'; import '../services/vault.dart'; /// A password field with a show/hide toggle. Nothing is trimmed, and any /// keyboard may be used (a numeric PIN is just a short password). class PasswordField extends StatefulWidget { final TextEditingController controller; final String label; final bool autofocus; final ValueChanged? onSubmitted; final ValueChanged? onChanged; final TextInputAction? action; const PasswordField({ required this.controller, required this.label, this.autofocus = false, this.onSubmitted, this.onChanged, this.action, super.key, }); @override State createState() => _PasswordFieldState(); } class _PasswordFieldState extends State { bool visible = false; @override Widget build(BuildContext context) => TextField( controller: widget.controller, obscureText: !visible, enableSuggestions: false, autocorrect: false, autofocus: widget.autofocus, keyboardType: visible ? TextInputType.visiblePassword : TextInputType.text, textInputAction: widget.action, onSubmitted: widget.onSubmitted, onChanged: widget.onChanged, decoration: InputDecoration( labelText: widget.label, suffixIcon: IconButton( tooltip: context.tr(visible ? 'hidePassword' : 'showPassword'), icon: Icon(visible ? Icons.visibility_off : Icons.visibility), onPressed: () => setState(() => visible = !visible), ), ), ); } /// Length counter, strength meter and short-password advice for a new /// master password. class PasswordAdvice extends StatelessWidget { final String password; const PasswordAdvice(this.password, {super.key}); @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; final n = passwordLength(password); final strength = passwordStrength(password); final inRange = n >= minPasswordLength && n <= maxPasswordLength; final colors = [ scheme.error, Colors.deepOrange, Colors.amber.shade700, Colors.lightGreen.shade700, Colors.green.shade700, ]; final muted = TextStyle(color: scheme.onSurfaceVariant, fontSize: 12); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( children: [ Expanded( child: Semantics( label: context.tr('passwordStrength'), value: context.tr('strength$strength'), child: ClipRRect( borderRadius: BorderRadius.circular(4), child: LinearProgressIndicator( value: n == 0 ? 0 : (strength + 1) / 5, minHeight: 6, color: colors[strength], backgroundColor: scheme.surfaceContainerHighest, ), ), ), ), const SizedBox(width: 12), Text( '$n / $maxPasswordLength', style: muted.copyWith( color: n > maxPasswordLength ? scheme.error : null, fontFeatures: const [FontFeature.tabularFigures()], ), ), ], ), const SizedBox(height: 4), Text( n == 0 ? context.tr('passwordLength') : '${context.tr('passwordStrength')}: ' '${context.tr('strength$strength')}' '${inRange ? '' : ' ยท ${context.tr('passwordLength')}'}', style: muted.copyWith(color: inRange || n == 0 ? null : scheme.error), ), if (inRange && n < 12) ...[ const SizedBox(height: 4), Text( context.tr('shortPasswordWarning'), style: muted.copyWith(color: Colors.deepOrange), ), ], ], ); } }