Fix state of details column.

This commit is contained in:
Dain Nilsson 2024-01-10 20:47:54 +01:00
parent b50ada490a
commit 0e709f0085
No known key found for this signature in database
GPG Key ID: F04367096FBA95E8
3 changed files with 206 additions and 161 deletions

View File

@ -39,102 +39,110 @@ import 'fingerprint_dialog.dart';
import 'key_actions.dart'; import 'key_actions.dart';
import 'rename_fingerprint_dialog.dart'; import 'rename_fingerprint_dialog.dart';
final _selectedItem = StateProvider<Object?>( class FidoUnlockedPage extends ConsumerStatefulWidget {
(ref) => null,
);
Widget _registerFingerprintActions(
DevicePath devicePath,
Fingerprint fingerprint, {
required WidgetRef ref,
required Widget Function(BuildContext context) builder,
Map<Type, Action<Intent>> actions = const {},
}) {
final hasFeature = ref.watch(featureProvider);
return Actions(
actions: {
if (hasFeature(features.fingerprintsEdit))
EditIntent: CallbackAction<EditIntent>(onInvoke: (_) async {
final renamed = await ref.read(withContextProvider)(
(context) => showBlurDialog<Fingerprint>(
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<DeleteIntent>(onInvoke: (_) async {
final deleted = await ref.read(withContextProvider)(
(context) => showBlurDialog<bool?>(
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<Type, Action<Intent>> actions = const {},
}) {
final hasFeature = ref.watch(featureProvider);
return Actions(
actions: {
if (hasFeature(features.credentialsDelete))
DeleteIntent: CallbackAction<DeleteIntent>(onInvoke: (_) async {
final deleted = await ref.read(withContextProvider)(
(context) => showBlurDialog<bool?>(
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 {
final DeviceNode node; final DeviceNode node;
final FidoState state; final FidoState state;
const FidoUnlockedPage(this.node, this.state, {super.key}); FidoUnlockedPage(this.node, this.state) : super(key: ObjectKey(node.path));
@override @override
Widget build(BuildContext context, WidgetRef ref) { ConsumerState<ConsumerStatefulWidget> createState() =>
_FidoUnlockedPageState();
}
class _FidoUnlockedPageState extends ConsumerState<FidoUnlockedPage> {
Object? _selected;
Widget _registerFingerprintActions(
Fingerprint fingerprint, {
required WidgetRef ref,
required Widget Function(BuildContext context) builder,
Map<Type, Action<Intent>> actions = const {},
}) {
final hasFeature = ref.watch(featureProvider);
return Actions(
actions: {
if (hasFeature(features.fingerprintsEdit))
EditIntent: CallbackAction<EditIntent>(onInvoke: (_) async {
final renamed = await ref.read(withContextProvider)(
(context) => showBlurDialog<Fingerprint>(
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<DeleteIntent>(onInvoke: (_) async {
final deleted = await ref.read(withContextProvider)(
(context) => showBlurDialog<bool?>(
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<Type, Action<Intent>> actions = const {},
}) {
final hasFeature = ref.watch(featureProvider);
return Actions(
actions: {
if (hasFeature(features.credentialsDelete))
DeleteIntent: CallbackAction<DeleteIntent>(onInvoke: (_) async {
final deleted = await ref.read(withContextProvider)(
(context) => showBlurDialog<bool?>(
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 l10n = AppLocalizations.of(context)!;
final selected = ref.watch(_selectedItem); final selected = _selected;
List<Widget Function(bool expanded)> children = []; List<Widget Function(bool expanded)> children = [];
if (state.credMgmt) { if (widget.state.credMgmt) {
final data = ref.watch(credentialProvider(node.path)).asData; final data = ref.watch(credentialProvider(widget.node.path)).asData;
if (data == null) { if (data == null) {
return _buildLoadingPage(context); return _buildLoadingPage(context);
} }
@ -143,13 +151,14 @@ class FidoUnlockedPage extends ConsumerWidget {
children.add((_) => ListTitle(l10n.s_passkeys)); children.add((_) => ListTitle(l10n.s_passkeys));
children.addAll( children.addAll(
creds.map((cred) => (expanded) => _registerCredentialActions( creds.map((cred) => (expanded) => _registerCredentialActions(
node.path,
cred, cred,
ref: ref, ref: ref,
actions: { actions: {
OpenIntent: CallbackAction<OpenIntent>(onInvoke: (_) { OpenIntent: CallbackAction<OpenIntent>(onInvoke: (_) {
if (expanded) { if (expanded) {
ref.read(_selectedItem.notifier).state = cred; setState(() {
_selected = cred;
});
return null; return null;
} else { } else {
return showBlurDialog( return showBlurDialog(
@ -170,8 +179,8 @@ class FidoUnlockedPage extends ConsumerWidget {
} }
int nFingerprints = 0; int nFingerprints = 0;
if (state.bioEnroll != null) { if (widget.state.bioEnroll != null) {
final data = ref.watch(fingerprintProvider(node.path)).asData; final data = ref.watch(fingerprintProvider(widget.node.path)).asData;
if (data == null) { if (data == null) {
return _buildLoadingPage(context); return _buildLoadingPage(context);
} }
@ -181,13 +190,14 @@ class FidoUnlockedPage extends ConsumerWidget {
children.add((_) => ListTitle(l10n.s_fingerprints)); children.add((_) => ListTitle(l10n.s_fingerprints));
children.addAll( children.addAll(
fingerprints.map((fp) => (expanded) => _registerFingerprintActions( fingerprints.map((fp) => (expanded) => _registerFingerprintActions(
node.path,
fp, fp,
ref: ref, ref: ref,
actions: { actions: {
OpenIntent: CallbackAction<OpenIntent>(onInvoke: (_) { OpenIntent: CallbackAction<OpenIntent>(onInvoke: (_) {
if (expanded) { if (expanded) {
ref.read(_selectedItem.notifier).state = fp; setState(() {
_selected = fp;
});
return null; return null;
} else { } else {
return showBlurDialog( return showBlurDialog(
@ -214,7 +224,9 @@ class FidoUnlockedPage extends ConsumerWidget {
actions: { actions: {
EscapeIntent: CallbackAction<EscapeIntent>(onInvoke: (intent) { EscapeIntent: CallbackAction<EscapeIntent>(onInvoke: (intent) {
if (selected != null) { if (selected != null) {
ref.read(_selectedItem.notifier).state = null; setState(() {
_selected = null;
});
} else { } else {
Actions.invoke(context, intent); Actions.invoke(context, intent);
} }
@ -225,7 +237,7 @@ class FidoUnlockedPage extends ConsumerWidget {
title: Text(l10n.s_webauthn), title: Text(l10n.s_webauthn),
keyActionsBuilder: switch (selected) { keyActionsBuilder: switch (selected) {
FidoCredential credential => (context) => FidoCredential credential => (context) =>
_registerCredentialActions(node.path, credential, _registerCredentialActions(credential,
ref: ref, ref: ref,
builder: (context) => Column( builder: (context) => Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
@ -274,7 +286,6 @@ class FidoUnlockedPage extends ConsumerWidget {
], ],
)), )),
Fingerprint fingerprint => (context) => _registerFingerprintActions( Fingerprint fingerprint => (context) => _registerFingerprintActions(
node.path,
fingerprint, fingerprint,
ref: ref, ref: ref,
builder: (context) => Column( builder: (context) => Column(
@ -309,11 +320,11 @@ class FidoUnlockedPage extends ConsumerWidget {
), ),
), ),
_ => hasActions _ => hasActions
? (context) => ? (context) => fidoBuildActions(
fidoBuildActions(context, node, state, nFingerprints) context, widget.node, widget.state, nFingerprints)
: null : null
}, },
keyActionsBadge: fidoShowActionsNotifier(state), keyActionsBadge: fidoShowActionsNotifier(widget.state),
builder: (context, expanded) => Column( builder: (context, expanded) => Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: children.map((f) => f(expanded)).toList()), 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( return MessagePage(
title: Text(l10n.s_webauthn), title: Text(l10n.s_webauthn),
graphic: Icon(Icons.fingerprint, graphic: Icon(Icons.fingerprint,
@ -329,9 +340,10 @@ class FidoUnlockedPage extends ConsumerWidget {
header: l10n.s_no_fingerprints, header: l10n.s_no_fingerprints,
message: l10n.l_add_one_or_more_fps, message: l10n.l_add_one_or_more_fps,
keyActionsBuilder: hasActions keyActionsBuilder: hasActions
? (context) => fidoBuildActions(context, node, state, 0) ? (context) =>
fidoBuildActions(context, widget.node, widget.state, 0)
: null, : null,
keyActionsBadge: fidoShowActionsNotifier(state), keyActionsBadge: fidoShowActionsNotifier(widget.state),
); );
} }
@ -342,9 +354,9 @@ class FidoUnlockedPage extends ConsumerWidget {
header: l10n.l_no_discoverable_accounts, header: l10n.l_no_discoverable_accounts,
message: l10n.l_register_sk_on_websites, message: l10n.l_register_sk_on_websites,
keyActionsBuilder: hasActions keyActionsBuilder: hasActions
? (context) => fidoBuildActions(context, node, state, 0) ? (context) => fidoBuildActions(context, widget.node, widget.state, 0)
: null, : null,
keyActionsBadge: fidoShowActionsNotifier(state), keyActionsBadge: fidoShowActionsNotifier(widget.state),
); );
} }

View File

@ -37,20 +37,23 @@ import 'actions.dart';
import 'key_actions.dart'; import 'key_actions.dart';
import 'slot_dialog.dart'; import 'slot_dialog.dart';
final _selectedSlot = StateProvider<OtpSlot?>( class OtpScreen extends ConsumerStatefulWidget {
(ref) => null,
);
class OtpScreen extends ConsumerWidget {
final DevicePath devicePath; final DevicePath devicePath;
const OtpScreen(this.devicePath, {super.key}); OtpScreen(this.devicePath) : super(key: ObjectKey(devicePath));
@override @override
Widget build(BuildContext context, WidgetRef ref) { ConsumerState<ConsumerStatefulWidget> createState() => _OtpScreenState();
}
class _OtpScreenState extends ConsumerState<OtpScreen> {
SlotId? _selected;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
final hasFeature = ref.watch(featureProvider); final hasFeature = ref.watch(featureProvider);
return ref.watch(otpStateProvider(devicePath)).when( return ref.watch(otpStateProvider(widget.devicePath)).when(
loading: () => MessagePage( loading: () => MessagePage(
title: Text(l10n.s_slots), title: Text(l10n.s_slots),
graphic: const CircularProgressIndicator(), graphic: const CircularProgressIndicator(),
@ -59,12 +62,16 @@ class OtpScreen extends ConsumerWidget {
error: (error, _) => error: (error, _) =>
AppFailurePage(title: Text(l10n.s_slots), cause: error), AppFailurePage(title: Text(l10n.s_slots), cause: error),
data: (otpState) { data: (otpState) {
final selected = ref.watch(_selectedSlot); final selected = _selected != null
? otpState.slots.firstWhere((e) => e.slot == _selected)
: null;
return Actions( return Actions(
actions: { actions: {
EscapeIntent: CallbackAction<EscapeIntent>(onInvoke: (intent) { EscapeIntent: CallbackAction<EscapeIntent>(onInvoke: (intent) {
if (selected != null) { if (selected != null) {
ref.read(_selectedSlot.notifier).state = null; setState(() {
_selected = null;
});
} else { } else {
Actions.invoke(context, intent); Actions.invoke(context, intent);
} }
@ -75,7 +82,7 @@ class OtpScreen extends ConsumerWidget {
title: Text(l10n.s_slots), title: Text(l10n.s_slots),
keyActionsBuilder: selected != null keyActionsBuilder: selected != null
? (context) => registerOtpActions( ? (context) => registerOtpActions(
devicePath, widget.devicePath,
selected, selected,
ref: ref, ref: ref,
builder: (context) => Column( builder: (context) => Column(
@ -119,36 +126,45 @@ class OtpScreen extends ConsumerWidget {
), ),
) )
: (hasFeature(features.actions) : (hasFeature(features.actions)
? (context) => ? (context) => otpBuildActions(
otpBuildActions(context, devicePath, otpState, ref) context, widget.devicePath, otpState, ref)
: null), : null),
builder: (context, expanded) { builder: (context, expanded) {
// De-select if window is resized to be non-expanded. // De-select if window is resized to be non-expanded.
if (!expanded) { if (!expanded) {
Timer.run(() { Timer.run(() {
ref.read(_selectedSlot.notifier).state = null; setState(() {
_selected = null;
});
}); });
} }
return Column(children: [ return Column(children: [
ListTitle(l10n.s_slots), ListTitle(l10n.s_slots),
...otpState.slots.map((e) => registerOtpActions(devicePath, e, ...otpState.slots
ref: ref, .map((e) => registerOtpActions(widget.devicePath, e,
actions: { ref: ref,
OpenIntent: actions: {
CallbackAction<OpenIntent>(onInvoke: (_) async { OpenIntent:
if (expanded) { CallbackAction<OpenIntent>(onInvoke: (_) async {
ref.read(_selectedSlot.notifier).state = e; if (expanded) {
} else { setState(() {
await showBlurDialog( _selected = e.slot;
context: context, });
barrierColor: Colors.transparent, } else {
builder: (context) => SlotDialog(e.slot), await showBlurDialog(
); context: context,
} barrierColor: Colors.transparent,
return null; builder: (context) => SlotDialog(e.slot),
}), );
}, }
builder: (context) => _SlotListItem(e, expanded))) return null;
}),
},
builder: (context) => _SlotListItem(
e,
expanded: expanded,
selected: e == selected,
)))
]); ]);
}, },
), ),
@ -160,8 +176,10 @@ class OtpScreen extends ConsumerWidget {
class _SlotListItem extends ConsumerWidget { class _SlotListItem extends ConsumerWidget {
final OtpSlot otpSlot; final OtpSlot otpSlot;
final bool expanded; final bool expanded;
final bool selected;
const _SlotListItem(this.otpSlot, this.expanded); const _SlotListItem(this.otpSlot,
{required this.expanded, required this.selected});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@ -170,7 +188,6 @@ class _SlotListItem extends ConsumerWidget {
final colorScheme = Theme.of(context).colorScheme; final colorScheme = Theme.of(context).colorScheme;
final isConfigured = otpSlot.isConfigured; final isConfigured = otpSlot.isConfigured;
final hasFeature = ref.watch(featureProvider); final hasFeature = ref.watch(featureProvider);
final selected = ref.watch(_selectedSlot) == otpSlot;
return AppListItem( return AppListItem(
selected: selected, selected: selected,

View File

@ -39,20 +39,23 @@ import 'cert_info_view.dart';
import 'key_actions.dart'; import 'key_actions.dart';
import 'slot_dialog.dart'; import 'slot_dialog.dart';
final _selectedSlot = StateProvider<PivSlot?>( class PivScreen extends ConsumerStatefulWidget {
(ref) => null,
);
class PivScreen extends ConsumerWidget {
final DevicePath devicePath; final DevicePath devicePath;
const PivScreen(this.devicePath, {super.key}); PivScreen(this.devicePath) : super(key: ObjectKey(devicePath));
@override @override
Widget build(BuildContext context, WidgetRef ref) { ConsumerState<ConsumerStatefulWidget> createState() => _PivScreenState();
}
class _PivScreenState extends ConsumerState<PivScreen> {
SlotId? _selected;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
final hasFeature = ref.watch(featureProvider); final hasFeature = ref.watch(featureProvider);
return ref.watch(pivStateProvider(devicePath)).when( return ref.watch(pivStateProvider(widget.devicePath)).when(
loading: () => MessagePage( loading: () => MessagePage(
title: Text(l10n.s_certificates), title: Text(l10n.s_certificates),
graphic: const CircularProgressIndicator(), graphic: const CircularProgressIndicator(),
@ -63,8 +66,11 @@ class PivScreen extends ConsumerWidget {
cause: error, cause: error,
), ),
data: (pivState) { data: (pivState) {
final pivSlots = ref.watch(pivSlotsProvider(devicePath)).asData; final pivSlots =
final selected = ref.watch(_selectedSlot); ref.watch(pivSlotsProvider(widget.devicePath)).asData;
final selected = _selected != null
? pivSlots?.value.firstWhere((e) => e.slot == _selected)
: null;
final theme = Theme.of(context); final theme = Theme.of(context);
final textTheme = theme.textTheme; final textTheme = theme.textTheme;
// This is what ListTile uses for subtitle // This is what ListTile uses for subtitle
@ -75,7 +81,9 @@ class PivScreen extends ConsumerWidget {
actions: { actions: {
EscapeIntent: CallbackAction<EscapeIntent>(onInvoke: (intent) { EscapeIntent: CallbackAction<EscapeIntent>(onInvoke: (intent) {
if (selected != null) { if (selected != null) {
ref.read(_selectedSlot.notifier).state = null; setState(() {
_selected = null;
});
} else { } else {
Actions.invoke(context, intent); Actions.invoke(context, intent);
} }
@ -87,7 +95,7 @@ class PivScreen extends ConsumerWidget {
keyActionsBuilder: selected != null keyActionsBuilder: selected != null
// TODO: Reuse slot dialog // TODO: Reuse slot dialog
? (context) => registerPivActions( ? (context) => registerPivActions(
devicePath, widget.devicePath,
pivState, pivState,
selected, selected,
ref: ref, ref: ref,
@ -127,20 +135,22 @@ class PivScreen extends ConsumerWidget {
), ),
if (hasFeature(features.actions)) ...[ if (hasFeature(features.actions)) ...[
pivBuildActions( pivBuildActions(
context, devicePath, pivState, ref), context, widget.devicePath, pivState, ref),
], ],
], ],
), ),
) )
: (hasFeature(features.actions) : (hasFeature(features.actions)
? (context) => ? (context) => pivBuildActions(
pivBuildActions(context, devicePath, pivState, ref) context, widget.devicePath, pivState, ref)
: null), : null),
builder: (context, expanded) { builder: (context, expanded) {
// De-select if window is resized to be non-expanded. // De-select if window is resized to be non-expanded.
if (!expanded) { if (!expanded) {
Timer.run(() { Timer.run(() {
ref.read(_selectedSlot.notifier).state = null; setState(() {
_selected = null;
});
}); });
} }
return Column( return Column(
@ -148,7 +158,7 @@ class PivScreen extends ConsumerWidget {
ListTitle(l10n.s_certificates), ListTitle(l10n.s_certificates),
if (pivSlots?.hasValue == true) if (pivSlots?.hasValue == true)
...pivSlots!.value.map((e) => registerPivActions( ...pivSlots!.value.map((e) => registerPivActions(
devicePath, widget.devicePath,
pivState, pivState,
e, e,
ref: ref, ref: ref,
@ -156,7 +166,9 @@ class PivScreen extends ConsumerWidget {
OpenIntent: CallbackAction<OpenIntent>( OpenIntent: CallbackAction<OpenIntent>(
onInvoke: (_) async { onInvoke: (_) async {
if (expanded) { if (expanded) {
ref.read(_selectedSlot.notifier).state = e; setState(() {
_selected = e.slot;
});
} else { } else {
await showBlurDialog( await showBlurDialog(
context: context, context: context,
@ -167,8 +179,11 @@ class PivScreen extends ConsumerWidget {
return null; return null;
}), }),
}, },
builder: (context) => builder: (context) => _CertificateListItem(
_CertificateListItem(e, expanded), e,
expanded: expanded,
selected: e == selected,
),
)), )),
], ],
); );
@ -183,8 +198,10 @@ class PivScreen extends ConsumerWidget {
class _CertificateListItem extends ConsumerWidget { class _CertificateListItem extends ConsumerWidget {
final PivSlot pivSlot; final PivSlot pivSlot;
final bool expanded; final bool expanded;
final bool selected;
const _CertificateListItem(this.pivSlot, this.expanded); const _CertificateListItem(this.pivSlot,
{required this.expanded, required this.selected});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@ -193,7 +210,6 @@ class _CertificateListItem extends ConsumerWidget {
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
final colorScheme = Theme.of(context).colorScheme; final colorScheme = Theme.of(context).colorScheme;
final hasFeature = ref.watch(featureProvider); final hasFeature = ref.watch(featureProvider);
final selected = ref.watch(_selectedSlot) == pivSlot;
return AppListItem( return AppListItem(
selected: selected, selected: selected,