mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2025-01-08 20:08:45 +03:00
Test of 3 column view.
This commit is contained in:
parent
6083de1303
commit
07fb161402
@ -75,6 +75,10 @@ class RefreshIntent extends Intent {
|
||||
const RefreshIntent();
|
||||
}
|
||||
|
||||
class EscapeIntent extends Intent {
|
||||
const EscapeIntent();
|
||||
}
|
||||
|
||||
/// Use cmd on macOS, use ctrl on the other platforms
|
||||
SingleActivator ctrlOrCmd(LogicalKeyboardKey key) =>
|
||||
SingleActivator(key, meta: Platform.isMacOS, control: !Platform.isMacOS);
|
||||
@ -149,6 +153,12 @@ Widget registerGlobalShortcuts(
|
||||
});
|
||||
return null;
|
||||
}),
|
||||
EscapeIntent: CallbackAction<EscapeIntent>(
|
||||
onInvoke: (_) {
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
return null;
|
||||
},
|
||||
),
|
||||
},
|
||||
child: Shortcuts(
|
||||
shortcuts: {
|
||||
@ -156,6 +166,8 @@ Widget registerGlobalShortcuts(
|
||||
const SingleActivator(LogicalKeyboardKey.copy): const CopyIntent(),
|
||||
ctrlOrCmd(LogicalKeyboardKey.keyF): const SearchIntent(),
|
||||
ctrlOrCmd(LogicalKeyboardKey.keyR): const RefreshIntent(),
|
||||
const SingleActivator(LogicalKeyboardKey.escape):
|
||||
const EscapeIntent(),
|
||||
if (isDesktop) ...{
|
||||
const SingleActivator(LogicalKeyboardKey.tab, control: true):
|
||||
const NextDeviceIntent(),
|
||||
|
@ -108,14 +108,11 @@ class ActionListSection extends ConsumerWidget {
|
||||
if (enabledChildren.isEmpty) {
|
||||
return const SizedBox();
|
||||
}
|
||||
final theme = Theme.of(context);
|
||||
return SizedBox(
|
||||
width: 360,
|
||||
child: Column(children: [
|
||||
ListTitle(
|
||||
title,
|
||||
textStyle: theme.textTheme.bodyLarge!
|
||||
.copyWith(color: theme.colorScheme.primary),
|
||||
),
|
||||
...enabledChildren,
|
||||
]),
|
||||
|
@ -30,6 +30,7 @@ class AppListItem extends ConsumerStatefulWidget {
|
||||
final Widget? trailing;
|
||||
final List<ActionItem> Function(BuildContext context)? buildPopupActions;
|
||||
final Intent? activationIntent;
|
||||
final bool selected;
|
||||
|
||||
const AppListItem({
|
||||
super.key,
|
||||
@ -39,6 +40,7 @@ class AppListItem extends ConsumerStatefulWidget {
|
||||
this.trailing,
|
||||
this.buildPopupActions,
|
||||
this.activationIntent,
|
||||
this.selected = false,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -114,6 +116,7 @@ class _AppListItemState extends ConsumerState<AppListItem> {
|
||||
children: [
|
||||
const SizedBox(height: 64),
|
||||
ListTile(
|
||||
selected: widget.selected,
|
||||
leading: widget.leading,
|
||||
title: Text(
|
||||
widget.title,
|
||||
|
@ -19,10 +19,11 @@ import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
import '../../core/state.dart';
|
||||
import '../../widgets/delayed_visibility.dart';
|
||||
import '../../widgets/file_drop_target.dart';
|
||||
import '../message.dart';
|
||||
import '../shortcuts.dart';
|
||||
import 'fs_dialog.dart';
|
||||
import 'keys.dart';
|
||||
import 'navigation.dart';
|
||||
|
||||
@ -59,20 +60,15 @@ class AppPage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) => LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final bool singleColumn;
|
||||
final bool hasRail;
|
||||
if (isAndroid) {
|
||||
final isPortrait = constraints.maxWidth < constraints.maxHeight;
|
||||
singleColumn = isPortrait || constraints.maxWidth < 600;
|
||||
hasRail = constraints.maxWidth > 600;
|
||||
} else {
|
||||
singleColumn = constraints.maxWidth < 600;
|
||||
hasRail = constraints.maxWidth > 400;
|
||||
final width = constraints.maxWidth;
|
||||
if (width < 400) {
|
||||
return _buildScaffold(context, true, false, false);
|
||||
}
|
||||
|
||||
if (singleColumn) {
|
||||
// Single column layout, maybe with rail
|
||||
return _buildScaffold(context, true, hasRail);
|
||||
if (width < 800) {
|
||||
return _buildScaffold(context, true, true, false);
|
||||
}
|
||||
if (width < 1000) {
|
||||
return _buildScaffold(context, true, true, true);
|
||||
} else {
|
||||
// Fully expanded layout, close existing drawer if open
|
||||
final scaffoldState = scaffoldGlobalKey.currentState;
|
||||
@ -99,7 +95,7 @@ class AppPage extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _buildScaffold(context, false, false),
|
||||
child: _buildScaffold(context, false, false, true),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -189,34 +185,48 @@ class AppPage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Scaffold _buildScaffold(BuildContext context, bool hasDrawer, bool hasRail) {
|
||||
Scaffold _buildScaffold(
|
||||
BuildContext context, bool hasDrawer, bool hasRail, bool hasManage) {
|
||||
var body =
|
||||
centered ? Center(child: _buildMainContent()) : _buildMainContent();
|
||||
if (hasRail) {
|
||||
if (onFileDropped != null) {
|
||||
body = FileDropTarget(
|
||||
onFileDropped: onFileDropped!,
|
||||
overlay: fileDropOverlay!,
|
||||
child: body,
|
||||
);
|
||||
}
|
||||
if (hasRail || hasManage) {
|
||||
body = Row(
|
||||
crossAxisAlignment: onFileDropped != null
|
||||
? CrossAxisAlignment.stretch
|
||||
: CrossAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 72,
|
||||
child: SingleChildScrollView(
|
||||
child: NavigationContent(
|
||||
key: _navKey,
|
||||
shouldPop: false,
|
||||
extended: false,
|
||||
if (hasRail)
|
||||
SizedBox(
|
||||
width: 72,
|
||||
child: SingleChildScrollView(
|
||||
child: NavigationContent(
|
||||
key: _navKey,
|
||||
shouldPop: false,
|
||||
extended: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: onFileDropped != null
|
||||
? FileDropTarget(
|
||||
onFileDropped: onFileDropped!,
|
||||
overlay: fileDropOverlay!,
|
||||
child: body,
|
||||
)
|
||||
: body,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.deferToChild,
|
||||
onTap: () {
|
||||
Actions.invoke(context, const EscapeIntent());
|
||||
},
|
||||
child: body,
|
||||
),
|
||||
),
|
||||
if (hasManage)
|
||||
SingleChildScrollView(
|
||||
child: SizedBox(
|
||||
width: 320,
|
||||
child: keyActionsBuilder?.call(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@ -242,7 +252,8 @@ class AppPage extends StatelessWidget {
|
||||
)
|
||||
: null,
|
||||
actions: [
|
||||
if (actionButtonBuilder == null && keyActionsBuilder != null)
|
||||
if (actionButtonBuilder == null &&
|
||||
(keyActionsBuilder != null && !hasManage))
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4),
|
||||
child: IconButton(
|
||||
@ -251,7 +262,12 @@ class AppPage extends StatelessWidget {
|
||||
showBlurDialog(
|
||||
context: context,
|
||||
barrierColor: Colors.transparent,
|
||||
builder: keyActionsBuilder!,
|
||||
builder: (context) => FsDialog(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 32),
|
||||
child: keyActionsBuilder!(context),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: keyActionsBadge
|
||||
|
@ -106,48 +106,37 @@ class SettingsPage extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final theme = Theme.of(context);
|
||||
final enableTranslations = ref.watch(communityTranslationsProvider);
|
||||
|
||||
return ResponsiveDialog(
|
||||
title: Text(l10n.s_settings),
|
||||
child: Theme(
|
||||
// Make the headers use the primary color to pop a bit.
|
||||
// Once M3 is implemented this will probably not be needed.
|
||||
data: theme.copyWith(
|
||||
textTheme: theme.textTheme.copyWith(
|
||||
labelLarge: theme.textTheme.labelLarge
|
||||
?.copyWith(color: theme.colorScheme.primary)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// add nfc options only on devices with NFC capability
|
||||
if (isAndroid && ref.watch(androidNfcSupportProvider)) ...[
|
||||
ListTitle(l10n.s_nfc_options),
|
||||
const NfcTapActionView(),
|
||||
const NfcKbdLayoutView(),
|
||||
const NfcBypassTouchView(),
|
||||
const NfcSilenceSoundsView(),
|
||||
],
|
||||
if (isAndroid) ...[
|
||||
ListTitle(l10n.s_usb_options),
|
||||
const UsbOpenAppView(),
|
||||
],
|
||||
ListTitle(l10n.s_appearance),
|
||||
const _ThemeModeView(),
|
||||
if (enableTranslations ||
|
||||
basicLocaleListResolution(
|
||||
PlatformDispatcher.instance.locales, officialLocales) !=
|
||||
basicLocaleListResolution(
|
||||
PlatformDispatcher.instance.locales,
|
||||
AppLocalizations.supportedLocales)) ...[
|
||||
ListTitle(l10n.s_language),
|
||||
const _CommunityTranslationsView(),
|
||||
],
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// add nfc options only on devices with NFC capability
|
||||
if (isAndroid && ref.watch(androidNfcSupportProvider)) ...[
|
||||
ListTitle(l10n.s_nfc_options),
|
||||
const NfcTapActionView(),
|
||||
const NfcKbdLayoutView(),
|
||||
const NfcBypassTouchView(),
|
||||
const NfcSilenceSoundsView(),
|
||||
],
|
||||
),
|
||||
if (isAndroid) ...[
|
||||
ListTitle(l10n.s_usb_options),
|
||||
const UsbOpenAppView(),
|
||||
],
|
||||
ListTitle(l10n.s_appearance),
|
||||
const _ThemeModeView(),
|
||||
if (enableTranslations ||
|
||||
basicLocaleListResolution(
|
||||
PlatformDispatcher.instance.locales, officialLocales) !=
|
||||
basicLocaleListResolution(PlatformDispatcher.instance.locales,
|
||||
AppLocalizations.supportedLocales)) ...[
|
||||
ListTitle(l10n.s_language),
|
||||
const _CommunityTranslationsView(),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -20,7 +20,6 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import '../../app/message.dart';
|
||||
import '../../app/models.dart';
|
||||
import '../../app/views/action_list.dart';
|
||||
import '../../app/views/fs_dialog.dart';
|
||||
import '../features.dart' as features;
|
||||
import '../keys.dart' as keys;
|
||||
import '../models.dart';
|
||||
@ -40,85 +39,78 @@ Widget fidoBuildActions(
|
||||
final colors = Theme.of(context).buttonTheme.colorScheme ??
|
||||
Theme.of(context).colorScheme;
|
||||
|
||||
return FsDialog(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 32),
|
||||
child: Column(
|
||||
children: [
|
||||
if (state.bioEnroll != null)
|
||||
ActionListSection(
|
||||
l10n.s_setup,
|
||||
children: [
|
||||
ActionListItem(
|
||||
key: keys.addFingerprintAction,
|
||||
feature: features.actionsAddFingerprint,
|
||||
actionStyle: ActionStyle.primary,
|
||||
icon: const Icon(Icons.fingerprint_outlined),
|
||||
title: l10n.s_add_fingerprint,
|
||||
subtitle: state.unlocked
|
||||
? l10n.l_fingerprints_used(fingerprints)
|
||||
: state.hasPin
|
||||
? l10n.l_unlock_pin_first
|
||||
: l10n.l_set_pin_first,
|
||||
trailing: fingerprints == 0
|
||||
? Icon(Icons.warning_amber, color: colors.tertiary)
|
||||
: null,
|
||||
onTap: state.unlocked && fingerprints < 5
|
||||
? (context) {
|
||||
Navigator.of(context).pop();
|
||||
showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
AddFingerprintDialog(node.path),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
],
|
||||
return Column(
|
||||
children: [
|
||||
if (state.bioEnroll != null)
|
||||
ActionListSection(
|
||||
l10n.s_setup,
|
||||
children: [
|
||||
ActionListItem(
|
||||
key: keys.addFingerprintAction,
|
||||
feature: features.actionsAddFingerprint,
|
||||
actionStyle: ActionStyle.primary,
|
||||
icon: const Icon(Icons.fingerprint_outlined),
|
||||
title: l10n.s_add_fingerprint,
|
||||
subtitle: state.unlocked
|
||||
? l10n.l_fingerprints_used(fingerprints)
|
||||
: state.hasPin
|
||||
? l10n.l_unlock_pin_first
|
||||
: l10n.l_set_pin_first,
|
||||
trailing: fingerprints == 0
|
||||
? Icon(Icons.warning_amber, color: colors.tertiary)
|
||||
: null,
|
||||
onTap: state.unlocked && fingerprints < 5
|
||||
? (context) {
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => AddFingerprintDialog(node.path),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
ActionListSection(
|
||||
l10n.s_manage,
|
||||
children: [
|
||||
ActionListItem(
|
||||
key: keys.managePinAction,
|
||||
feature: features.actionsPin,
|
||||
icon: const Icon(Icons.pin_outlined),
|
||||
title: state.hasPin ? l10n.s_change_pin : l10n.s_set_pin,
|
||||
subtitle: state.hasPin
|
||||
? (state.forcePinChange
|
||||
? l10n.s_pin_change_required
|
||||
: l10n.s_fido_pin_protection)
|
||||
: l10n.l_fido_pin_protection_optional,
|
||||
trailing:
|
||||
state.alwaysUv && !state.hasPin || state.forcePinChange
|
||||
? Icon(Icons.warning_amber, color: colors.tertiary)
|
||||
: null,
|
||||
onTap: (context) {
|
||||
Navigator.of(context).pop();
|
||||
showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => FidoPinDialog(node.path, state),
|
||||
);
|
||||
}),
|
||||
ActionListItem(
|
||||
key: keys.resetAction,
|
||||
feature: features.actionsReset,
|
||||
actionStyle: ActionStyle.error,
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
title: l10n.s_reset_fido,
|
||||
subtitle: l10n.l_factory_reset_this_app,
|
||||
onTap: (context) {
|
||||
Navigator.of(context).pop();
|
||||
showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => ResetDialog(node),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
ActionListSection(
|
||||
l10n.s_manage,
|
||||
children: [
|
||||
ActionListItem(
|
||||
key: keys.managePinAction,
|
||||
feature: features.actionsPin,
|
||||
icon: const Icon(Icons.pin_outlined),
|
||||
title: state.hasPin ? l10n.s_change_pin : l10n.s_set_pin,
|
||||
subtitle: state.hasPin
|
||||
? (state.forcePinChange
|
||||
? l10n.s_pin_change_required
|
||||
: l10n.s_fido_pin_protection)
|
||||
: l10n.l_fido_pin_protection_optional,
|
||||
trailing: state.alwaysUv && !state.hasPin || state.forcePinChange
|
||||
? Icon(Icons.warning_amber, color: colors.tertiary)
|
||||
: null,
|
||||
onTap: (context) {
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => FidoPinDialog(node.path, state),
|
||||
);
|
||||
}),
|
||||
ActionListItem(
|
||||
key: keys.resetAction,
|
||||
feature: features.actionsReset,
|
||||
actionStyle: ActionStyle.error,
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
title: l10n.s_reset_fido,
|
||||
subtitle: l10n.l_factory_reset_this_app,
|
||||
onTap: (context) {
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => ResetDialog(node),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -23,7 +23,6 @@ import '../../app/message.dart';
|
||||
import '../../app/models.dart';
|
||||
import '../../app/state.dart';
|
||||
import '../../app/views/action_list.dart';
|
||||
import '../../app/views/fs_dialog.dart';
|
||||
import '../../core/state.dart';
|
||||
import '../features.dart' as features;
|
||||
import '../icon_provider/icon_pack_dialog.dart';
|
||||
@ -43,98 +42,91 @@ Widget oathBuildActions(
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final capacity = oathState.version.isAtLeast(4) ? 32 : null;
|
||||
|
||||
return FsDialog(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 32),
|
||||
child: Column(
|
||||
children: [
|
||||
ActionListSection(l10n.s_setup, children: [
|
||||
ActionListItem(
|
||||
feature: features.actionsAdd,
|
||||
key: keys.addAccountAction,
|
||||
title: l10n.s_add_account,
|
||||
subtitle: used == null
|
||||
? l10n.l_unlock_first
|
||||
: (capacity != null
|
||||
? l10n.l_accounts_used(used, capacity)
|
||||
: ''),
|
||||
actionStyle: ActionStyle.primary,
|
||||
icon: const Icon(Icons.person_add_alt_1_outlined),
|
||||
onTap: used != null && (capacity == null || capacity > used)
|
||||
? (context) async {
|
||||
Navigator.of(context).pop();
|
||||
if (isAndroid) {
|
||||
final withContext = ref.read(withContextProvider);
|
||||
final qrScanner = ref.read(qrScannerProvider);
|
||||
if (qrScanner != null) {
|
||||
final qrData = await qrScanner.scanQr();
|
||||
await AndroidQrScanner.handleScannedData(
|
||||
qrData, withContext, qrScanner, l10n);
|
||||
} else {
|
||||
// no QR scanner - enter data manually
|
||||
await AndroidQrScanner.showAccountManualEntryDialog(
|
||||
withContext, l10n);
|
||||
}
|
||||
} else {
|
||||
await showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
AddAccountDialog(devicePath, oathState),
|
||||
);
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
ActionListSection(l10n.s_setup, children: [
|
||||
ActionListItem(
|
||||
feature: features.actionsAdd,
|
||||
key: keys.addAccountAction,
|
||||
title: l10n.s_add_account,
|
||||
subtitle: used == null
|
||||
? l10n.l_unlock_first
|
||||
: (capacity != null
|
||||
? l10n.l_accounts_used(used, capacity)
|
||||
: ''),
|
||||
actionStyle: ActionStyle.primary,
|
||||
icon: const Icon(Icons.person_add_alt_1_outlined),
|
||||
onTap: used != null && (capacity == null || capacity > used)
|
||||
? (context) async {
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
if (isAndroid) {
|
||||
final withContext = ref.read(withContextProvider);
|
||||
final qrScanner = ref.read(qrScannerProvider);
|
||||
if (qrScanner != null) {
|
||||
final qrData = await qrScanner.scanQr();
|
||||
await AndroidQrScanner.handleScannedData(
|
||||
qrData, withContext, qrScanner, l10n);
|
||||
} else {
|
||||
// no QR scanner - enter data manually
|
||||
await AndroidQrScanner.showAccountManualEntryDialog(
|
||||
withContext, l10n);
|
||||
}
|
||||
: null),
|
||||
]),
|
||||
ActionListSection(l10n.s_manage, children: [
|
||||
ActionListItem(
|
||||
key: keys.customIconsAction,
|
||||
feature: features.actionsIcons,
|
||||
title: l10n.s_custom_icons,
|
||||
subtitle: l10n.l_set_icons_for_accounts,
|
||||
icon: const Icon(Icons.image_outlined),
|
||||
onTap: (context) async {
|
||||
Navigator.of(context).pop();
|
||||
await ref.read(withContextProvider)((context) =>
|
||||
showBlurDialog(
|
||||
} else {
|
||||
await showBlurDialog(
|
||||
context: context,
|
||||
routeSettings:
|
||||
const RouteSettings(name: 'oath_icon_pack_dialog'),
|
||||
builder: (context) => const IconPackDialog(),
|
||||
));
|
||||
}),
|
||||
ActionListItem(
|
||||
key: keys.setOrManagePasswordAction,
|
||||
feature: features.actionsPassword,
|
||||
title: oathState.hasKey
|
||||
? l10n.s_manage_password
|
||||
: l10n.s_set_password,
|
||||
subtitle: l10n.l_optional_password_protection,
|
||||
icon: const Icon(Icons.password_outlined),
|
||||
onTap: (context) {
|
||||
Navigator.of(context).pop();
|
||||
showBlurDialog(
|
||||
builder: (context) =>
|
||||
AddAccountDialog(devicePath, oathState),
|
||||
);
|
||||
}
|
||||
}
|
||||
: null),
|
||||
]),
|
||||
ActionListSection(l10n.s_manage, children: [
|
||||
ActionListItem(
|
||||
key: keys.customIconsAction,
|
||||
feature: features.actionsIcons,
|
||||
title: l10n.s_custom_icons,
|
||||
subtitle: l10n.l_set_icons_for_accounts,
|
||||
icon: const Icon(Icons.image_outlined),
|
||||
onTap: (context) async {
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
await ref.read(withContextProvider)((context) => showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
ManagePasswordDialog(devicePath, oathState),
|
||||
);
|
||||
}),
|
||||
ActionListItem(
|
||||
key: keys.resetAction,
|
||||
feature: features.actionsReset,
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
actionStyle: ActionStyle.error,
|
||||
title: l10n.s_reset_oath,
|
||||
subtitle: l10n.l_factory_reset_this_app,
|
||||
onTap: (context) {
|
||||
Navigator.of(context).pop();
|
||||
showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => ResetDialog(devicePath),
|
||||
);
|
||||
}),
|
||||
]),
|
||||
],
|
||||
),
|
||||
),
|
||||
routeSettings:
|
||||
const RouteSettings(name: 'oath_icon_pack_dialog'),
|
||||
builder: (context) => const IconPackDialog(),
|
||||
));
|
||||
}),
|
||||
ActionListItem(
|
||||
key: keys.setOrManagePasswordAction,
|
||||
feature: features.actionsPassword,
|
||||
title:
|
||||
oathState.hasKey ? l10n.s_manage_password : l10n.s_set_password,
|
||||
subtitle: l10n.l_optional_password_protection,
|
||||
icon: const Icon(Icons.password_outlined),
|
||||
onTap: (context) {
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
ManagePasswordDialog(devicePath, oathState),
|
||||
);
|
||||
}),
|
||||
ActionListItem(
|
||||
key: keys.resetAction,
|
||||
feature: features.actionsReset,
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
actionStyle: ActionStyle.error,
|
||||
title: l10n.s_reset_oath,
|
||||
subtitle: l10n.l_factory_reset_this_app,
|
||||
onTap: (context) {
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => ResetDialog(devicePath),
|
||||
);
|
||||
}),
|
||||
]),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -21,7 +21,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../app/message.dart';
|
||||
import '../../app/models.dart';
|
||||
import '../../app/views/action_list.dart';
|
||||
import '../../app/views/fs_dialog.dart';
|
||||
import '../features.dart' as features;
|
||||
import '../keys.dart' as keys;
|
||||
import '../models.dart';
|
||||
@ -31,30 +30,25 @@ Widget otpBuildActions(BuildContext context, DevicePath devicePath,
|
||||
OtpState otpState, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
return FsDialog(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 32),
|
||||
child: Column(
|
||||
children: [
|
||||
ActionListSection(l10n.s_manage, children: [
|
||||
ActionListItem(
|
||||
key: keys.swapSlots,
|
||||
feature: features.actionsSwap,
|
||||
title: l10n.s_swap_slots,
|
||||
subtitle: l10n.l_swap_slots_desc,
|
||||
icon: const Icon(Icons.swap_vert_outlined),
|
||||
onTap: (otpState.slot1Configured || otpState.slot2Configured)
|
||||
? (context) {
|
||||
Navigator.of(context).pop();
|
||||
showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => SwapSlotsDialog(devicePath));
|
||||
}
|
||||
: null,
|
||||
)
|
||||
])
|
||||
],
|
||||
),
|
||||
),
|
||||
return Column(
|
||||
children: [
|
||||
ActionListSection(l10n.s_manage, children: [
|
||||
ActionListItem(
|
||||
key: keys.swapSlots,
|
||||
feature: features.actionsSwap,
|
||||
title: l10n.s_swap_slots,
|
||||
subtitle: l10n.l_swap_slots_desc,
|
||||
icon: const Icon(Icons.swap_vert_outlined),
|
||||
onTap: (otpState.slot1Configured || otpState.slot2Configured)
|
||||
? (context) {
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => SwapSlotsDialog(devicePath));
|
||||
}
|
||||
: null,
|
||||
)
|
||||
])
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -21,7 +21,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../app/message.dart';
|
||||
import '../../app/models.dart';
|
||||
import '../../app/views/action_list.dart';
|
||||
import '../../app/views/fs_dialog.dart';
|
||||
import '../features.dart' as features;
|
||||
import '../keys.dart' as keys;
|
||||
import '../models.dart';
|
||||
@ -42,115 +41,109 @@ Widget pivBuildActions(BuildContext context, DevicePath devicePath,
|
||||
final pukAttempts = pivState.metadata?.pukMetadata.attemptsRemaining;
|
||||
final alertIcon = Icon(Icons.warning_amber, color: colors.tertiary);
|
||||
|
||||
return FsDialog(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 32),
|
||||
child: Column(
|
||||
return Column(
|
||||
children: [
|
||||
ActionListSection(
|
||||
l10n.s_manage,
|
||||
children: [
|
||||
ActionListSection(
|
||||
l10n.s_manage,
|
||||
children: [
|
||||
ActionListItem(
|
||||
key: keys.managePinAction,
|
||||
feature: features.actionsPin,
|
||||
title: l10n.s_pin,
|
||||
subtitle: pinBlocked
|
||||
? (pukAttempts != 0
|
||||
? l10n.l_piv_pin_blocked
|
||||
: l10n.l_piv_pin_puk_blocked)
|
||||
: l10n.l_attempts_remaining(pivState.pinAttempts),
|
||||
icon: const Icon(Icons.pin_outlined),
|
||||
trailing: pinBlocked ? alertIcon : null,
|
||||
onTap: !(pinBlocked && pukAttempts == 0)
|
||||
? (context) {
|
||||
Navigator.of(context).pop();
|
||||
showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => ManagePinPukDialog(
|
||||
devicePath,
|
||||
target: pinBlocked
|
||||
? ManageTarget.unblock
|
||||
: ManageTarget.pin,
|
||||
),
|
||||
);
|
||||
}
|
||||
: null),
|
||||
ActionListItem(
|
||||
key: keys.managePukAction,
|
||||
feature: features.actionsPuk,
|
||||
title: l10n.s_puk,
|
||||
subtitle: pukAttempts != null
|
||||
? (pukAttempts == 0
|
||||
? l10n.l_piv_pin_puk_blocked
|
||||
: l10n.l_attempts_remaining(pukAttempts))
|
||||
: null,
|
||||
icon: const Icon(Icons.pin_outlined),
|
||||
trailing: pukAttempts == 0 ? alertIcon : null,
|
||||
onTap: pukAttempts != 0
|
||||
? (context) {
|
||||
Navigator.of(context).pop();
|
||||
showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => ManagePinPukDialog(devicePath,
|
||||
target: ManageTarget.puk),
|
||||
);
|
||||
}
|
||||
: null),
|
||||
ActionListItem(
|
||||
key: keys.manageManagementKeyAction,
|
||||
feature: features.actionsManagementKey,
|
||||
title: l10n.s_management_key,
|
||||
subtitle: usingDefaultMgmtKey
|
||||
? l10n.l_warning_default_key
|
||||
: (pivState.protectedKey
|
||||
? l10n.l_pin_protected_key
|
||||
: l10n.l_change_management_key),
|
||||
icon: const Icon(Icons.key_outlined),
|
||||
trailing: usingDefaultMgmtKey ? alertIcon : null,
|
||||
onTap: (context) {
|
||||
Navigator.of(context).pop();
|
||||
showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
ManageKeyDialog(devicePath, pivState),
|
||||
);
|
||||
}),
|
||||
ActionListItem(
|
||||
key: keys.resetAction,
|
||||
feature: features.actionsReset,
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
actionStyle: ActionStyle.error,
|
||||
title: l10n.s_reset_piv,
|
||||
subtitle: l10n.l_factory_reset_this_app,
|
||||
onTap: (context) {
|
||||
Navigator.of(context).pop();
|
||||
showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => ResetDialog(devicePath),
|
||||
);
|
||||
})
|
||||
],
|
||||
),
|
||||
// TODO
|
||||
/*
|
||||
if (false == true) ...[
|
||||
KeyActionTitle(l10n.s_setup),
|
||||
KeyActionItem(
|
||||
key: keys.setupMacOsAction,
|
||||
title: Text('Setup for macOS'),
|
||||
subtitle: Text('Create certificates for macOS login'),
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: theme.secondary,
|
||||
foregroundColor: theme.onSecondary,
|
||||
child: const Icon(Icons.laptop),
|
||||
),
|
||||
onTap: () async {
|
||||
Navigator.of(context).pop();
|
||||
}),
|
||||
],
|
||||
*/
|
||||
ActionListItem(
|
||||
key: keys.managePinAction,
|
||||
feature: features.actionsPin,
|
||||
title: l10n.s_pin,
|
||||
subtitle: pinBlocked
|
||||
? (pukAttempts != 0
|
||||
? l10n.l_piv_pin_blocked
|
||||
: l10n.l_piv_pin_puk_blocked)
|
||||
: l10n.l_attempts_remaining(pivState.pinAttempts),
|
||||
icon: const Icon(Icons.pin_outlined),
|
||||
trailing: pinBlocked ? alertIcon : null,
|
||||
onTap: !(pinBlocked && pukAttempts == 0)
|
||||
? (context) {
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => ManagePinPukDialog(
|
||||
devicePath,
|
||||
target: pinBlocked
|
||||
? ManageTarget.unblock
|
||||
: ManageTarget.pin,
|
||||
),
|
||||
);
|
||||
}
|
||||
: null),
|
||||
ActionListItem(
|
||||
key: keys.managePukAction,
|
||||
feature: features.actionsPuk,
|
||||
title: l10n.s_puk,
|
||||
subtitle: pukAttempts != null
|
||||
? (pukAttempts == 0
|
||||
? l10n.l_piv_pin_puk_blocked
|
||||
: l10n.l_attempts_remaining(pukAttempts))
|
||||
: null,
|
||||
icon: const Icon(Icons.pin_outlined),
|
||||
trailing: pukAttempts == 0 ? alertIcon : null,
|
||||
onTap: pukAttempts != 0
|
||||
? (context) {
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => ManagePinPukDialog(devicePath,
|
||||
target: ManageTarget.puk),
|
||||
);
|
||||
}
|
||||
: null),
|
||||
ActionListItem(
|
||||
key: keys.manageManagementKeyAction,
|
||||
feature: features.actionsManagementKey,
|
||||
title: l10n.s_management_key,
|
||||
subtitle: usingDefaultMgmtKey
|
||||
? l10n.l_warning_default_key
|
||||
: (pivState.protectedKey
|
||||
? l10n.l_pin_protected_key
|
||||
: l10n.l_change_management_key),
|
||||
icon: const Icon(Icons.key_outlined),
|
||||
trailing: usingDefaultMgmtKey ? alertIcon : null,
|
||||
onTap: (context) {
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => ManageKeyDialog(devicePath, pivState),
|
||||
);
|
||||
}),
|
||||
ActionListItem(
|
||||
key: keys.resetAction,
|
||||
feature: features.actionsReset,
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
actionStyle: ActionStyle.error,
|
||||
title: l10n.s_reset_piv,
|
||||
subtitle: l10n.l_factory_reset_this_app,
|
||||
onTap: (context) {
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => ResetDialog(devicePath),
|
||||
);
|
||||
})
|
||||
],
|
||||
),
|
||||
),
|
||||
// TODO
|
||||
/*
|
||||
if (false == true) ...[
|
||||
KeyActionTitle(l10n.s_setup),
|
||||
KeyActionItem(
|
||||
key: keys.setupMacOsAction,
|
||||
title: Text('Setup for macOS'),
|
||||
subtitle: Text('Create certificates for macOS login'),
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: theme.secondary,
|
||||
foregroundColor: theme.onSecondary,
|
||||
child: const Icon(Icons.laptop),
|
||||
),
|
||||
onTap: () async {
|
||||
Navigator.of(context).pop();
|
||||
}),
|
||||
],
|
||||
*/
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../app/message.dart';
|
||||
import '../../app/models.dart';
|
||||
import '../../app/shortcuts.dart';
|
||||
import '../../app/views/action_list.dart';
|
||||
import '../../app/views/app_failure_page.dart';
|
||||
import '../../app/views/app_list_item.dart';
|
||||
import '../../app/views/app_page.dart';
|
||||
@ -32,9 +33,14 @@ import '../keys.dart';
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
import 'actions.dart';
|
||||
import 'cert_info_view.dart';
|
||||
import 'key_actions.dart';
|
||||
import 'slot_dialog.dart';
|
||||
|
||||
final selectedSlot = StateProvider<PivSlot?>(
|
||||
(ref) => null,
|
||||
);
|
||||
|
||||
class PivScreen extends ConsumerWidget {
|
||||
final DevicePath devicePath;
|
||||
|
||||
@ -56,35 +62,99 @@ class PivScreen extends ConsumerWidget {
|
||||
),
|
||||
data: (pivState) {
|
||||
final pivSlots = ref.watch(pivSlotsProvider(devicePath)).asData;
|
||||
return AppPage(
|
||||
title: Text(l10n.s_certificates),
|
||||
keyActionsBuilder: hasFeature(features.actions)
|
||||
? (context) =>
|
||||
pivBuildActions(context, devicePath, pivState, ref)
|
||||
: null,
|
||||
child: Column(
|
||||
children: [
|
||||
ListTitle(l10n.s_certificates),
|
||||
if (pivSlots?.hasValue == true)
|
||||
...pivSlots!.value.map((e) => registerPivActions(
|
||||
final selected = ref.watch(selectedSlot);
|
||||
final theme = Theme.of(context);
|
||||
final textTheme = theme.textTheme;
|
||||
// This is what ListTile uses for subtitle
|
||||
final subtitleStyle = textTheme.bodyMedium!.copyWith(
|
||||
color: textTheme.bodySmall!.color,
|
||||
);
|
||||
return Actions(
|
||||
actions: {
|
||||
EscapeIntent: CallbackAction<EscapeIntent>(onInvoke: (intent) {
|
||||
if (selected != null) {
|
||||
ref.read(selectedSlot.notifier).state = null;
|
||||
} else {
|
||||
Actions.invoke(context, intent);
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
},
|
||||
child: AppPage(
|
||||
title: Text(l10n.s_certificates),
|
||||
keyActionsBuilder: hasFeature(features.actions)
|
||||
? (context) {
|
||||
if (selected == null) {
|
||||
return pivBuildActions(
|
||||
context, devicePath, pivState, ref);
|
||||
}
|
||||
return registerPivActions(
|
||||
devicePath,
|
||||
pivState,
|
||||
e,
|
||||
selected,
|
||||
ref: ref,
|
||||
actions: {
|
||||
OpenIntent:
|
||||
CallbackAction<OpenIntent>(onInvoke: (_) async {
|
||||
await showBlurDialog(
|
||||
context: context,
|
||||
barrierColor: Colors.transparent,
|
||||
builder: (context) => SlotDialog(e.slot),
|
||||
);
|
||||
return null;
|
||||
}),
|
||||
},
|
||||
builder: (context) => _CertificateListItem(e),
|
||||
)),
|
||||
],
|
||||
builder: (context) => Column(
|
||||
children: [
|
||||
ListTitle(selected.slot.getDisplayName(l10n)),
|
||||
if (selected.certInfo != null) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: CertInfoTable(selected.certInfo!),
|
||||
),
|
||||
] else ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 16.0),
|
||||
child: Text(
|
||||
l10n.l_no_certificate,
|
||||
softWrap: true,
|
||||
textAlign: TextAlign.center,
|
||||
style: subtitleStyle,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
ActionListSection.fromMenuActions(
|
||||
context,
|
||||
l10n.s_actions,
|
||||
actions: buildSlotActions(
|
||||
selected.certInfo != null, l10n),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
child: Column(
|
||||
children: [
|
||||
ListTitle(l10n.s_certificates),
|
||||
if (pivSlots?.hasValue == true)
|
||||
...pivSlots!.value.map((e) => registerPivActions(
|
||||
devicePath,
|
||||
pivState,
|
||||
e,
|
||||
ref: ref,
|
||||
actions: {
|
||||
OpenIntent: CallbackAction<OpenIntent>(
|
||||
onInvoke: (_) async {
|
||||
// TODO: Fix
|
||||
var expanded = 1 + 1 == 2;
|
||||
if (expanded) {
|
||||
ref.read(selectedSlot.notifier).state = e;
|
||||
} else {
|
||||
await showBlurDialog(
|
||||
context: context,
|
||||
barrierColor: Colors.transparent,
|
||||
builder: (context) => SlotDialog(e.slot),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
},
|
||||
builder: (context) => _CertificateListItem(e),
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
@ -104,10 +174,12 @@ class _CertificateListItem extends ConsumerWidget {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final hasFeature = ref.watch(featureProvider);
|
||||
final selected = ref.watch(selectedSlot) == pivSlot;
|
||||
|
||||
return Semantics(
|
||||
label: slot.getDisplayName(l10n),
|
||||
child: AppListItem(
|
||||
selected: selected,
|
||||
key: _getAppListItemKey(slot),
|
||||
leading: CircleAvatar(
|
||||
foregroundColor: colorScheme.onSecondary,
|
||||
|
@ -66,15 +66,14 @@ class _FileDropTargetState extends State<FileDropTarget> {
|
||||
},
|
||||
enable: !isAndroid,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
widget.child,
|
||||
if (_hovering)
|
||||
Positioned.fill(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: widget.overlay,
|
||||
),
|
||||
)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: widget.overlay,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -26,7 +26,11 @@ class ListTitle extends StatelessWidget {
|
||||
dense: true,
|
||||
title: Text(
|
||||
title,
|
||||
style: textStyle ?? Theme.of(context).textTheme.labelLarge,
|
||||
style: textStyle ??
|
||||
Theme.of(context)
|
||||
.textTheme
|
||||
.bodyLarge
|
||||
?.copyWith(color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user