Start refactoring actions.

This commit is contained in:
Dain Nilsson 2023-06-15 17:39:17 +02:00
parent 25c728b145
commit 52bff18471
No known key found for this signature in database
GPG Key ID: F04367096FBA95E8
24 changed files with 723 additions and 505 deletions

View File

@ -20,7 +20,6 @@ import 'dart:ui';
import 'package:flutter/material.dart';
import '../widgets/toast.dart';
import 'models.dart';
void Function() showMessage(
BuildContext context,
@ -29,36 +28,6 @@ void Function() showMessage(
}) =>
showToast(context, message, duration: duration);
Future<void> showBottomMenu(
BuildContext context, List<MenuAction> actions) async {
await showBlurDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Options'),
contentPadding: const EdgeInsets.only(bottom: 24, top: 4),
content: Column(
mainAxisSize: MainAxisSize.min,
children: actions
.map((a) => ListTile(
leading: a.icon,
title: Text(a.text),
contentPadding:
const EdgeInsets.symmetric(horizontal: 24),
enabled: a.intent != null,
onTap: a.intent == null
? null
: () {
Navigator.pop(context);
Actions.invoke(context, a.intent!);
},
))
.toList(),
),
);
});
}
Future<T?> showBlurDialog<T>({
required BuildContext context,
required Widget Function(BuildContext) builder,

View File

@ -116,14 +116,20 @@ class DeviceNode with _$DeviceNode {
map(usbYubiKey: (_) => Transport.usb, nfcReader: (_) => Transport.nfc);
}
enum ActionStyle { normal, primary, error }
@freezed
class MenuAction with _$MenuAction {
factory MenuAction({
required String text,
class ActionItem with _$ActionItem {
factory ActionItem({
required Widget icon,
String? trailing,
required String title,
String? subtitle,
String? shortcut,
Widget? trailing,
Intent? intent,
}) = _MenuAction;
ActionStyle? actionStyle,
Key? key,
}) = _ActionItem;
}
@freezed

View File

@ -624,30 +624,42 @@ abstract class NfcReaderNode extends DeviceNode {
}
/// @nodoc
mixin _$MenuAction {
String get text => throw _privateConstructorUsedError;
mixin _$ActionItem {
Widget get icon => throw _privateConstructorUsedError;
String? get trailing => throw _privateConstructorUsedError;
String get title => throw _privateConstructorUsedError;
String? get subtitle => throw _privateConstructorUsedError;
String? get shortcut => throw _privateConstructorUsedError;
Widget? get trailing => throw _privateConstructorUsedError;
Intent? get intent => throw _privateConstructorUsedError;
ActionStyle? get actionStyle => throw _privateConstructorUsedError;
Key? get key => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$MenuActionCopyWith<MenuAction> get copyWith =>
$ActionItemCopyWith<ActionItem> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $MenuActionCopyWith<$Res> {
factory $MenuActionCopyWith(
MenuAction value, $Res Function(MenuAction) then) =
_$MenuActionCopyWithImpl<$Res, MenuAction>;
abstract class $ActionItemCopyWith<$Res> {
factory $ActionItemCopyWith(
ActionItem value, $Res Function(ActionItem) then) =
_$ActionItemCopyWithImpl<$Res, ActionItem>;
@useResult
$Res call({String text, Widget icon, String? trailing, Intent? intent});
$Res call(
{Widget icon,
String title,
String? subtitle,
String? shortcut,
Widget? trailing,
Intent? intent,
ActionStyle? actionStyle,
Key? key});
}
/// @nodoc
class _$MenuActionCopyWithImpl<$Res, $Val extends MenuAction>
implements $MenuActionCopyWith<$Res> {
_$MenuActionCopyWithImpl(this._value, this._then);
class _$ActionItemCopyWithImpl<$Res, $Val extends ActionItem>
implements $ActionItemCopyWith<$Res> {
_$ActionItemCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
@ -657,140 +669,223 @@ class _$MenuActionCopyWithImpl<$Res, $Val extends MenuAction>
@pragma('vm:prefer-inline')
@override
$Res call({
Object? text = null,
Object? icon = null,
Object? title = null,
Object? subtitle = freezed,
Object? shortcut = freezed,
Object? trailing = freezed,
Object? intent = freezed,
Object? actionStyle = freezed,
Object? key = freezed,
}) {
return _then(_value.copyWith(
text: null == text
? _value.text
: text // ignore: cast_nullable_to_non_nullable
as String,
icon: null == icon
? _value.icon
: icon // ignore: cast_nullable_to_non_nullable
as Widget,
title: null == title
? _value.title
: title // ignore: cast_nullable_to_non_nullable
as String,
subtitle: freezed == subtitle
? _value.subtitle
: subtitle // ignore: cast_nullable_to_non_nullable
as String?,
shortcut: freezed == shortcut
? _value.shortcut
: shortcut // ignore: cast_nullable_to_non_nullable
as String?,
trailing: freezed == trailing
? _value.trailing
: trailing // ignore: cast_nullable_to_non_nullable
as String?,
as Widget?,
intent: freezed == intent
? _value.intent
: intent // ignore: cast_nullable_to_non_nullable
as Intent?,
actionStyle: freezed == actionStyle
? _value.actionStyle
: actionStyle // ignore: cast_nullable_to_non_nullable
as ActionStyle?,
key: freezed == key
? _value.key
: key // ignore: cast_nullable_to_non_nullable
as Key?,
) as $Val);
}
}
/// @nodoc
abstract class _$$_MenuActionCopyWith<$Res>
implements $MenuActionCopyWith<$Res> {
factory _$$_MenuActionCopyWith(
_$_MenuAction value, $Res Function(_$_MenuAction) then) =
__$$_MenuActionCopyWithImpl<$Res>;
abstract class _$$_ActionItemCopyWith<$Res>
implements $ActionItemCopyWith<$Res> {
factory _$$_ActionItemCopyWith(
_$_ActionItem value, $Res Function(_$_ActionItem) then) =
__$$_ActionItemCopyWithImpl<$Res>;
@override
@useResult
$Res call({String text, Widget icon, String? trailing, Intent? intent});
$Res call(
{Widget icon,
String title,
String? subtitle,
String? shortcut,
Widget? trailing,
Intent? intent,
ActionStyle? actionStyle,
Key? key});
}
/// @nodoc
class __$$_MenuActionCopyWithImpl<$Res>
extends _$MenuActionCopyWithImpl<$Res, _$_MenuAction>
implements _$$_MenuActionCopyWith<$Res> {
__$$_MenuActionCopyWithImpl(
_$_MenuAction _value, $Res Function(_$_MenuAction) _then)
class __$$_ActionItemCopyWithImpl<$Res>
extends _$ActionItemCopyWithImpl<$Res, _$_ActionItem>
implements _$$_ActionItemCopyWith<$Res> {
__$$_ActionItemCopyWithImpl(
_$_ActionItem _value, $Res Function(_$_ActionItem) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? text = null,
Object? icon = null,
Object? title = null,
Object? subtitle = freezed,
Object? shortcut = freezed,
Object? trailing = freezed,
Object? intent = freezed,
Object? actionStyle = freezed,
Object? key = freezed,
}) {
return _then(_$_MenuAction(
text: null == text
? _value.text
: text // ignore: cast_nullable_to_non_nullable
as String,
return _then(_$_ActionItem(
icon: null == icon
? _value.icon
: icon // ignore: cast_nullable_to_non_nullable
as Widget,
title: null == title
? _value.title
: title // ignore: cast_nullable_to_non_nullable
as String,
subtitle: freezed == subtitle
? _value.subtitle
: subtitle // ignore: cast_nullable_to_non_nullable
as String?,
shortcut: freezed == shortcut
? _value.shortcut
: shortcut // ignore: cast_nullable_to_non_nullable
as String?,
trailing: freezed == trailing
? _value.trailing
: trailing // ignore: cast_nullable_to_non_nullable
as String?,
as Widget?,
intent: freezed == intent
? _value.intent
: intent // ignore: cast_nullable_to_non_nullable
as Intent?,
actionStyle: freezed == actionStyle
? _value.actionStyle
: actionStyle // ignore: cast_nullable_to_non_nullable
as ActionStyle?,
key: freezed == key
? _value.key
: key // ignore: cast_nullable_to_non_nullable
as Key?,
));
}
}
/// @nodoc
class _$_MenuAction implements _MenuAction {
_$_MenuAction(
{required this.text, required this.icon, this.trailing, this.intent});
class _$_ActionItem implements _ActionItem {
_$_ActionItem(
{required this.icon,
required this.title,
this.subtitle,
this.shortcut,
this.trailing,
this.intent,
this.actionStyle,
this.key});
@override
final String text;
@override
final Widget icon;
@override
final String? trailing;
final String title;
@override
final String? subtitle;
@override
final String? shortcut;
@override
final Widget? trailing;
@override
final Intent? intent;
@override
final ActionStyle? actionStyle;
@override
final Key? key;
@override
String toString() {
return 'MenuAction(text: $text, icon: $icon, trailing: $trailing, intent: $intent)';
return 'ActionItem(icon: $icon, title: $title, subtitle: $subtitle, shortcut: $shortcut, trailing: $trailing, intent: $intent, actionStyle: $actionStyle, key: $key)';
}
@override
bool operator ==(dynamic other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$_MenuAction &&
(identical(other.text, text) || other.text == text) &&
other is _$_ActionItem &&
(identical(other.icon, icon) || other.icon == icon) &&
(identical(other.title, title) || other.title == title) &&
(identical(other.subtitle, subtitle) ||
other.subtitle == subtitle) &&
(identical(other.shortcut, shortcut) ||
other.shortcut == shortcut) &&
(identical(other.trailing, trailing) ||
other.trailing == trailing) &&
(identical(other.intent, intent) || other.intent == intent));
(identical(other.intent, intent) || other.intent == intent) &&
(identical(other.actionStyle, actionStyle) ||
other.actionStyle == actionStyle) &&
(identical(other.key, key) || other.key == key));
}
@override
int get hashCode => Object.hash(runtimeType, text, icon, trailing, intent);
int get hashCode => Object.hash(runtimeType, icon, title, subtitle, shortcut,
trailing, intent, actionStyle, key);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$_MenuActionCopyWith<_$_MenuAction> get copyWith =>
__$$_MenuActionCopyWithImpl<_$_MenuAction>(this, _$identity);
_$$_ActionItemCopyWith<_$_ActionItem> get copyWith =>
__$$_ActionItemCopyWithImpl<_$_ActionItem>(this, _$identity);
}
abstract class _MenuAction implements MenuAction {
factory _MenuAction(
{required final String text,
required final Widget icon,
final String? trailing,
final Intent? intent}) = _$_MenuAction;
abstract class _ActionItem implements ActionItem {
factory _ActionItem(
{required final Widget icon,
required final String title,
final String? subtitle,
final String? shortcut,
final Widget? trailing,
final Intent? intent,
final ActionStyle? actionStyle,
final Key? key}) = _$_ActionItem;
@override
String get text;
@override
Widget get icon;
@override
String? get trailing;
String get title;
@override
String? get subtitle;
@override
String? get shortcut;
@override
Widget? get trailing;
@override
Intent? get intent;
@override
ActionStyle? get actionStyle;
@override
Key? get key;
@override
@JsonKey(ignore: true)
_$$_MenuActionCopyWith<_$_MenuAction> get copyWith =>
_$$_ActionItemCopyWith<_$_ActionItem> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -17,16 +17,15 @@
import 'package:flutter/material.dart';
import '../../widgets/list_title.dart';
enum ActionStyle { normal, primary, error }
import '../models.dart';
class ActionListItem extends StatelessWidget {
final Widget icon;
final String title;
final String? subtitle;
final Widget icon;
final Widget? trailing;
final void Function(BuildContext context)? onTap;
final ActionStyle actionStyle;
final void Function()? onTap;
const ActionListItem({
super.key,
@ -62,7 +61,7 @@ class ActionListItem extends StatelessWidget {
),
),
trailing: trailing,
onTap: onTap,
onTap: onTap != null ? () => onTap?.call(context) : null,
enabled: onTap != null,
);
}
@ -74,6 +73,28 @@ class ActionListSection extends StatelessWidget {
const ActionListSection(this.title, {super.key, required this.children});
factory ActionListSection.fromMenuActions(BuildContext context, String title,
{Key? key, required List<ActionItem> actions}) {
return ActionListSection(
key: key,
title,
children: actions.map((action) {
final intent = action.intent;
return ActionListItem(
key: action.key,
actionStyle: action.actionStyle ?? ActionStyle.normal,
icon: action.icon,
title: action.title,
subtitle: action.subtitle,
onTap: intent != null
? (context) => Actions.invoke(context, intent)
: null,
trailing: action.trailing,
);
}).toList(),
);
}
@override
Widget build(BuildContext context) => SizedBox(
width: 360,

View File

@ -0,0 +1,66 @@
/*
* Copyright (C) 2022-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 'dart:async';
import 'package:flutter/material.dart';
import '../models.dart';
Future showPopupMenu(BuildContext context, Offset globalPosition,
List<ActionItem> actions) =>
showMenu(
context: context,
position: RelativeRect.fromLTRB(
globalPosition.dx,
globalPosition.dy,
globalPosition.dx,
0,
),
items: actions.map((e) => _buildMenuItem(context, e)).toList(),
);
PopupMenuItem _buildMenuItem(BuildContext context, ActionItem actionItem) {
final intent = actionItem.intent;
final enabled = intent != null;
final shortcut = actionItem.shortcut;
return PopupMenuItem(
enabled: enabled,
onTap: enabled
? () {
// Wait for popup menu to close before running action.
Timer.run(() {
Actions.invoke(context, intent);
});
}
: null,
child: ListTile(
key: actionItem.key,
enabled: enabled,
dense: true,
contentPadding: EdgeInsets.zero,
minLeadingWidth: 0,
title: Text(actionItem.title),
leading: actionItem.icon,
trailing: shortcut != null
? Opacity(
opacity: 0.5,
child: Text(shortcut, textScaleFactor: 0.7),
)
: null,
),
);
}

View File

@ -0,0 +1,137 @@
/*
* 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 'package:flutter/services.dart';
import '../../core/state.dart';
import '../models.dart';
import '../shortcuts.dart';
import 'action_popup_menu.dart';
class AppListItem extends StatefulWidget {
final Widget? leading;
final String title;
final String? subtitle;
final Widget? trailing;
final List<ActionItem> Function(BuildContext context)? buildPopupActions;
final Intent? activationIntent;
const AppListItem({
super.key,
this.leading,
required this.title,
this.subtitle,
this.trailing,
this.buildPopupActions,
this.activationIntent,
});
@override
State<StatefulWidget> createState() => _AppListItemState();
}
class _AppListItemState extends State<AppListItem> {
final FocusNode _focusNode = FocusNode();
int _lastTap = 0;
@override
void dispose() {
_focusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final subtitle = widget.subtitle;
final buildPopupActions = widget.buildPopupActions;
final activationIntent = widget.activationIntent;
final trailing = widget.trailing;
return Shortcuts(
shortcuts: {
LogicalKeySet(LogicalKeyboardKey.enter): const OpenIntent(),
LogicalKeySet(LogicalKeyboardKey.space): const OpenIntent(),
},
child: InkWell(
focusNode: _focusNode,
borderRadius: BorderRadius.circular(30),
onSecondaryTapDown: buildPopupActions == null
? null
: (details) {
showPopupMenu(
context,
details.globalPosition,
buildPopupActions(context),
);
},
onTap: () {
if (isDesktop) {
final now = DateTime.now().millisecondsSinceEpoch;
if (now - _lastTap < 500) {
setState(() {
_lastTap = 0;
});
Actions.invoke(context, activationIntent ?? const OpenIntent());
} else {
_focusNode.requestFocus();
setState(() {
_lastTap = now;
});
}
} else {
Actions.invoke<OpenIntent>(context, const OpenIntent());
}
},
onLongPress: activationIntent == null
? null
: () {
Actions.invoke(context, activationIntent);
},
child: Stack(
alignment: AlignmentDirectional.center,
children: [
const SizedBox(height: 64),
ListTile(
leading: widget.leading,
title: Text(
widget.title,
overflow: TextOverflow.fade,
maxLines: 1,
softWrap: false,
),
subtitle: subtitle != null
? Text(
subtitle,
overflow: TextOverflow.fade,
maxLines: 1,
softWrap: false,
)
: null,
trailing: trailing == null
? null
: Focus(
skipTraversal: true,
descendantsAreTraversable: false,
child: trailing,
),
),
],
),
),
);
}
}

45
lib/fido/keys.dart Normal file
View File

@ -0,0 +1,45 @@
/*
* Copyright (C) 2022-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';
const _prefix = 'fido.keys';
const _keyAction = '$_prefix.actions';
const _credentialAction = '$_prefix.credential.actions';
const _fingerprintAction = '$_prefix.fingerprint.actions';
// Key actions
const managePinAction = Key('$_keyAction.manage_pin');
const addFingerprintAction = Key('$_keyAction.add_fingerprint');
const resetAction = Key('$_keyAction.reset');
// Credential actions
const editCredentialAction = Key('$_credentialAction.edit');
const deleteCredentialAction = Key('$_credentialAction.delete');
// Fingerprint actions
const editFingerintAction = Key('$_fingerprintAction.edit');
const deleteFingerprintAction = Key('$_fingerprintAction.delete');
const saveButton = Key('$_prefix.save');
const deleteButton = Key('$_prefix.delete');
const unlockButton = Key('$_prefix.unlock');
const managementKeyField = Key('$_prefix.management_key');
const pinPukField = Key('$_prefix.pin_puk');
const newPinPukField = Key('$_prefix.new_pin_puk');
const confirmPinPukField = Key('$_prefix.confirm_pin_puk');
const subjectField = Key('$_prefix.subject');

View File

@ -0,0 +1,55 @@
/*
* 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 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../../app/models.dart';
import '../../app/shortcuts.dart';
import '../keys.dart' as keys;
List<ActionItem> buildFingerprintActions(AppLocalizations l10n) {
return [
ActionItem(
key: keys.editFingerintAction,
icon: const Icon(Icons.edit),
title: l10n.s_rename_fp,
subtitle: l10n.l_rename_fp_desc,
intent: const EditIntent(),
),
ActionItem(
key: keys.deleteFingerprintAction,
actionStyle: ActionStyle.error,
icon: const Icon(Icons.delete),
title: l10n.s_delete_fingerprint,
subtitle: l10n.l_delete_fingerprint_desc,
intent: const DeleteIntent(),
),
];
}
List<ActionItem> buildCredentialActions(AppLocalizations l10n) {
return [
ActionItem(
key: keys.deleteCredentialAction,
actionStyle: ActionStyle.error,
icon: const Icon(Icons.delete),
title: l10n.s_delete_passkey,
subtitle: l10n.l_delete_account_desc,
intent: const DeleteIntent(),
),
];
}

View File

@ -8,6 +8,7 @@ import '../../app/state.dart';
import '../../app/views/fs_dialog.dart';
import '../../app/views/action_list.dart';
import '../models.dart';
import 'actions.dart';
import 'delete_credential_dialog.dart';
class CredentialDialog extends ConsumerWidget {
@ -78,19 +79,10 @@ class CredentialDialog extends ConsumerWidget {
],
),
),
ActionListSection(
ActionListSection.fromMenuActions(
context,
l10n.s_actions,
children: [
ActionListItem(
actionStyle: ActionStyle.error,
icon: const Icon(Icons.delete),
title: l10n.s_delete_passkey,
subtitle: l10n.l_delete_account_desc,
onTap: () {
Actions.invoke(context, const DeleteIntent());
},
),
],
actions: buildCredentialActions(l10n),
),
],
),

View File

@ -8,6 +8,7 @@ import '../../app/state.dart';
import '../../app/views/fs_dialog.dart';
import '../../app/views/action_list.dart';
import '../models.dart';
import 'actions.dart';
import 'delete_fingerprint_dialog.dart';
import 'rename_fingerprint_dialog.dart';
@ -94,27 +95,10 @@ class FingerprintDialog extends ConsumerWidget {
],
),
),
ActionListSection(
ActionListSection.fromMenuActions(
context,
l10n.s_actions,
children: [
ActionListItem(
icon: const Icon(Icons.edit),
title: l10n.s_rename_fp,
subtitle: l10n.l_rename_fp_desc,
onTap: () {
Actions.invoke(context, const EditIntent());
},
),
ActionListItem(
actionStyle: ActionStyle.error,
icon: const Icon(Icons.delete),
title: l10n.s_delete_fingerprint,
subtitle: l10n.l_delete_fingerprint_desc,
onTap: () {
Actions.invoke(context, const DeleteIntent());
},
),
],
actions: buildFingerprintActions(l10n),
),
],
),

View File

@ -22,6 +22,7 @@ import '../../app/models.dart';
import '../../app/views/fs_dialog.dart';
import '../../app/views/action_list.dart';
import '../models.dart';
import '../keys.dart' as keys;
import 'add_fingerprint_dialog.dart';
import 'pin_dialog.dart';
import 'reset_dialog.dart';
@ -42,6 +43,7 @@ Widget fidoBuildActions(
l10n.s_setup,
children: [
ActionListItem(
key: keys.addFingerprintAction,
actionStyle: ActionStyle.primary,
icon: const Icon(Icons.fingerprint_outlined),
title: l10n.s_add_fingerprint,
@ -53,7 +55,7 @@ Widget fidoBuildActions(
trailing:
fingerprints == 0 ? const Icon(Icons.warning_amber) : null,
onTap: state.unlocked && fingerprints < 5
? () {
? (context) {
Navigator.of(context).pop();
showBlurDialog(
context: context,
@ -68,6 +70,7 @@ Widget fidoBuildActions(
l10n.s_manage,
children: [
ActionListItem(
key: keys.managePinAction,
icon: const Icon(Icons.pin_outlined),
title: state.hasPin ? l10n.s_change_pin : l10n.s_set_pin,
subtitle: state.hasPin
@ -76,7 +79,7 @@ Widget fidoBuildActions(
trailing: state.alwaysUv && !state.hasPin
? const Icon(Icons.warning_amber)
: null,
onTap: () {
onTap: (context) {
Navigator.of(context).pop();
showBlurDialog(
context: context,
@ -84,11 +87,12 @@ Widget fidoBuildActions(
);
}),
ActionListItem(
key: keys.resetAction,
actionStyle: ActionStyle.error,
icon: const Icon(Icons.delete_outline),
title: l10n.s_reset_fido,
subtitle: l10n.l_factory_reset_this_app,
onTap: () {
onTap: (context) {
Navigator.of(context).pop();
showBlurDialog(
context: context,

View File

@ -21,15 +21,20 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../app/message.dart';
import '../../app/models.dart';
import '../../app/shortcuts.dart';
import '../../app/views/app_list_item.dart';
import '../../app/views/app_page.dart';
import '../../app/views/graphics.dart';
import '../../app/views/message_page.dart';
import '../../widgets/list_title.dart';
import '../models.dart';
import '../state.dart';
import 'actions.dart';
import 'credential_dialog.dart';
import 'delete_credential_dialog.dart';
import 'delete_fingerprint_dialog.dart';
import 'fingerprint_dialog.dart';
import 'key_actions.dart';
import 'rename_fingerprint_dialog.dart';
class FidoUnlockedPage extends ConsumerWidget {
final DeviceNode node;
@ -51,13 +56,20 @@ class FidoUnlockedPage extends ConsumerWidget {
children.add(ListTitle(l10n.s_passkeys));
children.addAll(creds.map((cred) => Actions(
actions: {
OpenIntent: CallbackAction<OpenIntent>(onInvoke: (_) async {
await showBlurDialog(
OpenIntent: CallbackAction<OpenIntent>(
onInvoke: (_) => showBlurDialog(
context: context,
builder: (context) => CredentialDialog(cred),
)),
DeleteIntent: CallbackAction<DeleteIntent>(
onInvoke: (_) => showBlurDialog(
context: context,
builder: (context) => CredentialDialog(cred),
);
return null;
}),
builder: (context) => DeleteCredentialDialog(
node.path,
cred,
),
),
),
},
child: _CredentialListItem(cred),
)));
@ -76,13 +88,27 @@ class FidoUnlockedPage extends ConsumerWidget {
children.add(ListTitle(l10n.s_fingerprints));
children.addAll(fingerprints.map((fp) => Actions(
actions: {
OpenIntent: CallbackAction<OpenIntent>(onInvoke: (_) async {
await showBlurDialog(
context: context,
builder: (context) => FingerprintDialog(fp),
);
return null;
}),
OpenIntent: CallbackAction<OpenIntent>(
onInvoke: (_) => showBlurDialog(
context: context,
builder: (context) => FingerprintDialog(fp),
)),
EditIntent: CallbackAction<EditIntent>(
onInvoke: (_) => showBlurDialog(
context: context,
builder: (context) => RenameFingerprintDialog(
node.path,
fp,
),
)),
DeleteIntent: CallbackAction<DeleteIntent>(
onInvoke: (_) => showBlurDialog(
context: context,
builder: (context) => DeleteFingerprintDialog(
node.path,
fp,
),
)),
},
child: _FingerprintListItem(fp),
)));
@ -136,28 +162,20 @@ class _CredentialListItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListTile(
return AppListItem(
leading: CircleAvatar(
foregroundColor: Theme.of(context).colorScheme.onPrimary,
backgroundColor: Theme.of(context).colorScheme.primary,
child: const Icon(Icons.person),
),
title: Text(
credential.userName,
softWrap: false,
overflow: TextOverflow.fade,
),
subtitle: Text(
credential.rpId,
softWrap: false,
overflow: TextOverflow.fade,
),
title: credential.userName,
subtitle: credential.rpId,
trailing: OutlinedButton(
onPressed: () {
Actions.maybeInvoke<OpenIntent>(context, const OpenIntent());
},
onPressed: Actions.handler(context, const OpenIntent()),
child: const Icon(Icons.more_horiz),
),
buildPopupActions: (context) =>
buildCredentialActions(AppLocalizations.of(context)!),
);
}
}
@ -168,23 +186,19 @@ class _FingerprintListItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListTile(
return AppListItem(
leading: CircleAvatar(
foregroundColor: Theme.of(context).colorScheme.onSecondary,
backgroundColor: Theme.of(context).colorScheme.secondary,
child: const Icon(Icons.fingerprint),
),
title: Text(
fingerprint.label,
softWrap: false,
overflow: TextOverflow.fade,
),
title: fingerprint.label,
trailing: OutlinedButton(
onPressed: () {
Actions.maybeInvoke<OpenIntent>(context, const OpenIntent());
},
onPressed: Actions.handler(context, const OpenIntent()),
child: const Icon(Icons.more_horiz),
),
buildPopupActions: (context) =>
buildFingerprintActions(AppLocalizations.of(context)!),
);
}
}

View File

@ -400,6 +400,7 @@
"@_certificates": {},
"s_certificate": "Certificate",
"s_certificates": "Certificates",
"s_csr": "CSR",
"s_subject": "Subject",
"l_export_csr_file": "Save CSR to file",

View File

@ -17,18 +17,28 @@
import 'package:flutter/material.dart';
const _prefix = 'oath.keys';
const setOrManagePasswordAction = Key('$_prefix.set_or_manage_password');
const addAccountAction = Key('$_prefix.add_account');
const resetAction = Key('$_prefix.reset');
const customIconsAction = Key('$_prefix.custom_icons');
const noAccountsView = Key('$_prefix.no_accounts');
const _keyAction = '$_prefix.actions';
const _accountAction = '$_prefix.account.actions';
// This is global so we can access it from the global Ctrl+F shortcut.
final searchAccountsField = GlobalKey();
// Key actions
const setOrManagePasswordAction =
Key('$_keyAction.action.set_or_manage_password');
const addAccountAction = Key('$_keyAction.add_account');
const resetAction = Key('$_keyAction.reset');
const customIconsAction = Key('$_keyAction.custom_icons');
// Credential actions
const copyAction = Key('$_accountAction.copy');
const calculateAction = Key('$_accountAction.calculate');
const togglePinAction = Key('$_accountAction.toggle_pin');
const editAction = Key('$_accountAction.edit');
const deleteAction = Key('$_accountAction.delete');
const noAccountsView = Key('$_prefix.no_accounts');
const passwordField = Key('$_prefix.password');
const currentPasswordField = Key('$_prefix.current_password');
const newPasswordField = Key('$_prefix.new_password');

View File

@ -39,44 +39,6 @@ class AccountDialog extends ConsumerWidget {
const AccountDialog(this.credential, {super.key});
List<ActionListItem> _buildActions(
BuildContext context, AccountHelper helper) {
final l10n = AppLocalizations.of(context)!;
final actions = helper.buildActions();
final copy =
actions.firstWhere(((e) => e.text == l10n.l_copy_to_clipboard));
final delete = actions.firstWhere(((e) => e.text == l10n.s_delete_account));
final canCopy = copy.intent != null;
final actionStyles = {
copy: canCopy ? ActionStyle.primary : ActionStyle.normal,
delete: ActionStyle.error,
};
// If we can't copy, but can calculate, highlight that button instead
if (!canCopy) {
final calculates = actions.where(((e) => e.text == l10n.s_calculate));
if (calculates.isNotEmpty) {
actionStyles[calculates.first] = ActionStyle.primary;
}
}
return actions.map((e) {
final intent = e.intent;
return ActionListItem(
actionStyle: actionStyles[e] ?? ActionStyle.normal,
icon: e.icon,
title: e.text,
subtitle: e.trailing,
onTap: intent != null
? () {
Actions.invoke(context, intent);
}
: null,
);
}).toList();
}
@override
Widget build(BuildContext context, WidgetRef ref) {
// TODO: Solve this in a cleaner way
@ -192,9 +154,10 @@ class AccountDialog extends ConsumerWidget {
),
),
const SizedBox(height: 32),
ActionListSection(
ActionListSection.fromMenuActions(
context,
AppLocalizations.of(context)!.s_actions,
children: _buildActions(context, helper),
actions: helper.buildActions(),
),
],
),

View File

@ -14,6 +14,7 @@
* limitations under the License.
*/
import 'dart:io';
import 'dart:ui';
import 'package:flutter/material.dart';
@ -28,6 +29,7 @@ import '../../widgets/circle_timer.dart';
import '../../widgets/custom_icons.dart';
import '../models.dart';
import '../state.dart';
import '../keys.dart' as keys;
import 'actions.dart';
/// Support class for presenting an OATH account.
@ -52,7 +54,7 @@ class AccountHelper {
String get title => credential.issuer ?? credential.name;
String? get subtitle => credential.issuer != null ? credential.name : null;
List<MenuAction> buildActions() => _ref
List<ActionItem> buildActions() => _ref
.watch(currentDeviceDataProvider)
.maybeWhen(
data: (data) {
@ -61,40 +63,50 @@ class AccountHelper {
final ready = expired || credential.oathType == OathType.hotp;
final pinned = _ref.watch(favoritesProvider).contains(credential.id);
final l10n = AppLocalizations.of(_context)!;
final canCopy = code != null && !expired;
return [
MenuAction(
ActionItem(
key: keys.copyAction,
icon: const Icon(Icons.copy),
text: l10n.l_copy_to_clipboard,
trailing: l10n.l_copy_code_desc,
intent: code == null || expired ? null : const CopyIntent(),
title: l10n.l_copy_to_clipboard,
subtitle: l10n.l_copy_code_desc,
shortcut: Platform.isMacOS ? '\u2318 C' : 'Ctrl+C',
actionStyle: canCopy ? ActionStyle.primary : null,
intent: canCopy ? const CopyIntent() : null,
),
if (manual)
MenuAction(
ActionItem(
key: keys.calculateAction,
actionStyle: !canCopy ? ActionStyle.primary : null,
icon: const Icon(Icons.refresh),
text: l10n.s_calculate,
trailing: l10n.l_calculate_code_desc,
title: l10n.s_calculate,
subtitle: l10n.l_calculate_code_desc,
intent: ready ? const CalculateIntent() : null,
),
MenuAction(
ActionItem(
key: keys.togglePinAction,
icon: pinned
? pushPinStrokeIcon
: const Icon(Icons.push_pin_outlined),
text: pinned ? l10n.s_unpin_account : l10n.s_pin_account,
trailing: l10n.l_pin_account_desc,
title: pinned ? l10n.s_unpin_account : l10n.s_pin_account,
subtitle: l10n.l_pin_account_desc,
intent: const TogglePinIntent(),
),
if (data.info.version.isAtLeast(5, 3))
MenuAction(
ActionItem(
key: keys.editAction,
icon: const Icon(Icons.edit_outlined),
text: l10n.s_rename_account,
trailing: l10n.l_rename_account_desc,
title: l10n.s_rename_account,
subtitle: l10n.l_rename_account_desc,
intent: const EditIntent(),
),
MenuAction(
ActionItem(
key: keys.deleteAction,
actionStyle: ActionStyle.error,
icon: const Icon(Icons.delete_outline),
text: l10n.s_delete_account,
trailing: l10n.l_delete_account_desc,
title: l10n.s_delete_account,
subtitle: l10n.l_delete_account_desc,
intent: const DeleteIntent(),
),
];

View File

@ -14,19 +14,15 @@
* limitations under the License.
*/
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../../app/message.dart';
import '../../app/shortcuts.dart';
import '../../app/state.dart';
import '../../core/state.dart';
import '../../widgets/menu_list_tile.dart';
import '../../app/views/app_list_item.dart';
import '../models.dart';
import '../state.dart';
import 'account_dialog.dart';
@ -51,15 +47,6 @@ String _a11yCredentialLabel(String? issuer, String name, String? code) {
class _AccountViewState extends ConsumerState<AccountView> {
OathCredential get credential => widget.credential;
final _focusNode = FocusNode();
int _lastTap = 0;
@override
void dispose() {
_focusNode.dispose();
super.dispose();
}
Color _iconColor(int shade) {
final colors = [
Colors.red[shade],
@ -90,26 +77,6 @@ class _AccountViewState extends ConsumerState<AccountView> {
return colors[label.hashCode % colors.length]!;
}
List<PopupMenuItem> _buildPopupMenu(
BuildContext context, AccountHelper helper) {
final shortcut = Platform.isMacOS ? '\u2318 C' : 'Ctrl+C';
final copyText = AppLocalizations.of(context)!.l_copy_to_clipboard;
return helper.buildActions().map((e) {
final intent = e.intent;
return buildMenuItem(
leading: e.icon,
title: Text(e.text),
action: intent != null
? () {
Actions.invoke(context, intent);
}
: null,
trailing: e.text == copyText ? shortcut : null,
);
}).toList();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
@ -171,83 +138,27 @@ class _AccountViewState extends ConsumerState<AccountView> {
child: Semantics(
label: _a11yCredentialLabel(
credential.issuer, credential.name, helper.code?.value),
child: InkWell(
focusNode: _focusNode,
borderRadius: BorderRadius.circular(30),
onSecondaryTapDown: (details) {
showMenu(
context: context,
position: RelativeRect.fromLTRB(
details.globalPosition.dx,
details.globalPosition.dy,
details.globalPosition.dx,
0,
),
items: _buildPopupMenu(context, helper),
);
},
onTap: () {
if (isDesktop) {
final now = DateTime.now().millisecondsSinceEpoch;
if (now - _lastTap < 500) {
setState(() {
_lastTap = 0;
});
Actions.maybeInvoke(context, const CopyIntent());
} else {
_focusNode.requestFocus();
setState(() {
_lastTap = now;
});
}
} else {
Actions.maybeInvoke<OpenIntent>(
context, const OpenIntent());
}
},
onLongPress: () {
Actions.maybeInvoke(context, const CopyIntent());
},
child: ListTile(
leading: showAvatar
? AccountIcon(
issuer: credential.issuer,
defaultWidget: circleAvatar)
: null,
title: Text(
helper.title,
overflow: TextOverflow.fade,
maxLines: 1,
softWrap: false,
),
subtitle: subtitle != null
? Text(
subtitle,
overflow: TextOverflow.fade,
maxLines: 1,
softWrap: false,
)
: null,
trailing: Focus(
skipTraversal: true,
descendantsAreTraversable: false,
child: helper.code != null
? FilledButton.tonalIcon(
icon: helper.buildCodeIcon(),
label: helper.buildCodeLabel(),
onPressed: () {
Actions.maybeInvoke<OpenIntent>(
context, const OpenIntent());
},
)
: FilledButton.tonal(
onPressed: () {
Actions.maybeInvoke<OpenIntent>(
context, const OpenIntent());
},
child: helper.buildCodeIcon()),
),
),
child: AppListItem(
leading: showAvatar
? AccountIcon(
issuer: credential.issuer,
defaultWidget: circleAvatar)
: null,
title: helper.title,
subtitle: subtitle,
trailing: helper.code != null
? FilledButton.tonalIcon(
icon: helper.buildCodeIcon(),
label: helper.buildCodeLabel(),
onPressed:
Actions.handler(context, const OpenIntent()),
)
: FilledButton.tonal(
onPressed:
Actions.handler(context, const OpenIntent()),
child: helper.buildCodeIcon()),
activationIntent: const CopyIntent(),
buildPopupActions: (_) => helper.buildActions(),
),
));
});

View File

@ -58,7 +58,7 @@ Widget oathBuildActions(
? l10n.l_accounts_used(used, capacity)
: ''),
onTap: used != null && (capacity == null || capacity > used)
? () async {
? (context) async {
final credentials = ref.read(credentialsProvider);
final withContext = ref.read(withContextProvider);
Navigator.of(context).pop();
@ -98,7 +98,7 @@ Widget oathBuildActions(
title: l10n.s_custom_icons,
subtitle: l10n.l_set_icons_for_accounts,
icon: const Icon(Icons.image_outlined),
onTap: () async {
onTap: (context) async {
Navigator.of(context).pop();
await ref.read(withContextProvider)((context) => showBlurDialog(
context: context,
@ -114,7 +114,7 @@ Widget oathBuildActions(
: l10n.s_set_password,
subtitle: l10n.l_optional_password_protection,
icon: const Icon(Icons.password_outlined),
onTap: () {
onTap: (context) {
Navigator.of(context).pop();
showBlurDialog(
context: context,
@ -128,7 +128,7 @@ Widget oathBuildActions(
actionStyle: ActionStyle.error,
title: l10n.s_reset_oath,
subtitle: l10n.l_factory_reset_this_app,
onTap: () {
onTap: (context) {
Navigator.of(context).pop();
showBlurDialog(
context: context,

View File

@ -17,13 +17,22 @@
import 'package:flutter/material.dart';
const _prefix = 'piv.keys';
const _keyAction = '$_prefix.actions';
const _slotAction = '$_prefix.slot.actions';
const managePinAction = Key('$_prefix.manage_pin');
const managePukAction = Key('$_prefix.manage_puk');
const manageManagementKeyAction = Key('$_prefix.manage_management_key');
const resetAction = Key('$_prefix.reset');
// Key actions
const managePinAction = Key('$_keyAction.manage_pin');
const managePukAction = Key('$_keyAction.manage_puk');
const manageManagementKeyAction = Key('$_keyAction.manage_management_key');
const resetAction = Key('$_keyAction.reset');
const setupMacOsAction = Key('$_keyAction.setup_macos');
// Slot actions
const generateAction = Key('$_slotAction.generate');
const importAction = Key('$_slotAction.import');
const exportAction = Key('$_slotAction.export');
const deleteAction = Key('$_slotAction.delete');
const setupMacOsAction = Key('$_prefix.setup_macos');
const saveButton = Key('$_prefix.save');
const deleteButton = Key('$_prefix.delete');
const unlockButton = Key('$_prefix.unlock');

View File

@ -27,20 +27,13 @@ import '../../app/state.dart';
import '../../app/models.dart';
import '../models.dart';
import '../state.dart';
import '../keys.dart' as keys;
import 'authentication_dialog.dart';
import 'delete_certificate_dialog.dart';
import 'generate_key_dialog.dart';
import 'import_file_dialog.dart';
import 'pin_dialog.dart';
class AuthenticateIntent extends Intent {
const AuthenticateIntent();
}
class VerifyPinIntent extends Intent {
const VerifyPinIntent();
}
class GenerateIntent extends Intent {
const GenerateIntent();
}
@ -85,9 +78,6 @@ Widget registerPivActions(
}) =>
Actions(
actions: {
AuthenticateIntent: CallbackAction<AuthenticateIntent>(
onInvoke: (intent) => _authenticate(ref, devicePath, pivState),
),
GenerateIntent:
CallbackAction<GenerateIntent>(onInvoke: (intent) async {
if (!pivState.protectedKey &&
@ -221,17 +211,46 @@ Widget registerPivActions(
),
) ??
false);
// Needs to move to slot dialog(?) or react to state change
// Pop the slot dialog if deleted
if (deleted == true) {
await withContext((context) async {
Navigator.of(context).pop();
});
}
return deleted;
}),
...actions,
},
child: Builder(builder: builder),
);
List<ActionItem> buildSlotActions(bool hasCert, AppLocalizations l10n) {
return [
ActionItem(
key: keys.generateAction,
icon: const Icon(Icons.add_outlined),
actionStyle: ActionStyle.primary,
title: l10n.s_generate_key,
subtitle: l10n.l_generate_desc,
intent: const GenerateIntent(),
),
ActionItem(
key: keys.importAction,
icon: const Icon(Icons.file_download_outlined),
title: l10n.l_import_file,
subtitle: l10n.l_import_desc,
intent: const ImportIntent(),
),
if (hasCert) ...[
ActionItem(
key: keys.exportAction,
icon: const Icon(Icons.file_upload_outlined),
title: l10n.l_export_certificate,
subtitle: l10n.l_export_certificate_desc,
intent: const ExportIntent(),
),
ActionItem(
key: keys.deleteAction,
actionStyle: ActionStyle.error,
icon: const Icon(Icons.delete_outline),
title: l10n.l_delete_certificate,
subtitle: l10n.l_delete_certificate_desc,
intent: const DeleteIntent(),
),
],
];
}

View File

@ -51,7 +51,7 @@ Widget pivBuildActions(BuildContext context, DevicePath devicePath,
? l10n.l_piv_pin_blocked
: l10n.l_attempts_remaining(pivState.pinAttempts),
icon: const Icon(Icons.pin_outlined),
onTap: () {
onTap: (context) {
Navigator.of(context).pop();
showBlurDialog(
context: context,
@ -69,7 +69,7 @@ Widget pivBuildActions(BuildContext context, DevicePath devicePath,
? l10n.l_attempts_remaining(pukAttempts)
: null,
icon: const Icon(Icons.pin_outlined),
onTap: () {
onTap: (context) {
Navigator.of(context).pop();
showBlurDialog(
context: context,
@ -89,7 +89,7 @@ Widget pivBuildActions(BuildContext context, DevicePath devicePath,
trailing: usingDefaultMgmtKey
? const Icon(Icons.warning_amber)
: null,
onTap: () {
onTap: (context) {
Navigator.of(context).pop();
showBlurDialog(
context: context,
@ -102,7 +102,7 @@ Widget pivBuildActions(BuildContext context, DevicePath devicePath,
actionStyle: ActionStyle.error,
title: l10n.s_reset_piv,
subtitle: l10n.l_factory_reset_this_app,
onTap: () {
onTap: (context) {
Navigator.of(context).pop();
showBlurDialog(
context: context,

View File

@ -22,10 +22,13 @@ import '../../app/message.dart';
import '../../app/models.dart';
import '../../app/shortcuts.dart';
import '../../app/views/app_failure_page.dart';
import '../../app/views/app_list_item.dart';
import '../../app/views/app_page.dart';
import '../../app/views/message_page.dart';
import '../../widgets/list_title.dart';
import '../models.dart';
import '../state.dart';
import 'actions.dart';
import 'key_actions.dart';
import 'slot_dialog.dart';
@ -55,8 +58,13 @@ class PivScreen extends ConsumerWidget {
pivBuildActions(context, devicePath, pivState, ref),
child: Column(
children: [
ListTitle(l10n.s_certificates),
if (pivSlots?.hasValue == true)
...pivSlots!.value.map((e) => Actions(
...pivSlots!.value.map((e) => registerPivActions(
devicePath,
pivState,
e,
ref: ref,
actions: {
OpenIntent:
CallbackAction<OpenIntent>(onInvoke: (_) async {
@ -67,8 +75,8 @@ class PivScreen extends ConsumerWidget {
return null;
}),
},
child: _CertificateListItem(e),
))
builder: (context) => _CertificateListItem(e),
)),
],
),
);
@ -87,32 +95,24 @@ class _CertificateListItem extends StatelessWidget {
final certInfo = pivSlot.certInfo;
final l10n = AppLocalizations.of(context)!;
final colorScheme = Theme.of(context).colorScheme;
return ListTile(
return AppListItem(
leading: CircleAvatar(
foregroundColor: colorScheme.onSecondary,
backgroundColor: colorScheme.secondary,
child: const Icon(Icons.approval),
),
title: Text(
slot.getDisplayName(l10n),
softWrap: false,
overflow: TextOverflow.fade,
),
title: slot.getDisplayName(l10n),
subtitle: certInfo != null
? Text(
l10n.l_subject_issuer(certInfo.subject, certInfo.issuer),
softWrap: false,
overflow: TextOverflow.fade,
)
: Text(pivSlot.hasKey == true
? l10n.l_subject_issuer(certInfo.subject, certInfo.issuer)
: pivSlot.hasKey == true
? l10n.l_key_no_certificate
: l10n.l_no_certificate),
: l10n.l_no_certificate,
trailing: OutlinedButton(
onPressed: () {
Actions.maybeInvoke<OpenIntent>(context, const OpenIntent());
},
onPressed: Actions.handler(context, const OpenIntent()),
child: const Icon(Icons.more_horiz),
),
buildPopupActions: (context) => buildSlotActions(certInfo != null, l10n),
);
}
}

View File

@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../app/shortcuts.dart';
import '../../app/state.dart';
import '../../app/views/fs_dialog.dart';
import '../../app/views/action_list.dart';
@ -26,6 +25,10 @@ class SlotDialog extends ConsumerWidget {
final l10n = AppLocalizations.of(context)!;
final textTheme = Theme.of(context).textTheme;
// This is what ListTile uses for subtitle
final subtitleStyle = textTheme.bodyMedium!.copyWith(
color: textTheme.bodySmall!.color,
);
final pivState = ref.watch(pivStateProvider(node.path)).valueOrNull;
final slotData = ref.watch(pivSlotsProvider(node.path).select((value) =>
@ -64,38 +67,26 @@ class SlotDialog extends ConsumerWidget {
certInfo.subject, certInfo.issuer),
softWrap: true,
textAlign: TextAlign.center,
// This is what ListTile uses for subtitle
style: textTheme.bodyMedium!.copyWith(
color: textTheme.bodySmall!.color,
),
style: subtitleStyle,
),
Text(
l10n.l_serial(certInfo.serial),
softWrap: true,
textAlign: TextAlign.center,
// This is what ListTile uses for subtitle
style: textTheme.bodyMedium!.copyWith(
color: textTheme.bodySmall!.color,
),
style: subtitleStyle,
),
Text(
l10n.l_certificate_fingerprint(certInfo.fingerprint),
softWrap: true,
textAlign: TextAlign.center,
// This is what ListTile uses for subtitle
style: textTheme.bodyMedium!.copyWith(
color: textTheme.bodySmall!.color,
),
style: subtitleStyle,
),
Text(
l10n.l_valid(
certInfo.notValidBefore, certInfo.notValidAfter),
softWrap: true,
textAlign: TextAlign.center,
// This is what ListTile uses for subtitle
style: textTheme.bodyMedium!.copyWith(
color: textTheme.bodySmall!.color,
),
style: subtitleStyle,
),
] else ...[
Padding(
@ -104,10 +95,7 @@ class SlotDialog extends ConsumerWidget {
l10n.l_no_certificate,
softWrap: true,
textAlign: TextAlign.center,
// This is what ListTile uses for subtitle
style: textTheme.bodyMedium!.copyWith(
color: textTheme.bodySmall!.color,
),
style: subtitleStyle,
),
),
],
@ -115,46 +103,10 @@ class SlotDialog extends ConsumerWidget {
],
),
),
ActionListSection(
ActionListSection.fromMenuActions(
context,
l10n.s_actions,
children: [
ActionListItem(
icon: const Icon(Icons.add_outlined),
actionStyle: ActionStyle.primary,
title: l10n.s_generate_key,
subtitle: l10n.l_generate_desc,
onTap: () {
Actions.invoke(context, const GenerateIntent());
},
),
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) ...[
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());
},
),
ActionListItem(
actionStyle: ActionStyle.error,
icon: const Icon(Icons.delete_outline),
title: l10n.l_delete_certificate,
subtitle: l10n.l_delete_certificate_desc,
onTap: () {
Actions.invoke(context, const DeleteIntent());
},
),
],
],
actions: buildSlotActions(certInfo != null, l10n),
),
],
),

View File

@ -1,47 +0,0 @@
/*
* Copyright (C) 2022 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 'dart:async';
import 'package:flutter/material.dart';
PopupMenuItem buildMenuItem({
required Widget title,
Widget? leading,
String? trailing,
void Function()? action,
}) =>
PopupMenuItem(
enabled: action != null,
onTap: () {
// Wait for popup menu to close before running action.
Timer.run(action!);
},
child: ListTile(
enabled: action != null,
dense: true,
contentPadding: EdgeInsets.zero,
minLeadingWidth: 0,
title: title,
leading: leading,
trailing: trailing != null
? Opacity(
opacity: 0.5,
child: Text(trailing, textScaleFactor: 0.7),
)
: null,
),
);