Add grid/list view to passkeys

This commit is contained in:
Elias Bonnici 2024-06-14 13:04:55 +02:00
parent 6b773d446f
commit 0b8bcf8c67
No known key found for this signature in database
GPG Key ID: 5EAC28EA3F980CCF
7 changed files with 208 additions and 99 deletions

View File

@ -21,6 +21,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../app/models.dart';
import '../widgets/flex_box.dart';
bool get isDesktop => const [
TargetPlatform.windows,
@ -119,3 +120,22 @@ final featureProvider = Provider<FeatureProvider>((ref) {
return isEnabled;
});
class LayoutNotifier extends StateNotifier<FlexLayout> {
final String _key;
final SharedPreferences _prefs;
final FlexLayout initialLayout;
LayoutNotifier(this._key, this._prefs, [this.initialLayout = FlexLayout.list])
: super(_fromName(_prefs.getString(_key), initialLayout));
void setLayout(FlexLayout layout) {
state = layout;
_prefs.setString(_key, layout.name);
}
static FlexLayout _fromName(String? name, FlexLayout initialLayout) =>
FlexLayout.values.firstWhere(
(element) => element.name == name,
orElse: () => initialLayout,
);
}

View File

@ -18,6 +18,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../app/models.dart';
import '../core/state.dart';
import '../widgets/flex_box.dart';
import 'models.dart';
final passkeysSearchProvider =
@ -32,6 +33,11 @@ class PasskeysSearchNotifier extends StateNotifier<String> {
}
}
final passkeysLayoutProvider =
StateNotifierProvider<LayoutNotifier, FlexLayout>(
(ref) => LayoutNotifier('FIDO_PASSKEYS_LAYOUT', ref.watch(prefProvider)),
);
final fidoStateProvider = AsyncNotifierProvider.autoDispose
.family<FidoStateNotifier, FidoState, DevicePath>(
() => throw UnimplementedError(),

View File

@ -38,6 +38,7 @@ import '../../exception/no_data_exception.dart';
import '../../management/models.dart';
import '../../widgets/app_input_decoration.dart';
import '../../widgets/app_text_form_field.dart';
import '../../widgets/flex_box.dart';
import '../../widgets/list_title.dart';
import '../features.dart' as features;
import '../models.dart';
@ -219,6 +220,7 @@ class _FidoUnlockedPageState extends ConsumerState<_FidoUnlockedPage> {
late FocusNode searchFocus;
late TextEditingController searchController;
FidoCredential? _selected;
bool _canRequestFocus = true;
@override
void initState() {
@ -376,58 +378,144 @@ class _FidoUnlockedPageState extends ConsumerState<_FidoUnlockedPage> {
},
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: searchField,
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(48),
borderSide: BorderSide(
width: 0,
style: searchFocus.hasFocus
? BorderStyle.solid
: BorderStyle.none,
return Consumer(
builder: (context, ref, child) {
final layout = ref.watch(passkeysLayoutProvider);
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0, vertical: 8.0),
child: AppTextFormField(
key: searchField,
controller: searchController,
canRequestFocus: _canRequestFocus,
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(48),
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_passkeys,
isDense: true,
prefixIcon: const Padding(
padding: EdgeInsetsDirectional.only(start: 8.0),
child: Icon(Icons.search_outlined),
),
suffixIcons: [
if (searchController.text.isNotEmpty)
IconButton(
icon: const Icon(Icons.clear),
iconSize: 16,
onPressed: () {
searchController.clear();
ref
.read(passkeysSearchProvider.notifier)
.setFilter('');
setState(() {});
},
),
if (searchController.text.isEmpty &&
!searchFocus.hasFocus) ...[
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
// need this to maintain consistent distance
// between icons
padding: const EdgeInsets.only(left: 17.0),
child: Container(
color:
Theme.of(context).colorScheme.background,
width: 1,
height: 45,
),
),
],
),
MouseRegion(
onEnter: (event) {
if (!searchFocus.hasFocus) {
setState(() {
_canRequestFocus = false;
});
}
},
onExit: (event) {
setState(() {
_canRequestFocus = true;
});
},
child: IconButton(
tooltip: l10n.s_list_layout,
onPressed: () {
ref
.read(passkeysLayoutProvider.notifier)
.setLayout(FlexLayout.list);
},
icon: Icon(
Symbols.list,
color: layout == FlexLayout.list
? Theme.of(context).colorScheme.primary
: null,
),
),
),
MouseRegion(
onEnter: (event) {
if (!searchFocus.hasFocus) {
setState(() {
_canRequestFocus = false;
});
}
},
onExit: (event) {
setState(() {
_canRequestFocus = true;
});
},
child: IconButton(
tooltip: l10n.s_grid_layout,
onPressed: () {
ref
.read(passkeysLayoutProvider.notifier)
.setLayout(FlexLayout.grid);
},
icon: Icon(Symbols.grid_view,
color: layout == FlexLayout.grid
? Theme.of(context).colorScheme.primary
: null),
),
),
]
],
),
),
contentPadding: const EdgeInsets.all(16),
fillColor: Theme.of(context).hoverColor,
filled: true,
hintText: l10n.s_search_passkeys,
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(passkeysSearchProvider.notifier)
.setFilter('');
setState(() {});
},
)
: null,
),
onChanged: (value) {
ref.read(passkeysSearchProvider.notifier).setFilter(value);
setState(() {});
},
textInputAction: TextInputAction.next,
onFieldSubmitted: (value) {
Focus.of(context).focusInDirection(TraversalDirection.down);
},
).init(),
onChanged: (value) {
ref
.read(passkeysSearchProvider.notifier)
.setFilter(value);
setState(() {});
},
textInputAction: TextInputAction.next,
onFieldSubmitted: (value) {
Focus.of(context)
.focusInDirection(TraversalDirection.down);
},
).init(),
);
},
);
}),
),
@ -483,21 +571,28 @@ class _FidoUnlockedPageState extends ConsumerState<_FidoUnlockedPage> {
}),
}
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (filteredCredentials.isEmpty)
Center(
child: Text(l10n.s_no_passkeys),
),
...filteredCredentials.map(
(cred) => _CredentialListItem(
cred,
expanded: expanded,
selected: _selected == cred,
),
),
],
child: Consumer(
builder: (context, ref, child) {
final layout = ref.watch(passkeysLayoutProvider);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (filteredCredentials.isEmpty)
Center(
child: Text(l10n.s_no_passkeys),
),
FlexBox<FidoCredential>(
items: filteredCredentials,
itemBuilder: (cred) => _CredentialListItem(
cred,
expanded: expanded,
selected: _selected == cred,
),
layout: layout,
)
],
);
},
),
);
},

