From 9236701f02da739438a2ba08125fd04cf8fe12e1 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Thu, 2 Dec 2021 11:44:17 +0100 Subject: [PATCH] Implement theming and re-arrange views. --- lib/about_page.dart | 12 +- lib/app.dart | 18 --- lib/app/app.dart | 23 ++++ lib/app/models.dart | 9 ++ lib/app/models.freezed.dart | 163 ++++++++++++++++++++++++ lib/app/state.dart | 101 ++++++++++++--- lib/app/views/device_avatar.dart | 26 ++++ lib/app/views/device_picker_dialog.dart | 67 ---------- lib/app/views/main_actions_dialog.dart | 85 ++++++++++++ lib/app/views/main_drawer.dart | 108 +++++++++++++--- lib/app/views/main_page.dart | 44 +++++-- lib/main.dart | 2 +- lib/oath/menu_actions.dart | 46 +++++++ lib/oath/state.dart | 32 +++-- lib/oath/views/oath_screen.dart | 23 +--- lib/theme.dart | 23 ++++ pubspec.lock | 14 +- yubikey-manager | 2 +- 18 files changed, 624 insertions(+), 174 deletions(-) delete mode 100755 lib/app.dart create mode 100755 lib/app/app.dart create mode 100755 lib/app/views/device_avatar.dart delete mode 100755 lib/app/views/device_picker_dialog.dart create mode 100755 lib/app/views/main_actions_dialog.dart create mode 100755 lib/oath/menu_actions.dart create mode 100755 lib/theme.dart diff --git a/lib/about_page.dart b/lib/about_page.dart index b704d064..0af299c0 100755 --- a/lib/about_page.dart +++ b/lib/about_page.dart @@ -37,7 +37,7 @@ class AboutPage extends ConsumerWidget { ref.read(logLevelProvider.notifier).setLevel(Level.INFO); log.info('Log level changed to INFO'); }, - child: const Text('INFO'), + child: const Text('Info'), ), TextButton( onPressed: () { @@ -46,10 +46,18 @@ class AboutPage extends ConsumerWidget { .setLevel(Level.CONFIG); log.config('Log level changed to CONFIG'); }, - child: const Text('DEBUG'), + child: const Text('Config'), + ), + TextButton( + onPressed: () { + ref.read(logLevelProvider.notifier).setLevel(Level.FINE); + log.fine('Log level changed to FINE'); + }, + child: const Text('Fine'), ), ], ), + const Divider(), TextButton( onPressed: () async { log.info('Running diagnostics...'); diff --git a/lib/app.dart b/lib/app.dart deleted file mode 100755 index 78ee468e..00000000 --- a/lib/app.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:flutter/material.dart'; - -class YubicoAuthenticatorApp extends StatelessWidget { - final Widget page; - const YubicoAuthenticatorApp({required this.page, Key? key}) - : super(key: key); - - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Yubico Authenticator', - theme: ThemeData( - primarySwatch: Colors.blue, - ), - home: page, - ); - } -} diff --git a/lib/app/app.dart b/lib/app/app.dart new file mode 100755 index 00000000..3a3c896c --- /dev/null +++ b/lib/app/app.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'state.dart'; +import '../theme.dart'; + +class YubicoAuthenticatorApp extends ConsumerWidget { + final Widget page; + const YubicoAuthenticatorApp({required this.page, Key? key}) + : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return MaterialApp( + title: 'Yubico Authenticator', + theme: AppTheme.lightTheme, + darkTheme: AppTheme.darkTheme, + themeMode: ref.watch(themeModeProvider), + home: page, + debugShowCheckedModeBanner: false, + ); + } +} diff --git a/lib/app/models.dart b/lib/app/models.dart index e2e3ba23..d317ad63 100755 --- a/lib/app/models.dart +++ b/lib/app/models.dart @@ -1,3 +1,4 @@ +import 'package:flutter/material.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import '../../management/models.dart'; @@ -19,3 +20,11 @@ class DeviceNode with _$DeviceNode { factory DeviceNode.fromJson(Map json) => _$DeviceNodeFromJson(json); } + +@freezed +class MenuAction with _$MenuAction { + factory MenuAction( + {required String text, + required Icon icon, + void Function()? action}) = _MenuAction; +} diff --git a/lib/app/models.freezed.dart b/lib/app/models.freezed.dart index a128e67c..13562189 100755 --- a/lib/app/models.freezed.dart +++ b/lib/app/models.freezed.dart @@ -251,3 +251,166 @@ abstract class _DeviceNode implements DeviceNode { _$DeviceNodeCopyWith<_DeviceNode> get copyWith => throw _privateConstructorUsedError; } + +/// @nodoc +class _$MenuActionTearOff { + const _$MenuActionTearOff(); + + _MenuAction call( + {required String text, required Icon icon, void Function()? action}) { + return _MenuAction( + text: text, + icon: icon, + action: action, + ); + } +} + +/// @nodoc +const $MenuAction = _$MenuActionTearOff(); + +/// @nodoc +mixin _$MenuAction { + String get text => throw _privateConstructorUsedError; + Icon get icon => throw _privateConstructorUsedError; + void Function()? get action => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $MenuActionCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $MenuActionCopyWith<$Res> { + factory $MenuActionCopyWith( + MenuAction value, $Res Function(MenuAction) then) = + _$MenuActionCopyWithImpl<$Res>; + $Res call({String text, Icon icon, void Function()? action}); +} + +/// @nodoc +class _$MenuActionCopyWithImpl<$Res> implements $MenuActionCopyWith<$Res> { + _$MenuActionCopyWithImpl(this._value, this._then); + + final MenuAction _value; + // ignore: unused_field + final $Res Function(MenuAction) _then; + + @override + $Res call({ + Object? text = freezed, + Object? icon = freezed, + Object? action = freezed, + }) { + return _then(_value.copyWith( + text: text == freezed + ? _value.text + : text // ignore: cast_nullable_to_non_nullable + as String, + icon: icon == freezed + ? _value.icon + : icon // ignore: cast_nullable_to_non_nullable + as Icon, + action: action == freezed + ? _value.action + : action // ignore: cast_nullable_to_non_nullable + as void Function()?, + )); + } +} + +/// @nodoc +abstract class _$MenuActionCopyWith<$Res> implements $MenuActionCopyWith<$Res> { + factory _$MenuActionCopyWith( + _MenuAction value, $Res Function(_MenuAction) then) = + __$MenuActionCopyWithImpl<$Res>; + @override + $Res call({String text, Icon icon, void Function()? action}); +} + +/// @nodoc +class __$MenuActionCopyWithImpl<$Res> extends _$MenuActionCopyWithImpl<$Res> + implements _$MenuActionCopyWith<$Res> { + __$MenuActionCopyWithImpl( + _MenuAction _value, $Res Function(_MenuAction) _then) + : super(_value, (v) => _then(v as _MenuAction)); + + @override + _MenuAction get _value => super._value as _MenuAction; + + @override + $Res call({ + Object? text = freezed, + Object? icon = freezed, + Object? action = freezed, + }) { + return _then(_MenuAction( + text: text == freezed + ? _value.text + : text // ignore: cast_nullable_to_non_nullable + as String, + icon: icon == freezed + ? _value.icon + : icon // ignore: cast_nullable_to_non_nullable + as Icon, + action: action == freezed + ? _value.action + : action // ignore: cast_nullable_to_non_nullable + as void Function()?, + )); + } +} + +/// @nodoc + +class _$_MenuAction implements _MenuAction { + _$_MenuAction({required this.text, required this.icon, this.action}); + + @override + final String text; + @override + final Icon icon; + @override + final void Function()? action; + + @override + String toString() { + return 'MenuAction(text: $text, icon: $icon, action: $action)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _MenuAction && + (identical(other.text, text) || other.text == text) && + (identical(other.icon, icon) || other.icon == icon) && + (identical(other.action, action) || other.action == action)); + } + + @override + int get hashCode => Object.hash(runtimeType, text, icon, action); + + @JsonKey(ignore: true) + @override + _$MenuActionCopyWith<_MenuAction> get copyWith => + __$MenuActionCopyWithImpl<_MenuAction>(this, _$identity); +} + +abstract class _MenuAction implements MenuAction { + factory _MenuAction( + {required String text, + required Icon icon, + void Function()? action}) = _$_MenuAction; + + @override + String get text; + @override + Icon get icon; + @override + void Function()? get action; + @override + @JsonKey(ignore: true) + _$MenuActionCopyWith<_MenuAction> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/app/state.dart b/lib/app/state.dart index 660622f3..7eef1f20 100755 --- a/lib/app/state.dart +++ b/lib/app/state.dart @@ -1,17 +1,55 @@ import 'dart:async'; import 'dart:convert'; +import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:logging/logging.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import '../../core/rpc.dart'; -import '../../core/state.dart'; - +import '../core/models.dart'; +import '../core/state.dart'; +import '../core/rpc.dart'; +import '../oath/menu_actions.dart'; import 'models.dart'; final log = Logger('app.state'); +final themeModeProvider = StateNotifierProvider( + (ref) => ThemeModeNotifier(ref.watch(prefProvider))); + +class ThemeModeNotifier extends StateNotifier { + static const String _key = 'APP_STATE_THEME'; + final SharedPreferences _prefs; + ThemeModeNotifier(this._prefs) : super(_fromName(_prefs.getString(_key))); + + void setThemeMode(ThemeMode mode) { + state = mode; + _prefs.setString(_key, mode.name); + } + + static ThemeMode _fromName(String? name) { + switch (name) { + case 'light': + return ThemeMode.light; + case 'dark': + return ThemeMode.dark; + default: + return ThemeMode.system; + } + } +} + +final searchProvider = + StateNotifierProvider((ref) => SearchNotifier()); + +class SearchNotifier extends StateNotifier { + SearchNotifier() : super(''); + + setFilter(String value) { + state = value; + } +} + final attachedDevicesProvider = StateNotifierProvider>( (ref) => AttachedDeviceNotifier(ref.watch(rpcProvider))); @@ -31,24 +69,28 @@ class AttachedDeviceNotifier extends StateNotifier> { } void _pollUsb() async { - var scan = await _rpc.command('scan', ['usb']); + try { + var scan = await _rpc.command('scan', ['usb']); - if (_usbState != scan['state']) { - var usbResult = await _rpc.command('get', ['usb']); - log.info('USB state change', jsonEncode(usbResult)); + if (_usbState != scan['state'] || state.length != scan['pids'].length) { + var usbResult = await _rpc.command('get', ['usb']); + log.info('USB state change', jsonEncode(usbResult)); - _usbState = usbResult['data']['state']; - - List devices = []; - for (String id in (usbResult['children'] as Map).keys) { - var path = ['usb', id]; - var deviceResult = await _rpc.command('get', path); - devices - .add(DeviceNode.fromJson({'path': path, ...deviceResult['data']})); - } - if (mounted) { - state = devices; + List devices = []; + for (String id in (usbResult['children'] as Map).keys) { + var path = ['usb', id]; + var deviceResult = await _rpc.command('get', path); + devices.add( + DeviceNode.fromJson({'path': path, ...deviceResult['data']})); + } + _usbState = usbResult['data']['state']; + log.info('USB state updated'); + if (mounted) { + state = devices; + } } + } on RpcError catch (e) { + log.severe('Error polling USB', jsonEncode(e)); } if (mounted) { _pollTimer = Timer(const Duration(milliseconds: 500), _pollUsb); @@ -90,6 +132,16 @@ class CurrentDeviceNotifier extends StateNotifier { } } +final sortedDevicesProvider = Provider>((ref) { + final devices = ref.watch(attachedDevicesProvider).toList(); + devices.sort((a, b) => a.name.compareTo(b.name)); + final device = ref.watch(currentDeviceProvider); + if (device != null) { + return [device, ...devices.where((e) => e != device)]; + } + return devices; +}); + final subPageProvider = StateNotifierProvider( (ref) => SubPageNotifier(SubPage.authenticator)); @@ -100,3 +152,16 @@ class SubPageNotifier extends StateNotifier { state = page; } } + +typedef BuildActions = List Function(BuildContext); + +final menuActionsProvider = Provider.autoDispose((ref) { + switch (ref.watch(subPageProvider)) { + case SubPage.authenticator: + return (context) => buildOathMenuActions(context, ref); + case SubPage.yubikey: + // TODO: Handle this case. + break; + } + return (_) => []; +}); diff --git a/lib/app/views/device_avatar.dart b/lib/app/views/device_avatar.dart new file mode 100755 index 00000000..d57c05f2 --- /dev/null +++ b/lib/app/views/device_avatar.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; + +import '../models.dart'; +import 'device_images.dart'; + +class DeviceAvatar extends StatelessWidget { + final DeviceNode device; + final bool selected; + + const DeviceAvatar(this.device, {this.selected = false, Key? key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return CircleAvatar( + child: CircleAvatar( + child: getProductImage(device), + backgroundColor: Theme.of(context).colorScheme.background, + ), + radius: 22, + backgroundColor: selected + ? Theme.of(context).colorScheme.secondary + : Colors.transparent, + ); + } +} diff --git a/lib/app/views/device_picker_dialog.dart b/lib/app/views/device_picker_dialog.dart deleted file mode 100755 index 84940c48..00000000 --- a/lib/app/views/device_picker_dialog.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:yubico_authenticator/app/views/device_images.dart'; - -import '../../about_page.dart'; -import '../models.dart'; -import '../state.dart'; - -class DevicePickerDialog extends ConsumerWidget { - const DevicePickerDialog({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final device = ref.watch(currentDeviceProvider); - final devices = ref.watch(attachedDevicesProvider); - - Widget _buildDeviceInfo(DeviceNode device) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - children: [ - Row( - children: [ - CircleAvatar( - child: getProductImage(device), - radius: 40.0, - ), - const SizedBox(width: 16.0), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(device.name), - Text('Version: ${device.info.version}'), - Text('Serial: ${device.info.serial}'), - ], - ), - ], - ), - const Divider(), - ], - ), - ); - } - - return SimpleDialog( - //title: Text(device?.name ?? 'No YubiKey'), - children: [ - if (device != null) _buildDeviceInfo(device), - ...devices.where((e) => e != device).map((e) => TextButton( - child: Text('${e.name} (${e.info.serial})'), - onPressed: () { - ref.read(currentDeviceProvider.notifier).setCurrentDevice(e); - Navigator.of(context).pop(); - }, - )), - const Divider(), - TextButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute(builder: (context) => const AboutPage()), - ); - }, - child: const Text('About Yubico Authenticator...')) - ], - ); - } -} diff --git a/lib/app/views/main_actions_dialog.dart b/lib/app/views/main_actions_dialog.dart new file mode 100755 index 00000000..46e08204 --- /dev/null +++ b/lib/app/views/main_actions_dialog.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../models.dart'; +import '../state.dart'; +import 'device_avatar.dart'; + +class MainActionsDialog extends ConsumerWidget { + const MainActionsDialog({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final devices = ref.watch(sortedDevicesProvider); + final device = ref.watch(currentDeviceProvider); + final actions = ref.watch(menuActionsProvider)(context); + + return SimpleDialog( + //title: Text(device?.name ?? 'No YubiKey'), + children: [ + ...devices.map((e) => Padding( + padding: const EdgeInsets.all(8.0), + child: DeviceRow( + e, + selected: e == device, + onPressed: () { + Navigator.of(context).pop(); + ref.read(currentDeviceProvider.notifier).setCurrentDevice(e); + }, + ), + )), + if (actions.isNotEmpty) const Divider(), + ...actions.map((a) => ListTile( + dense: true, + leading: a.icon, + title: Text(a.text), + onTap: () { + Navigator.of(context).pop(); + a.action?.call(); + }, + )), + ], + ); + } +} + +class DeviceRow extends StatelessWidget { + final DeviceNode device; + final bool selected; + final Function() onPressed; + const DeviceRow( + this.device, { + this.selected = false, + required this.onPressed, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return TextButton( + onPressed: onPressed, + child: Row( + children: [ + DeviceAvatar( + device, + selected: selected, + ), + const SizedBox(width: 16.0), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + device.name, + style: Theme.of(context).textTheme.headline6, + ), + Text( + 'S/N: ${device.info.serial} F/W: ${device.info.version}', + style: Theme.of(context).textTheme.bodyText1, + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/app/views/main_drawer.dart b/lib/app/views/main_drawer.dart index f629f7ed..12cd5450 100755 --- a/lib/app/views/main_drawer.dart +++ b/lib/app/views/main_drawer.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../about_page.dart'; import '../models.dart'; import '../state.dart'; @@ -25,26 +26,103 @@ class MainPageDrawer extends ConsumerWidget { return Drawer( child: ListView( children: [ - const DrawerHeader( - decoration: BoxDecoration( - color: Colors.blue, + Padding( + padding: const EdgeInsets.all(12.0), + child: Text( + 'Yubico Authenticator', + style: Theme.of(context).textTheme.headline6, ), - child: Text('Hello'), ), - ...SubPage.values.map((value) => ListTile( - title: Text( - value.displayName, - style: Theme.of(context).textTheme.headline6, - ), - tileColor: value == currentSubPage ? Colors.blueGrey : null, - enabled: value != currentSubPage, - onTap: () { - ref.read(subPageProvider.notifier).setSubPage(value); - Navigator.of(context).pop(); - }, + const Divider(), + ...SubPage.values.map((page) => DrawerItem( + titleText: page.displayName, + icon: const Icon(Icons.miscellaneous_services), + selected: page == currentSubPage, + onTap: page != currentSubPage + ? () { + ref.read(subPageProvider.notifier).setSubPage(page); + Navigator.of(context).pop(); + } + : null, )), + Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + 'CONFIGURATION', + style: Theme.of(context).textTheme.bodyText2, + ), + ), + DrawerItem( + titleText: 'Placeholder Light mode', + icon: const Icon(Icons.alarm), + onTap: () { + ref + .read(themeModeProvider.notifier) + .setThemeMode(ThemeMode.light); + Navigator.of(context).pop(); + }, + ), + DrawerItem( + titleText: 'Placeholder Dark mode', + icon: const Icon(Icons.house), + onTap: () { + ref.read(themeModeProvider.notifier).setThemeMode(ThemeMode.dark); + Navigator.of(context).pop(); + }, + ), + const Divider(), + DrawerItem( + titleText: 'About Yubico Authenticator', + icon: const Icon(Icons.settings_applications), + onTap: () { + Navigator.of(context) + ..pop() + ..push( + MaterialPageRoute(builder: (context) => const AboutPage()), + ); + //Navigator.of(context).pop(); + }, + ), ], ), ); } } + +class DrawerItem extends StatelessWidget { + final bool selected; + final String titleText; + final Icon icon; + final void Function()? onTap; + + const DrawerItem({ + required this.titleText, + required this.icon, + this.onTap, + this.selected = false, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(right: 8), + child: ListTile( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.horizontal(right: Radius.circular(20)), + ), + dense: true, + selected: selected, + selectedColor: Theme.of(context).backgroundColor, + selectedTileColor: Theme.of(context).colorScheme.secondary, + leading: icon, + title: Text( + titleText, + //style: Theme.of(context).textTheme.headline6, + ), + //enabled: value != currentSubPage, + onTap: onTap, + ), + ); + } +} diff --git a/lib/app/views/main_page.dart b/lib/app/views/main_page.dart index c64e4f3b..3096fbf9 100755 --- a/lib/app/views/main_page.dart +++ b/lib/app/views/main_page.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:yubico_authenticator/app/views/device_images.dart'; -import 'device_picker_dialog.dart'; +import 'device_avatar.dart'; +import 'main_actions_dialog.dart'; import 'main_drawer.dart'; import 'no_device_screen.dart'; import 'device_info_screen.dart'; @@ -13,7 +13,10 @@ import '../../oath/views/oath_screen.dart'; class MainPage extends ConsumerWidget { const MainPage({Key? key}) : super(key: key); - Widget _buildSubPage(SubPage subPage, DeviceNode device) { + Widget _buildSubPage(SubPage subPage, DeviceNode? device) { + if (device == null) { + return const NoDeviceScreen(); + } // TODO: If page not supported by device, do something? switch (subPage) { case SubPage.authenticator: @@ -30,25 +33,42 @@ class MainPage extends ConsumerWidget { return Scaffold( appBar: AppBar( - title: const Text('Yubico Authenticator'), + //title: const Text('Yubico Authenticator'), + /* + backgroundColor: Colors.grey.shade900, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: const BorderRadius.all(Radius.circular(40)), + side: BorderSide( + width: 8, color: Theme.of(context).scaffoldBackgroundColor), + ), + */ + title: TextField( + decoration: const InputDecoration( + hintText: 'Search...', + border: InputBorder.none, + ), + onChanged: (value) { + ref.read(searchProvider.notifier).setFilter(value); + }, + ), actions: [ - IconButton( - icon: currentDevice == null + InkWell( + //iconSize: 32, + child: currentDevice == null ? const Icon(Icons.info) - : getProductImage(currentDevice), - onPressed: () { + : DeviceAvatar(currentDevice, selected: true), + onTap: () { showDialog( context: context, - builder: (context) => const DevicePickerDialog(), + builder: (context) => const MainActionsDialog(), ); }, ) ], ), drawer: const MainPageDrawer(), - body: currentDevice == null - ? const NoDeviceScreen() - : _buildSubPage(subPage, currentDevice), + body: _buildSubPage(subPage, currentDevice), ); } } diff --git a/lib/main.dart b/lib/main.dart index 70ea7e03..195e06c3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,7 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:logging/logging.dart'; -import 'app.dart'; +import 'app/app.dart'; import 'app/views/main_page.dart'; import 'core/rpc.dart'; import 'core/state.dart'; diff --git a/lib/oath/menu_actions.dart b/lib/oath/menu_actions.dart new file mode 100755 index 00000000..d66dd2d9 --- /dev/null +++ b/lib/oath/menu_actions.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../app/models.dart'; +import '../app/state.dart'; +import 'state.dart'; +import 'views/add_account_page.dart'; + +List buildOathMenuActions( + BuildContext context, AutoDisposeProviderRef ref) { + final device = ref.watch(currentDeviceProvider); + if (device != null) { + final state = ref.watch(oathStateProvider(device.path)); + if (state != null) { + return [ + if (!state.locked) + MenuAction( + text: 'Add credential', + icon: const Icon(Icons.add), + action: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => OathAddAccountPage(device: device), + ), + ); + }, + ), + MenuAction( + text: 'Factory reset', + icon: const Icon(Icons.delete_forever), + action: () { + ScaffoldMessenger.of(context) + ..clearSnackBars() + ..showSnackBar( + const SnackBar( + content: Text('Not implemented'), + duration: Duration(seconds: 2), + ), + ); + }, + ), + ]; + } + } + return []; +} diff --git a/lib/oath/state.dart b/lib/oath/state.dart index fe7baf20..8c0de181 100755 --- a/lib/oath/state.dart +++ b/lib/oath/state.dart @@ -7,6 +7,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:logging/logging.dart'; +import '../app/state.dart'; import '../core/state.dart'; import 'models.dart'; @@ -241,6 +242,7 @@ class FavoriteNotifier extends StateNotifier { } } +/* final searchFilterProvider = StateNotifierProvider( (ref) => SearchFilterNotifier()); @@ -251,7 +253,7 @@ class SearchFilterNotifier extends StateNotifier { setFilter(String value) { state = value; } -} +}*/ final filteredCredentialsProvider = StateNotifierProvider.autoDispose .family, List>( @@ -261,7 +263,7 @@ final filteredCredentialsProvider = StateNotifierProvider.autoDispose credential: ref.watch(favoriteProvider(credential.id)) }; return FilteredCredentialsNotifier( - full, favorites, ref.watch(searchFilterProvider)); + full, favorites, ref.watch(searchProvider)); }); class FilteredCredentialsNotifier extends StateNotifier> { @@ -271,15 +273,19 @@ class FilteredCredentialsNotifier extends StateNotifier> { List full, this.favorites, this.query, - ) : super(full - .where((pair) => - "${pair.credential.issuer ?? ''}:${pair.credential.name}" - .toLowerCase() - .contains(query.toLowerCase())) - .toList() - ..sort((a, b) { - String searchKey(OathCredential c) => - (favorites[c] == true ? '0' : '1') + (c.issuer ?? '') + c.name; - return searchKey(a.credential).compareTo(searchKey(b.credential)); - })); + ) : super( + full + .where((pair) => + "${pair.credential.issuer ?? ''}:${pair.credential.name}" + .toLowerCase() + .contains(query.toLowerCase())) + .toList() + ..sort((a, b) { + String searchKey(OathCredential c) => + (favorites[c] == true ? '0' : '1') + + (c.issuer ?? '') + + c.name; + return searchKey(a.credential).compareTo(searchKey(b.credential)); + }), + ); } diff --git a/lib/oath/views/oath_screen.dart b/lib/oath/views/oath_screen.dart index 96bd4b3a..030236f0 100755 --- a/lib/oath/views/oath_screen.dart +++ b/lib/oath/views/oath_screen.dart @@ -4,7 +4,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../app/models.dart'; import '../state.dart'; import 'account_list.dart'; -import 'add_account_page.dart'; class OathScreen extends ConsumerWidget { final DeviceNode device; @@ -43,25 +42,9 @@ class OathScreen extends ConsumerWidget { ], ); } - return Column( - children: [ - TextField( - onChanged: (value) { - ref.read(searchFilterProvider.notifier).setFilter(value); - }, - decoration: const InputDecoration(labelText: 'Search'), - ), - AccountList(device, ref.watch(filteredCredentialsProvider(accounts))), - TextButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => OathAddAccountPage(device: device)), - ); - }, - child: const Text('Add'), - ), - ], + return AccountList( + device, + ref.watch(filteredCredentialsProvider(accounts)), ); } } diff --git a/lib/theme.dart b/lib/theme.dart new file mode 100755 index 00000000..adc9a0bd --- /dev/null +++ b/lib/theme.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; + +class AppTheme { + static ThemeData get lightTheme => ThemeData( + brightness: Brightness.light, + ); + + static ThemeData get darkTheme => ThemeData( + brightness: Brightness.dark, + colorScheme: + ColorScheme.fromSwatch(brightness: Brightness.dark).copyWith( + secondary: const Color(0xffa8c86c), + ), + textTheme: TextTheme( + bodyText1: TextStyle( + color: Colors.grey.shade400, + ), + bodyText2: TextStyle( + color: Colors.grey.shade500, + ), + ), + ); +} diff --git a/pubspec.lock b/pubspec.lock index 224b6968..07a71150 100755 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,14 +7,14 @@ packages: name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "30.0.0" + version: "31.0.0" analyzer: dependency: transitive description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "2.7.0" + version: "2.8.0" args: dependency: transitive description: @@ -208,7 +208,7 @@ packages: name: flutter_riverpod url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.0.1" flutter_test: dependency: "direct dev" description: flutter @@ -288,14 +288,14 @@ packages: name: json_annotation url: "https://pub.dartlang.org" source: hosted - version: "4.3.0" + version: "4.4.0" json_serializable: dependency: "direct dev" description: name: json_serializable url: "https://pub.dartlang.org" source: hosted - version: "6.0.1" + version: "6.1.0" lints: dependency: transitive description: @@ -414,7 +414,7 @@ packages: name: riverpod url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.0.1" shared_preferences: dependency: "direct main" description: @@ -496,7 +496,7 @@ packages: name: source_gen url: "https://pub.dartlang.org" source: hosted - version: "1.1.1" + version: "1.2.0" source_helper: dependency: transitive description: diff --git a/yubikey-manager b/yubikey-manager index 1088e1ad..2ff1dcd6 160000 --- a/yubikey-manager +++ b/yubikey-manager @@ -1 +1 @@ -Subproject commit 1088e1adeed9ab7b8e7abfa57855b48558fd4c64 +Subproject commit 2ff1dcd6e0a533414add4c5d171387b1f1837790