mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-11-22 00:12:09 +03:00
Add dropdown layout selection for small screens
This commit is contained in:
parent
35cc0ebb40
commit
e45506427a
@ -124,18 +124,16 @@ final featureProvider = Provider<FeatureProvider>((ref) {
|
||||
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));
|
||||
LayoutNotifier(this._key, this._prefs)
|
||||
: super(_fromName(_prefs.getString(_key)));
|
||||
|
||||
void setLayout(FlexLayout layout) {
|
||||
state = layout;
|
||||
_prefs.setString(_key, layout.name);
|
||||
}
|
||||
|
||||
static FlexLayout _fromName(String? name, FlexLayout initialLayout) =>
|
||||
FlexLayout.values.firstWhere(
|
||||
static FlexLayout _fromName(String? name) => FlexLayout.values.firstWhere(
|
||||
(element) => element.name == name,
|
||||
orElse: () => initialLayout,
|
||||
orElse: () => FlexLayout.list,
|
||||
);
|
||||
}
|
||||
|
@ -376,153 +376,121 @@ class _FidoUnlockedPageState extends ConsumerState<_FidoUnlockedPage> {
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
},
|
||||
child: Builder(builder: (context) {
|
||||
child: LayoutBuilder(builder: (context, constraints) {
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
|
||||
final width = constraints.maxWidth;
|
||||
final showLayoutOptions = width > 600;
|
||||
return Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final layout = ref.watch(passkeysLayoutProvider);
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final width = constraints.maxWidth;
|
||||
final showLayoutOptions = width > 600;
|
||||
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,
|
||||
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 &&
|
||||
showLayoutOptions) ...[
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
...FlexLayout.values.map(
|
||||
(e) => MouseRegion(
|
||||
onEnter: (event) {
|
||||
if (!searchFocus.hasFocus) {
|
||||
setState(() {
|
||||
_canRequestFocus = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
onExit: (event) {
|
||||
setState(() {
|
||||
_canRequestFocus = true;
|
||||
});
|
||||
},
|
||||
child: IconButton(
|
||||
tooltip: e.getDisplayName(l10n),
|
||||
onPressed: () {
|
||||
ref
|
||||
.read(passkeysLayoutProvider.notifier)
|
||||
.setLayout(e);
|
||||
},
|
||||
icon: Icon(
|
||||
e.icon,
|
||||
color: e == layout
|
||||
? 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),
|
||||
),
|
||||
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 &&
|
||||
showLayoutOptions) ...[
|
||||
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),
|
||||
),
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
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(),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -127,6 +127,7 @@
|
||||
"s_list_layout": null,
|
||||
"s_grid_layout": null,
|
||||
"s_mixed_layout": null,
|
||||
"s_select_layout": null,
|
||||
|
||||
"@_yubikey_selection": {},
|
||||
"s_select_to_scan": "Zum Scannen auswählen",
|
||||
|
@ -127,6 +127,7 @@
|
||||
"s_list_layout": "List layout",
|
||||
"s_grid_layout": "Grid layout",
|
||||
"s_mixed_layout": "Mixed layout",
|
||||
"s_select_layout": "Select layout",
|
||||
|
||||
"@_yubikey_selection": {},
|
||||
"s_select_to_scan": "Select to scan",
|
||||
|
@ -127,6 +127,7 @@
|
||||
"s_list_layout": null,
|
||||
"s_grid_layout": null,
|
||||
"s_mixed_layout": null,
|
||||
"s_select_layout": null,
|
||||
|
||||
"@_yubikey_selection": {},
|
||||
"s_select_to_scan": "Sélectionner pour scanner",
|
||||
|
@ -127,6 +127,7 @@
|
||||
"s_list_layout": null,
|
||||
"s_grid_layout": null,
|
||||
"s_mixed_layout": null,
|
||||
"s_select_layout": null,
|
||||
|
||||
"@_yubikey_selection": {},
|
||||
"s_select_to_scan": "選択してスキャン",
|
||||
|
@ -127,6 +127,7 @@
|
||||
"s_list_layout": null,
|
||||
"s_grid_layout": null,
|
||||
"s_mixed_layout": null,
|
||||
"s_select_layout": null,
|
||||
|
||||
"@_yubikey_selection": {},
|
||||
"s_select_to_scan": "Wybierz, aby skanować",
|
||||
|
@ -258,3 +258,5 @@ class CredentialData with _$CredentialData {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
enum OathLayout { list, grid, mixed }
|
||||
|
@ -24,7 +24,6 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../app/models.dart';
|
||||
import '../app/state.dart';
|
||||
import '../core/state.dart';
|
||||
import '../widgets/flex_box.dart';
|
||||
import 'models.dart';
|
||||
|
||||
final accountsSearchProvider =
|
||||
@ -39,15 +38,44 @@ class AccountsSearchNotifier extends StateNotifier<String> {
|
||||
}
|
||||
}
|
||||
|
||||
final oathPinnedLayoutProvider =
|
||||
StateNotifierProvider<LayoutNotifier, FlexLayout>(
|
||||
(ref) => LayoutNotifier(
|
||||
'OATH_STATE_LAYOUT_PINNED', ref.watch(prefProvider), FlexLayout.grid),
|
||||
);
|
||||
final oathLayoutProvider =
|
||||
StateNotifierProvider.autoDispose<OathLayoutNotfier, OathLayout>((ref) {
|
||||
final device = ref.watch(currentDeviceProvider);
|
||||
List<OathPair> credentials = device != null
|
||||
? ref.read(filteredCredentialsProvider(
|
||||
ref.read(credentialListProvider(device.path)) ?? []))
|
||||
: [];
|
||||
final favorites = ref.watch(favoritesProvider);
|
||||
final pinnedCreds =
|
||||
credentials.where((entry) => favorites.contains(entry.credential.id));
|
||||
return OathLayoutNotfier(
|
||||
'OATH_STATE_LAYOUT', ref.watch(prefProvider), pinnedCreds.isNotEmpty);
|
||||
});
|
||||
|
||||
final oathLayoutProvider = StateNotifierProvider<LayoutNotifier, FlexLayout>(
|
||||
(ref) => LayoutNotifier('OATH_STATE_LAYOUT', ref.watch(prefProvider)),
|
||||
);
|
||||
class OathLayoutNotfier extends StateNotifier<OathLayout> {
|
||||
final String _key;
|
||||
final SharedPreferences _prefs;
|
||||
OathLayoutNotfier(this._key, this._prefs, bool _hasPinned)
|
||||
: super(_fromName(_prefs.getString(_key), _hasPinned));
|
||||
|
||||
void setLayout(OathLayout layout) {
|
||||
state = layout;
|
||||
_prefs.setString(_key, layout.name);
|
||||
}
|
||||
|
||||
static OathLayout _fromName(String? name, bool hasPinned) {
|
||||
final layout = OathLayout.values.firstWhere(
|
||||
(element) => element.name == name,
|
||||
orElse: () => OathLayout.list,
|
||||
);
|
||||
// Default to list view if current key does not have
|
||||
// pinned credentials
|
||||
if (layout == OathLayout.mixed && !hasPinned) {
|
||||
return OathLayout.list;
|
||||
}
|
||||
return layout;
|
||||
}
|
||||
}
|
||||
|
||||
final oathStateProvider = AsyncNotifierProvider.autoDispose
|
||||
.family<OathStateNotifier, OathState, DevicePath>(
|
||||
|
@ -46,8 +46,13 @@ class AccountList extends ConsumerWidget {
|
||||
final creds =
|
||||
credentials.where((entry) => !favorites.contains(entry.credential.id));
|
||||
|
||||
final pinnedLayout = ref.watch(oathPinnedLayoutProvider);
|
||||
final layout = ref.watch(oathLayoutProvider);
|
||||
final oathLayout = ref.watch(oathLayoutProvider);
|
||||
final pinnedLayout =
|
||||
(oathLayout == OathLayout.grid || oathLayout == OathLayout.mixed)
|
||||
? FlexLayout.grid
|
||||
: FlexLayout.list;
|
||||
final normalLayout =
|
||||
oathLayout == OathLayout.grid ? FlexLayout.grid : FlexLayout.list;
|
||||
|
||||
return FocusTraversalGroup(
|
||||
policy: WidgetOrderTraversalPolicy(),
|
||||
@ -81,7 +86,7 @@ class AccountList extends ConsumerWidget {
|
||||
// ),
|
||||
// ),
|
||||
Padding(
|
||||
padding: layout == FlexLayout.grid
|
||||
padding: normalLayout == FlexLayout.grid
|
||||
? const EdgeInsets.only(left: 16.0, right: 16)
|
||||
: const EdgeInsets.all(0),
|
||||
child: FlexBox<OathPair>(
|
||||
@ -90,9 +95,9 @@ class AccountList extends ConsumerWidget {
|
||||
value.credential,
|
||||
expanded: expanded,
|
||||
selected: value.credential == selected,
|
||||
large: layout == FlexLayout.grid,
|
||||
large: normalLayout == FlexLayout.grid,
|
||||
),
|
||||
layout: layout,
|
||||
layout: normalLayout,
|
||||
runSpacing: 8.0,
|
||||
),
|
||||
),
|
||||
|
@ -39,7 +39,6 @@ import '../../management/models.dart';
|
||||
import '../../widgets/app_input_decoration.dart';
|
||||
import '../../widgets/app_text_form_field.dart';
|
||||
import '../../widgets/file_drop_overlay.dart';
|
||||
import '../../widgets/flex_box.dart';
|
||||
import '../../widgets/list_title.dart';
|
||||
import '../../widgets/tooltip_if_truncated.dart';
|
||||
import '../features.dart' as features;
|
||||
@ -54,6 +53,19 @@ import 'key_actions.dart';
|
||||
import 'unlock_form.dart';
|
||||
import 'utils.dart';
|
||||
|
||||
extension on OathLayout {
|
||||
IconData get _icon => switch (this) {
|
||||
OathLayout.list => Symbols.list,
|
||||
OathLayout.grid => Symbols.grid_view,
|
||||
OathLayout.mixed => Symbols.vertical_split
|
||||
};
|
||||
String getDisplayName(AppLocalizations l10n) => switch (this) {
|
||||
OathLayout.list => l10n.s_list_layout,
|
||||
OathLayout.grid => l10n.s_grid_layout,
|
||||
OathLayout.mixed => l10n.s_mixed_layout
|
||||
};
|
||||
}
|
||||
|
||||
class OathScreen extends ConsumerWidget {
|
||||
final DevicePath devicePath;
|
||||
|
||||
@ -378,13 +390,11 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
},
|
||||
child: Builder(builder: (context) {
|
||||
child: LayoutBuilder(builder: (context, constraints) {
|
||||
final width = constraints.maxWidth;
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
return Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final pinnedLayout = ref.watch(oathPinnedLayoutProvider);
|
||||
final layout = ref.watch(oathLayoutProvider);
|
||||
|
||||
final credentials = ref.watch(filteredCredentialsProvider(
|
||||
ref.watch(credentialListProvider(widget.devicePath)) ??
|
||||
[]));
|
||||
@ -392,13 +402,11 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
|
||||
final pinnedCreds = credentials
|
||||
.where((entry) => favorites.contains(entry.credential.id));
|
||||
|
||||
final mixedView = pinnedLayout == FlexLayout.grid &&
|
||||
layout == FlexLayout.list;
|
||||
final listView =
|
||||
(pinnedLayout == FlexLayout.list || pinnedCreds.isEmpty) &&
|
||||
layout == FlexLayout.list;
|
||||
final gridView = pinnedLayout == FlexLayout.grid &&
|
||||
layout == FlexLayout.grid;
|
||||
final availableLayouts = pinnedCreds.isNotEmpty
|
||||
? OathLayout.values
|
||||
: OathLayout.values
|
||||
.where((element) => element != OathLayout.mixed);
|
||||
final oathLayout = ref.watch(oathLayoutProvider);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
@ -462,67 +470,38 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
|
||||
),
|
||||
],
|
||||
),
|
||||
MouseRegion(
|
||||
onEnter: (event) {
|
||||
if (!searchFocus.hasFocus) {
|
||||
setState(() {
|
||||
_canRequestFocus = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
onExit: (event) {
|
||||
setState(() {
|
||||
_canRequestFocus = true;
|
||||
});
|
||||
},
|
||||
child: IconButton(
|
||||
tooltip: l10n.s_list_layout,
|
||||
onPressed: () {
|
||||
ref
|
||||
.read(oathPinnedLayoutProvider.notifier)
|
||||
.setLayout(FlexLayout.list);
|
||||
ref
|
||||
.read(oathLayoutProvider.notifier)
|
||||
.setLayout(FlexLayout.list);
|
||||
},
|
||||
icon: Icon(
|
||||
Symbols.list,
|
||||
color: listView
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: null,
|
||||
if (width >= 380)
|
||||
...availableLayouts.map(
|
||||
(e) => MouseRegion(
|
||||
onEnter: (event) {
|
||||
if (!searchFocus.hasFocus) {
|
||||
setState(() {
|
||||
_canRequestFocus = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
onExit: (event) {
|
||||
setState(() {
|
||||
_canRequestFocus = true;
|
||||
});
|
||||
},
|
||||
child: IconButton(
|
||||
tooltip: e.getDisplayName(l10n),
|
||||
onPressed: () {
|
||||
ref
|
||||
.read(oathLayoutProvider.notifier)
|
||||
.setLayout(e);
|
||||
},
|
||||
icon: Icon(
|
||||
e._icon,
|
||||
color: e == oathLayout
|
||||
? 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(oathPinnedLayoutProvider.notifier)
|
||||
.setLayout(FlexLayout.grid);
|
||||
ref
|
||||
.read(oathLayoutProvider.notifier)
|
||||
.setLayout(FlexLayout.grid);
|
||||
},
|
||||
icon: Icon(Symbols.grid_view,
|
||||
color: gridView
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: null),
|
||||
),
|
||||
),
|
||||
if (pinnedCreds.isNotEmpty)
|
||||
if (width < 380)
|
||||
MouseRegion(
|
||||
onEnter: (event) {
|
||||
if (!searchFocus.hasFocus) {
|
||||
@ -536,24 +515,45 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
|
||||
_canRequestFocus = true;
|
||||
});
|
||||
},
|
||||
child: IconButton(
|
||||
tooltip: l10n.s_mixed_layout,
|
||||
onPressed: () {
|
||||
ref
|
||||
.read(oathPinnedLayoutProvider.notifier)
|
||||
.setLayout(FlexLayout.grid);
|
||||
ref
|
||||
.read(oathLayoutProvider.notifier)
|
||||
.setLayout(FlexLayout.list);
|
||||
},
|
||||
child: PopupMenuButton(
|
||||
constraints: const BoxConstraints.tightFor(),
|
||||
tooltip: 'Select layout',
|
||||
popUpAnimationStyle:
|
||||
AnimationStyle(duration: Duration.zero),
|
||||
icon: Icon(
|
||||
Symbols.vertical_split,
|
||||
color: mixedView
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: null,
|
||||
oathLayout._icon,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
itemBuilder: (context) => [
|
||||
...availableLayouts.map(
|
||||
(e) => PopupMenuItem(
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.center,
|
||||
children: [
|
||||
Tooltip(
|
||||
message: e.getDisplayName(l10n),
|
||||
child: Icon(
|
||||
e._icon,
|
||||
color: e == oathLayout
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.primary
|
||||
: null,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
ref
|
||||
.read(oathLayoutProvider.notifier)
|
||||
.setLayout(e);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
]
|
||||
],
|
||||
),
|
||||
|
@ -1,6 +1,20 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
enum FlexLayout { grid, list }
|
||||
enum FlexLayout {
|
||||
grid,
|
||||
list;
|
||||
|
||||
IconData get icon => switch (this) {
|
||||
FlexLayout.list => Symbols.list,
|
||||
FlexLayout.grid => Symbols.grid_view
|
||||
};
|
||||
String getDisplayName(AppLocalizations l10n) => switch (this) {
|
||||
FlexLayout.list => l10n.s_list_layout,
|
||||
FlexLayout.grid => l10n.s_grid_layout
|
||||
};
|
||||
}
|
||||
|
||||
class FlexBox<T> extends StatelessWidget {
|
||||
final List<T> items;
|
||||
|
Loading…
Reference in New Issue
Block a user