diff --git a/lib/app/state.dart b/lib/app/state.dart index c1e675b3b..c714a854f 100755 --- a/lib/app/state.dart +++ b/lib/app/state.dart @@ -160,6 +160,32 @@ final primaryColorProvider = Provider((ref) { return lastUsedColor != null ? Color(lastUsedColor) : defaultColor; }); +final hiddenDevicesProvider = + StateNotifierProvider>( + (ref) => HiddenDevicesNotifier(ref.watch(prefProvider))); + +class HiddenDevicesNotifier extends StateNotifier> { + static const String _key = 'DEVICE_PICKER_HIDDEN'; + final SharedPreferences _prefs; + + HiddenDevicesNotifier(this._prefs) : super(_prefs.getStringList(_key) ?? []); + + void showAll() { + state = []; + _prefs.setStringList(_key, state); + } + + void hideDevice(DevicePath devicePath) { + state = [...state, devicePath.key]; + _prefs.setStringList(_key, state); + } + + void showDevice(DevicePath devicePath) { + state = state.where((e) => e != devicePath.key).toList(); + _prefs.setStringList(_key, state); + } +} + // Override with platform implementation final attachedDevicesProvider = NotifierProvider>( diff --git a/lib/app/views/device_picker.dart b/lib/app/views/device_picker.dart index a1046f6a1..184b3e3cd 100644 --- a/lib/app/views/device_picker.dart +++ b/lib/app/views/device_picker.dart @@ -19,7 +19,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:material_symbols_icons/symbols.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import '../../android/state.dart'; import '../../core/state.dart'; @@ -30,27 +29,6 @@ import 'device_avatar.dart'; import 'keys.dart' as keys; import 'keys.dart'; -final _hiddenDevicesProvider = - StateNotifierProvider<_HiddenDevicesNotifier, List>( - (ref) => _HiddenDevicesNotifier(ref.watch(prefProvider))); - -class _HiddenDevicesNotifier extends StateNotifier> { - static const String _key = 'DEVICE_PICKER_HIDDEN'; - final SharedPreferences _prefs; - - _HiddenDevicesNotifier(this._prefs) : super(_prefs.getStringList(_key) ?? []); - - void showAll() { - state = []; - _prefs.setStringList(_key, state); - } - - void hideDevice(DevicePath devicePath) { - state = [...state, devicePath.key]; - _prefs.setStringList(_key, state); - } -} - class DevicePickerContent extends ConsumerWidget { final bool extended; @@ -59,7 +37,7 @@ class DevicePickerContent extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final l10n = AppLocalizations.of(context)!; - final hidden = ref.watch(_hiddenDevicesProvider); + final hidden = ref.watch(hiddenDevicesProvider); final devices = ref .watch(attachedDevicesProvider) .where((e) => !hidden.contains(e.path.key)) @@ -343,17 +321,17 @@ class _DeviceRowState extends ConsumerState<_DeviceRow> { List _getMenuItems( BuildContext context, WidgetRef ref, DeviceNode? node) { final l10n = AppLocalizations.of(context)!; - final hidden = ref.watch(_hiddenDevicesProvider); + final hidden = ref.watch(hiddenDevicesProvider); return [ if (isDesktop && hidden.isNotEmpty) PopupMenuItem( enabled: hidden.isNotEmpty, onTap: () { - ref.read(_hiddenDevicesProvider.notifier).showAll(); + ref.read(hiddenDevicesProvider.notifier).showAll(); }, child: ListTile( - title: Text(l10n.s_show_hidden_devices), + title: Text(l10n.s_show_hidden_readers), leading: const Icon(Symbols.visibility), dense: true, contentPadding: EdgeInsets.zero, @@ -363,10 +341,10 @@ class _DeviceRowState extends ConsumerState<_DeviceRow> { if (isDesktop && node is NfcReaderNode) PopupMenuItem( onTap: () { - ref.read(_hiddenDevicesProvider.notifier).hideDevice(node.path); + ref.read(hiddenDevicesProvider.notifier).hideDevice(node.path); }, child: ListTile( - title: Text(l10n.s_hide_device), + title: Text(l10n.s_hide_reader), leading: const Icon(Symbols.visibility_off), dense: true, contentPadding: EdgeInsets.zero, diff --git a/lib/app/views/keys.dart b/lib/app/views/keys.dart index 184362468..693d9397f 100644 --- a/lib/app/views/keys.dart +++ b/lib/app/views/keys.dart @@ -62,6 +62,8 @@ const settingDrawerIcon = Key('$_prefix.settings_drawer_icon'); const helpDrawerIcon = Key('$_prefix.setting_drawer_icon'); const themeModeSetting = Key('$_prefix.settings.theme_mode'); const languageSetting = Key('$_prefix.settings.language'); +const toggleDevicesSetting = Key('$_prefix.settings.toggle_devices'); +const customIconSetting = Key('$_prefix.settings.custom_icons'); Key themeModeOption(ThemeMode mode) => Key('$_prefix.theme_mode.${mode.name}'); const tosButton = Key('$_prefix.tos_button'); const privacyButton = Key('$_prefix.privacy_button'); diff --git a/lib/app/views/main_page.dart b/lib/app/views/main_page.dart index 02fa79900..d4c5279ec 100755 --- a/lib/app/views/main_page.dart +++ b/lib/app/views/main_page.dart @@ -75,6 +75,7 @@ class MainPage extends ConsumerWidget { 'user_interaction_prompt', 'oath_add_account', 'icon_pack_dialog', + 'toggle_readers_dialog', 'android_qr_scanner_view', ].contains(route.settings.name); }); diff --git a/lib/app/views/settings_page.dart b/lib/app/views/settings_page.dart index 4c7694a89..28dfd1b2d 100755 --- a/lib/app/views/settings_page.dart +++ b/lib/app/views/settings_page.dart @@ -17,9 +17,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:material_symbols_icons/symbols.dart'; import '../../android/state.dart'; import '../../android/views/settings_views.dart'; +import '../../core/models.dart'; import '../../core/state.dart'; import '../../widgets/list_title.dart'; import '../../widgets/responsive_dialog.dart'; @@ -175,6 +177,7 @@ class _IconsView extends ConsumerWidget { return ListTile( title: Text(l10n.s_custom_icons), subtitle: Text(l10n.l_set_icons_for_accounts), + key: keys.customIconSetting, onTap: () { showDialog( // Avoid duplicate SafeAreas @@ -190,6 +193,113 @@ class _IconsView extends ConsumerWidget { } } +class _ToggleReadersDialog extends ConsumerWidget { + const _ToggleReadersDialog(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final l10n = AppLocalizations.of(context)!; + final theme = Theme.of(context); + final textTheme = theme.textTheme; + final colorScheme = theme.colorScheme; + final hidden = ref.watch(hiddenDevicesProvider); + final nfcDevices = ref + .watch(attachedDevicesProvider) + .where((e) => e.transport == Transport.nfc); + if (nfcDevices.isEmpty) { + // Pop dialog if no NFC devices + Navigator.of(context).pop(); + } + return ResponsiveDialog( + title: Text(l10n.s_toggle_readers), + dialogMaxWidth: 500, + builder: (context, _) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text(l10n.l_toggle_readers_desc), + const SizedBox(height: 8.0), + ...nfcDevices.map( + (e) => Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Symbols.contactless, + color: colorScheme.onSurfaceVariant, + ), + const SizedBox(width: 12.0), + Flexible( + child: Text( + e.name, + style: textTheme.bodyMedium + ?.copyWith(color: colorScheme.onSurfaceVariant), + softWrap: false, + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox( + width: 12.0, + ) + ], + ), + ), + Switch( + value: !hidden.contains(e.path.key), + onChanged: (show) { + if (!show) { + ref + .read(hiddenDevicesProvider.notifier) + .hideDevice(e.path); + } else { + ref + .read(hiddenDevicesProvider.notifier) + .showDevice(e.path); + } + }, + ) + ], + ), + ) + ], + ), + ), + ); + } +} + +class _ToggleReadersView extends ConsumerWidget { + const _ToggleReadersView(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final l10n = AppLocalizations.of(context)!; + final nfcDevices = ref + .watch(attachedDevicesProvider) + .where((e) => e.transport == Transport.nfc); + + return ListTile( + title: Text(l10n.s_toggle_readers), + subtitle: Text(l10n.l_toggle_readers_desc), + key: keys.toggleDevicesSetting, + enabled: nfcDevices.isNotEmpty, + onTap: () { + showDialog( + context: context, + routeSettings: const RouteSettings(name: 'toggle_readers_dialog'), + builder: (context) => _ToggleReadersDialog(), + ); + }, + ); + } +} + class SettingsPage extends ConsumerWidget { const SettingsPage({super.key}); @@ -219,6 +329,7 @@ class SettingsPage extends ConsumerWidget { const _ThemeModeView(), const _IconsView(), ListTitle(l10n.s_options), + if (!isAndroid) const _ToggleReadersView(), const _LanguageView() ], ), diff --git a/lib/desktop/state.dart b/lib/desktop/state.dart index 3a383b7aa..081730275 100755 --- a/lib/desktop/state.dart +++ b/lib/desktop/state.dart @@ -197,9 +197,15 @@ class DesktopCurrentDeviceNotifier extends CurrentDeviceNotifier { DeviceNode? build() { SharedPreferences prefs = ref.watch(prefProvider); final devices = ref.watch(attachedDevicesProvider); + final hidden = ref.watch(hiddenDevicesProvider); final lastDevice = prefs.getString(_lastDevice) ?? ''; - var node = devices.where((dev) => dev.path.key == lastDevice).firstOrNull; + // Ensure hidden devices are deselected + var node = devices + .where( + (dev) => dev.path.key == lastDevice && !hidden.contains(dev.path.key), + ) + .firstOrNull; if (node == null) { final parts = lastDevice.split('/'); if (parts.firstOrNull == 'pid') { diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 31b58bbfa..4caeb6898 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -150,8 +150,10 @@ "@_yubikey_selection": {}, "s_select_to_scan": "Zum Scannen auswählen", - "s_hide_device": "Gerät verstecken", - "s_show_hidden_devices": "Versteckte Geräte anzeigen", + "s_hide_reader": null, + "s_show_hidden_readers": null, + "s_toggle_readers": null, + "l_toggle_readers_desc": null, "s_sn_serial": "S/N: {serial}", "@s_sn_serial": { "placeholders": { diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index ff914d44d..9cb08563f 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -150,8 +150,10 @@ "@_yubikey_selection": {}, "s_select_to_scan": "Select to scan", - "s_hide_device": "Hide device", - "s_show_hidden_devices": "Show hidden devices", + "s_hide_reader": "Hide reader", + "s_show_hidden_readers": "Show hidden readers", + "s_toggle_readers": "Toggle readers", + "l_toggle_readers_desc": "Show or hide readers", "s_sn_serial": "S/N: {serial}", "@s_sn_serial": { "placeholders": { diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 56112768f..18584a44b 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -150,8 +150,10 @@ "@_yubikey_selection": {}, "s_select_to_scan": "Sélectionner pour scanner", - "s_hide_device": "Masquer appareil", - "s_show_hidden_devices": "Afficher appareils masqués", + "s_hide_reader": null, + "s_show_hidden_readers": null, + "s_toggle_readers": null, + "l_toggle_readers_desc": null, "s_sn_serial": "N/S\u00a0: {serial}", "@s_sn_serial": { "placeholders": { diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 0432f27e2..38b50fba3 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -150,8 +150,10 @@ "@_yubikey_selection": {}, "s_select_to_scan": "選択してスキャン", - "s_hide_device": "デバイスを非表示", - "s_show_hidden_devices": "非表示のデバイスを表示", + "s_hide_reader": null, + "s_show_hidden_readers": null, + "s_toggle_readers": null, + "l_toggle_readers_desc": null, "s_sn_serial": "S/N:{serial}", "@s_sn_serial": { "placeholders": { diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index f2c9c197c..e2ea20380 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -150,8 +150,10 @@ "@_yubikey_selection": {}, "s_select_to_scan": "Wybierz, aby zeskanować", - "s_hide_device": "Ukryj urządzenie", - "s_show_hidden_devices": "Pokaż ukryte urządzenia", + "s_hide_reader": null, + "s_show_hidden_readers": null, + "s_toggle_readers": null, + "l_toggle_readers_desc": null, "s_sn_serial": "S/N: {serial}", "@s_sn_serial": { "placeholders": { diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index ee152c38f..9ea526e90 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -150,8 +150,10 @@ "@_yubikey_selection": {}, "s_select_to_scan": "Vyberte na skenovanie", - "s_hide_device": "Skryť zariadenie", - "s_show_hidden_devices": "Zobraziť skryté zariadenia", + "s_hide_reader": null, + "s_show_hidden_readers": null, + "s_toggle_readers": null, + "l_toggle_readers_desc": null, "s_sn_serial": "S/N: {serial}", "@s_sn_serial": { "placeholders": { diff --git a/lib/l10n/app_vi.arb b/lib/l10n/app_vi.arb index 61632304c..593dd7a85 100644 --- a/lib/l10n/app_vi.arb +++ b/lib/l10n/app_vi.arb @@ -150,8 +150,10 @@ "@_yubikey_selection": {}, "s_select_to_scan": "Chọn để quét", - "s_hide_device": "Ẩn thiết bị", - "s_show_hidden_devices": "Hiển thị các thiết bị ẩn", + "s_hide_reader": null, + "s_show_hidden_readers": null, + "s_toggle_readers": null, + "l_toggle_readers_desc": null, "s_sn_serial": "S/N: {serial}", "@s_sn_serial": { "placeholders": {