From 0e709f0085d56ca1a8fa9b84ee22bd185bdd6f8b Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Wed, 10 Jan 2024 20:47:54 +0100 Subject: [PATCH] Fix state of details column. --- lib/fido/views/unlocked_page.dart | 222 ++++++++++++++++-------------- lib/otp/views/otp_screen.dart | 85 +++++++----- lib/piv/views/piv_screen.dart | 60 +++++--- 3 files changed, 206 insertions(+), 161 deletions(-) diff --git a/lib/fido/views/unlocked_page.dart b/lib/fido/views/unlocked_page.dart index b47286da..64237e7f 100755 --- a/lib/fido/views/unlocked_page.dart +++ b/lib/fido/views/unlocked_page.dart @@ -39,102 +39,110 @@ import 'fingerprint_dialog.dart'; import 'key_actions.dart'; import 'rename_fingerprint_dialog.dart'; -final _selectedItem = StateProvider( - (ref) => null, -); - -Widget _registerFingerprintActions( - DevicePath devicePath, - Fingerprint fingerprint, { - required WidgetRef ref, - required Widget Function(BuildContext context) builder, - Map> actions = const {}, -}) { - final hasFeature = ref.watch(featureProvider); - return Actions( - actions: { - if (hasFeature(features.fingerprintsEdit)) - EditIntent: CallbackAction(onInvoke: (_) async { - final renamed = await ref.read(withContextProvider)( - (context) => showBlurDialog( - context: context, - builder: (context) => RenameFingerprintDialog( - devicePath, - fingerprint, - ), - )); - if (renamed != null && ref.read(_selectedItem) == fingerprint) { - ref.read(_selectedItem.notifier).state = renamed; - } - return renamed; - }), - if (hasFeature(features.fingerprintsDelete)) - DeleteIntent: CallbackAction(onInvoke: (_) async { - final deleted = await ref.read(withContextProvider)( - (context) => showBlurDialog( - context: context, - builder: (context) => DeleteFingerprintDialog( - devicePath, - fingerprint, - ), - )); - if (deleted == true && ref.read(_selectedItem) == fingerprint) { - ref.read(_selectedItem.notifier).state = null; - } - return deleted; - }), - ...actions, - }, - child: Builder(builder: builder), - ); -} - -Widget _registerCredentialActions( - DevicePath devicePath, - FidoCredential credential, { - required WidgetRef ref, - required Widget Function(BuildContext context) builder, - Map> actions = const {}, -}) { - final hasFeature = ref.watch(featureProvider); - return Actions( - actions: { - if (hasFeature(features.credentialsDelete)) - DeleteIntent: CallbackAction(onInvoke: (_) async { - final deleted = await ref.read(withContextProvider)( - (context) => showBlurDialog( - context: context, - builder: (context) => DeleteCredentialDialog( - devicePath, - credential, - ), - ), - ); - if (deleted == true && ref.read(_selectedItem) == credential) { - ref.read(_selectedItem.notifier).state = null; - } - return deleted; - }), - ...actions, - }, - child: Builder(builder: builder), - ); -} - -class FidoUnlockedPage extends ConsumerWidget { +class FidoUnlockedPage extends ConsumerStatefulWidget { final DeviceNode node; final FidoState state; - const FidoUnlockedPage(this.node, this.state, {super.key}); + FidoUnlockedPage(this.node, this.state) : super(key: ObjectKey(node.path)); @override - Widget build(BuildContext context, WidgetRef ref) { + ConsumerState createState() => + _FidoUnlockedPageState(); +} + +class _FidoUnlockedPageState extends ConsumerState { + Object? _selected; + + Widget _registerFingerprintActions( + Fingerprint fingerprint, { + required WidgetRef ref, + required Widget Function(BuildContext context) builder, + Map> actions = const {}, + }) { + final hasFeature = ref.watch(featureProvider); + return Actions( + actions: { + if (hasFeature(features.fingerprintsEdit)) + EditIntent: CallbackAction(onInvoke: (_) async { + final renamed = await ref.read(withContextProvider)( + (context) => showBlurDialog( + context: context, + builder: (context) => RenameFingerprintDialog( + widget.node.path, + fingerprint, + ), + )); + if (_selected == fingerprint && renamed != null) { + setState(() { + _selected = renamed; + }); + } + return renamed; + }), + if (hasFeature(features.fingerprintsDelete)) + DeleteIntent: CallbackAction(onInvoke: (_) async { + final deleted = await ref.read(withContextProvider)( + (context) => showBlurDialog( + context: context, + builder: (context) => DeleteFingerprintDialog( + widget.node.path, + fingerprint, + ), + )); + if (_selected == fingerprint && deleted == true) { + setState(() { + _selected = null; + }); + } + return deleted; + }), + ...actions, + }, + child: Builder(builder: builder), + ); + } + + Widget _registerCredentialActions( + FidoCredential credential, { + required WidgetRef ref, + required Widget Function(BuildContext context) builder, + Map> actions = const {}, + }) { + final hasFeature = ref.watch(featureProvider); + return Actions( + actions: { + if (hasFeature(features.credentialsDelete)) + DeleteIntent: CallbackAction(onInvoke: (_) async { + final deleted = await ref.read(withContextProvider)( + (context) => showBlurDialog( + context: context, + builder: (context) => DeleteCredentialDialog( + widget.node.path, + credential, + ), + ), + ); + if (_selected == credential && deleted == true) { + setState(() { + _selected = null; + }); + } + return deleted; + }), + ...actions, + }, + child: Builder(builder: builder), + ); + } + + @override + Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; - final selected = ref.watch(_selectedItem); + final selected = _selected; List children = []; - if (state.credMgmt) { - final data = ref.watch(credentialProvider(node.path)).asData; + if (widget.state.credMgmt) { + final data = ref.watch(credentialProvider(widget.node.path)).asData; if (data == null) { return _buildLoadingPage(context); } @@ -143,13 +151,14 @@ class FidoUnlockedPage extends ConsumerWidget { children.add((_) => ListTitle(l10n.s_passkeys)); children.addAll( creds.map((cred) => (expanded) => _registerCredentialActions( - node.path, cred, ref: ref, actions: { OpenIntent: CallbackAction(onInvoke: (_) { if (expanded) { - ref.read(_selectedItem.notifier).state = cred; + setState(() { + _selected = cred; + }); return null; } else { return showBlurDialog( @@ -170,8 +179,8 @@ class FidoUnlockedPage extends ConsumerWidget { } int nFingerprints = 0; - if (state.bioEnroll != null) { - final data = ref.watch(fingerprintProvider(node.path)).asData; + if (widget.state.bioEnroll != null) { + final data = ref.watch(fingerprintProvider(widget.node.path)).asData; if (data == null) { return _buildLoadingPage(context); } @@ -181,13 +190,14 @@ class FidoUnlockedPage extends ConsumerWidget { children.add((_) => ListTitle(l10n.s_fingerprints)); children.addAll( fingerprints.map((fp) => (expanded) => _registerFingerprintActions( - node.path, fp, ref: ref, actions: { OpenIntent: CallbackAction(onInvoke: (_) { if (expanded) { - ref.read(_selectedItem.notifier).state = fp; + setState(() { + _selected = fp; + }); return null; } else { return showBlurDialog( @@ -214,7 +224,9 @@ class FidoUnlockedPage extends ConsumerWidget { actions: { EscapeIntent: CallbackAction(onInvoke: (intent) { if (selected != null) { - ref.read(_selectedItem.notifier).state = null; + setState(() { + _selected = null; + }); } else { Actions.invoke(context, intent); } @@ -225,7 +237,7 @@ class FidoUnlockedPage extends ConsumerWidget { title: Text(l10n.s_webauthn), keyActionsBuilder: switch (selected) { FidoCredential credential => (context) => - _registerCredentialActions(node.path, credential, + _registerCredentialActions(credential, ref: ref, builder: (context) => Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -274,7 +286,6 @@ class FidoUnlockedPage extends ConsumerWidget { ], )), Fingerprint fingerprint => (context) => _registerFingerprintActions( - node.path, fingerprint, ref: ref, builder: (context) => Column( @@ -309,11 +320,11 @@ class FidoUnlockedPage extends ConsumerWidget { ), ), _ => hasActions - ? (context) => - fidoBuildActions(context, node, state, nFingerprints) + ? (context) => fidoBuildActions( + context, widget.node, widget.state, nFingerprints) : null }, - keyActionsBadge: fidoShowActionsNotifier(state), + keyActionsBadge: fidoShowActionsNotifier(widget.state), builder: (context, expanded) => Column( crossAxisAlignment: CrossAxisAlignment.start, children: children.map((f) => f(expanded)).toList()), @@ -321,7 +332,7 @@ class FidoUnlockedPage extends ConsumerWidget { ); } - if (state.bioEnroll != null) { + if (widget.state.bioEnroll != null) { return MessagePage( title: Text(l10n.s_webauthn), graphic: Icon(Icons.fingerprint, @@ -329,9 +340,10 @@ class FidoUnlockedPage extends ConsumerWidget { header: l10n.s_no_fingerprints, message: l10n.l_add_one_or_more_fps, keyActionsBuilder: hasActions - ? (context) => fidoBuildActions(context, node, state, 0) + ? (context) => + fidoBuildActions(context, widget.node, widget.state, 0) : null, - keyActionsBadge: fidoShowActionsNotifier(state), + keyActionsBadge: fidoShowActionsNotifier(widget.state), ); } @@ -342,9 +354,9 @@ class FidoUnlockedPage extends ConsumerWidget { header: l10n.l_no_discoverable_accounts, message: l10n.l_register_sk_on_websites, keyActionsBuilder: hasActions - ? (context) => fidoBuildActions(context, node, state, 0) + ? (context) => fidoBuildActions(context, widget.node, widget.state, 0) : null, - keyActionsBadge: fidoShowActionsNotifier(state), + keyActionsBadge: fidoShowActionsNotifier(widget.state), ); } diff --git a/lib/otp/views/otp_screen.dart b/lib/otp/views/otp_screen.dart index dfa882a7..45fdf391 100644 --- a/lib/otp/views/otp_screen.dart +++ b/lib/otp/views/otp_screen.dart @@ -37,20 +37,23 @@ import 'actions.dart'; import 'key_actions.dart'; import 'slot_dialog.dart'; -final _selectedSlot = StateProvider( - (ref) => null, -); - -class OtpScreen extends ConsumerWidget { +class OtpScreen extends ConsumerStatefulWidget { final DevicePath devicePath; - const OtpScreen(this.devicePath, {super.key}); + OtpScreen(this.devicePath) : super(key: ObjectKey(devicePath)); @override - Widget build(BuildContext context, WidgetRef ref) { + ConsumerState createState() => _OtpScreenState(); +} + +class _OtpScreenState extends ConsumerState { + SlotId? _selected; + + @override + Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; final hasFeature = ref.watch(featureProvider); - return ref.watch(otpStateProvider(devicePath)).when( + return ref.watch(otpStateProvider(widget.devicePath)).when( loading: () => MessagePage( title: Text(l10n.s_slots), graphic: const CircularProgressIndicator(), @@ -59,12 +62,16 @@ class OtpScreen extends ConsumerWidget { error: (error, _) => AppFailurePage(title: Text(l10n.s_slots), cause: error), data: (otpState) { - final selected = ref.watch(_selectedSlot); + final selected = _selected != null + ? otpState.slots.firstWhere((e) => e.slot == _selected) + : null; return Actions( actions: { EscapeIntent: CallbackAction(onInvoke: (intent) { if (selected != null) { - ref.read(_selectedSlot.notifier).state = null; + setState(() { + _selected = null; + }); } else { Actions.invoke(context, intent); } @@ -75,7 +82,7 @@ class OtpScreen extends ConsumerWidget { title: Text(l10n.s_slots), keyActionsBuilder: selected != null ? (context) => registerOtpActions( - devicePath, + widget.devicePath, selected, ref: ref, builder: (context) => Column( @@ -119,36 +126,45 @@ class OtpScreen extends ConsumerWidget { ), ) : (hasFeature(features.actions) - ? (context) => - otpBuildActions(context, devicePath, otpState, ref) + ? (context) => otpBuildActions( + context, widget.devicePath, otpState, ref) : null), builder: (context, expanded) { // De-select if window is resized to be non-expanded. if (!expanded) { Timer.run(() { - ref.read(_selectedSlot.notifier).state = null; + setState(() { + _selected = null; + }); }); } return Column(children: [ ListTitle(l10n.s_slots), - ...otpState.slots.map((e) => registerOtpActions(devicePath, e, - ref: ref, - actions: { - OpenIntent: - CallbackAction(onInvoke: (_) async { - if (expanded) { - ref.read(_selectedSlot.notifier).state = e; - } else { - await showBlurDialog( - context: context, - barrierColor: Colors.transparent, - builder: (context) => SlotDialog(e.slot), - ); - } - return null; - }), - }, - builder: (context) => _SlotListItem(e, expanded))) + ...otpState.slots + .map((e) => registerOtpActions(widget.devicePath, e, + ref: ref, + actions: { + OpenIntent: + CallbackAction(onInvoke: (_) async { + if (expanded) { + setState(() { + _selected = e.slot; + }); + } else { + await showBlurDialog( + context: context, + barrierColor: Colors.transparent, + builder: (context) => SlotDialog(e.slot), + ); + } + return null; + }), + }, + builder: (context) => _SlotListItem( + e, + expanded: expanded, + selected: e == selected, + ))) ]); }, ), @@ -160,8 +176,10 @@ class OtpScreen extends ConsumerWidget { class _SlotListItem extends ConsumerWidget { final OtpSlot otpSlot; final bool expanded; + final bool selected; - const _SlotListItem(this.otpSlot, this.expanded); + const _SlotListItem(this.otpSlot, + {required this.expanded, required this.selected}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -170,7 +188,6 @@ class _SlotListItem extends ConsumerWidget { final colorScheme = Theme.of(context).colorScheme; final isConfigured = otpSlot.isConfigured; final hasFeature = ref.watch(featureProvider); - final selected = ref.watch(_selectedSlot) == otpSlot; return AppListItem( selected: selected, diff --git a/lib/piv/views/piv_screen.dart b/lib/piv/views/piv_screen.dart index ac307f72..7a8ca58f 100644 --- a/lib/piv/views/piv_screen.dart +++ b/lib/piv/views/piv_screen.dart @@ -39,20 +39,23 @@ import 'cert_info_view.dart'; import 'key_actions.dart'; import 'slot_dialog.dart'; -final _selectedSlot = StateProvider( - (ref) => null, -); - -class PivScreen extends ConsumerWidget { +class PivScreen extends ConsumerStatefulWidget { final DevicePath devicePath; - const PivScreen(this.devicePath, {super.key}); + PivScreen(this.devicePath) : super(key: ObjectKey(devicePath)); @override - Widget build(BuildContext context, WidgetRef ref) { + ConsumerState createState() => _PivScreenState(); +} + +class _PivScreenState extends ConsumerState { + SlotId? _selected; + + @override + Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; final hasFeature = ref.watch(featureProvider); - return ref.watch(pivStateProvider(devicePath)).when( + return ref.watch(pivStateProvider(widget.devicePath)).when( loading: () => MessagePage( title: Text(l10n.s_certificates), graphic: const CircularProgressIndicator(), @@ -63,8 +66,11 @@ class PivScreen extends ConsumerWidget { cause: error, ), data: (pivState) { - final pivSlots = ref.watch(pivSlotsProvider(devicePath)).asData; - final selected = ref.watch(_selectedSlot); + final pivSlots = + ref.watch(pivSlotsProvider(widget.devicePath)).asData; + final selected = _selected != null + ? pivSlots?.value.firstWhere((e) => e.slot == _selected) + : null; final theme = Theme.of(context); final textTheme = theme.textTheme; // This is what ListTile uses for subtitle @@ -75,7 +81,9 @@ class PivScreen extends ConsumerWidget { actions: { EscapeIntent: CallbackAction(onInvoke: (intent) { if (selected != null) { - ref.read(_selectedSlot.notifier).state = null; + setState(() { + _selected = null; + }); } else { Actions.invoke(context, intent); } @@ -87,7 +95,7 @@ class PivScreen extends ConsumerWidget { keyActionsBuilder: selected != null // TODO: Reuse slot dialog ? (context) => registerPivActions( - devicePath, + widget.devicePath, pivState, selected, ref: ref, @@ -127,20 +135,22 @@ class PivScreen extends ConsumerWidget { ), if (hasFeature(features.actions)) ...[ pivBuildActions( - context, devicePath, pivState, ref), + context, widget.devicePath, pivState, ref), ], ], ), ) : (hasFeature(features.actions) - ? (context) => - pivBuildActions(context, devicePath, pivState, ref) + ? (context) => pivBuildActions( + context, widget.devicePath, pivState, ref) : null), builder: (context, expanded) { // De-select if window is resized to be non-expanded. if (!expanded) { Timer.run(() { - ref.read(_selectedSlot.notifier).state = null; + setState(() { + _selected = null; + }); }); } return Column( @@ -148,7 +158,7 @@ class PivScreen extends ConsumerWidget { ListTitle(l10n.s_certificates), if (pivSlots?.hasValue == true) ...pivSlots!.value.map((e) => registerPivActions( - devicePath, + widget.devicePath, pivState, e, ref: ref, @@ -156,7 +166,9 @@ class PivScreen extends ConsumerWidget { OpenIntent: CallbackAction( onInvoke: (_) async { if (expanded) { - ref.read(_selectedSlot.notifier).state = e; + setState(() { + _selected = e.slot; + }); } else { await showBlurDialog( context: context, @@ -167,8 +179,11 @@ class PivScreen extends ConsumerWidget { return null; }), }, - builder: (context) => - _CertificateListItem(e, expanded), + builder: (context) => _CertificateListItem( + e, + expanded: expanded, + selected: e == selected, + ), )), ], ); @@ -183,8 +198,10 @@ class PivScreen extends ConsumerWidget { class _CertificateListItem extends ConsumerWidget { final PivSlot pivSlot; final bool expanded; + final bool selected; - const _CertificateListItem(this.pivSlot, this.expanded); + const _CertificateListItem(this.pivSlot, + {required this.expanded, required this.selected}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -193,7 +210,6 @@ class _CertificateListItem extends ConsumerWidget { final l10n = AppLocalizations.of(context)!; final colorScheme = Theme.of(context).colorScheme; final hasFeature = ref.watch(featureProvider); - final selected = ref.watch(_selectedSlot) == pivSlot; return AppListItem( selected: selected,