View File

@ -39,34 +39,16 @@ class AccountsSearchNotifier extends StateNotifier<String> {
}
}
final pinnedLayoutProvider = StateNotifierProvider<LayoutNotifier, FlexLayout>(
final oathPinnedLayoutProvider =
StateNotifierProvider<LayoutNotifier, FlexLayout>(
(ref) => LayoutNotifier(
'OATH_STATE_LAYOUT_PINNED', ref.watch(prefProvider), FlexLayout.grid),
);
final layoutProvider = StateNotifierProvider<LayoutNotifier, FlexLayout>(
final oathLayoutProvider = StateNotifierProvider<LayoutNotifier, FlexLayout>(
(ref) => LayoutNotifier('OATH_STATE_LAYOUT', ref.watch(prefProvider)),
);
class LayoutNotifier extends StateNotifier<FlexLayout> {
final String _key;
final SharedPreferences _prefs;
final FlexLayout initialLayout;
LayoutNotifier(this._key, this._prefs, [this.initialLayout = FlexLayout.list])
: super(_fromName(_prefs.getString(_key), initialLayout));
void setLayout(FlexLayout layout) {
state = layout;
_prefs.setString(_key, layout.name);
}
static FlexLayout _fromName(String? name, FlexLayout initialLayout) =>
FlexLayout.values.firstWhere(
(element) => element.name == name,
orElse: () => initialLayout,
);
}
final oathStateProvider = AsyncNotifierProvider.autoDispose
.family<OathStateNotifier, OathState, DevicePath>(
() => throw UnimplementedError(),

View File

@ -46,8 +46,8 @@ class AccountList extends ConsumerWidget {
final creds =
credentials.where((entry) => !favorites.contains(entry.credential.id));
final pinnedLayout = ref.watch(pinnedLayoutProvider);
final layout = ref.watch(layoutProvider);
final pinnedLayout = ref.watch(oathPinnedLayoutProvider);
final layout = ref.watch(oathLayoutProvider);
return FocusTraversalGroup(
policy: WidgetOrderTraversalPolicy(),
@ -69,6 +69,7 @@ class AccountList extends ConsumerWidget {
large: pinnedLayout == FlexLayout.grid,
),
layout: pinnedLayout,
runSpacing: 8.0,
),
),
if (pinnedCreds.isNotEmpty && creds.isNotEmpty)
@ -92,6 +93,7 @@ class AccountList extends ConsumerWidget {
large: layout == FlexLayout.grid,
),
layout: layout,
runSpacing: 8.0,
),
),
],

