Test of 3 column view.

This commit is contained in:
Dain Nilsson 2024-01-09 12:50:26 +01:00
parent 6083de1303
commit 07fb161402
No known key found for this signature in database
GPG Key ID: F04367096FBA95E8
12 changed files with 477 additions and 414 deletions

View File

@ -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(),

View File

@ -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,
]),

View File

@ -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,

View File

@ -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

View File

@ -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(),
],
],
),
);
}

View File

@ -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),
);
},
),
],
),
),
)
],
);
}

View File

@ -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),
);
}),
]),
],
);
}

View File

@ -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,
)
])
],
);
}

View File

@ -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();
}),
],
*/
],
);
}

View File

@ -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,

View File

@ -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,
),
],
),
);

View File

@ -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),
),
);
}