mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-12-23 10:11:52 +03:00
Change AppPage
view.
This commit is contained in:
parent
d759b57b4d
commit
ffdac2b443
@ -27,9 +27,8 @@ import '../state.dart';
|
||||
import 'message_page.dart';
|
||||
|
||||
class AppFailurePage extends ConsumerWidget {
|
||||
final Widget? title;
|
||||
final Object cause;
|
||||
const AppFailurePage({this.title, required this.cause, super.key}) : super();
|
||||
const AppFailurePage({required this.cause, super.key}) : super();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@ -99,7 +98,6 @@ class AppFailurePage extends ConsumerWidget {
|
||||
}
|
||||
|
||||
return MessagePage(
|
||||
title: title,
|
||||
graphic: graphic,
|
||||
header: header,
|
||||
message: message,
|
||||
|
@ -32,7 +32,7 @@ final _navKey = GlobalKey();
|
||||
final _navExpandedKey = GlobalKey();
|
||||
|
||||
class AppPage extends StatelessWidget {
|
||||
final Widget? title;
|
||||
final String? title;
|
||||
final Widget Function(BuildContext context, bool expanded) builder;
|
||||
final Widget Function(BuildContext context)? detailViewBuilder;
|
||||
final List<Widget> actions;
|
||||
@ -150,9 +150,29 @@ class AppPage extends StatelessWidget {
|
||||
));
|
||||
}
|
||||
|
||||
List<Widget> _buildTitle(BuildContext context) {
|
||||
return title != null
|
||||
? [
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0),
|
||||
child: Text(title!,
|
||||
style: Theme.of(context).textTheme.displaySmall!.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary)),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
)
|
||||
]
|
||||
: [];
|
||||
}
|
||||
|
||||
Widget _buildMainContent(BuildContext context, bool expanded) {
|
||||
final content = Column(
|
||||
children: [
|
||||
..._buildTitle(context),
|
||||
builder(context, expanded),
|
||||
if (actions.isNotEmpty)
|
||||
Align(
|
||||
@ -248,10 +268,7 @@ class AppPage extends StatelessWidget {
|
||||
return Scaffold(
|
||||
key: scaffoldGlobalKey,
|
||||
appBar: AppBar(
|
||||
title: title,
|
||||
titleSpacing: hasDrawer ? 2 : 8,
|
||||
centerTitle: true,
|
||||
titleTextStyle: Theme.of(context).textTheme.titleLarge,
|
||||
scrolledUnderElevation: 0.0,
|
||||
leadingWidth: hasRail ? 84 : null,
|
||||
leading: hasRail
|
||||
? const Row(
|
||||
|
@ -21,7 +21,6 @@ import 'package:flutter/material.dart';
|
||||
import 'app_page.dart';
|
||||
|
||||
class MessagePage extends StatelessWidget {
|
||||
final Widget? title;
|
||||
final Widget? graphic;
|
||||
final String? header;
|
||||
final String? message;
|
||||
@ -35,7 +34,6 @@ class MessagePage extends StatelessWidget {
|
||||
|
||||
const MessagePage({
|
||||
super.key,
|
||||
this.title,
|
||||
this.graphic,
|
||||
this.header,
|
||||
this.message,
|
||||
@ -50,7 +48,6 @@ class MessagePage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => AppPage(
|
||||
title: title,
|
||||
centered: true,
|
||||
actions: actions,
|
||||
keyActionsBuilder: keyActionsBuilder,
|
||||
|
@ -36,7 +36,7 @@ class FidoScreen extends ConsumerWidget {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return ref.watch(fidoStateProvider(deviceData.node.path)).when(
|
||||
loading: () => AppPage(
|
||||
title: Text(l10n.s_webauthn),
|
||||
title: l10n.s_webauthn,
|
||||
centered: true,
|
||||
delayedContent: true,
|
||||
builder: (context, _) => const CircularProgressIndicator(),
|
||||
@ -47,7 +47,6 @@ class FidoScreen extends ConsumerWidget {
|
||||
0;
|
||||
if (Capability.fido2.value & supported == 0) {
|
||||
return MessagePage(
|
||||
title: Text(l10n.s_webauthn),
|
||||
graphic: Icon(Icons.security,
|
||||
size: 96, color: Theme.of(context).colorScheme.primary),
|
||||
header: l10n.l_ready_to_use,
|
||||
@ -59,14 +58,12 @@ class FidoScreen extends ConsumerWidget {
|
||||
0;
|
||||
if (Capability.fido2.value & enabled == 0) {
|
||||
return MessagePage(
|
||||
title: Text(l10n.s_webauthn),
|
||||
header: l10n.s_fido_disabled,
|
||||
message: l10n.l_webauthn_req_fido2,
|
||||
);
|
||||
}
|
||||
|
||||
return AppFailurePage(
|
||||
title: Text(l10n.s_webauthn),
|
||||
cause: error,
|
||||
);
|
||||
},
|
||||
|
@ -44,7 +44,6 @@ class FidoLockedPage extends ConsumerWidget {
|
||||
if (!state.hasPin) {
|
||||
if (state.bioEnroll != null) {
|
||||
return MessagePage(
|
||||
title: Text(l10n.s_webauthn),
|
||||
graphic: Icon(Icons.fingerprint,
|
||||
size: 96, color: Theme.of(context).colorScheme.primary),
|
||||
header: l10n.s_no_fingerprints,
|
||||
@ -54,7 +53,6 @@ class FidoLockedPage extends ConsumerWidget {
|
||||
);
|
||||
} else {
|
||||
return MessagePage(
|
||||
title: Text(l10n.s_webauthn),
|
||||
graphic: Icon(Icons.security,
|
||||
size: 96, color: Theme.of(context).colorScheme.primary),
|
||||
header: state.credMgmt
|
||||
@ -69,7 +67,6 @@ class FidoLockedPage extends ConsumerWidget {
|
||||
|
||||
if (!state.credMgmt && state.bioEnroll == null) {
|
||||
return MessagePage(
|
||||
title: Text(l10n.s_webauthn),
|
||||
graphic: Icon(Icons.security,
|
||||
size: 96, color: Theme.of(context).colorScheme.primary),
|
||||
header: l10n.l_ready_to_use,
|
||||
@ -81,7 +78,6 @@ class FidoLockedPage extends ConsumerWidget {
|
||||
|
||||
if (state.forcePinChange) {
|
||||
return MessagePage(
|
||||
title: Text(l10n.s_webauthn),
|
||||
header: l10n.s_pin_change_required,
|
||||
message: l10n.l_pin_change_required_desc,
|
||||
keyActionsBuilder: hasActions ? _buildActions : null,
|
||||
@ -90,7 +86,7 @@ class FidoLockedPage extends ConsumerWidget {
|
||||
}
|
||||
|
||||
return AppPage(
|
||||
title: Text(l10n.s_webauthn),
|
||||
title: l10n.s_webauthn,
|
||||
keyActionsBuilder: hasActions ? _buildActions : null,
|
||||
builder: (context, _) => Column(
|
||||
children: [
|
||||
@ -157,7 +153,7 @@ class _PinEntryFormState extends ConsumerState<_PinEntryForm> {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final noFingerprints = widget._state.bioEnroll == false;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 18.0, right: 18, top: 32),
|
||||
padding: const EdgeInsets.only(left: 18.0, right: 18, top: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
@ -167,7 +167,7 @@ class _FidoUnlockedPageState extends ConsumerState<FidoUnlockedPage> {
|
||||
}),
|
||||
},
|
||||
builder: (context) => AppPage(
|
||||
title: Text(l10n.s_webauthn),
|
||||
title: l10n.s_webauthn,
|
||||
detailViewBuilder: switch (_selected) {
|
||||
FidoCredential credential => (context) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
@ -290,7 +290,6 @@ class _FidoUnlockedPageState extends ConsumerState<FidoUnlockedPage> {
|
||||
|
||||
if (widget.state.bioEnroll != null) {
|
||||
return MessagePage(
|
||||
title: Text(l10n.s_webauthn),
|
||||
graphic: Icon(Icons.fingerprint,
|
||||
size: 96, color: Theme.of(context).colorScheme.primary),
|
||||
header: l10n.s_no_fingerprints,
|
||||
@ -304,7 +303,6 @@ class _FidoUnlockedPageState extends ConsumerState<FidoUnlockedPage> {
|
||||
}
|
||||
|
||||
return MessagePage(
|
||||
title: Text(l10n.s_webauthn),
|
||||
graphic: Icon(Icons.security,
|
||||
size: 96, color: Theme.of(context).colorScheme.primary),
|
||||
header: l10n.l_no_discoverable_accounts,
|
||||
@ -317,7 +315,7 @@ class _FidoUnlockedPageState extends ConsumerState<FidoUnlockedPage> {
|
||||
}
|
||||
|
||||
Widget _buildLoadingPage(BuildContext context) => AppPage(
|
||||
title: Text(AppLocalizations.of(context)!.s_webauthn),
|
||||
title: AppLocalizations.of(context)!.s_webauthn,
|
||||
centered: true,
|
||||
delayedContent: true,
|
||||
builder: (context, _) => const CircularProgressIndicator(),
|
||||
|
@ -18,7 +18,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../widgets/list_title.dart';
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
import 'account_view.dart';
|
||||
@ -50,7 +49,6 @@ class AccountList extends ConsumerWidget {
|
||||
policy: WidgetOrderTraversalPolicy(),
|
||||
child: Column(
|
||||
children: [
|
||||
if (pinnedCreds.isNotEmpty) ListTitle(l10n.s_pinned),
|
||||
...pinnedCreds.map(
|
||||
(entry) => AccountView(
|
||||
entry.credential,
|
||||
@ -58,7 +56,11 @@ class AccountList extends ConsumerWidget {
|
||||
selected: entry.credential == selected,
|
||||
),
|
||||
),
|
||||
if (creds.isNotEmpty) ListTitle(l10n.s_accounts),
|
||||
if (pinnedCreds.isNotEmpty)
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Divider(),
|
||||
),
|
||||
...creds.map(
|
||||
(entry) => AccountView(
|
||||
entry.credential,
|
||||
|
@ -54,15 +54,12 @@ class OathScreen extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return ref.watch(oathStateProvider(devicePath)).when(
|
||||
loading: () => MessagePage(
|
||||
title: Text(l10n.s_authenticator),
|
||||
graphic: const CircularProgressIndicator(),
|
||||
loading: () => const MessagePage(
|
||||
graphic: CircularProgressIndicator(),
|
||||
delayedContent: true,
|
||||
),
|
||||
error: (error, _) => AppFailurePage(
|
||||
title: Text(l10n.s_authenticator),
|
||||
cause: error,
|
||||
),
|
||||
data: (oathState) => oathState.locked
|
||||
@ -82,12 +79,12 @@ class _LockedView extends ConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final hasActions = ref.watch(featureProvider)(features.actions);
|
||||
return AppPage(
|
||||
title: Text(AppLocalizations.of(context)!.s_authenticator),
|
||||
title: AppLocalizations.of(context)!.s_accounts,
|
||||
keyActionsBuilder: hasActions
|
||||
? (context) => oathBuildActions(context, devicePath, oathState, ref)
|
||||
: null,
|
||||
builder: (context, _) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 18),
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: UnlockForm(
|
||||
devicePath,
|
||||
keystore: oathState.keystore,
|
||||
@ -158,7 +155,6 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
|
||||
|
||||
if (numCreds == 0) {
|
||||
return MessagePage(
|
||||
title: Text(l10n.s_authenticator),
|
||||
key: keys.noAccountsView,
|
||||
graphic: Icon(Icons.people,
|
||||
size: 96, color: Theme.of(context).colorScheme.primary),
|
||||
@ -232,66 +228,7 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
|
||||
}),
|
||||
},
|
||||
builder: (context) => AppPage(
|
||||
title: Focus(
|
||||
canRequestFocus: false,
|
||||
onKeyEvent: (node, event) {
|
||||
if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
|
||||
node.focusInDirection(TraversalDirection.down);
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
},
|
||||
child: Builder(builder: (context) {
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
return AppTextFormField(
|
||||
key: keys.searchAccountsField,
|
||||
controller: searchController,
|
||||
focusNode: searchFocus,
|
||||
// Use the default style, but with a smaller font size:
|
||||
style: textTheme.titleMedium
|
||||
?.copyWith(fontSize: textTheme.titleSmall?.fontSize),
|
||||
decoration: AppInputDecoration(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
borderSide: BorderSide(
|
||||
width: 0,
|
||||
style: searchFocus.hasFocus
|
||||
? BorderStyle.solid
|
||||
: BorderStyle.none,
|
||||
),
|
||||
),
|
||||
contentPadding: const EdgeInsets.all(16),
|
||||
fillColor: Theme.of(context).hoverColor,
|
||||
filled: true,
|
||||
hintText: l10n.s_search_accounts,
|
||||
isDense: true,
|
||||
prefixIcon: const Padding(
|
||||
padding: EdgeInsetsDirectional.only(start: 8.0),
|
||||
child: Icon(Icons.search_outlined),
|
||||
),
|
||||
suffixIcon: searchController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
iconSize: 16,
|
||||
onPressed: () {
|
||||
searchController.clear();
|
||||
ref.read(searchProvider.notifier).setFilter('');
|
||||
setState(() {});
|
||||
},
|
||||
)
|
||||
: null,
|
||||
),
|
||||
onChanged: (value) {
|
||||
ref.read(searchProvider.notifier).setFilter(value);
|
||||
setState(() {});
|
||||
},
|
||||
textInputAction: TextInputAction.next,
|
||||
onFieldSubmitted: (value) {
|
||||
Focus.of(context).focusInDirection(TraversalDirection.down);
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
title: l10n.s_accounts,
|
||||
keyActionsBuilder: hasActions
|
||||
? (context) => oathBuildActions(
|
||||
context,
|
||||
@ -397,15 +334,91 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
|
||||
return null;
|
||||
}),
|
||||
},
|
||||
child: Consumer(
|
||||
builder: (context, ref, _) {
|
||||
return AccountList(
|
||||
ref.watch(credentialListProvider(widget.devicePath)) ??
|
||||
[],
|
||||
expanded: expanded,
|
||||
selected: _selected,
|
||||
);
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
Focus(
|
||||
canRequestFocus: false,
|
||||
onKeyEvent: (node, event) {
|
||||
if (event.logicalKey ==
|
||||
LogicalKeyboardKey.arrowDown) {
|
||||
node.focusInDirection(TraversalDirection.down);
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
},
|
||||
child: Builder(builder: (context) {
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0, vertical: 8.0),
|
||||
child: AppTextFormField(
|
||||
key: keys.searchAccountsField,
|
||||
controller: searchController,
|
||||
focusNode: searchFocus,
|
||||
// Use the default style, but with a smaller font size:
|
||||
style: textTheme.titleMedium?.copyWith(
|
||||
fontSize: textTheme.titleSmall?.fontSize),
|
||||
decoration: AppInputDecoration(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
borderSide: BorderSide(
|
||||
width: 0,
|
||||
style: searchFocus.hasFocus
|
||||
? BorderStyle.solid
|
||||
: BorderStyle.none,
|
||||
),
|
||||
),
|
||||
contentPadding: const EdgeInsets.all(16),
|
||||
fillColor: Theme.of(context).hoverColor,
|
||||
filled: true,
|
||||
hintText: l10n.s_search_accounts,
|
||||
isDense: true,
|
||||
prefixIcon: const Padding(
|
||||
padding:
|
||||
EdgeInsetsDirectional.only(start: 8.0),
|
||||
child: Icon(Icons.search_outlined),
|
||||
),
|
||||
suffixIcon: searchController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
iconSize: 16,
|
||||
onPressed: () {
|
||||
searchController.clear();
|
||||
ref
|
||||
.read(searchProvider.notifier)
|
||||
.setFilter('');
|
||||
setState(() {});
|
||||
},
|
||||
)
|
||||
: null,
|
||||
),
|
||||
onChanged: (value) {
|
||||
ref
|
||||
.read(searchProvider.notifier)
|
||||
.setFilter(value);
|
||||
setState(() {});
|
||||
},
|
||||
textInputAction: TextInputAction.next,
|
||||
onFieldSubmitted: (value) {
|
||||
Focus.of(context)
|
||||
.focusInDirection(TraversalDirection.down);
|
||||
},
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
Consumer(
|
||||
builder: (context, ref, _) {
|
||||
return AccountList(
|
||||
ref.watch(credentialListProvider(
|
||||
widget.devicePath)) ??
|
||||
[],
|
||||
expanded: expanded,
|
||||
selected: _selected,
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
: const CircularProgressIndicator();
|
||||
|
@ -66,7 +66,7 @@ class _UnlockFormState extends ConsumerState<UnlockForm> {
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 18.0, right: 18, top: 14),
|
||||
padding: const EdgeInsets.only(left: 18.0, right: 18),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
@ -54,13 +54,11 @@ class _OtpScreenState extends ConsumerState<OtpScreen> {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final hasFeature = ref.watch(featureProvider);
|
||||
return ref.watch(otpStateProvider(widget.devicePath)).when(
|
||||
loading: () => MessagePage(
|
||||
title: Text(l10n.s_slots),
|
||||
graphic: const CircularProgressIndicator(),
|
||||
loading: () => const MessagePage(
|
||||
graphic: CircularProgressIndicator(),
|
||||
delayedContent: true,
|
||||
),
|
||||
error: (error, _) =>
|
||||
AppFailurePage(title: Text(l10n.s_slots), cause: error),
|
||||
error: (error, _) => AppFailurePage(cause: error),
|
||||
data: (otpState) {
|
||||
final selected = _selected != null
|
||||
? otpState.slots.firstWhere((e) => e.slot == _selected)
|
||||
@ -92,7 +90,7 @@ class _OtpScreenState extends ConsumerState<OtpScreen> {
|
||||
}),
|
||||
},
|
||||
child: AppPage(
|
||||
title: Text(l10n.s_slots),
|
||||
title: l10n.s_slots,
|
||||
detailViewBuilder: selected != null
|
||||
? (context) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
|
@ -56,13 +56,11 @@ class _PivScreenState extends ConsumerState<PivScreen> {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final hasFeature = ref.watch(featureProvider);
|
||||
return ref.watch(pivStateProvider(widget.devicePath)).when(
|
||||
loading: () => MessagePage(
|
||||
title: Text(l10n.s_certificates),
|
||||
graphic: const CircularProgressIndicator(),
|
||||
loading: () => const MessagePage(
|
||||
graphic: CircularProgressIndicator(),
|
||||
delayedContent: true,
|
||||
),
|
||||
error: (error, _) => AppFailurePage(
|
||||
title: Text(l10n.s_certificates),
|
||||
cause: error,
|
||||
),
|
||||
data: (pivState) {
|
||||
@ -105,7 +103,7 @@ class _PivScreenState extends ConsumerState<PivScreen> {
|
||||
),
|
||||
},
|
||||
child: AppPage(
|
||||
title: Text(l10n.s_certificates),
|
||||
title: l10n.s_certificates,
|
||||
detailViewBuilder: selected != null
|
||||
? (context) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
|
Loading…
Reference in New Issue
Block a user