mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-11-22 00:12:09 +03:00
Add grid/list view to passkeys
This commit is contained in:
parent
6b773d446f
commit
0b8bcf8c67
@ -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,
|
||||
);
|
||||
}
|
||||
|
@ -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(),
|
||||
|
@ -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,
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
|
@ -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(),
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -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(
|
||||
|
@ -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) ...[
|
||||
|
Loading…
Reference in New Issue
Block a user