Extract ActionList classes.

This commit is contained in:
Dain Nilsson 2023-06-09 14:46:16 +02:00
parent 3f821497cd
commit 4bd322a268
No known key found for this signature in database
GPG Key ID: F04367096FBA95E8
8 changed files with 425 additions and 405 deletions

View File

@ -0,0 +1,89 @@
/*
* Copyright (C) 2023 Yubico.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import 'package:flutter/material.dart';
import '../../widgets/list_title.dart';
class ActionListItem extends StatelessWidget {
final String title;
final String? subtitle;
final Widget? leading;
final Widget? icon;
final Color? foregroundColor;
final Color? backgroundColor;
final Widget? trailing;
final void Function()? onTap;
const ActionListItem({
super.key,
required this.title,
this.subtitle,
this.leading,
this.icon,
this.foregroundColor,
this.backgroundColor,
this.trailing,
this.onTap,
});
@override
Widget build(BuildContext context) {
// Either leading is defined only, or we need at least an icon.
assert((leading != null &&
(icon == null &&
foregroundColor == null &&
backgroundColor == null)) ||
(leading == null && icon != null));
final theme =
ButtonTheme.of(context).colorScheme ?? Theme.of(context).colorScheme;
return ListTile(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(30)),
title: Text(title),
subtitle: subtitle != null ? Text(subtitle!) : null,
leading: leading ??
CircleAvatar(
foregroundColor: foregroundColor ?? theme.onSecondary,
backgroundColor: backgroundColor ?? theme.secondary,
child: icon,
),
trailing: trailing,
onTap: onTap,
enabled: onTap != null,
);
}
}
class ActionListSection extends StatelessWidget {
final String title;
final List<ActionListItem> children;
const ActionListSection(this.title, {super.key, required this.children});
@override
Widget build(BuildContext context) => SizedBox(
width: 360,
child: Column(children: [
ListTitle(
title,
textStyle: Theme.of(context).textTheme.bodyLarge,
),
...children,
]),
);
}

View File

@ -6,7 +6,7 @@ import '../../app/message.dart';
import '../../app/shortcuts.dart';
import '../../app/state.dart';
import '../../app/views/fs_dialog.dart';
import '../../widgets/list_title.dart';
import '../../app/views/action_list.dart';
import '../models.dart';
import 'delete_credential_dialog.dart';
@ -24,6 +24,9 @@ class CredentialDialog extends ConsumerWidget {
return const SizedBox();
}
final l10n = AppLocalizations.of(context)!;
final theme =
ButtonTheme.of(context).colorScheme ?? Theme.of(context).colorScheme;
return Actions(
actions: {
DeleteIntent: CallbackAction<DeleteIntent>(onInvoke: (_) async {
@ -77,38 +80,25 @@ class CredentialDialog extends ConsumerWidget {
],
),
),
ListTitle(AppLocalizations.of(context)!.s_actions,
textStyle: Theme.of(context).textTheme.bodyLarge),
_CredentialDialogActions(),
],
),
),
),
);
}
}
class _CredentialDialogActions extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final theme =
ButtonTheme.of(context).colorScheme ?? Theme.of(context).colorScheme;
return Column(
ActionListSection(
l10n.s_actions,
children: [
ListTile(
leading: CircleAvatar(
ActionListItem(
backgroundColor: theme.error,
foregroundColor: theme.onError,
child: const Icon(Icons.delete),
),
title: Text(l10n.s_delete_passkey),
subtitle: Text(l10n.l_delete_account_desc),
icon: const Icon(Icons.delete),
title: l10n.s_delete_passkey,
subtitle: l10n.l_delete_account_desc,
onTap: () {
Actions.invoke(context, const DeleteIntent());
},
),
],
),
],
),
),
),
);
}
}

View File

@ -6,7 +6,7 @@ import '../../app/message.dart';
import '../../app/shortcuts.dart';
import '../../app/state.dart';
import '../../app/views/fs_dialog.dart';
import '../../widgets/list_title.dart';
import '../../app/views/action_list.dart';
import '../models.dart';
import 'delete_fingerprint_dialog.dart';
import 'rename_fingerprint_dialog.dart';
@ -25,6 +25,9 @@ class FingerprintDialog extends ConsumerWidget {
return const SizedBox();
}
final l10n = AppLocalizations.of(context)!;
final theme =
ButtonTheme.of(context).colorScheme ?? Theme.of(context).colorScheme;
return Actions(
actions: {
EditIntent: CallbackAction<EditIntent>(onInvoke: (_) async {
@ -93,50 +96,33 @@ class FingerprintDialog extends ConsumerWidget {
],
),
),
ListTitle(AppLocalizations.of(context)!.s_actions,
textStyle: Theme.of(context).textTheme.bodyLarge),
_FingerprintDialogActions(),
],
),
),
),
);
}
}
class _FingerprintDialogActions extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final theme =
ButtonTheme.of(context).colorScheme ?? Theme.of(context).colorScheme;
return Column(
ActionListSection(
l10n.s_actions,
children: [
ListTile(
leading: CircleAvatar(
backgroundColor: theme.secondary,
foregroundColor: theme.onSecondary,
child: const Icon(Icons.edit),
),
title: Text(l10n.s_rename_fp),
subtitle: Text(l10n.l_rename_fp_desc),
ActionListItem(
icon: const Icon(Icons.edit),
title: l10n.s_rename_fp,
subtitle: l10n.l_rename_fp_desc,
onTap: () {
Actions.invoke(context, const EditIntent());
},
),
ListTile(
leading: CircleAvatar(
ActionListItem(
backgroundColor: theme.error,
foregroundColor: theme.onError,
child: const Icon(Icons.delete),
),
title: Text(l10n.s_delete_fingerprint),
subtitle: Text(l10n.l_delete_fingerprint_desc),
icon: const Icon(Icons.delete),
title: l10n.s_delete_fingerprint,
subtitle: l10n.l_delete_fingerprint_desc,
onTap: () {
Actions.invoke(context, const DeleteIntent());
},
),
],
),
],
),
),
),
);
}
}

View File

@ -20,7 +20,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../../app/message.dart';
import '../../app/models.dart';
import '../../app/views/fs_dialog.dart';
import '../../widgets/list_title.dart';
import '../../app/views/action_list.dart';
import '../models.dart';
import 'add_fingerprint_dialog.dart';
import 'pin_dialog.dart';
@ -39,24 +39,22 @@ Widget fidoBuildActions(
return FsDialog(
child: Column(
children: [
if (state.bioEnroll != null) ...[
ListTitle(l10n.s_setup,
textStyle: Theme.of(context).textTheme.bodyLarge),
ListTile(
leading: CircleAvatar(
if (state.bioEnroll != null)
ActionListSection(
l10n.s_setup,
children: [
ActionListItem(
backgroundColor: theme.primary,
foregroundColor: theme.onPrimary,
child: const Icon(Icons.fingerprint_outlined),
),
title: Text(l10n.s_add_fingerprint),
icon: const Icon(Icons.fingerprint_outlined),
title: l10n.s_add_fingerprint,
subtitle: state.unlocked
? Text(l10n.l_fingerprints_used(fingerprints))
: Text(state.hasPin
? l10n.l_fingerprints_used(fingerprints)
: state.hasPin
? l10n.l_unlock_pin_first
: l10n.l_set_pin_first),
: l10n.l_set_pin_first,
trailing:
fingerprints == 0 ? const Icon(Icons.warning_amber) : null,
enabled: state.unlocked && fingerprints < 5,
onTap: state.unlocked && fingerprints < 5
? () {
Navigator.of(context).pop();
@ -68,18 +66,16 @@ Widget fidoBuildActions(
: null,
),
],
ListTitle(l10n.s_manage,
textStyle: Theme.of(context).textTheme.bodyLarge),
ListTile(
leading: CircleAvatar(
backgroundColor: theme.secondary,
foregroundColor: theme.onSecondary,
child: const Icon(Icons.pin_outlined),
),
title: Text(state.hasPin ? l10n.s_change_pin : l10n.s_set_pin),
subtitle: Text(state.hasPin
ActionListSection(
l10n.s_manage,
children: [
ActionListItem(
icon: const Icon(Icons.pin_outlined),
title: state.hasPin ? l10n.s_change_pin : l10n.s_set_pin,
subtitle: state.hasPin
? l10n.s_fido_pin_protection
: l10n.l_fido_pin_protection_optional),
: l10n.l_fido_pin_protection_optional,
trailing: state.alwaysUv && !state.hasPin
? const Icon(Icons.warning_amber)
: null,
@ -90,14 +86,12 @@ Widget fidoBuildActions(
builder: (context) => FidoPinDialog(node.path, state),
);
}),
ListTile(
leading: CircleAvatar(
ActionListItem(
foregroundColor: theme.onError,
backgroundColor: theme.error,
child: const Icon(Icons.delete_outline),
),
title: Text(l10n.s_reset_fido),
subtitle: Text(l10n.l_factory_reset_this_app),
icon: const Icon(Icons.delete_outline),
title: l10n.s_reset_fido,
subtitle: l10n.l_factory_reset_this_app,
onTap: () {
Navigator.of(context).pop();
showBlurDialog(
@ -107,6 +101,8 @@ Widget fidoBuildActions(
},
),
],
)
],
),
);
}

View File

@ -24,9 +24,9 @@ import '../../app/message.dart';
import '../../app/shortcuts.dart';
import '../../app/state.dart';
import '../../app/views/fs_dialog.dart';
import '../../app/views/action_list.dart';
import '../../core/models.dart';
import '../../core/state.dart';
import '../../widgets/list_title.dart';
import '../models.dart';
import '../state.dart';
import 'account_helper.dart';
@ -39,7 +39,8 @@ class AccountDialog extends ConsumerWidget {
const AccountDialog(this.credential, {super.key});
List<Widget> _buildActions(BuildContext context, AccountHelper helper) {
List<ActionListItem> _buildActions(
BuildContext context, AccountHelper helper) {
final l10n = AppLocalizations.of(context)!;
final actions = helper.buildActions();
@ -66,7 +67,7 @@ class AccountDialog extends ConsumerWidget {
final intent = e.intent;
final (firstColor, secondColor) =
colors[e] ?? (theme.secondary, theme.onSecondary);
return ListTile(
return ActionListItem(
leading: CircleAvatar(
backgroundColor:
intent != null ? firstColor : theme.secondary.withOpacity(0.2),
@ -74,8 +75,8 @@ class AccountDialog extends ConsumerWidget {
//disabledBackgroundColor: theme.onSecondary.withOpacity(0.2),
child: e.icon,
),
title: Text(e.text),
subtitle: e.trailing != null ? Text(e.trailing!) : null,
title: e.text,
subtitle: e.trailing,
onTap: intent != null
? () {
Actions.invoke(context, intent);
@ -200,9 +201,10 @@ class AccountDialog extends ConsumerWidget {
),
),
const SizedBox(height: 32),
ListTitle(AppLocalizations.of(context)!.s_actions,
textStyle: Theme.of(context).textTheme.bodyLarge),
..._buildActions(context, helper),
ActionListSection(
AppLocalizations.of(context)!.s_actions,
children: _buildActions(context, helper),
),
],
),
),

View File

@ -23,9 +23,9 @@ import '../../app/message.dart';
import '../../app/models.dart';
import '../../app/state.dart';
import '../../app/views/fs_dialog.dart';
import '../../app/views/action_list.dart';
import '../../core/state.dart';
import '../../exception/cancellation_exception.dart';
import '../../widgets/list_title.dart';
import '../models.dart';
import '../state.dart';
import '../keys.dart' as keys;
@ -47,20 +47,18 @@ Widget oathBuildActions(
return FsDialog(
child: Column(
children: [
ListTitle(l10n.s_setup,
textStyle: Theme.of(context).textTheme.bodyLarge),
ListTile(
title: Text(l10n.s_add_account),
ActionListSection(l10n.s_setup, children: [
ActionListItem(
key: keys.addAccountAction,
leading: CircleAvatar(
title: l10n.s_add_account,
backgroundColor: theme.primary,
foregroundColor: theme.onPrimary,
child: const Icon(Icons.person_add_alt_1_outlined),
),
subtitle: Text(used == null
icon: const Icon(Icons.person_add_alt_1_outlined),
subtitle: used == null
? l10n.l_unlock_first
: (capacity != null ? l10n.l_accounts_used(used, capacity) : '')),
enabled: used != null && (capacity == null || capacity > used),
: (capacity != null
? l10n.l_accounts_used(used, capacity)
: ''),
onTap: used != null && (capacity == null || capacity > used)
? () async {
final credentials = ref.read(credentialsProvider);
@ -95,17 +93,13 @@ Widget oathBuildActions(
}
: null,
),
ListTitle(l10n.s_manage,
textStyle: Theme.of(context).textTheme.bodyLarge),
ListTile(
]),
ActionListSection(l10n.s_manage, children: [
ActionListItem(
key: keys.customIconsAction,
title: Text(l10n.s_custom_icons),
subtitle: Text(l10n.l_set_icons_for_accounts),
leading: CircleAvatar(
backgroundColor: theme.secondary,
foregroundColor: theme.onSecondary,
child: const Icon(Icons.image_outlined),
),
title: l10n.s_custom_icons,
subtitle: l10n.l_set_icons_for_accounts,
icon: const Icon(Icons.image_outlined),
onTap: () async {
Navigator.of(context).pop();
await ref.read(withContextProvider)((context) => showBlurDialog(
@ -115,16 +109,13 @@ Widget oathBuildActions(
builder: (context) => const IconPackDialog(),
));
}),
ListTile(
ActionListItem(
key: keys.setOrManagePasswordAction,
title: Text(oathState.hasKey
title: oathState.hasKey
? l10n.s_manage_password
: l10n.s_set_password),
subtitle: Text(l10n.l_optional_password_protection),
leading: CircleAvatar(
backgroundColor: theme.secondary,
foregroundColor: theme.onSecondary,
child: const Icon(Icons.password_outlined)),
: l10n.s_set_password,
subtitle: l10n.l_optional_password_protection,
icon: const Icon(Icons.password_outlined),
onTap: () {
Navigator.of(context).pop();
showBlurDialog(
@ -133,15 +124,13 @@ Widget oathBuildActions(
ManagePasswordDialog(devicePath, oathState),
);
}),
ListTile(
ActionListItem(
key: keys.resetAction,
title: Text(l10n.s_reset_oath),
subtitle: Text(l10n.l_factory_reset_this_app),
leading: CircleAvatar(
title: l10n.s_reset_oath,
subtitle: l10n.l_factory_reset_this_app,
foregroundColor: theme.onError,
backgroundColor: theme.error,
child: const Icon(Icons.delete_outline),
),
icon: const Icon(Icons.delete_outline),
onTap: () {
Navigator.of(context).pop();
showBlurDialog(
@ -149,6 +138,7 @@ Widget oathBuildActions(
builder: (context) => ResetDialog(devicePath),
);
}),
]),
],
),
);

View File

@ -21,7 +21,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../../app/message.dart';
import '../../app/models.dart';
import '../../app/views/fs_dialog.dart';
import '../../widgets/list_title.dart';
import '../../app/views/action_list.dart';
import '../models.dart';
import '../keys.dart' as keys;
import 'manage_key_dialog.dart';
@ -43,63 +43,54 @@ Widget pivBuildActions(BuildContext context, DevicePath devicePath,
return FsDialog(
child: Column(
children: [
ListTitle(l10n.s_manage,
textStyle: Theme.of(context).textTheme.bodyLarge),
ListTile(
ActionListSection(
l10n.s_manage,
children: [
ActionListItem(
key: keys.managePinAction,
title: Text(l10n.s_pin),
subtitle: Text(pinBlocked
title: l10n.s_pin,
subtitle: pinBlocked
? l10n.l_piv_pin_blocked
: l10n.l_attempts_remaining(pivState.pinAttempts)),
leading: CircleAvatar(
foregroundColor: theme.onSecondary,
backgroundColor: theme.secondary,
child: const Icon(Icons.pin_outlined),
),
: l10n.l_attempts_remaining(pivState.pinAttempts),
icon: const Icon(Icons.pin_outlined),
onTap: () {
Navigator.of(context).pop();
showBlurDialog(
context: context,
builder: (context) => ManagePinPukDialog(
devicePath,
target: pinBlocked ? ManageTarget.unblock : ManageTarget.pin,
target:
pinBlocked ? ManageTarget.unblock : ManageTarget.pin,
),
);
}),
ListTile(
ActionListItem(
key: keys.managePukAction,
title: Text(l10n.s_puk),
title: l10n.s_puk,
subtitle: pukAttempts != null
? Text(l10n.l_attempts_remaining(pukAttempts))
? l10n.l_attempts_remaining(pukAttempts)
: null,
leading: CircleAvatar(
foregroundColor: theme.onSecondary,
backgroundColor: theme.secondary,
child: const Icon(Icons.pin_outlined),
),
icon: const Icon(Icons.pin_outlined),
onTap: () {
Navigator.of(context).pop();
showBlurDialog(
context: context,
builder: (context) =>
ManagePinPukDialog(devicePath, target: ManageTarget.puk),
builder: (context) => ManagePinPukDialog(devicePath,
target: ManageTarget.puk),
);
}),
ListTile(
ActionListItem(
key: keys.manageManagementKeyAction,
title: Text(l10n.s_management_key),
subtitle: Text(usingDefaultMgmtKey
title: l10n.s_management_key,
subtitle: usingDefaultMgmtKey
? l10n.l_warning_default_key
: (pivState.protectedKey
? l10n.l_pin_protected_key
: l10n.l_change_management_key)),
leading: CircleAvatar(
foregroundColor: theme.onSecondary,
backgroundColor: theme.secondary,
child: const Icon(Icons.key_outlined),
),
trailing:
usingDefaultMgmtKey ? const Icon(Icons.warning_amber) : null,
: l10n.l_change_management_key),
icon: const Icon(Icons.key_outlined),
trailing: usingDefaultMgmtKey
? const Icon(Icons.warning_amber)
: null,
onTap: () {
Navigator.of(context).pop();
showBlurDialog(
@ -107,28 +98,27 @@ Widget pivBuildActions(BuildContext context, DevicePath devicePath,
builder: (context) => ManageKeyDialog(devicePath, pivState),
);
}),
ListTile(
ActionListItem(
key: keys.resetAction,
title: Text(l10n.s_reset_piv),
subtitle: Text(l10n.l_factory_reset_this_app),
leading: CircleAvatar(
title: l10n.s_reset_piv,
subtitle: l10n.l_factory_reset_this_app,
foregroundColor: theme.onError,
backgroundColor: theme.error,
child: const Icon(Icons.delete_outline),
),
icon: const Icon(Icons.delete_outline),
onTap: () {
Navigator.of(context).pop();
showBlurDialog(
context: context,
builder: (context) => ResetDialog(devicePath),
);
}),
})
],
),
// TODO
/*
if (false == true) ...[
ListTitle(l10n.s_setup,
textStyle: Theme.of(context).textTheme.bodyLarge),
ListTile(
KeyActionTitle(l10n.s_setup),
KeyActionItem(
key: keys.setupMacOsAction,
title: Text('Setup for macOS'),
subtitle: Text('Create certificates for macOS login'),

View File

@ -5,7 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../app/shortcuts.dart';
import '../../app/state.dart';
import '../../app/views/fs_dialog.dart';
import '../../widgets/list_title.dart';
import '../../app/views/action_list.dart';
import '../models.dart';
import '../state.dart';
import 'actions.dart';
@ -27,6 +27,8 @@ class SlotDialog extends ConsumerWidget {
final l10n = AppLocalizations.of(context)!;
final textTheme = Theme.of(context).textTheme;
final theme =
ButtonTheme.of(context).colorScheme ?? Theme.of(context).colorScheme;
final slotData = ref.watch(pivSlotsProvider(node.path).select((value) =>
value.whenOrNull(
@ -115,78 +117,53 @@ class SlotDialog extends ConsumerWidget {
],
),
),
ListTitle(l10n.s_actions, textStyle: textTheme.bodyLarge),
_SlotDialogActions(certInfo),
],
),
),
),
);
}
}
class _SlotDialogActions extends StatelessWidget {
final CertInfo? certInfo;
const _SlotDialogActions(this.certInfo);
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final theme =
ButtonTheme.of(context).colorScheme ?? Theme.of(context).colorScheme;
return Column(
ActionListSection(
l10n.s_actions,
children: [
ListTile(
leading: CircleAvatar(
ActionListItem(
backgroundColor: theme.primary,
foregroundColor: theme.onPrimary,
child: const Icon(Icons.add_outlined),
),
title: Text(l10n.s_generate_key),
subtitle: Text(l10n.l_generate_desc),
icon: const Icon(Icons.add_outlined),
title: l10n.s_generate_key,
subtitle: l10n.l_generate_desc,
onTap: () {
Actions.invoke(context, const GenerateIntent());
},
),
ListTile(
leading: CircleAvatar(
backgroundColor: theme.secondary,
foregroundColor: theme.onSecondary,
child: const Icon(Icons.file_download_outlined),
),
title: Text(l10n.l_import_file),
subtitle: Text(l10n.l_import_desc),
ActionListItem(
icon: const Icon(Icons.file_download_outlined),
title: l10n.l_import_file,
subtitle: l10n.l_import_desc,
onTap: () {
Actions.invoke(context, const ImportIntent());
},
),
if (certInfo != null) ...[
ListTile(
leading: CircleAvatar(
backgroundColor: theme.secondary,
foregroundColor: theme.onSecondary,
child: const Icon(Icons.file_upload_outlined),
),
title: Text(l10n.l_export_certificate),
subtitle: Text(l10n.l_export_certificate_desc),
ActionListItem(
icon: const Icon(Icons.file_upload_outlined),
title: l10n.l_export_certificate,
subtitle: l10n.l_export_certificate_desc,
onTap: () {
Actions.invoke(context, const ExportIntent());
},
),
ListTile(
leading: CircleAvatar(
ActionListItem(
backgroundColor: theme.error,
foregroundColor: theme.onError,
child: const Icon(Icons.delete_outline),
),
title: Text(l10n.l_delete_certificate),
subtitle: Text(l10n.l_delete_certificate_desc),
icon: const Icon(Icons.delete_outline),
title: l10n.l_delete_certificate,
subtitle: l10n.l_delete_certificate_desc,
onTap: () {
Actions.invoke(context, const DeleteIntent());
},
),
],
],
),
],
),
),
),
);
}
}