View File

@ -382,8 +382,8 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
final textTheme = Theme.of(context).textTheme;
return Consumer(
builder: (context, ref, child) {
final pinnedLayout = ref.watch(pinnedLayoutProvider);
final layout = ref.watch(layoutProvider);
final pinnedLayout = ref.watch(oathPinnedLayoutProvider);
final layout = ref.watch(oathLayoutProvider);
final credentials = ref.watch(filteredCredentialsProvider(
ref.watch(credentialListProvider(widget.devicePath)) ??
@ -479,10 +479,10 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
tooltip: l10n.s_list_layout,
onPressed: () {
ref
.read(pinnedLayoutProvider.notifier)
.read(oathPinnedLayoutProvider.notifier)
.setLayout(FlexLayout.list);
ref
.read(layoutProvider.notifier)
.read(oathLayoutProvider.notifier)
.setLayout(FlexLayout.list);
},
icon: Icon(
@ -510,10 +510,10 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
tooltip: l10n.s_grid_layout,
onPressed: () {
ref
.read(pinnedLayoutProvider.notifier)
.read(oathPinnedLayoutProvider.notifier)
.setLayout(FlexLayout.grid);
ref
.read(layoutProvider.notifier)
.read(oathLayoutProvider.notifier)
.setLayout(FlexLayout.grid);
},
icon: Icon(Symbols.grid_view,
@ -540,10 +540,10 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
tooltip: l10n.s_mixed_layout,
onPressed: () {
ref
.read(pinnedLayoutProvider.notifier)
.read(oathPinnedLayoutProvider.notifier)
.setLayout(FlexLayout.grid);
ref
.read(layoutProvider.notifier)
.read(oathLayoutProvider.notifier)
.setLayout(FlexLayout.list);
},
icon: Icon(

View File

@ -6,11 +6,13 @@ class FlexBox<T> extends StatelessWidget {
final List<T> items;
final Widget Function(T value) itemBuilder;
final FlexLayout layout;
final double? runSpacing;
const FlexBox({
super.key,
required this.items,
required this.itemBuilder,
this.layout = FlexLayout.list,
this.runSpacing,
});
int getItemsPerRow(double width) {
@ -69,8 +71,10 @@ class FlexBox<T> extends StatelessWidget {
return Column(
children: [
for (final c in chunks) ...[
if (chunks.indexOf(c) > 0 && layout == FlexLayout.grid)
const SizedBox(height: 8.0),
if (chunks.indexOf(c) > 0 &&
layout == FlexLayout.grid &&
runSpacing != null)
SizedBox(height: runSpacing),
Row(
children: [
for (final entry in c) ...[