Implement theming and re-arrange views.

This commit is contained in:
Dain Nilsson 2021-12-02 11:44:17 +01:00
parent 4c875865a8
commit 9236701f02
No known key found for this signature in database
GPG Key ID: F04367096FBA95E8
18 changed files with 624 additions and 174 deletions

View File

@ -37,7 +37,7 @@ class AboutPage extends ConsumerWidget {
ref.read(logLevelProvider.notifier).setLevel(Level.INFO); ref.read(logLevelProvider.notifier).setLevel(Level.INFO);
log.info('Log level changed to INFO'); log.info('Log level changed to INFO');
}, },
child: const Text('INFO'), child: const Text('Info'),
), ),
TextButton( TextButton(
onPressed: () { onPressed: () {
@ -46,10 +46,18 @@ class AboutPage extends ConsumerWidget {
.setLevel(Level.CONFIG); .setLevel(Level.CONFIG);
log.config('Log level changed to 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( TextButton(
onPressed: () async { onPressed: () async {
log.info('Running diagnostics...'); log.info('Running diagnostics...');

View File

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

23
lib/app/app.dart Executable file
View File

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

View File

@ -1,3 +1,4 @@
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import '../../management/models.dart'; import '../../management/models.dart';
@ -19,3 +20,11 @@ class DeviceNode with _$DeviceNode {
factory DeviceNode.fromJson(Map<String, dynamic> json) => factory DeviceNode.fromJson(Map<String, dynamic> json) =>
_$DeviceNodeFromJson(json); _$DeviceNodeFromJson(json);
} }
@freezed
class MenuAction with _$MenuAction {
factory MenuAction(
{required String text,
required Icon icon,
void Function()? action}) = _MenuAction;
}

View File

@ -251,3 +251,166 @@ abstract class _DeviceNode implements DeviceNode {
_$DeviceNodeCopyWith<_DeviceNode> get copyWith => _$DeviceNodeCopyWith<_DeviceNode> get copyWith =>
throw _privateConstructorUsedError; 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<MenuAction> 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;
}

View File

@ -1,17 +1,55 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import '../../core/rpc.dart'; import '../core/models.dart';
import '../../core/state.dart'; import '../core/state.dart';
import '../core/rpc.dart';
import '../oath/menu_actions.dart';
import 'models.dart'; import 'models.dart';
final log = Logger('app.state'); final log = Logger('app.state');
final themeModeProvider = StateNotifierProvider<ThemeModeNotifier, ThemeMode>(
(ref) => ThemeModeNotifier(ref.watch(prefProvider)));
class ThemeModeNotifier extends StateNotifier<ThemeMode> {
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<SearchNotifier, String>((ref) => SearchNotifier());
class SearchNotifier extends StateNotifier<String> {
SearchNotifier() : super('');
setFilter(String value) {
state = value;
}
}
final attachedDevicesProvider = final attachedDevicesProvider =
StateNotifierProvider<AttachedDeviceNotifier, List<DeviceNode>>( StateNotifierProvider<AttachedDeviceNotifier, List<DeviceNode>>(
(ref) => AttachedDeviceNotifier(ref.watch(rpcProvider))); (ref) => AttachedDeviceNotifier(ref.watch(rpcProvider)));
@ -31,24 +69,28 @@ class AttachedDeviceNotifier extends StateNotifier<List<DeviceNode>> {
} }
void _pollUsb() async { void _pollUsb() async {
var scan = await _rpc.command('scan', ['usb']); try {
var scan = await _rpc.command('scan', ['usb']);
if (_usbState != scan['state']) { if (_usbState != scan['state'] || state.length != scan['pids'].length) {
var usbResult = await _rpc.command('get', ['usb']); var usbResult = await _rpc.command('get', ['usb']);
log.info('USB state change', jsonEncode(usbResult)); log.info('USB state change', jsonEncode(usbResult));
_usbState = usbResult['data']['state']; List<DeviceNode> devices = [];
for (String id in (usbResult['children'] as Map).keys) {
List<DeviceNode> devices = []; var path = ['usb', id];
for (String id in (usbResult['children'] as Map).keys) { var deviceResult = await _rpc.command('get', path);
var path = ['usb', id]; devices.add(
var deviceResult = await _rpc.command('get', path); DeviceNode.fromJson({'path': path, ...deviceResult['data']}));
devices }
.add(DeviceNode.fromJson({'path': path, ...deviceResult['data']})); _usbState = usbResult['data']['state'];
} log.info('USB state updated');
if (mounted) { if (mounted) {
state = devices; state = devices;
}
} }
} on RpcError catch (e) {
log.severe('Error polling USB', jsonEncode(e));
} }
if (mounted) { if (mounted) {
_pollTimer = Timer(const Duration(milliseconds: 500), _pollUsb); _pollTimer = Timer(const Duration(milliseconds: 500), _pollUsb);
@ -90,6 +132,16 @@ class CurrentDeviceNotifier extends StateNotifier<DeviceNode?> {
} }
} }
final sortedDevicesProvider = Provider<List<DeviceNode>>((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<SubPageNotifier, SubPage>( final subPageProvider = StateNotifierProvider<SubPageNotifier, SubPage>(
(ref) => SubPageNotifier(SubPage.authenticator)); (ref) => SubPageNotifier(SubPage.authenticator));
@ -100,3 +152,16 @@ class SubPageNotifier extends StateNotifier<SubPage> {
state = page; state = page;
} }
} }
typedef BuildActions = List<MenuAction> Function(BuildContext);
final menuActionsProvider = Provider.autoDispose<BuildActions>((ref) {
switch (ref.watch(subPageProvider)) {
case SubPage.authenticator:
return (context) => buildOathMenuActions(context, ref);
case SubPage.yubikey:
// TODO: Handle this case.
break;
}
return (_) => [];
});

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../about_page.dart';
import '../models.dart'; import '../models.dart';
import '../state.dart'; import '../state.dart';
@ -25,26 +26,103 @@ class MainPageDrawer extends ConsumerWidget {
return Drawer( return Drawer(
child: ListView( child: ListView(
children: [ children: [
const DrawerHeader( Padding(
decoration: BoxDecoration( padding: const EdgeInsets.all(12.0),
color: Colors.blue, child: Text(
'Yubico Authenticator',
style: Theme.of(context).textTheme.headline6,
), ),
child: Text('Hello'),
), ),
...SubPage.values.map((value) => ListTile( const Divider(),
title: Text( ...SubPage.values.map((page) => DrawerItem(
value.displayName, titleText: page.displayName,
style: Theme.of(context).textTheme.headline6, icon: const Icon(Icons.miscellaneous_services),
), selected: page == currentSubPage,
tileColor: value == currentSubPage ? Colors.blueGrey : null, onTap: page != currentSubPage
enabled: value != currentSubPage, ? () {
onTap: () { ref.read(subPageProvider.notifier).setSubPage(page);
ref.read(subPageProvider.notifier).setSubPage(value); Navigator.of(context).pop();
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,
),
);
}
}

View File

@ -1,8 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.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 'main_drawer.dart';
import 'no_device_screen.dart'; import 'no_device_screen.dart';
import 'device_info_screen.dart'; import 'device_info_screen.dart';
@ -13,7 +13,10 @@ import '../../oath/views/oath_screen.dart';
class MainPage extends ConsumerWidget { class MainPage extends ConsumerWidget {
const MainPage({Key? key}) : super(key: key); 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? // TODO: If page not supported by device, do something?
switch (subPage) { switch (subPage) {
case SubPage.authenticator: case SubPage.authenticator:
@ -30,25 +33,42 @@ class MainPage extends ConsumerWidget {
return Scaffold( return Scaffold(
appBar: AppBar( 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: [ actions: [
IconButton( InkWell(
icon: currentDevice == null //iconSize: 32,
child: currentDevice == null
? const Icon(Icons.info) ? const Icon(Icons.info)
: getProductImage(currentDevice), : DeviceAvatar(currentDevice, selected: true),
onPressed: () { onTap: () {
showDialog( showDialog(
context: context, context: context,
builder: (context) => const DevicePickerDialog(), builder: (context) => const MainActionsDialog(),
); );
}, },
) )
], ],
), ),
drawer: const MainPageDrawer(), drawer: const MainPageDrawer(),
body: currentDevice == null body: _buildSubPage(subPage, currentDevice),
? const NoDeviceScreen()
: _buildSubPage(subPage, currentDevice),
); );
} }
} }

View File

@ -6,7 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'app.dart'; import 'app/app.dart';
import 'app/views/main_page.dart'; import 'app/views/main_page.dart';
import 'core/rpc.dart'; import 'core/rpc.dart';
import 'core/state.dart'; import 'core/state.dart';

46
lib/oath/menu_actions.dart Executable file
View File

@ -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<MenuAction> 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 [];
}

View File

@ -7,6 +7,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import '../app/state.dart';
import '../core/state.dart'; import '../core/state.dart';
import 'models.dart'; import 'models.dart';
@ -241,6 +242,7 @@ class FavoriteNotifier extends StateNotifier<bool> {
} }
} }
/*
final searchFilterProvider = final searchFilterProvider =
StateNotifierProvider<SearchFilterNotifier, String>( StateNotifierProvider<SearchFilterNotifier, String>(
(ref) => SearchFilterNotifier()); (ref) => SearchFilterNotifier());
@ -251,7 +253,7 @@ class SearchFilterNotifier extends StateNotifier<String> {
setFilter(String value) { setFilter(String value) {
state = value; state = value;
} }
} }*/
final filteredCredentialsProvider = StateNotifierProvider.autoDispose final filteredCredentialsProvider = StateNotifierProvider.autoDispose
.family<FilteredCredentialsNotifier, List<OathPair>, List<OathPair>>( .family<FilteredCredentialsNotifier, List<OathPair>, List<OathPair>>(
@ -261,7 +263,7 @@ final filteredCredentialsProvider = StateNotifierProvider.autoDispose
credential: ref.watch(favoriteProvider(credential.id)) credential: ref.watch(favoriteProvider(credential.id))
}; };
return FilteredCredentialsNotifier( return FilteredCredentialsNotifier(
full, favorites, ref.watch(searchFilterProvider)); full, favorites, ref.watch(searchProvider));
}); });
class FilteredCredentialsNotifier extends StateNotifier<List<OathPair>> { class FilteredCredentialsNotifier extends StateNotifier<List<OathPair>> {
@ -271,15 +273,19 @@ class FilteredCredentialsNotifier extends StateNotifier<List<OathPair>> {
List<OathPair> full, List<OathPair> full,
this.favorites, this.favorites,
this.query, this.query,
) : super(full ) : super(
.where((pair) => full
"${pair.credential.issuer ?? ''}:${pair.credential.name}" .where((pair) =>
.toLowerCase() "${pair.credential.issuer ?? ''}:${pair.credential.name}"
.contains(query.toLowerCase())) .toLowerCase()
.toList() .contains(query.toLowerCase()))
..sort((a, b) { .toList()
String searchKey(OathCredential c) => ..sort((a, b) {
(favorites[c] == true ? '0' : '1') + (c.issuer ?? '') + c.name; String searchKey(OathCredential c) =>
return searchKey(a.credential).compareTo(searchKey(b.credential)); (favorites[c] == true ? '0' : '1') +
})); (c.issuer ?? '') +
c.name;
return searchKey(a.credential).compareTo(searchKey(b.credential));
}),
);
} }

View File

@ -4,7 +4,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../app/models.dart'; import '../../app/models.dart';
import '../state.dart'; import '../state.dart';
import 'account_list.dart'; import 'account_list.dart';
import 'add_account_page.dart';
class OathScreen extends ConsumerWidget { class OathScreen extends ConsumerWidget {
final DeviceNode device; final DeviceNode device;
@ -43,25 +42,9 @@ class OathScreen extends ConsumerWidget {
], ],
); );
} }
return Column( return AccountList(
children: [ device,
TextField( ref.watch(filteredCredentialsProvider(accounts)),
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'),
),
],
); );
} }
} }

