mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-11-25 23:14:18 +03:00
Start refactoring actions.
This commit is contained in:
parent
25c728b145
commit
52bff18471
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
66
lib/app/views/action_popup_menu.dart
Normal file
66
lib/app/views/action_popup_menu.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
137
lib/app/views/app_list_item.dart
Normal file
137
lib/app/views/app_list_item.dart
Normal 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
45
lib/fido/keys.dart
Normal 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');
|
55
lib/fido/views/actions.dart
Normal file
55
lib/fido/views/actions.dart
Normal 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(),
|
||||
),
|
||||
];
|
||||
}
|
@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -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,
|
||||
|
@ -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)!),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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');
|
||||
|
@ -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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -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(),
|
||||
),
|
||||
];
|
||||
|
@ -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(),
|
||||
),
|
||||
));
|
||||
});
|
||||
|
@ -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,
|
||||
|
@ -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');
|
||||
|
@ -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(),
|
||||
),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -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,
|
||||
),
|
||||
);
|
Loading…
Reference in New Issue
Block a user