mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-12-23 18:22:39 +03:00
Move Chip actions to Popup menu.
This commit is contained in:
parent
5605df31fb
commit
cb407691e0
@ -9,12 +9,14 @@ class AppPage extends ConsumerWidget {
|
||||
final Widget? title;
|
||||
final Widget child;
|
||||
final List<Widget> actions;
|
||||
final List<PopupMenuEntry> keyActions;
|
||||
final bool centered;
|
||||
AppPage({
|
||||
super.key,
|
||||
this.title,
|
||||
required this.child,
|
||||
this.actions = const [],
|
||||
this.keyActions = const [],
|
||||
this.centered = false,
|
||||
});
|
||||
|
||||
@ -82,10 +84,10 @@ class AppPage extends ConsumerWidget {
|
||||
title: title,
|
||||
centerTitle: true,
|
||||
titleTextStyle: Theme.of(context).textTheme.titleLarge,
|
||||
actions: const [
|
||||
actions: [
|
||||
Padding(
|
||||
padding: EdgeInsets.only(right: 6),
|
||||
child: DeviceButton(),
|
||||
padding: const EdgeInsets.only(right: 6),
|
||||
child: DeviceButton(actions: keyActions),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -1,7 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../widgets/custom_icons.dart';
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
import 'device_images.dart';
|
||||
|
||||
class DeviceAvatar extends StatelessWidget {
|
||||
@ -38,6 +40,27 @@ class DeviceAvatar extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
|
||||
factory DeviceAvatar.currentDevice(WidgetRef ref, {double? radius}) {
|
||||
final deviceNode = ref.watch(currentDeviceProvider);
|
||||
if (deviceNode != null) {
|
||||
return ref.watch(currentDeviceDataProvider).maybeWhen(
|
||||
data: (data) => DeviceAvatar.yubiKeyData(
|
||||
data,
|
||||
radius: radius,
|
||||
),
|
||||
orElse: () => DeviceAvatar.deviceNode(
|
||||
deviceNode,
|
||||
radius: radius,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return DeviceAvatar(
|
||||
radius: radius,
|
||||
child: const Icon(Icons.usb),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final radius = this.radius ?? 20;
|
||||
|
@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
@ -5,57 +7,121 @@ import '../message.dart';
|
||||
import '../state.dart';
|
||||
import 'device_avatar.dart';
|
||||
import 'device_picker_dialog.dart';
|
||||
import 'device_utils.dart';
|
||||
|
||||
class DeviceButton extends ConsumerWidget {
|
||||
class _CircledDeviceAvatar extends ConsumerWidget {
|
||||
final double radius;
|
||||
const DeviceButton({super.key, this.radius = 16});
|
||||
const _CircledDeviceAvatar(this.radius);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final deviceNode = ref.watch(currentDeviceProvider);
|
||||
Widget deviceWidget;
|
||||
if (deviceNode != null) {
|
||||
deviceWidget = ref.watch(currentDeviceDataProvider).maybeWhen(
|
||||
data: (data) => DeviceAvatar.yubiKeyData(
|
||||
data,
|
||||
radius: radius - 1,
|
||||
),
|
||||
orElse: () => DeviceAvatar.deviceNode(
|
||||
deviceNode,
|
||||
radius: radius - 1,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
deviceWidget = DeviceAvatar(
|
||||
radius: radius - 1,
|
||||
child: const Icon(Icons.usb),
|
||||
);
|
||||
}
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: IconButton(
|
||||
tooltip: 'Select YubiKey or device',
|
||||
icon: OverflowBox(
|
||||
maxHeight: 44,
|
||||
maxWidth: 44,
|
||||
child: CircleAvatar(
|
||||
Widget build(BuildContext context, WidgetRef ref) => CircleAvatar(
|
||||
radius: radius,
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
child: IconTheme(
|
||||
// Force the standard icon theme
|
||||
data: IconTheme.of(context),
|
||||
child: deviceWidget,
|
||||
),
|
||||
child: DeviceAvatar.currentDevice(ref, radius: radius - 1),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class DeviceButton extends ConsumerWidget {
|
||||
final double radius;
|
||||
final List<PopupMenuEntry> actions;
|
||||
const DeviceButton({super.key, this.actions = const [], this.radius = 16});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: IconButton(
|
||||
tooltip: 'More actions',
|
||||
icon: OverflowBox(
|
||||
maxHeight: 44,
|
||||
maxWidth: 44,
|
||||
child: _CircledDeviceAvatar(radius),
|
||||
),
|
||||
onPressed: () {
|
||||
showBlurDialog(
|
||||
final withContext = ref.read(withContextProvider);
|
||||
|
||||
showMenu(
|
||||
context: context,
|
||||
position: const RelativeRect.fromLTRB(100, 0, 0, 0),
|
||||
items: <PopupMenuEntry>[
|
||||
PopupMenuItem(
|
||||
padding: const EdgeInsets.only(left: 11, right: 16),
|
||||
onTap: () {
|
||||
// Wait for menu to close, and use the main context to open
|
||||
Timer.run(() {
|
||||
withContext(
|
||||
(context) async {
|
||||
await showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => const DevicePickerDialog(),
|
||||
routeSettings: const RouteSettings(name: 'device_picker'),
|
||||
routeSettings:
|
||||
const RouteSettings(name: 'device_picker'),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
child: _SlideInWidget(radius: radius),
|
||||
),
|
||||
if (actions.isNotEmpty) const PopupMenuDivider(),
|
||||
...actions,
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SlideInWidget extends ConsumerStatefulWidget {
|
||||
final double radius;
|
||||
const _SlideInWidget({required this.radius});
|
||||
|
||||
@override
|
||||
ConsumerState<_SlideInWidget> createState() => _SlideInWidgetState();
|
||||
}
|
||||
|
||||
class _SlideInWidgetState extends ConsumerState<_SlideInWidget>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
vsync: this,
|
||||
)..forward();
|
||||
late final Animation<Offset> _offsetAnimation = Tween<Offset>(
|
||||
begin: const Offset(0.9, 0.0),
|
||||
end: Offset.zero,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeOut,
|
||||
));
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final messages = getDeviceMessages(
|
||||
ref.watch(currentDeviceProvider),
|
||||
ref.watch(currentDeviceDataProvider),
|
||||
);
|
||||
return SlideTransition(
|
||||
position: _offsetAnimation,
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
minLeadingWidth: 0,
|
||||
horizontalTitleGap: 13,
|
||||
leading: _CircledDeviceAvatar(widget.radius),
|
||||
title: Text(messages.removeAt(0)),
|
||||
subtitle: Text(messages.first),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -9,16 +9,7 @@ import '../../management/models.dart';
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
import 'device_avatar.dart';
|
||||
|
||||
String _getInfoString(DeviceInfo info) {
|
||||
final serial = info.serial;
|
||||
var subtitle = '';
|
||||
if (serial != null) {
|
||||
subtitle += 'S/N: $serial ';
|
||||
}
|
||||
subtitle += 'F/W: ${info.version}';
|
||||
return subtitle;
|
||||
}
|
||||
import 'device_utils.dart';
|
||||
|
||||
final _hiddenDevicesProvider =
|
||||
StateNotifierProvider<_HiddenDevicesNotifier, List<String>>(
|
||||
@ -224,37 +215,17 @@ class _CurrentDeviceRow extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isNfc = node is NfcReaderNode;
|
||||
final hero = data.maybeWhen(
|
||||
data: (data) => DeviceAvatar.yubiKeyData(data, radius: 64),
|
||||
orElse: () => DeviceAvatar.deviceNode(node, radius: 64),
|
||||
);
|
||||
|
||||
final messages = data.whenOrNull(
|
||||
data: (data) => [_getInfoString(data.info)],
|
||||
error: (error, _) {
|
||||
switch (error) {
|
||||
case 'unknown-device':
|
||||
return ['Unrecognized device'];
|
||||
case 'device-inaccessible':
|
||||
return ['Device inacessible'];
|
||||
}
|
||||
return null;
|
||||
},
|
||||
) ??
|
||||
['No YubiKey present'];
|
||||
|
||||
String name =
|
||||
data.asData?.value.name ?? (isNfc ? messages.removeAt(0) : node.name);
|
||||
if (isNfc) {
|
||||
messages.add(node.name);
|
||||
}
|
||||
final messages = getDeviceMessages(node, data);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
_HeroAvatar(child: hero),
|
||||
ListTile(
|
||||
title: Text(name, textAlign: TextAlign.center),
|
||||
title: Text(messages.removeAt(0), textAlign: TextAlign.center),
|
||||
isThreeLine: messages.length > 1,
|
||||
subtitle: Text(messages.join('\n'), textAlign: TextAlign.center),
|
||||
)
|
||||
@ -280,7 +251,7 @@ class _DeviceRow extends ConsumerWidget {
|
||||
subtitle: Text(
|
||||
node.when(
|
||||
usbYubiKey: (_, __, ___, info) =>
|
||||
info == null ? 'Device inaccessible' : _getInfoString(info),
|
||||
info == null ? 'Device inaccessible' : getDeviceInfoString(info),
|
||||
nfcReader: (_, __) => 'Select to scan',
|
||||
),
|
||||
),
|
||||
|
44
lib/app/views/device_utils.dart
Executable file
44
lib/app/views/device_utils.dart
Executable file
@ -0,0 +1,44 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../management/models.dart';
|
||||
import '../models.dart';
|
||||
|
||||
String getDeviceInfoString(DeviceInfo info) {
|
||||
final serial = info.serial;
|
||||
var subtitle = '';
|
||||
if (serial != null) {
|
||||
subtitle += 'S/N: $serial ';
|
||||
}
|
||||
subtitle += 'F/W: ${info.version}';
|
||||
return subtitle;
|
||||
}
|
||||
|
||||
List<String> getDeviceMessages(DeviceNode? node, AsyncValue<YubiKeyData> data) {
|
||||
if (node == null) {
|
||||
return ['Insert a YubiKey', 'USB'];
|
||||
}
|
||||
final messages = data.whenOrNull(
|
||||
data: (data) => [getDeviceInfoString(data.info)],
|
||||
error: (error, _) {
|
||||
switch (error) {
|
||||
case 'unknown-device':
|
||||
return ['Unrecognized device'];
|
||||
case 'device-inaccessible':
|
||||
return ['Device inacessible'];
|
||||
}
|
||||
return null;
|
||||
},
|
||||
) ??
|
||||
['No YubiKey present'];
|
||||
|
||||
final name = data.asData?.value.name;
|
||||
if (name != null) {
|
||||
messages.insert(0, name);
|
||||
}
|
||||
|
||||
if (node is NfcReaderNode) {
|
||||
messages.add(node.name);
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
@ -8,6 +8,7 @@ class MessagePage extends StatelessWidget {
|
||||
final String? header;
|
||||
final String? message;
|
||||
final List<Widget> actions;
|
||||
final List<PopupMenuEntry> keyActions;
|
||||
|
||||
const MessagePage({
|
||||
super.key,
|
||||
@ -16,6 +17,7 @@ class MessagePage extends StatelessWidget {
|
||||
this.header,
|
||||
this.message,
|
||||
this.actions = const [],
|
||||
this.keyActions = const [],
|
||||
});
|
||||
|
||||
@override
|
||||
@ -23,6 +25,7 @@ class MessagePage extends StatelessWidget {
|
||||
title: title,
|
||||
centered: true,
|
||||
actions: actions,
|
||||
keyActions: keyActions,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
|
@ -6,7 +6,7 @@ import '../../app/models.dart';
|
||||
import '../../app/views/app_page.dart';
|
||||
import '../../app/views/graphics.dart';
|
||||
import '../../app/views/message_page.dart';
|
||||
import '../../theme.dart';
|
||||
import '../../widgets/menu_list_tile.dart';
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
import 'pin_dialog.dart';
|
||||
@ -27,7 +27,7 @@ class FidoLockedPage extends ConsumerWidget {
|
||||
graphic: noFingerprints,
|
||||
header: 'No fingerprints',
|
||||
message: 'Set a PIN to register fingerprints',
|
||||
actions: _buildActions(context),
|
||||
keyActions: _buildActions(context),
|
||||
);
|
||||
} else {
|
||||
return MessagePage(
|
||||
@ -36,7 +36,7 @@ class FidoLockedPage extends ConsumerWidget {
|
||||
header: state.credMgmt ? 'No discoverable accounts' : 'Ready to use',
|
||||
message:
|
||||
'Optionally set a PIN to protect access to your YubiKey\nRegister as a Security Key on websites',
|
||||
actions: _buildActions(context),
|
||||
keyActions: _buildActions(context),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -47,13 +47,13 @@ class FidoLockedPage extends ConsumerWidget {
|
||||
graphic: manageAccounts,
|
||||
header: 'Ready to use',
|
||||
message: 'Register as a Security Key on websites',
|
||||
actions: _buildActions(context),
|
||||
keyActions: _buildActions(context),
|
||||
);
|
||||
}
|
||||
|
||||
return AppPage(
|
||||
title: const Text('WebAuthn'),
|
||||
actions: _buildActions(context),
|
||||
keyActions: _buildActions(context),
|
||||
child: Column(
|
||||
children: [
|
||||
_PinEntryForm(state, node),
|
||||
@ -62,50 +62,28 @@ class FidoLockedPage extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildActions(BuildContext context) => [
|
||||
List<PopupMenuEntry> _buildActions(BuildContext context) => [
|
||||
if (!state.hasPin)
|
||||
OutlinedButton.icon(
|
||||
style: state.bioEnroll != null
|
||||
? AppTheme.primaryOutlinedButtonStyle(context)
|
||||
: null,
|
||||
label: const Text('Set PIN'),
|
||||
icon: const Icon(Icons.pin),
|
||||
onPressed: () {
|
||||
buildMenuItem(
|
||||
title: const Text('Set PIN'),
|
||||
leading: const Icon(Icons.pin),
|
||||
action: () {
|
||||
showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => FidoPinDialog(node.path, state),
|
||||
);
|
||||
},
|
||||
),
|
||||
OutlinedButton.icon(
|
||||
label: const Text('Options'),
|
||||
icon: const Icon(Icons.tune),
|
||||
onPressed: () {
|
||||
showBottomMenu(context, [
|
||||
if (state.hasPin)
|
||||
MenuAction(
|
||||
text: 'Change PIN',
|
||||
icon: const Icon(Icons.pin),
|
||||
action: (context) {
|
||||
showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => FidoPinDialog(node.path, state),
|
||||
);
|
||||
},
|
||||
),
|
||||
MenuAction(
|
||||
text: 'Reset FIDO',
|
||||
icon: const Icon(Icons.delete),
|
||||
action: (context) {
|
||||
buildMenuItem(
|
||||
title: const Text('Reset FIDO'),
|
||||
leading: const Icon(Icons.delete),
|
||||
action: () {
|
||||
showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => ResetDialog(node),
|
||||
);
|
||||
},
|
||||
),
|
||||
]);
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -6,8 +6,8 @@ import '../../app/models.dart';
|
||||
import '../../app/views/app_page.dart';
|
||||
import '../../app/views/graphics.dart';
|
||||
import '../../app/views/message_page.dart';
|
||||
import '../../theme.dart';
|
||||
import '../../widgets/list_title.dart';
|
||||
import '../../widgets/menu_list_tile.dart';
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
import 'add_fingerprint_dialog.dart';
|
||||
@ -121,7 +121,7 @@ class FidoUnlockedPage extends ConsumerWidget {
|
||||
if (children.isNotEmpty) {
|
||||
return AppPage(
|
||||
title: const Text('WebAuthn'),
|
||||
actions: _buildActions(context),
|
||||
keyActions: _buildKeyActions(context),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start, children: children),
|
||||
);
|
||||
@ -133,7 +133,7 @@ class FidoUnlockedPage extends ConsumerWidget {
|
||||
graphic: noFingerprints,
|
||||
header: 'No fingerprints',
|
||||
message: 'Add one or more (up to five) fingerprints',
|
||||
actions: _buildActions(context, fingerprintPrimary: true),
|
||||
keyActions: _buildKeyActions(context),
|
||||
);
|
||||
}
|
||||
|
||||
@ -142,7 +142,7 @@ class FidoUnlockedPage extends ConsumerWidget {
|
||||
graphic: manageAccounts,
|
||||
header: 'No discoverable accounts',
|
||||
message: 'Register as a Security Key on websites',
|
||||
actions: _buildActions(context),
|
||||
keyActions: _buildKeyActions(context),
|
||||
);
|
||||
}
|
||||
|
||||
@ -152,50 +152,37 @@ class FidoUnlockedPage extends ConsumerWidget {
|
||||
child: const CircularProgressIndicator(),
|
||||
);
|
||||
|
||||
List<Widget> _buildActions(BuildContext context,
|
||||
{bool fingerprintPrimary = false}) =>
|
||||
[
|
||||
List<PopupMenuEntry> _buildKeyActions(BuildContext context) => [
|
||||
if (state.bioEnroll != null)
|
||||
OutlinedButton.icon(
|
||||
style: fingerprintPrimary
|
||||
? AppTheme.primaryOutlinedButtonStyle(context)
|
||||
: null,
|
||||
label: const Text('Add fingerprint'),
|
||||
icon: const Icon(Icons.fingerprint),
|
||||
onPressed: () {
|
||||
buildMenuItem(
|
||||
title: const Text('Add fingerprint'),
|
||||
leading: const Icon(Icons.fingerprint),
|
||||
action: () {
|
||||
showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => AddFingerprintDialog(node.path),
|
||||
);
|
||||
},
|
||||
),
|
||||
OutlinedButton.icon(
|
||||
label: const Text('Options'),
|
||||
icon: const Icon(Icons.tune),
|
||||
onPressed: () {
|
||||
showBottomMenu(context, [
|
||||
MenuAction(
|
||||
text: 'Change PIN',
|
||||
icon: const Icon(Icons.pin),
|
||||
action: (context) {
|
||||
buildMenuItem(
|
||||
title: const Text('Change PIN'),
|
||||
leading: const Icon(Icons.pin),
|
||||
action: () {
|
||||
showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => FidoPinDialog(node.path, state),
|
||||
);
|
||||
},
|
||||
),
|
||||
MenuAction(
|
||||
text: 'Reset FIDO',
|
||||
icon: const Icon(Icons.delete),
|
||||
action: (context) {
|
||||
buildMenuItem(
|
||||
title: const Text('Reset FIDO'),
|
||||
leading: const Icon(Icons.delete),
|
||||
action: () {
|
||||
showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => ResetDialog(node),
|
||||
);
|
||||
},
|
||||
),
|
||||
]);
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
|
@ -1,11 +1,10 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../app/message.dart';
|
||||
import '../../app/shortcuts.dart';
|
||||
import '../../app/state.dart';
|
||||
import '../../widgets/menu_list_tile.dart';
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
import 'account_dialog.dart';
|
||||
@ -44,23 +43,16 @@ class AccountView extends ConsumerWidget with AccountMixin {
|
||||
List<PopupMenuItem> _buildPopupMenu(BuildContext context, WidgetRef ref) {
|
||||
return buildActions(context, ref).map((e) {
|
||||
final action = e.action;
|
||||
return PopupMenuItem(
|
||||
enabled: action != null,
|
||||
onTap: () {
|
||||
// As soon as onTap returns, the Navigator is popped,
|
||||
// closing the topmost item. Since we sometimes open new dialogs in
|
||||
// the action, make sure that happens *after* the pop.
|
||||
Timer(Duration.zero, () {
|
||||
action?.call(context);
|
||||
});
|
||||
},
|
||||
child: ListTile(
|
||||
return buildMenuItem(
|
||||
leading: e.icon,
|
||||
title: Text(e.text),
|
||||
enabled: action != null,
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
action: action != null
|
||||
? () {
|
||||
ref.read(withContextProvider)((context) async {
|
||||
action.call(context);
|
||||
});
|
||||
}
|
||||
: null,
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ import '../../app/views/app_loading_screen.dart';
|
||||
import '../../app/views/app_page.dart';
|
||||
import '../../app/views/graphics.dart';
|
||||
import '../../app/views/message_page.dart';
|
||||
import '../../theme.dart';
|
||||
import '../../widgets/menu_list_tile.dart';
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
import 'account_list.dart';
|
||||
@ -52,16 +52,11 @@ class _LockedView extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) => AppPage(
|
||||
title: const Text('Authenticator'),
|
||||
actions: [
|
||||
OutlinedButton.icon(
|
||||
label: const Text('Options'),
|
||||
icon: const Icon(Icons.tune),
|
||||
onPressed: () {
|
||||
showBottomMenu(context, [
|
||||
MenuAction(
|
||||
text: 'Manage password',
|
||||
icon: const Icon(Icons.password),
|
||||
action: (context) {
|
||||
keyActions: [
|
||||
buildMenuItem(
|
||||
title: const Text('Manage password'),
|
||||
leading: const Icon(Icons.password),
|
||||
action: () {
|
||||
showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
@ -69,19 +64,16 @@ class _LockedView extends ConsumerWidget {
|
||||
);
|
||||
},
|
||||
),
|
||||
MenuAction(
|
||||
text: 'Reset OATH',
|
||||
icon: const Icon(Icons.delete),
|
||||
action: (context) {
|
||||
buildMenuItem(
|
||||
title: const Text('Reset OATH'),
|
||||
leading: const Icon(Icons.delete),
|
||||
action: () {
|
||||
showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => ResetDialog(devicePath),
|
||||
);
|
||||
},
|
||||
),
|
||||
]);
|
||||
},
|
||||
),
|
||||
],
|
||||
child: Column(
|
||||
children: [
|
||||
@ -124,14 +116,17 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isEmpty = ref.watch(credentialListProvider(widget.devicePath)
|
||||
.select((value) => value?.isEmpty == true));
|
||||
if (isEmpty) {
|
||||
final credentials = ref.watch(credentialListProvider(widget.devicePath));
|
||||
if (credentials?.isEmpty == true) {
|
||||
return MessagePage(
|
||||
title: const Text('Authenticator'),
|
||||
graphic: noAccounts,
|
||||
header: 'No accounts',
|
||||
actions: _buildActions(context, true),
|
||||
keyActions: _buildActions(
|
||||
context,
|
||||
used: 0,
|
||||
capacity: widget.oathState.version.isAtLeast(4) ? 32 : null,
|
||||
),
|
||||
);
|
||||
}
|
||||
return Actions(
|
||||
@ -179,19 +174,25 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
|
||||
);
|
||||
}),
|
||||
),
|
||||
actions: _buildActions(context, false),
|
||||
keyActions: _buildActions(
|
||||
context,
|
||||
used: credentials?.length ?? 0,
|
||||
capacity: widget.oathState.version.isAtLeast(4) ? 32 : null,
|
||||
),
|
||||
child: AccountList(widget.devicePath, widget.oathState),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildActions(BuildContext context, bool isEmpty) {
|
||||
List<PopupMenuEntry> _buildActions(BuildContext context,
|
||||
{required int used, int? capacity}) {
|
||||
return [
|
||||
OutlinedButton.icon(
|
||||
style: isEmpty ? AppTheme.primaryOutlinedButtonStyle(context) : null,
|
||||
label: const Text('Add account'),
|
||||
icon: const Icon(Icons.person_add_alt_1),
|
||||
onPressed: () {
|
||||
buildMenuItem(
|
||||
title: const Text('Add account'),
|
||||
leading: const Icon(Icons.person_add_alt_1),
|
||||
trailing: capacity != null ? '$used/$capacity' : null,
|
||||
action: capacity == null || capacity > used
|
||||
? () {
|
||||
showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => OathAddAccountPage(
|
||||
@ -200,38 +201,29 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
|
||||
openQrScanner: Platform.isAndroid,
|
||||
),
|
||||
);
|
||||
},
|
||||
}
|
||||
: null,
|
||||
),
|
||||
OutlinedButton.icon(
|
||||
label: const Text('Options'),
|
||||
icon: const Icon(Icons.tune),
|
||||
onPressed: () {
|
||||
showBottomMenu(context, [
|
||||
MenuAction(
|
||||
text:
|
||||
widget.oathState.hasKey ? 'Manage password' : 'Set password',
|
||||
icon: const Icon(Icons.password),
|
||||
action: (context) {
|
||||
buildMenuItem(
|
||||
title: Text(
|
||||
widget.oathState.hasKey ? 'Manage password' : 'Set password'),
|
||||
leading: const Icon(Icons.password),
|
||||
action: () {
|
||||
showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
ManagePasswordDialog(widget.devicePath, widget.oathState),
|
||||
);
|
||||
},
|
||||
),
|
||||
MenuAction(
|
||||
text: 'Reset OATH',
|
||||
icon: const Icon(Icons.delete),
|
||||
action: (context) {
|
||||
}),
|
||||
buildMenuItem(
|
||||
title: const Text('Reset OATH'),
|
||||
leading: const Icon(Icons.delete),
|
||||
action: () {
|
||||
showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => ResetDialog(widget.devicePath),
|
||||
);
|
||||
},
|
||||
),
|
||||
]);
|
||||
},
|
||||
),
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
31
lib/widgets/menu_list_tile.dart
Executable file
31
lib/widgets/menu_list_tile.dart
Executable file
@ -0,0 +1,31 @@
|
||||
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