23
lib/theme.dart Executable file
View File

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

View File

@ -7,14 +7,14 @@ packages:
name: _fe_analyzer_shared name: _fe_analyzer_shared
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "30.0.0" version: "31.0.0"
analyzer: analyzer:
dependency: transitive dependency: transitive
description: description:
name: analyzer name: analyzer
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.7.0" version: "2.8.0"
args: args:
dependency: transitive dependency: transitive
description: description:
@ -208,7 +208,7 @@ packages:
name: flutter_riverpod name: flutter_riverpod
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.0" version: "1.0.1"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@ -288,14 +288,14 @@ packages:
name: json_annotation name: json_annotation
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.3.0" version: "4.4.0"
json_serializable: json_serializable:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: json_serializable name: json_serializable
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "6.0.1" version: "6.1.0"
lints: lints:
dependency: transitive dependency: transitive
description: description:
@ -414,7 +414,7 @@ packages:
name: riverpod name: riverpod
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.0" version: "1.0.1"
shared_preferences: shared_preferences:
dependency: "direct main" dependency: "direct main"
description: description:
@ -496,7 +496,7 @@ packages:
name: source_gen name: source_gen
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.1" version: "1.2.0"
source_helper: source_helper:
dependency: transitive dependency: transitive
description: description:

@ -1 +1 @@
Subproject commit 1088e1adeed9ab7b8e7abfa57855b48558fd4c64 Subproject commit 2ff1dcd6e0a533414add4c5d171387b1f1837790