From f6cf1e6b574196269188ab274327b9c8a907bf40 Mon Sep 17 00:00:00 2001 From: alpheios-one <275321969+alpheios-one@users.noreply.github.com> Date: Thu, 24 Sep 2026 07:02:39 +0200 Subject: [PATCH 1/5] feat(tank-presets): hide individual built-in tank presets from the pickers Each built-in preset on the Tank Presets page gets a switch that hides it from every tank picker (tank editor, cylinder configurations, weight planner, transmitters, blender). The settings page keeps listing all of them, dimmed when hidden, so a preset can be shown again. - New per-diver setting hiddenTankPresetIds, stored as a JSON list in diver_settings.hidden_tank_preset_ids (schema v227, additive column with a beforeOpen backstop; the sync floor stays). - tankPresetsProvider filters hidden built-in presets; custom presets are never hidden, and the default preset always stays offered. - The default preset has no switch and cannot be hidden. Starring a hidden preset makes it the default and shows it again. - A tank, transmitter or weight-planner rig that already uses a hidden preset keeps showing it in its dropdown. - Import matching and default resolution still read the full catalog. Closes #2305 --- lib/core/database/database.dart | 37 ++++- .../presentation/widgets/tank_editor.dart | 10 +- .../diver_settings_repository.dart | 7 + .../providers/settings_providers.dart | 26 ++++ .../services/tank_preset_visibility.dart | 65 +++++++++ .../presentation/pages/tank_presets_page.dart | 42 +++++- .../providers/tank_preset_providers.dart | 18 ++- .../pages/transmitter_edit_page.dart | 8 +- .../presentation/widgets/rig_composer.dart | 13 +- lib/l10n/arb/app_ar.arb | 2 + lib/l10n/arb/app_de.arb | 2 + lib/l10n/arb/app_en.arb | 2 + lib/l10n/arb/app_es.arb | 2 + lib/l10n/arb/app_fr.arb | 2 + lib/l10n/arb/app_he.arb | 2 + lib/l10n/arb/app_hu.arb | 2 + lib/l10n/arb/app_it.arb | 2 + lib/l10n/arb/app_localizations.dart | 12 ++ lib/l10n/arb/app_localizations_ar.dart | 7 + lib/l10n/arb/app_localizations_de.dart | 7 + lib/l10n/arb/app_localizations_en.dart | 7 + lib/l10n/arb/app_localizations_es.dart | 7 + lib/l10n/arb/app_localizations_fr.dart | 8 ++ lib/l10n/arb/app_localizations_he.dart | 7 + lib/l10n/arb/app_localizations_hu.dart | 7 + lib/l10n/arb/app_localizations_it.dart | 7 + lib/l10n/arb/app_localizations_nl.dart | 7 + lib/l10n/arb/app_localizations_pt.dart | 7 + lib/l10n/arb/app_localizations_zh.dart | 7 + lib/l10n/arb/app_nl.arb | 2 + lib/l10n/arb/app_pt.arb | 2 + lib/l10n/arb/app_zh.arb | 2 + ...ration_v226_media_cloud_asset_id_test.dart | 10 +- ...gration_v227_hidden_tank_presets_test.dart | 78 ++++++++++ .../widgets/tank_editor_test.dart | 62 ++++++++ ...s_repository_hidden_tank_presets_test.dart | 120 ++++++++++++++++ .../pages/settings_page_shared_data_test.dart | 11 ++ .../pages/settings_page_test.dart | 11 ++ .../settings_notifier_real_test.dart | 42 ++++++ .../presentation/pages/records_page_test.dart | 11 ++ .../services/tank_preset_visibility_test.dart | 111 ++++++++++++++ .../pages/tank_presets_page_test.dart | 136 ++++++++++++++++++ .../providers/tank_preset_providers_test.dart | 69 +++++++++ test/helpers/mock_providers.dart | 15 ++ 44 files changed, 997 insertions(+), 17 deletions(-) create mode 100644 lib/features/tank_presets/domain/services/tank_preset_visibility.dart create mode 100644 test/core/database/migration_v227_hidden_tank_presets_test.dart create mode 100644 test/features/settings/data/repositories/diver_settings_repository_hidden_tank_presets_test.dart create mode 100644 test/features/tank_presets/domain/services/tank_preset_visibility_test.dart diff --git a/lib/core/database/database.dart b/lib/core/database/database.dart index 62f7de5b7f..875303c07f 100644 --- a/lib/core/database/database.dart +++ b/lib/core/database/database.dart @@ -2178,6 +2178,10 @@ class DiverSettings extends Table { // manual region override (ISO country code). TextColumn get hiddenChamberIds => text().nullable()(); TextColumn get emergencyRegion => text().nullable()(); + + /// v227: built-in tank presets the diver hid from the pickers (issue + /// #2305), JSON list of preset slugs. Null or absent = none hidden. + TextColumn get hiddenTankPresetIds => text().nullable()(); // Appearance settings BoolColumn get showDepthColoredDiveCards => boolean().withDefault(const Constant(false))(); @@ -4295,7 +4299,7 @@ class AppDatabase extends _$AppDatabase { /// The current schema version as a static constant so that pre-open checks /// (e.g. version-mismatch guard) can reference it without an instance. - static const int currentSchemaVersion = 226; + static const int currentSchemaVersion = 227; /// The oldest schema whose reader can apply this build's sync payloads /// without loss or misinterpretation (the compatibility floor). @@ -4951,6 +4955,11 @@ class AppDatabase extends _$AppDatabase { // sync, not here. Additive and nullable, so the floor stays at 224. // 225 is held by PR #1978 (tissue loading import). 226, + // v227: diver_settings.hidden_tank_preset_ids (issue #2305). Additive + // nullable column, no backfill. The floor stays: an older reader simply + // shows every built-in preset. Renumbered from 225, which is held by PR + // #1978, after v226 landed while this was in review. + 227, ]; /// Idempotent DDL for the v106 connector-suggestion columns (Lightroom @@ -6442,6 +6451,23 @@ class AppDatabase extends _$AppDatabase { } } + /// v227: diver_settings.hidden_tank_preset_ids (issue #2305). Additive + /// column, default null, so every built-in preset stays visible until the + /// diver hides one. Idempotent, so it is safe to call from both onUpgrade + /// and the beforeOpen backstop. + Future _assertHiddenTankPresetIdsColumn() async { + final cols = await customSelect( + "PRAGMA table_info('diver_settings')", + ).get(); + if (cols.isEmpty) return; + final names = cols.map((c) => c.read('name')).toSet(); + if (!names.contains('hidden_tank_preset_ids')) { + await customStatement( + 'ALTER TABLE diver_settings ADD COLUMN hidden_tank_preset_ids TEXT', + ); + } + } + /// v223: buddies.linked_diver_id and dives.outing_id (issue #2002). /// Idempotent, so it is safe from both onUpgrade and the beforeOpen /// backstop, and a no-op for either table when it does not exist yet. @@ -12547,8 +12573,17 @@ class AppDatabase extends _$AppDatabase { await _assertMediaCloudAssetIdColumn(); } if (from < 226) await reportProgress(); + // v227: diver_settings.hidden_tank_preset_ids (issue #2305). + // Column-only rung, no backfill: null reads back as "none hidden". + if (from < 227) { + await _assertHiddenTankPresetIdsColumn(); + } + if (from < 227) await reportProgress(); }, beforeOpen: (details) async { + // v227 backstop: the hidden built-in tank presets. + await _assertHiddenTankPresetIdsColumn(); + // v222 backstop: the per-site vertical exaggeration overrides. await _assertSeascapeVerticalExaggerationOverridesColumn(); diff --git a/lib/features/dive_log/presentation/widgets/tank_editor.dart b/lib/features/dive_log/presentation/widgets/tank_editor.dart index d464a93e6a..138e098f30 100644 --- a/lib/features/dive_log/presentation/widgets/tank_editor.dart +++ b/lib/features/dive_log/presentation/widgets/tank_editor.dart @@ -14,6 +14,7 @@ import 'package:submersion/core/utils/number_input.dart'; import 'package:submersion/core/utils/unit_formatter.dart'; import 'package:submersion/features/settings/presentation/providers/settings_providers.dart'; import 'package:submersion/features/tank_presets/domain/entities/tank_preset_entity.dart'; +import 'package:submersion/features/tank_presets/domain/services/tank_preset_visibility.dart'; import 'package:submersion/features/tank_presets/presentation/providers/tank_preset_providers.dart'; import 'package:submersion/features/dive_log/domain/entities/dive.dart'; import 'package:submersion/features/dive_log/presentation/widgets/tank_enum_display.dart'; @@ -474,15 +475,18 @@ class _TankEditorState extends ConsumerState { child: presetsAsync.when( loading: () => const LinearProgressIndicator(), error: (e, st) => Text('Error: $e'), - data: (presets) { + data: (visiblePresets) { + final presetName = + _selectedPreset?.name ?? widget.tank.presetName; + // A tank logged with a preset the diver has since hidden keeps + // showing it (issue #2305). + final presets = withKeptTankPresets(visiblePresets, [presetName]); final customPresets = presets.where((p) => !p.isBuiltIn).toList(); final builtInPresets = presets.where((p) => p.isBuiltIn).toList(); // Find the matching preset from the loaded list to ensure object equality // This is necessary because DropdownButtonFormField requires the value // to be the exact same instance as one of the items - final presetName = - _selectedPreset?.name ?? widget.tank.presetName; final matchingPreset = presetName != null ? presets.where((p) => p.name == presetName).firstOrNull : null; diff --git a/lib/features/settings/data/repositories/diver_settings_repository.dart b/lib/features/settings/data/repositories/diver_settings_repository.dart index 602bc870a0..110446e238 100644 --- a/lib/features/settings/data/repositories/diver_settings_repository.dart +++ b/lib/features/settings/data/repositories/diver_settings_repository.dart @@ -139,6 +139,9 @@ class DiverSettingsRepository { ), hiddenChamberIds: Value(_encodeDisabledRules(s.hiddenChamberIds)), emergencyRegion: Value(s.emergencyRegion), + hiddenTankPresetIds: Value( + _encodeDisabledRules(s.hiddenTankPresetIds), + ), showAscentRateColors: Value(s.showAscentRateColors), showNdlOnProfile: Value(s.showNdlOnProfile), lastStopDepth: Value(s.lastStopDepth), @@ -329,6 +332,9 @@ class DiverSettingsRepository { _encodeDisabledRules(settings.hiddenChamberIds), ), emergencyRegion: Value(settings.emergencyRegion), + hiddenTankPresetIds: Value( + _encodeDisabledRules(settings.hiddenTankPresetIds), + ), showAscentRateColors: Value(settings.showAscentRateColors), showNdlOnProfile: Value(settings.showNdlOnProfile), lastStopDepth: Value(settings.lastStopDepth), @@ -561,6 +567,7 @@ class DiverSettingsRepository { conditionDisabledRules: _decodeDisabledRules(row.conditionDisabledRules), hiddenChamberIds: _decodeDisabledRules(row.hiddenChamberIds), emergencyRegion: row.emergencyRegion, + hiddenTankPresetIds: _decodeDisabledRules(row.hiddenTankPresetIds), showAscentRateColors: row.showAscentRateColors, showNdlOnProfile: row.showNdlOnProfile, lastStopDepth: row.lastStopDepth, diff --git a/lib/features/settings/presentation/providers/settings_providers.dart b/lib/features/settings/presentation/providers/settings_providers.dart index c42f6d8563..12adbfd222 100644 --- a/lib/features/settings/presentation/providers/settings_providers.dart +++ b/lib/features/settings/presentation/providers/settings_providers.dart @@ -205,6 +205,10 @@ class AppSettings { final String? defaultTankPreset; final bool applyDefaultTankToImports; + /// Built-in tank preset slugs hidden from the pickers (issue #2305). The + /// Tank Presets page still lists them, import matching still uses them. + final Set hiddenTankPresetIds; + // Decompression & Safety settings /// Gradient Factor Low (0-100, typically 30) final int gfLow; @@ -573,6 +577,7 @@ class AppSettings { this.defaultStartPressure = 200, this.defaultTankPreset = 'al80', this.applyDefaultTankToImports = false, + this.hiddenTankPresetIds = const {}, // Decompression defaults this.gfLow = 50, this.gfHigh = 85, @@ -752,6 +757,7 @@ class AppSettings { String? defaultTankPreset, bool clearDefaultTankPreset = false, bool? applyDefaultTankToImports, + Set? hiddenTankPresetIds, int? gfLow, int? gfHigh, double? ppO2MaxWorking, @@ -903,6 +909,7 @@ class AppSettings { : (defaultTankPreset ?? this.defaultTankPreset), applyDefaultTankToImports: applyDefaultTankToImports ?? this.applyDefaultTankToImports, + hiddenTankPresetIds: hiddenTankPresetIds ?? this.hiddenTankPresetIds, gfLow: gfLow ?? this.gfLow, gfHigh: gfHigh ?? this.gfHigh, ppO2MaxWorking: ppO2MaxWorking ?? this.ppO2MaxWorking, @@ -1531,14 +1538,33 @@ class SettingsNotifier extends StateNotifier { await _saveSettings(); } + /// Also shows [presetName] again if it was hidden: the default preset is + /// always offered in the pickers (issue #2305). Future setDefaultTankPreset(String? presetName) async { state = state.copyWith( defaultTankPreset: presetName, clearDefaultTankPreset: presetName == null, + hiddenTankPresetIds: presetName == null + ? null + : ({...state.hiddenTankPresetIds}..remove(presetName)), ); await _saveSettings(); } + /// Hides or shows a built-in tank preset in the pickers (issue #2305). + /// The current default preset cannot be hidden, so hiding it is a no-op. + Future setTankPresetHidden(String presetName, bool hidden) async { + if (hidden && presetName == state.defaultTankPreset) return; + final ids = {...state.hiddenTankPresetIds}; + if (hidden) { + ids.add(presetName); + } else { + ids.remove(presetName); + } + state = state.copyWith(hiddenTankPresetIds: ids); + await _saveSettings(); + } + Future setApplyDefaultTankToImports(bool value) async { state = state.copyWith(applyDefaultTankToImports: value); await _saveSettings(); diff --git a/lib/features/tank_presets/domain/services/tank_preset_visibility.dart b/lib/features/tank_presets/domain/services/tank_preset_visibility.dart new file mode 100644 index 0000000000..93fba73778 --- /dev/null +++ b/lib/features/tank_presets/domain/services/tank_preset_visibility.dart @@ -0,0 +1,65 @@ +import 'package:submersion/core/constants/tank_presets.dart'; +import 'package:submersion/features/tank_presets/domain/entities/tank_preset_entity.dart'; + +// Which tank presets the diver's pickers offer (issue #2305). +// +// A diver can hide built-in presets they never use. Hiding only narrows the +// pickers: import matching and default resolution keep reading the full +// catalog, and the Tank Presets settings page keeps listing everything so a +// hidden preset can be shown again. + +/// [all] without the built-in presets named in [hidden]. +/// +/// Custom presets are never hidden, even when one shares a built-in slug. +/// The [defaultPresetName] always stays, so a default that ended up in the +/// hidden set (the settings page never allows that, but a synced row could +/// carry it) is still offered where it is applied. +List visibleTankPresets( + List all, + Set hidden, { + String? defaultPresetName, +}) { + if (hidden.isEmpty) return all; + return [ + for (final preset in all) + if (!preset.isBuiltIn || + preset.name == defaultPresetName || + !hidden.contains(preset.name)) + preset, + ]; +} + +/// [visible] plus any hidden built-in preset named in [keep], so a picker +/// whose current value is a hidden preset still shows that value. +/// +/// Restored presets go back to their catalog position among the built-in +/// ones, custom presets stay first, and every preset already in [visible] +/// keeps its instance. Returns [visible] itself when nothing is missing. +List withKeptTankPresets( + List visible, + Iterable keep, +) { + final present = {for (final preset in visible) preset.name}; + final missing = { + for (final name in keep) + if (name != null && + !present.contains(name) && + TankPresets.byName(name) != null) + name, + }; + if (missing.isEmpty) return visible; + + final builtInByName = { + for (final preset in visible) + if (preset.isBuiltIn) preset.name: preset, + }; + return [ + for (final preset in visible) + if (!preset.isBuiltIn) preset, + for (final builtIn in TankPresets.all) + if (builtInByName[builtIn.name] case final existing?) + existing + else if (missing.contains(builtIn.name)) + TankPresetEntity.fromBuiltIn(builtIn), + ]; +} diff --git a/lib/features/tank_presets/presentation/pages/tank_presets_page.dart b/lib/features/tank_presets/presentation/pages/tank_presets_page.dart index 07a2a19290..9ea97dfa33 100644 --- a/lib/features/tank_presets/presentation/pages/tank_presets_page.dart +++ b/lib/features/tank_presets/presentation/pages/tank_presets_page.dart @@ -98,6 +98,15 @@ class TankPresetsPage extends ConsumerWidget { context, context.l10n.tankPresets_builtInPresets, ), + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), + child: Text( + context.l10n.tankPresets_builtInPresets_description, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), ...builtInPresets.map( (preset) => _buildPresetTile( context, @@ -106,6 +115,9 @@ class TankPresetsPage extends ConsumerWidget { units, canEdit: false, isDefault: settings.defaultTankPreset == preset.name, + isHidden: + settings.defaultTankPreset != preset.name && + settings.hiddenTankPresetIds.contains(preset.name), ), ), ], @@ -134,6 +146,7 @@ class TankPresetsPage extends ConsumerWidget { UnitFormatter units, { required bool canEdit, required bool isDefault, + bool isHidden = false, }) { final volumeStr = units.formatTankVolume( preset.volumeLiters, @@ -146,9 +159,14 @@ class TankPresetsPage extends ConsumerWidget { ); return ListTile( + // A hidden built-in preset stays listed so it can be shown again, but + // reads as inactive. The trailing controls stay usable. + enabled: !isHidden, leading: Icon( MdiIcons.divingScubaTank, - color: canEdit + color: isHidden + ? Theme.of(context).disabledColor + : canEdit ? Theme.of(context).colorScheme.secondary : Theme.of(context).colorScheme.primary, ), @@ -182,6 +200,28 @@ class TankPresetsPage extends ConsumerWidget { ? context.l10n.tankPresets_currentDefault : context.l10n.tankPresets_setAsDefault, ), + // Built-in presets can be hidden from the pickers (issue #2305), + // except the default one. Its switch keeps its space so the stars + // stay aligned down the list. + if (!canEdit) + Visibility( + visible: !isDefault, + maintainSize: true, + maintainAnimation: true, + maintainState: true, + child: Tooltip( + message: context.l10n.tankPresets_showInPickers, + child: Switch( + key: ValueKey('tank-preset-visible-${preset.name}'), + value: !isHidden, + onChanged: isDefault + ? null + : (visible) => ref + .read(settingsProvider.notifier) + .setTankPresetHidden(preset.name, !visible), + ), + ), + ), if (canEdit) ...[ IconButton( icon: const Icon(Icons.edit_outlined), diff --git a/lib/features/tank_presets/presentation/providers/tank_preset_providers.dart b/lib/features/tank_presets/presentation/providers/tank_preset_providers.dart index fc349a9a3a..ac75146c90 100644 --- a/lib/features/tank_presets/presentation/providers/tank_preset_providers.dart +++ b/lib/features/tank_presets/presentation/providers/tank_preset_providers.dart @@ -1,8 +1,10 @@ import 'package:submersion/core/providers/provider.dart'; import 'package:submersion/features/divers/presentation/providers/diver_providers.dart'; +import 'package:submersion/features/settings/presentation/providers/settings_providers.dart'; import 'package:submersion/features/tank_presets/data/repositories/tank_preset_repository.dart'; import 'package:submersion/features/tank_presets/domain/entities/tank_preset_entity.dart'; +import 'package:submersion/features/tank_presets/domain/services/tank_preset_visibility.dart'; import 'package:submersion/core/utils/log_failure.dart'; /// Repository provider @@ -10,8 +12,11 @@ final tankPresetRepositoryProvider = Provider((ref) { return TankPresetRepository(); }); -/// All tank presets provider (custom + built-in, custom first) -/// Includes built-in presets plus custom presets for the current diver +/// Tank presets the pickers offer (custom + built-in, custom first) +/// Includes built-in presets plus custom presets for the current diver, +/// minus the built-in presets the diver hid (issue #2305). The Tank Presets +/// settings page reads [tankPresetListNotifierProvider] instead, which keeps +/// every preset so a hidden one can be shown again. /// /// Stays a [FutureProvider] so imperative /// `ref.read(tankPresetsProvider.future)` reads still resolve, while @@ -20,11 +25,18 @@ final tankPresetRepositoryProvider = Provider((ref) { /// a cached one-shot snapshot. final tankPresetsProvider = FutureProvider>((ref) async { final repository = ref.watch(tankPresetRepositoryProvider); + final hidden = ref.watch( + settingsProvider.select((s) => s.hiddenTankPresetIds), + ); + final defaultPresetName = ref.watch( + settingsProvider.select((s) => s.defaultTankPreset), + ); final validatedDiverId = await ref.watch( validatedCurrentDiverIdProvider.future, ); ref.invalidateSelfWhen(repository.watchTankPresetsChanges()); - return repository.getAllPresets(diverId: validatedDiverId); + final all = await repository.getAllPresets(diverId: validatedDiverId); + return visibleTankPresets(all, hidden, defaultPresetName: defaultPresetName); }); /// Custom (user-defined) tank presets only for the current diver diff --git a/lib/features/transmitters/presentation/pages/transmitter_edit_page.dart b/lib/features/transmitters/presentation/pages/transmitter_edit_page.dart index a51cebf54d..ed51403f59 100644 --- a/lib/features/transmitters/presentation/pages/transmitter_edit_page.dart +++ b/lib/features/transmitters/presentation/pages/transmitter_edit_page.dart @@ -16,6 +16,7 @@ import 'package:submersion/features/equipment/domain/entities/equipment_item.dar import 'package:submersion/features/equipment/presentation/providers/equipment_providers.dart'; import 'package:submersion/features/settings/presentation/providers/settings_providers.dart'; import 'package:submersion/features/tank_presets/domain/entities/tank_preset_entity.dart'; +import 'package:submersion/features/tank_presets/domain/services/tank_preset_visibility.dart'; import 'package:submersion/features/tank_presets/presentation/providers/tank_preset_providers.dart'; import 'package:submersion/features/transmitters/data/repositories/transmitter_repository.dart'; import 'package:submersion/features/transmitters/domain/entities/transmitter.dart'; @@ -510,7 +511,12 @@ class _TransmitterEditPageState extends ConsumerState { presets.when( loading: () => const LinearProgressIndicator(), error: (e, st) => Text('${l10n.common_label_error}: $e'), - data: (list) { + data: (visibleList) { + // A preset the diver has since hidden stays selectable + // while it is this transmitter's value (issue #2305). + final list = withKeptTankPresets(visibleList, [ + _presetName, + ]); final matching = _presetName == null ? null : list diff --git a/lib/features/weight_planner/presentation/widgets/rig_composer.dart b/lib/features/weight_planner/presentation/widgets/rig_composer.dart index b21cd477b6..30c55441b8 100644 --- a/lib/features/weight_planner/presentation/widgets/rig_composer.dart +++ b/lib/features/weight_planner/presentation/widgets/rig_composer.dart @@ -11,6 +11,7 @@ import 'package:submersion/features/equipment/domain/entities/equipment_item.dar import 'package:submersion/features/equipment/domain/entities/equipment_set.dart'; import 'package:submersion/features/equipment/presentation/widgets/service_status_indicator.dart'; import 'package:submersion/features/tank_presets/domain/entities/tank_preset_entity.dart'; +import 'package:submersion/features/tank_presets/domain/services/tank_preset_visibility.dart'; import 'package:submersion/features/tank_presets/presentation/providers/tank_preset_providers.dart'; import 'package:submersion/l10n/l10n_extension.dart'; @@ -120,7 +121,13 @@ class RigComposer extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); - final presets = ref.watch(tankPresetsProvider).valueOrNull ?? const []; + final visiblePresets = + ref.watch(tankPresetsProvider).valueOrNull ?? const []; + // A tank already in the rig keeps its preset in the dropdown even after + // the diver hides it (issue #2305); adding a tank offers visible ones. + final presets = withKeptTankPresets(visiblePresets, [ + for (final tank in tanks) tank.name, + ]); return Card( child: Padding( @@ -200,9 +207,9 @@ class RigComposer extends ConsumerWidget { TextButton.icon( icon: const Icon(Icons.add, size: 18), label: Text(context.l10n.tools_weight_addTank), - onPressed: presets.isEmpty + onPressed: visiblePresets.isEmpty ? null - : () => onTankAdded(presets.first), + : () => onTankAdded(visiblePresets.first), ), ], ), diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index 9f36865195..73978adef7 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -6347,6 +6347,7 @@ "tags_title_manageTags": "إدارة الوسوم", "tankPresets_addPreset": "إضافة إعداد أسطوانة", "tankPresets_builtInPresets": "الإعدادات المدمجة", + "tankPresets_builtInPresets_description": "أوقف الإعدادات التي لا تستخدمها لإخفائها من قوائم اختيار الأسطوانة. يظهر الإعداد الافتراضي دائمًا.", "tankPresets_currentDefault": "الافتراضي الحالي", "tankPresets_customPresets": "الإعدادات المخصصة", "tankPresets_defaultSettings": "الخزان الافتراضي", @@ -6395,6 +6396,7 @@ "tankPresets_new_title": "إعداد أسطوانة جديد", "tankPresets_noPresets": "لا توجد إعدادات أسطوانات", "tankPresets_setAsDefault": "تعيين كافتراضي", + "tankPresets_showInPickers": "إظهار في قوائم اختيار الأسطوانة", "tankPresets_title": "إعدادات الأسطوانات", "tank_al100_description": "أسطوانة ألومنيوم 100 قدم مكعب", "tank_al100_displayName": "AL100", diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index 6ec4f676d9..b87205e1ac 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -6451,6 +6451,7 @@ "tags_title_manageTags": "Tags verwalten", "tankPresets_addPreset": "Flaschenvorlage hinzufügen", "tankPresets_builtInPresets": "Integrierte Vorlagen", + "tankPresets_builtInPresets_description": "Schalte Vorlagen aus, die du nicht verwendest, um sie in der Tankauswahl auszublenden. Die Standardvorlage wird immer angezeigt.", "tankPresets_currentDefault": "Aktueller Standard", "tankPresets_customPresets": "Eigene Vorlagen", "tankPresets_defaultSettings": "Standardtank", @@ -6499,6 +6500,7 @@ "tankPresets_new_title": "Neue Flaschenvorlage", "tankPresets_noPresets": "Keine Flaschenvorlagen verfügbar", "tankPresets_setAsDefault": "Als Standard festlegen", + "tankPresets_showInPickers": "In der Tankauswahl anzeigen", "tankPresets_title": "Flaschenvorlagen", "tank_al100_description": "Aluminium 100 cuft", "tank_al100_displayName": "AL100", diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 02822d959a..de3aa07010 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -16921,6 +16921,7 @@ "maps_regionSelector_selectRegionButton": "Select Region", "tankPresets_addPreset": "Add tank preset", "tankPresets_builtInPresets": "Built-in Presets", + "tankPresets_builtInPresets_description": "Switch off the presets you don't use to hide them from the tank pickers. The default preset is always shown.", "tankPresets_currentDefault": "Current default", "tankPresets_customPresets": "Custom Presets", "tankPresets_defaultSettings": "Default Tank", @@ -17046,6 +17047,7 @@ "tankPresets_new_title": "New Tank Preset", "tankPresets_noPresets": "No tank presets available", "tankPresets_setAsDefault": "Set as default", + "tankPresets_showInPickers": "Show in tank pickers", "tankPresets_title": "Tank Presets", "tools_gpsLogger_description": "Record your position during a dive day and match imported dives to GPS locations automatically.", "tools_gpsLogger_subtitle": "Record a surface track", diff --git a/lib/l10n/arb/app_es.arb b/lib/l10n/arb/app_es.arb index 829564d1b3..4a0713c5ed 100644 --- a/lib/l10n/arb/app_es.arb +++ b/lib/l10n/arb/app_es.arb @@ -6347,6 +6347,7 @@ "tags_title_manageTags": "Administrar Etiquetas", "tankPresets_addPreset": "Añadir preset de tanque", "tankPresets_builtInPresets": "Presets incluidos", + "tankPresets_builtInPresets_description": "Desactiva los presets que no uses para ocultarlos en los selectores de tanque. El preset predeterminado siempre se muestra.", "tankPresets_currentDefault": "Predeterminado actual", "tankPresets_customPresets": "Presets personalizados", "tankPresets_defaultSettings": "Tanque predeterminado", @@ -6395,6 +6396,7 @@ "tankPresets_new_title": "Nuevo preset de tanque", "tankPresets_noPresets": "No hay presets de tanque disponibles", "tankPresets_setAsDefault": "Establecer como predeterminado", + "tankPresets_showInPickers": "Mostrar en los selectores de tanque", "tankPresets_title": "Presets de tanque", "tank_al100_description": "Aluminio 100 cu ft", "tank_al100_displayName": "AL100", diff --git a/lib/l10n/arb/app_fr.arb b/lib/l10n/arb/app_fr.arb index c6e84e249b..d29824de2a 100644 --- a/lib/l10n/arb/app_fr.arb +++ b/lib/l10n/arb/app_fr.arb @@ -6245,6 +6245,7 @@ "tags_title_manageTags": "Gérer les étiquettes", "tankPresets_addPreset": "Ajouter un preset de bloc", "tankPresets_builtInPresets": "Presets intégrés", + "tankPresets_builtInPresets_description": "Désactivez les presets que vous n'utilisez pas pour les masquer dans les sélecteurs de bloc. Le preset par défaut est toujours affiché.", "tankPresets_currentDefault": "Par défaut actuel", "tankPresets_customPresets": "Presets personnalisés", "tankPresets_defaultSettings": "Bloc par défaut", @@ -6293,6 +6294,7 @@ "tankPresets_new_title": "Nouveau preset de bloc", "tankPresets_noPresets": "Aucun preset de bloc disponible", "tankPresets_setAsDefault": "Définir par défaut", + "tankPresets_showInPickers": "Afficher dans les sélecteurs de bloc", "tankPresets_title": "Presets de blocs", "tank_al100_description": "Bloc aluminium 100 cu ft", "tank_al100_displayName": "AL100", diff --git a/lib/l10n/arb/app_he.arb b/lib/l10n/arb/app_he.arb index d3be762e0c..fd687d0650 100644 --- a/lib/l10n/arb/app_he.arb +++ b/lib/l10n/arb/app_he.arb @@ -6347,6 +6347,7 @@ "tags_title_manageTags": "נהל תגיות", "tankPresets_addPreset": "הוסף תבנית מיכל", "tankPresets_builtInPresets": "תבניות מובנות", + "tankPresets_builtInPresets_description": "כבה תבניות שאינך משתמש בהן כדי להסתיר אותן מבוררי הבלונים. תבנית ברירת המחדל מוצגת תמיד.", "tankPresets_currentDefault": "ברירת מחדל נוכחית", "tankPresets_customPresets": "תבניות מותאמות אישית", "tankPresets_defaultSettings": "מיכל ברירת מחדל", @@ -6395,6 +6396,7 @@ "tankPresets_new_title": "תבנית מיכל חדשה", "tankPresets_noPresets": "אין תבניות מיכל זמינות", "tankPresets_setAsDefault": "הגדר כברירת מחדל", + "tankPresets_showInPickers": "הצג בבוררי הבלונים", "tankPresets_title": "תבניות מיכל", "tank_al100_description": "אלומיניום 100 cuft", "tank_al100_displayName": "AL100", diff --git a/lib/l10n/arb/app_hu.arb b/lib/l10n/arb/app_hu.arb index 55a803fee4..f0ecb97855 100644 --- a/lib/l10n/arb/app_hu.arb +++ b/lib/l10n/arb/app_hu.arb @@ -6245,6 +6245,7 @@ "tags_title_manageTags": "Címkék kezelése", "tankPresets_addPreset": "Palacksablon hozzáadása", "tankPresets_builtInPresets": "Beépített sablonok", + "tankPresets_builtInPresets_description": "Kapcsold ki a nem használt sablonokat, hogy ne jelenjenek meg a palackválasztókban. Az alapértelmezett sablon mindig látható.", "tankPresets_currentDefault": "Jelenlegi alapértelmezett", "tankPresets_customPresets": "Egyedi sablonok", "tankPresets_defaultSettings": "Alapértelmezett palack", @@ -6293,6 +6294,7 @@ "tankPresets_new_title": "Új palacksablon", "tankPresets_noPresets": "Nincsenek elérhető palacksablonok", "tankPresets_setAsDefault": "Beállítás alapértelmezettként", + "tankPresets_showInPickers": "Megjelenítés a palackválasztókban", "tankPresets_title": "Palacksablonok", "tank_al100_description": "Alumínium 100 cu ft", "tank_al100_displayName": "AL100", diff --git a/lib/l10n/arb/app_it.arb b/lib/l10n/arb/app_it.arb index 9a097c61e6..2d8d8794c5 100644 --- a/lib/l10n/arb/app_it.arb +++ b/lib/l10n/arb/app_it.arb @@ -6241,6 +6241,7 @@ "tags_title_manageTags": "Gestisci Tag", "tankPresets_addPreset": "Aggiungi preset bombola", "tankPresets_builtInPresets": "Preset predefiniti", + "tankPresets_builtInPresets_description": "Disattiva i preset che non usi per nasconderli nei selettori di bombola. Il preset predefinito viene sempre mostrato.", "tankPresets_currentDefault": "Predefinito attuale", "tankPresets_customPresets": "Preset personalizzati", "tankPresets_defaultSettings": "Bombola predefinita", @@ -6289,6 +6290,7 @@ "tankPresets_new_title": "Nuovo preset bombola", "tankPresets_noPresets": "Nessun preset bombola disponibile", "tankPresets_setAsDefault": "Imposta come predefinito", + "tankPresets_showInPickers": "Mostra nei selettori di bombola", "tankPresets_title": "Preset bombole", "tank_al100_description": "Alluminio 100 cu ft", "tank_al100_displayName": "AL100", diff --git a/lib/l10n/arb/app_localizations.dart b/lib/l10n/arb/app_localizations.dart index 2d7fb9bad1..181b465c54 100644 --- a/lib/l10n/arb/app_localizations.dart +++ b/lib/l10n/arb/app_localizations.dart @@ -41808,6 +41808,12 @@ abstract class AppLocalizations { /// **'Built-in Presets'** String get tankPresets_builtInPresets; + /// No description provided for @tankPresets_builtInPresets_description. + /// + /// In en, this message translates to: + /// **'Switch off the presets you don\'t use to hide them from the tank pickers. The default preset is always shown.'** + String get tankPresets_builtInPresets_description; + /// No description provided for @tankPresets_currentDefault. /// /// In en, this message translates to: @@ -42054,6 +42060,12 @@ abstract class AppLocalizations { /// **'Set as default'** String get tankPresets_setAsDefault; + /// No description provided for @tankPresets_showInPickers. + /// + /// In en, this message translates to: + /// **'Show in tank pickers'** + String get tankPresets_showInPickers; + /// No description provided for @tankPresets_title. /// /// In en, this message translates to: diff --git a/lib/l10n/arb/app_localizations_ar.dart b/lib/l10n/arb/app_localizations_ar.dart index 000eb873ab..175f064bfe 100644 --- a/lib/l10n/arb/app_localizations_ar.dart +++ b/lib/l10n/arb/app_localizations_ar.dart @@ -25825,6 +25825,10 @@ class AppLocalizationsAr extends AppLocalizations { @override String get tankPresets_builtInPresets => 'الإعدادات المدمجة'; + @override + String get tankPresets_builtInPresets_description => + 'أوقف الإعدادات التي لا تستخدمها لإخفائها من قوائم اختيار الأسطوانة. يظهر الإعداد الافتراضي دائمًا.'; + @override String get tankPresets_currentDefault => 'الافتراضي الحالي'; @@ -25975,6 +25979,9 @@ class AppLocalizationsAr extends AppLocalizations { @override String get tankPresets_setAsDefault => 'تعيين كافتراضي'; + @override + String get tankPresets_showInPickers => 'إظهار في قوائم اختيار الأسطوانة'; + @override String get tankPresets_title => 'إعدادات الأسطوانات'; diff --git a/lib/l10n/arb/app_localizations_de.dart b/lib/l10n/arb/app_localizations_de.dart index 0857d1efe7..6e70a45a28 100644 --- a/lib/l10n/arb/app_localizations_de.dart +++ b/lib/l10n/arb/app_localizations_de.dart @@ -26123,6 +26123,10 @@ class AppLocalizationsDe extends AppLocalizations { @override String get tankPresets_builtInPresets => 'Integrierte Vorlagen'; + @override + String get tankPresets_builtInPresets_description => + 'Schalte Vorlagen aus, die du nicht verwendest, um sie in der Tankauswahl auszublenden. Die Standardvorlage wird immer angezeigt.'; + @override String get tankPresets_currentDefault => 'Aktueller Standard'; @@ -26275,6 +26279,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get tankPresets_setAsDefault => 'Als Standard festlegen'; + @override + String get tankPresets_showInPickers => 'In der Tankauswahl anzeigen'; + @override String get tankPresets_title => 'Flaschenvorlagen'; diff --git a/lib/l10n/arb/app_localizations_en.dart b/lib/l10n/arb/app_localizations_en.dart index 81bbdae280..25693566ee 100644 --- a/lib/l10n/arb/app_localizations_en.dart +++ b/lib/l10n/arb/app_localizations_en.dart @@ -25747,6 +25747,10 @@ class AppLocalizationsEn extends AppLocalizations { @override String get tankPresets_builtInPresets => 'Built-in Presets'; + @override + String get tankPresets_builtInPresets_description => + 'Switch off the presets you don\'t use to hide them from the tank pickers. The default preset is always shown.'; + @override String get tankPresets_currentDefault => 'Current default'; @@ -25898,6 +25902,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get tankPresets_setAsDefault => 'Set as default'; + @override + String get tankPresets_showInPickers => 'Show in tank pickers'; + @override String get tankPresets_title => 'Tank Presets'; diff --git a/lib/l10n/arb/app_localizations_es.dart b/lib/l10n/arb/app_localizations_es.dart index 2efd63cbde..9fea1aeb48 100644 --- a/lib/l10n/arb/app_localizations_es.dart +++ b/lib/l10n/arb/app_localizations_es.dart @@ -26197,6 +26197,10 @@ class AppLocalizationsEs extends AppLocalizations { @override String get tankPresets_builtInPresets => 'Presets incluidos'; + @override + String get tankPresets_builtInPresets_description => + 'Desactiva los presets que no uses para ocultarlos en los selectores de tanque. El preset predeterminado siempre se muestra.'; + @override String get tankPresets_currentDefault => 'Predeterminado actual'; @@ -26350,6 +26354,9 @@ class AppLocalizationsEs extends AppLocalizations { @override String get tankPresets_setAsDefault => 'Establecer como predeterminado'; + @override + String get tankPresets_showInPickers => 'Mostrar en los selectores de tanque'; + @override String get tankPresets_title => 'Presets de tanque'; diff --git a/lib/l10n/arb/app_localizations_fr.dart b/lib/l10n/arb/app_localizations_fr.dart index eded22b0a8..aab4ab538b 100644 --- a/lib/l10n/arb/app_localizations_fr.dart +++ b/lib/l10n/arb/app_localizations_fr.dart @@ -26270,6 +26270,10 @@ class AppLocalizationsFr extends AppLocalizations { @override String get tankPresets_builtInPresets => 'Presets intégrés'; + @override + String get tankPresets_builtInPresets_description => + 'Désactivez les presets que vous n\'utilisez pas pour les masquer dans les sélecteurs de bloc. Le preset par défaut est toujours affiché.'; + @override String get tankPresets_currentDefault => 'Par défaut actuel'; @@ -26422,6 +26426,10 @@ class AppLocalizationsFr extends AppLocalizations { @override String get tankPresets_setAsDefault => 'Définir par défaut'; + @override + String get tankPresets_showInPickers => + 'Afficher dans les sélecteurs de bloc'; + @override String get tankPresets_title => 'Presets de blocs'; diff --git a/lib/l10n/arb/app_localizations_he.dart b/lib/l10n/arb/app_localizations_he.dart index dc04e1ac5c..c74965ebd7 100644 --- a/lib/l10n/arb/app_localizations_he.dart +++ b/lib/l10n/arb/app_localizations_he.dart @@ -25537,6 +25537,10 @@ class AppLocalizationsHe extends AppLocalizations { @override String get tankPresets_builtInPresets => 'תבניות מובנות'; + @override + String get tankPresets_builtInPresets_description => + 'כבה תבניות שאינך משתמש בהן כדי להסתיר אותן מבוררי הבלונים. תבנית ברירת המחדל מוצגת תמיד.'; + @override String get tankPresets_currentDefault => 'ברירת מחדל נוכחית'; @@ -25687,6 +25691,9 @@ class AppLocalizationsHe extends AppLocalizations { @override String get tankPresets_setAsDefault => 'הגדר כברירת מחדל'; + @override + String get tankPresets_showInPickers => 'הצג בבוררי הבלונים'; + @override String get tankPresets_title => 'תבניות מיכל'; diff --git a/lib/l10n/arb/app_localizations_hu.dart b/lib/l10n/arb/app_localizations_hu.dart index 1e81324929..b37a9a9eda 100644 --- a/lib/l10n/arb/app_localizations_hu.dart +++ b/lib/l10n/arb/app_localizations_hu.dart @@ -26095,6 +26095,10 @@ class AppLocalizationsHu extends AppLocalizations { @override String get tankPresets_builtInPresets => 'Beépített sablonok'; + @override + String get tankPresets_builtInPresets_description => + 'Kapcsold ki a nem használt sablonokat, hogy ne jelenjenek meg a palackválasztókban. Az alapértelmezett sablon mindig látható.'; + @override String get tankPresets_currentDefault => 'Jelenlegi alapértelmezett'; @@ -26246,6 +26250,9 @@ class AppLocalizationsHu extends AppLocalizations { @override String get tankPresets_setAsDefault => 'Beállítás alapértelmezettként'; + @override + String get tankPresets_showInPickers => 'Megjelenítés a palackválasztókban'; + @override String get tankPresets_title => 'Palacksablonok'; diff --git a/lib/l10n/arb/app_localizations_it.dart b/lib/l10n/arb/app_localizations_it.dart index 4ed91261ca..a806266083 100644 --- a/lib/l10n/arb/app_localizations_it.dart +++ b/lib/l10n/arb/app_localizations_it.dart @@ -26176,6 +26176,10 @@ class AppLocalizationsIt extends AppLocalizations { @override String get tankPresets_builtInPresets => 'Preset predefiniti'; + @override + String get tankPresets_builtInPresets_description => + 'Disattiva i preset che non usi per nasconderli nei selettori di bombola. Il preset predefinito viene sempre mostrato.'; + @override String get tankPresets_currentDefault => 'Predefinito attuale'; @@ -26328,6 +26332,9 @@ class AppLocalizationsIt extends AppLocalizations { @override String get tankPresets_setAsDefault => 'Imposta come predefinito'; + @override + String get tankPresets_showInPickers => 'Mostra nei selettori di bombola'; + @override String get tankPresets_title => 'Preset bombole'; diff --git a/lib/l10n/arb/app_localizations_nl.dart b/lib/l10n/arb/app_localizations_nl.dart index e1336d6594..557dca046d 100644 --- a/lib/l10n/arb/app_localizations_nl.dart +++ b/lib/l10n/arb/app_localizations_nl.dart @@ -25972,6 +25972,10 @@ class AppLocalizationsNl extends AppLocalizations { @override String get tankPresets_builtInPresets => 'Standaard instellingen'; + @override + String get tankPresets_builtInPresets_description => + 'Schakel voorinstellingen uit die je niet gebruikt om ze te verbergen in de flessenkiezers. De standaardvoorinstelling wordt altijd getoond.'; + @override String get tankPresets_currentDefault => 'Huidige standaard'; @@ -26124,6 +26128,9 @@ class AppLocalizationsNl extends AppLocalizations { @override String get tankPresets_setAsDefault => 'Als standaard instellen'; + @override + String get tankPresets_showInPickers => 'Tonen in flessenkiezers'; + @override String get tankPresets_title => 'Flesinstellingen'; diff --git a/lib/l10n/arb/app_localizations_pt.dart b/lib/l10n/arb/app_localizations_pt.dart index 85a710a83d..7db546fbf1 100644 --- a/lib/l10n/arb/app_localizations_pt.dart +++ b/lib/l10n/arb/app_localizations_pt.dart @@ -26175,6 +26175,10 @@ class AppLocalizationsPt extends AppLocalizations { @override String get tankPresets_builtInPresets => 'Presets Integrados'; + @override + String get tankPresets_builtInPresets_description => + 'Desative os presets que você não usa para ocultá-los nos seletores de cilindro. O preset padrão é sempre exibido.'; + @override String get tankPresets_currentDefault => 'Padrão atual'; @@ -26328,6 +26332,9 @@ class AppLocalizationsPt extends AppLocalizations { @override String get tankPresets_setAsDefault => 'Definir como padrão'; + @override + String get tankPresets_showInPickers => 'Mostrar nos seletores de cilindro'; + @override String get tankPresets_title => 'Presets de Cilindro'; diff --git a/lib/l10n/arb/app_localizations_zh.dart b/lib/l10n/arb/app_localizations_zh.dart index dd54ea19f3..bba130cd38 100644 --- a/lib/l10n/arb/app_localizations_zh.dart +++ b/lib/l10n/arb/app_localizations_zh.dart @@ -24804,6 +24804,10 @@ class AppLocalizationsZh extends AppLocalizations { @override String get tankPresets_builtInPresets => '内置预设'; + @override + String get tankPresets_builtInPresets_description => + '关闭不使用的预设,即可在气瓶选择列表中隐藏它们。默认预设始终显示。'; + @override String get tankPresets_currentDefault => '当前默认'; @@ -24951,6 +24955,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get tankPresets_setAsDefault => '设为默认'; + @override + String get tankPresets_showInPickers => '在气瓶选择列表中显示'; + @override String get tankPresets_title => '气瓶预设'; diff --git a/lib/l10n/arb/app_nl.arb b/lib/l10n/arb/app_nl.arb index 6b54afc80f..b06d60c533 100644 --- a/lib/l10n/arb/app_nl.arb +++ b/lib/l10n/arb/app_nl.arb @@ -6347,6 +6347,7 @@ "tags_title_manageTags": "Tags beheren", "tankPresets_addPreset": "Flesinstelling toevoegen", "tankPresets_builtInPresets": "Standaard instellingen", + "tankPresets_builtInPresets_description": "Schakel voorinstellingen uit die je niet gebruikt om ze te verbergen in de flessenkiezers. De standaardvoorinstelling wordt altijd getoond.", "tankPresets_currentDefault": "Huidige standaard", "tankPresets_customPresets": "Eigen instellingen", "tankPresets_defaultSettings": "Standaardfles", @@ -6395,6 +6396,7 @@ "tankPresets_new_title": "Nieuwe flesinstelling", "tankPresets_noPresets": "Geen flesinstellingen beschikbaar", "tankPresets_setAsDefault": "Als standaard instellen", + "tankPresets_showInPickers": "Tonen in flessenkiezers", "tankPresets_title": "Flesinstellingen", "tank_al100_description": "Aluminium 100 cu ft", "tank_al100_displayName": "AL100", diff --git a/lib/l10n/arb/app_pt.arb b/lib/l10n/arb/app_pt.arb index e97e5756ed..82fd618b49 100644 --- a/lib/l10n/arb/app_pt.arb +++ b/lib/l10n/arb/app_pt.arb @@ -6347,6 +6347,7 @@ "tags_title_manageTags": "Gerenciar Tags", "tankPresets_addPreset": "Adicionar preset de cilindro", "tankPresets_builtInPresets": "Presets Integrados", + "tankPresets_builtInPresets_description": "Desative os presets que você não usa para ocultá-los nos seletores de cilindro. O preset padrão é sempre exibido.", "tankPresets_currentDefault": "Padrão atual", "tankPresets_customPresets": "Presets Personalizados", "tankPresets_defaultSettings": "Cilindro padrão", @@ -6395,6 +6396,7 @@ "tankPresets_new_title": "Novo Preset de Cilindro", "tankPresets_noPresets": "Nenhum preset de cilindro disponível", "tankPresets_setAsDefault": "Definir como padrão", + "tankPresets_showInPickers": "Mostrar nos seletores de cilindro", "tankPresets_title": "Presets de Cilindro", "tank_al100_description": "Alumínio 100 cu ft", "tank_al100_displayName": "AL100", diff --git a/lib/l10n/arb/app_zh.arb b/lib/l10n/arb/app_zh.arb index efa6461cb4..b9ba2faa3a 100644 --- a/lib/l10n/arb/app_zh.arb +++ b/lib/l10n/arb/app_zh.arb @@ -6528,6 +6528,7 @@ "tankPresets_applyToImports": "同时应用到导入的潜水", "tankPresets_applyToImports_subtitle": "使用默认预设为导入的潜水填充缺失的气瓶数据", "tankPresets_builtInPresets": "内置预设", + "tankPresets_builtInPresets_description": "关闭不使用的预设,即可在气瓶选择列表中隐藏它们。默认预设始终显示。", "tankPresets_currentDefault": "当前默认", "tankPresets_customPresets": "自定义预设", "tankPresets_defaultSettings": "默认气瓶", @@ -6567,6 +6568,7 @@ "tankPresets_new_title": "新建气瓶预设", "tankPresets_noPresets": "无可用气瓶预设", "tankPresets_setAsDefault": "设为默认", + "tankPresets_showInPickers": "在气瓶选择列表中显示", "tankPresets_title": "气瓶预设", "tank_al100_description": "铝制 100 立方英尺", "tank_al100_displayName": "AL100", diff --git a/test/core/database/migration_v226_media_cloud_asset_id_test.dart b/test/core/database/migration_v226_media_cloud_asset_id_test.dart index bebab7e81e..db73094db3 100644 --- a/test/core/database/migration_v226_media_cloud_asset_id_test.dart +++ b/test/core/database/migration_v226_media_cloud_asset_id_test.dart @@ -45,13 +45,13 @@ void main() { }; } - test('v226 is the current schema version and is in the ladder', () { - // The newest rung owns the exact assertion; relax it to - // greaterThanOrEqualTo when the next one lands. - expect(AppDatabase.currentSchemaVersion, 226); + test('v226 is at or below the current schema version and in the ladder', () { + // Relaxed once v227 (hidden tank presets) landed on top; the newest + // rung owns the exact assertion. + expect(AppDatabase.currentSchemaVersion, greaterThanOrEqualTo(226)); expect(AppDatabase.migrationVersions, contains(226)); // Counted from 225 so it holds whether or not #1978's rung has landed. - expect(AppDatabase.migrationStepCount(225), 1); + expect(AppDatabase.migrationStepCount(225), greaterThanOrEqualTo(1)); }); test('the column is additive and did not move the sync floor', () { diff --git a/test/core/database/migration_v227_hidden_tank_presets_test.dart b/test/core/database/migration_v227_hidden_tank_presets_test.dart new file mode 100644 index 0000000000..805fcf3094 --- /dev/null +++ b/test/core/database/migration_v227_hidden_tank_presets_test.dart @@ -0,0 +1,78 @@ +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:submersion/core/database/database.dart'; + +void main() { + Future> diverSettingsColumns(AppDatabase db) async { + final cols = await db + .customSelect("PRAGMA table_info('diver_settings')") + .get(); + return cols.map((c) => c.read('name')).toSet(); + } + + NativeDatabase strandedAt(int? userVersion) => NativeDatabase.memory( + setup: (rawDb) { + if (userVersion != null) { + rawDb.execute('PRAGMA user_version = $userVersion'); + } + rawDb.execute(''' + CREATE TABLE diver_settings ( + id TEXT NOT NULL PRIMARY KEY, + created_at INTEGER, + updated_at INTEGER + ) + '''); + }, + ); + + test('v227 is the current schema version and is in the ladder', () { + // The newest rung owns the exact assertion; relax it to + // greaterThanOrEqualTo when the next one lands. + expect(AppDatabase.currentSchemaVersion, 227); + expect(AppDatabase.migrationVersions, contains(227)); + expect(AppDatabase.migrationStepCount(226), 1); + }); + + test('this rung is additive and did not move the sync floor', () { + // The floor is owned by the v224 media fact clocks. An older reader + // simply shows every built-in preset. + expect(AppDatabase.minimumCompatibleSchemaVersion, 224); + }); + + test('a fresh database has the column, nullable', () async { + final db = AppDatabase(NativeDatabase.memory()); + addTearDown(db.close); + + final cols = await db + .customSelect("PRAGMA table_info('diver_settings')") + .get(); + final byName = {for (final c in cols) c.read('name'): c}; + expect(byName, contains('hidden_tank_preset_ids')); + expect(byName['hidden_tank_preset_ids']!.read('notnull'), 0); + }); + + test('a v226 database upgrades and gains the column', () async { + final db = AppDatabase(strandedAt(226)); + addTearDown(db.close); + + expect(await diverSettingsColumns(db), contains('hidden_tank_preset_ids')); + final version = await db.customSelect('PRAGMA user_version').getSingle(); + expect(version.read('user_version'), AppDatabase.currentSchemaVersion); + }); + + test( + 'a database stranded without the column regains it via beforeOpen', + () async { + // Already at the current version, e.g. after a version collision on a + // parallel branch: no rung runs, only the backstop can add the column. + final db = AppDatabase(strandedAt(AppDatabase.currentSchemaVersion)); + addTearDown(db.close); + + expect( + await diverSettingsColumns(db), + contains('hidden_tank_preset_ids'), + ); + }, + ); +} diff --git a/test/features/dive_log/presentation/widgets/tank_editor_test.dart b/test/features/dive_log/presentation/widgets/tank_editor_test.dart index f5da28e85f..fe7daa1d3b 100644 --- a/test/features/dive_log/presentation/widgets/tank_editor_test.dart +++ b/test/features/dive_log/presentation/widgets/tank_editor_test.dart @@ -17,6 +17,68 @@ import '../../../../helpers/mock_providers.dart'; void main() { group('TankEditor', () { + testWidgets('a tank keeps showing a preset the diver has hidden (#2305)', ( + tester, + ) async { + SharedPreferences.setMockInitialValues({}); + final prefs = await SharedPreferences.getInstance(); + final builtInPresets = TankPresets.all + .map((p) => TankPresetEntity.fromBuiltIn(p)) + .toList(); + // What tankPresetsProvider serves once HP80 is hidden. + final visiblePresets = builtInPresets + .where((p) => p.name != 'hp80') + .toList(); + + const tank = DiveTank( + id: 'tank-hidden', + volume: 10.2, + workingPressure: 237.317, + gasMix: GasMix(o2: 21.0, he: 0.0), + presetName: 'hp80', + ); + + await tester.pumpWidget( + ProviderScope( + overrides: [ + sharedPreferencesProvider.overrideWithValue(prefs), + settingsProvider.overrideWith((ref) => MockSettingsNotifier()), + currentDiverIdProvider.overrideWith( + (ref) => MockCurrentDiverIdNotifier(), + ), + tankPresetListNotifierProvider.overrideWith( + (ref) => _MockTankPresetListNotifier(builtInPresets), + ), + tankPresetsProvider.overrideWith( + (ref) => Future.value(visiblePresets), + ), + ].cast(), + child: MaterialApp( + locale: const Locale('en'), + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: Scaffold( + body: SingleChildScrollView( + child: TankEditor( + tank: tank, + tankNumber: 1, + onChanged: (_) {}, + onRemove: () {}, + ), + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + final dropdown = tester.widget>( + find.byType(DropdownButton), + ); + expect(dropdown.value?.name, 'hp80'); + expect(find.text('HP80'), findsOneWidget); + }); + testWidgets('renders pressure values in metric (bar)', (tester) async { SharedPreferences.setMockInitialValues({}); final prefs = await SharedPreferences.getInstance(); diff --git a/test/features/settings/data/repositories/diver_settings_repository_hidden_tank_presets_test.dart b/test/features/settings/data/repositories/diver_settings_repository_hidden_tank_presets_test.dart new file mode 100644 index 0000000000..48a5e50d5c --- /dev/null +++ b/test/features/settings/data/repositories/diver_settings_repository_hidden_tank_presets_test.dart @@ -0,0 +1,120 @@ +import 'package:drift/drift.dart' show Variable; +import 'package:flutter_test/flutter_test.dart'; +import 'package:submersion/core/database/database.dart'; +import 'package:submersion/core/services/database_service.dart'; +import 'package:submersion/features/settings/data/repositories/diver_settings_repository.dart'; +import 'package:submersion/features/settings/presentation/providers/settings_providers.dart'; + +import '../../../../helpers/test_database.dart'; + +void main() { + group('AppSettings.hiddenTankPresetIds', () { + test('defaults to empty, so every built-in preset starts visible', () { + const settings = AppSettings(); + expect(settings.hiddenTankPresetIds, isEmpty); + }); + + test('copyWith carries the set', () { + const settings = AppSettings(); + final updated = settings.copyWith(hiddenTankPresetIds: const {'al80'}); + expect(updated.hiddenTankPresetIds, {'al80'}); + // Unrelated settings survive. + expect(updated.defaultTankPreset, settings.defaultTankPreset); + }); + }); + + group('DiverSettingsRepository hidden tank preset persistence', () { + late AppDatabase db; + late DiverSettingsRepository repository; + + setUp(() async { + db = await setUpTestDatabase(); + repository = DiverSettingsRepository(); + final now = DateTime.now().millisecondsSinceEpoch; + await db + .into(db.divers) + .insert( + DiversCompanion.insert( + id: 'd1', + name: 'Test Diver', + createdAt: now, + updatedAt: now, + ), + ); + }); + + tearDown(() { + DatabaseService.instance.resetForTesting(); + }); + + Future storeRaw(String? raw) => db.customUpdate( + 'UPDATE diver_settings SET hidden_tank_preset_ids = ? ' + "WHERE diver_id = 'd1'", + variables: [Variable(raw)], + ); + + test('new settings default to none hidden', () async { + await repository.createSettingsForDiver('d1'); + final loaded = await repository.getSettingsForDiver('d1'); + expect(loaded!.hiddenTankPresetIds, isEmpty); + }); + + test('creating settings with hidden presets stores them', () async { + await repository.createSettingsForDiver( + 'd1', + settings: const AppSettings(hiddenTankPresetIds: {'hp80', 'al40'}), + ); + final loaded = await repository.getSettingsForDiver('d1'); + expect(loaded!.hiddenTankPresetIds, {'hp80', 'al40'}); + }); + + test('round-trips a set with several presets', () async { + await repository.createSettingsForDiver('d1'); + await repository.updateSettingsForDiver( + 'd1', + const AppSettings(hiddenTankPresetIds: {'al80', 'lp85'}), + ); + final loaded = await repository.getSettingsForDiver('d1'); + expect(loaded!.hiddenTankPresetIds, {'al80', 'lp85'}); + }); + + test('clearing back to empty stores null', () async { + await repository.createSettingsForDiver('d1'); + await repository.updateSettingsForDiver( + 'd1', + const AppSettings(hiddenTankPresetIds: {'al80'}), + ); + await repository.updateSettingsForDiver('d1', const AppSettings()); + final loaded = await repository.getSettingsForDiver('d1'); + expect(loaded!.hiddenTankPresetIds, isEmpty); + final row = await db + .customSelect( + "SELECT hidden_tank_preset_ids FROM diver_settings " + "WHERE diver_id = 'd1'", + ) + .getSingle(); + expect(row.read('hidden_tank_preset_ids'), isNull); + }); + + test('malformed stored JSON degrades to empty, never throws', () async { + await repository.createSettingsForDiver('d1'); + await storeRaw('not json'); + final loaded = await repository.getSettingsForDiver('d1'); + expect(loaded!.hiddenTankPresetIds, isEmpty); + }); + + test('a non-list JSON value degrades to empty', () async { + await repository.createSettingsForDiver('d1'); + await storeRaw('{"al80": true}'); + final loaded = await repository.getSettingsForDiver('d1'); + expect(loaded!.hiddenTankPresetIds, isEmpty); + }); + + test('non-string entries are dropped, the rest survive', () async { + await repository.createSettingsForDiver('d1'); + await storeRaw('["al80", 42, null, "hp100"]'); + final loaded = await repository.getSettingsForDiver('d1'); + expect(loaded!.hiddenTankPresetIds, {'al80', 'hp100'}); + }); + }); +} diff --git a/test/features/settings/presentation/pages/settings_page_shared_data_test.dart b/test/features/settings/presentation/pages/settings_page_shared_data_test.dart index b805db7714..5528a9b5ba 100644 --- a/test/features/settings/presentation/pages/settings_page_shared_data_test.dart +++ b/test/features/settings/presentation/pages/settings_page_shared_data_test.dart @@ -205,6 +205,17 @@ class _MockSettingsNotifier extends StateNotifier state = state.copyWith(hiddenChamberIds: ids); } + @override + Future setTankPresetHidden(String presetName, bool hidden) async { + final ids = {...state.hiddenTankPresetIds}; + if (hidden) { + ids.add(presetName); + } else { + ids.remove(presetName); + } + state = state.copyWith(hiddenTankPresetIds: ids); + } + @override Future setEmergencyRegion(String? countryCode) async => state = countryCode == null diff --git a/test/features/settings/presentation/pages/settings_page_test.dart b/test/features/settings/presentation/pages/settings_page_test.dart index c67793d313..3c75e67a12 100644 --- a/test/features/settings/presentation/pages/settings_page_test.dart +++ b/test/features/settings/presentation/pages/settings_page_test.dart @@ -126,6 +126,17 @@ class _MockSettingsNotifier extends StateNotifier state = state.copyWith(hiddenChamberIds: ids); } + @override + Future setTankPresetHidden(String presetName, bool hidden) async { + final ids = {...state.hiddenTankPresetIds}; + if (hidden) { + ids.add(presetName); + } else { + ids.remove(presetName); + } + state = state.copyWith(hiddenTankPresetIds: ids); + } + @override Future setEmergencyRegion(String? countryCode) async => state = countryCode == null diff --git a/test/features/settings/presentation/providers/settings_notifier_real_test.dart b/test/features/settings/presentation/providers/settings_notifier_real_test.dart index d36a9b0338..53d0d55c62 100644 --- a/test/features/settings/presentation/providers/settings_notifier_real_test.dart +++ b/test/features/settings/presentation/providers/settings_notifier_real_test.dart @@ -139,6 +139,48 @@ void main() { expect(container.read(settingsProvider).hiddenChamberIds, isEmpty); }); + test('setTankPresetHidden toggles hidden tank presets', () async { + container.read(settingsProvider.notifier); + await waitForInit(); + + expect(container.read(settingsProvider).hiddenTankPresetIds, isEmpty); + await container + .read(settingsProvider.notifier) + .setTankPresetHidden('hp80', true); + expect(container.read(settingsProvider).hiddenTankPresetIds, {'hp80'}); + await container + .read(settingsProvider.notifier) + .setTankPresetHidden('hp80', false); + expect(container.read(settingsProvider).hiddenTankPresetIds, isEmpty); + }); + + test('setTankPresetHidden never hides the default preset', () async { + container.read(settingsProvider.notifier); + await waitForInit(); + + final notifier = container.read(settingsProvider.notifier); + await notifier.setDefaultTankPreset('steel12'); + await notifier.setTankPresetHidden('steel12', true); + expect(container.read(settingsProvider).hiddenTankPresetIds, isEmpty); + }); + + test('setDefaultTankPreset shows a hidden preset again', () async { + container.read(settingsProvider.notifier); + await waitForInit(); + + final notifier = container.read(settingsProvider.notifier); + await notifier.setTankPresetHidden('hp80', true); + await notifier.setTankPresetHidden('lp85', true); + await notifier.setDefaultTankPreset('hp80'); + final settings = container.read(settingsProvider); + expect(settings.defaultTankPreset, 'hp80'); + expect(settings.hiddenTankPresetIds, {'lp85'}); + + // Clearing the default leaves the hidden set alone. + await notifier.setDefaultTankPreset(null); + expect(container.read(settingsProvider).hiddenTankPresetIds, {'lp85'}); + }); + test('setHomeChipEnabled toggles hidden home chips', () async { container.read(settingsProvider.notifier); await waitForInit(); diff --git a/test/features/statistics/presentation/pages/records_page_test.dart b/test/features/statistics/presentation/pages/records_page_test.dart index 29ab6e0350..17399da835 100644 --- a/test/features/statistics/presentation/pages/records_page_test.dart +++ b/test/features/statistics/presentation/pages/records_page_test.dart @@ -95,6 +95,17 @@ class _MockSettingsNotifier extends StateNotifier state = state.copyWith(hiddenChamberIds: ids); } + @override + Future setTankPresetHidden(String presetName, bool hidden) async { + final ids = {...state.hiddenTankPresetIds}; + if (hidden) { + ids.add(presetName); + } else { + ids.remove(presetName); + } + state = state.copyWith(hiddenTankPresetIds: ids); + } + @override Future setEmergencyRegion(String? countryCode) async => state = countryCode == null diff --git a/test/features/tank_presets/domain/services/tank_preset_visibility_test.dart b/test/features/tank_presets/domain/services/tank_preset_visibility_test.dart new file mode 100644 index 0000000000..515a79acc7 --- /dev/null +++ b/test/features/tank_presets/domain/services/tank_preset_visibility_test.dart @@ -0,0 +1,111 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:submersion/core/constants/enums.dart'; +import 'package:submersion/core/constants/tank_presets.dart'; +import 'package:submersion/features/tank_presets/domain/entities/tank_preset_entity.dart'; +import 'package:submersion/features/tank_presets/domain/services/tank_preset_visibility.dart'; + +TankPresetEntity _custom(String name) => TankPresetEntity( + id: name, + diverId: 'd1', + name: name, + displayName: name, + volumeLiters: 12, + workingPressureBar: 232, + material: TankMaterial.steel, + createdAt: DateTime(2026), + updatedAt: DateTime(2026), +); + +List _all({List custom = const []}) => [ + ...custom, + ...TankPresets.all.map(TankPresetEntity.fromBuiltIn), +]; + +List _names(List presets) => + presets.map((p) => p.name).toList(); + +void main() { + group('visibleTankPresets', () { + test('returns the list unchanged when nothing is hidden', () { + final all = _all(custom: [_custom('mine')]); + expect(visibleTankPresets(all, const {}), same(all)); + }); + + test('drops hidden built-in presets and keeps the order', () { + final visible = visibleTankPresets(_all(), const {'al80', 'hp100'}); + expect(_names(visible), isNot(contains('al80'))); + expect(_names(visible), isNot(contains('hp100'))); + expect( + _names(visible), + TankPresets.all + .map((p) => p.name) + .where((n) => n != 'al80' && n != 'hp100') + .toList(), + ); + }); + + test('never hides a custom preset that shares a built-in slug', () { + final all = _all(custom: [_custom('al80')]); + final visible = visibleTankPresets(all, const {'al80'}); + expect(visible.where((p) => p.name == 'al80').single.isBuiltIn, isFalse); + }); + + test('always keeps the default preset, even if marked hidden', () { + final visible = visibleTankPresets(_all(), const { + 'al80', + 'steel12', + }, defaultPresetName: 'steel12'); + expect(_names(visible), contains('steel12')); + expect(_names(visible), isNot(contains('al80'))); + }); + + test('ignores unknown names in the hidden set', () { + final all = _all(); + expect(visibleTankPresets(all, const {'no-such-preset'}), hasLength(13)); + }); + + test('can hide every built-in preset', () { + final everything = TankPresets.all.map((p) => p.name).toSet(); + expect(visibleTankPresets(_all(), everything), isEmpty); + }); + }); + + group('withKeptTankPresets', () { + test('returns the list unchanged when every kept name is present', () { + final visible = _all(); + expect(withKeptTankPresets(visible, const ['al80', null]), same(visible)); + }); + + test('restores a hidden built-in preset at its catalog position', () { + final visible = visibleTankPresets(_all(), const {'al80'}); + final kept = withKeptTankPresets(visible, const ['al80']); + expect(_names(kept), TankPresets.all.map((p) => p.name).toList()); + }); + + test('keeps custom presets first', () { + final visible = visibleTankPresets( + _all(custom: [_custom('mine')]), + const {'hp80'}, + ); + final kept = withKeptTankPresets(visible, const ['hp80']); + expect(kept.first.name, 'mine'); + expect(_names(kept), contains('hp80')); + }); + + test('reuses the existing instances of the visible presets', () { + final visible = visibleTankPresets(_all(), const {'al80'}); + final kept = withKeptTankPresets(visible, const ['al80']); + final al100 = visible.firstWhere((p) => p.name == 'al100'); + expect(kept.firstWhere((p) => p.name == 'al100'), same(al100)); + }); + + test('ignores null and names that are not built-in presets', () { + final visible = visibleTankPresets(_all(), const {'al80'}); + expect( + withKeptTankPresets(visible, const [null, 'unknown']), + same(visible), + ); + }); + }); +} diff --git a/test/features/tank_presets/presentation/pages/tank_presets_page_test.dart b/test/features/tank_presets/presentation/pages/tank_presets_page_test.dart index 28a2be7b67..beff8ecf62 100644 --- a/test/features/tank_presets/presentation/pages/tank_presets_page_test.dart +++ b/test/features/tank_presets/presentation/pages/tank_presets_page_test.dart @@ -186,6 +186,142 @@ void main() { expect(find.text('Steel 12L'), findsOneWidget); }); }); + + group('TankPresetsPage hiding built-in presets (issue #2305)', () { + late MockSettingsNotifier mockSettings; + + Future pumpPage( + WidgetTester tester, { + List? presets, + }) async { + // Tall enough that every built-in tile is built at once. + tester.view.physicalSize = const Size(1000, 3000); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.reset); + + SharedPreferences.setMockInitialValues({}); + final prefs = await SharedPreferences.getInstance(); + mockSettings = MockSettingsNotifier(); + final router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (context, state) => const TankPresetsPage(), + ), + ], + ); + + await tester.pumpWidget( + ProviderScope( + overrides: [ + sharedPreferencesProvider.overrideWithValue(prefs), + settingsProvider.overrideWith((ref) => mockSettings), + currentDiverIdProvider.overrideWith( + (ref) => MockCurrentDiverIdNotifier(), + ), + tankPresetListNotifierProvider.overrideWith( + (ref) => _MockTankPresetListNotifier( + presets ?? + TankPresets.all.map(TankPresetEntity.fromBuiltIn).toList(), + ), + ), + ].cast(), + child: MaterialApp.router( + locale: const Locale('en'), + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + routerConfig: router, + ), + ), + ); + await tester.pumpAndSettle(); + } + + Finder switchFor(String name) => + find.byKey(ValueKey('tank-preset-visible-$name')); + + testWidgets('every built-in preset but the default has a switch', ( + tester, + ) async { + await pumpPage(tester); + + // AL80 is the default out of the box. + expect(mockSettings.state.defaultTankPreset, 'al80'); + expect(switchFor('al80').hitTestable(), findsNothing); + for (final preset in TankPresets.all.where((p) => p.name != 'al80')) { + expect(switchFor(preset.name).hitTestable(), findsOneWidget); + expect(tester.widget(switchFor(preset.name)).value, isTrue); + } + }); + + testWidgets('a custom preset has no switch', (tester) async { + final now = DateTime(2026); + await pumpPage( + tester, + presets: [ + TankPresetEntity( + id: 'custom-1', + name: 'mine', + displayName: 'My Tank', + volumeLiters: 12, + workingPressureBar: 232, + material: TankMaterial.steel, + createdAt: now, + updatedAt: now, + ), + ], + ); + + expect(find.text('My Tank'), findsOneWidget); + expect(find.byType(Switch), findsOneWidget); // applyToImports only + }); + + testWidgets('switching a preset off hides it and on shows it again', ( + tester, + ) async { + await pumpPage(tester); + + await tester.tap(switchFor('hp80')); + await tester.pumpAndSettle(); + expect(mockSettings.state.hiddenTankPresetIds, {'hp80'}); + expect(tester.widget(switchFor('hp80')).value, isFalse); + final tile = tester.widget( + find.ancestor(of: find.text('HP80'), matching: find.byType(ListTile)), + ); + expect(tile.enabled, isFalse); + + await tester.tap(switchFor('hp80')); + await tester.pumpAndSettle(); + expect(mockSettings.state.hiddenTankPresetIds, isEmpty); + expect(tester.widget(switchFor('hp80')).value, isTrue); + }); + + testWidgets('starring a hidden preset makes it the visible default', ( + tester, + ) async { + await pumpPage(tester); + await tester.tap(switchFor('lp85')); + await tester.pumpAndSettle(); + expect(mockSettings.state.hiddenTankPresetIds, {'lp85'}); + + final lp85Tile = find.ancestor( + of: find.text('LP85'), + matching: find.byType(ListTile), + ); + await tester.tap( + find.descendant( + of: lp85Tile, + matching: find.byIcon(Icons.star_outline), + ), + ); + await tester.pumpAndSettle(); + + expect(mockSettings.state.defaultTankPreset, 'lp85'); + expect(mockSettings.state.hiddenTankPresetIds, isEmpty); + expect(switchFor('lp85').hitTestable(), findsNothing); + expect(switchFor('al80').hitTestable(), findsOneWidget); + }); + }); } /// Simple mock that directly holds preset data in state diff --git a/test/features/tank_presets/presentation/providers/tank_preset_providers_test.dart b/test/features/tank_presets/presentation/providers/tank_preset_providers_test.dart index ee280c87bd..6afc502aed 100644 --- a/test/features/tank_presets/presentation/providers/tank_preset_providers_test.dart +++ b/test/features/tank_presets/presentation/providers/tank_preset_providers_test.dart @@ -11,6 +11,7 @@ import 'package:submersion/features/tank_presets/data/repositories/tank_preset_r import 'package:submersion/features/tank_presets/domain/entities/tank_preset_entity.dart'; import 'package:submersion/features/tank_presets/presentation/providers/tank_preset_providers.dart'; +import '../../../../helpers/mock_providers.dart'; import '../../../../helpers/test_database.dart'; Diver _makeDiver({String name = 'Default', bool isDefault = true}) { @@ -123,6 +124,74 @@ void main() { ); }); + group('hidden built-in presets (issue #2305)', () { + late MockSettingsNotifier settings; + + ProviderContainer makeHidingContainer() { + settings = MockSettingsNotifier(); + return ProviderContainer( + overrides: [ + sharedPreferencesProvider.overrideWithValue(prefs), + settingsProvider.overrideWith((ref) => settings), + ], + ); + } + + test('tankPresetsProvider leaves hidden built-in presets out', () async { + await seedCurrentDiver(); + final container = makeHidingContainer(); + addTearDown(container.dispose); + await settings.setTankPresetHidden('hp80', true); + + final names = (await container.read( + tankPresetsProvider.future, + )).map((p) => p.name); + expect(names, isNot(contains('hp80'))); + expect(names, contains('al80')); + }); + + test('tankPresetsProvider follows a change to the hidden set', () async { + await seedCurrentDiver(); + final container = makeHidingContainer(); + addTearDown(container.dispose); + final sub = container.listen(tankPresetsProvider, (_, _) {}); + addTearDown(sub.close); + + expect( + (await container.read(tankPresetsProvider.future)).map((p) => p.name), + contains('lp85'), + ); + await settings.setTankPresetHidden('lp85', true); + expect( + (await container.read(tankPresetsProvider.future)).map((p) => p.name), + isNot(contains('lp85')), + ); + await settings.setTankPresetHidden('lp85', false); + expect( + (await container.read(tankPresetsProvider.future)).map((p) => p.name), + contains('lp85'), + ); + }); + + test('the settings page list keeps every preset', () async { + await seedCurrentDiver(); + final container = makeHidingContainer(); + addTearDown(container.dispose); + await settings.setTankPresetHidden('hp80', true); + final sub = container.listen(tankPresetListNotifierProvider, (_, _) {}); + addTearDown(sub.close); + + while (container.read(tankPresetListNotifierProvider).isLoading) { + await Future.delayed(Duration.zero); + } + final names = container + .read(tankPresetListNotifierProvider) + .value! + .map((p) => p.name); + expect(names, contains('hp80')); + }); + }); + group('tankPresetListNotifierProvider ' '(TankPresetListNotifier._silentReloadPresets)', () { test('silently reloads the list when a preset is written directly to the ' diff --git a/test/helpers/mock_providers.dart b/test/helpers/mock_providers.dart index 5451711425..c111e172ac 100644 --- a/test/helpers/mock_providers.dart +++ b/test/helpers/mock_providers.dart @@ -159,7 +159,22 @@ class MockSettingsNotifier extends StateNotifier state = state.copyWith( defaultTankPreset: presetName, clearDefaultTankPreset: presetName == null, + hiddenTankPresetIds: presetName == null + ? null + : ({...state.hiddenTankPresetIds}..remove(presetName)), ); + @override + Future setTankPresetHidden(String presetName, bool hidden) async { + if (hidden && presetName == state.defaultTankPreset) return; + final ids = {...state.hiddenTankPresetIds}; + if (hidden) { + ids.add(presetName); + } else { + ids.remove(presetName); + } + state = state.copyWith(hiddenTankPresetIds: ids); + } + @override Future setApplyDefaultTankToImports(bool value) async => state = state.copyWith(applyDefaultTankToImports: value); From 115d764be17f906c63ddc3666c8f945715525d20 Mon Sep 17 00:00:00 2001 From: alpheios-one <275321969+alpheios-one@users.noreply.github.com> Date: Wed, 23 Sep 2026 23:36:13 +0200 Subject: [PATCH 2/5] fix(tank-presets): address review on hidden tank presets - Weight planner: keep a hidden preset only in the dropdown of the tank that uses it, instead of offering it to every tank in the rig. - tankPresetsProvider selects the hidden set by content, so a reload that decodes an equal set does not re-run it. - setDefaultTankPreset only rewrites the hidden set when the new default was actually hidden. - A hidden preset's tile is dimmed rather than disabled, since its star and switch stay usable (and are announced as such). - Drop the unreachable null branch of the switch's onChanged. Refs #2305 --- .../providers/settings_providers.dart | 6 +++--- .../presentation/pages/tank_presets_page.dart | 15 +++++++-------- .../providers/tank_preset_providers.dart | 14 +++++++++++--- .../presentation/widgets/rig_composer.dart | 13 +++++++------ .../pages/tank_presets_page_test.dart | 4 +++- test/helpers/mock_providers.dart | 6 +++--- 6 files changed, 34 insertions(+), 24 deletions(-) diff --git a/lib/features/settings/presentation/providers/settings_providers.dart b/lib/features/settings/presentation/providers/settings_providers.dart index 12adbfd222..b6eeb977c9 100644 --- a/lib/features/settings/presentation/providers/settings_providers.dart +++ b/lib/features/settings/presentation/providers/settings_providers.dart @@ -1544,9 +1544,9 @@ class SettingsNotifier extends StateNotifier { state = state.copyWith( defaultTankPreset: presetName, clearDefaultTankPreset: presetName == null, - hiddenTankPresetIds: presetName == null - ? null - : ({...state.hiddenTankPresetIds}..remove(presetName)), + hiddenTankPresetIds: state.hiddenTankPresetIds.contains(presetName) + ? ({...state.hiddenTankPresetIds}..remove(presetName)) + : null, ); await _saveSettings(); } diff --git a/lib/features/tank_presets/presentation/pages/tank_presets_page.dart b/lib/features/tank_presets/presentation/pages/tank_presets_page.dart index 9ea97dfa33..7ed22d6099 100644 --- a/lib/features/tank_presets/presentation/pages/tank_presets_page.dart +++ b/lib/features/tank_presets/presentation/pages/tank_presets_page.dart @@ -159,9 +159,10 @@ class TankPresetsPage extends ConsumerWidget { ); return ListTile( - // A hidden built-in preset stays listed so it can be shown again, but - // reads as inactive. The trailing controls stay usable. - enabled: !isHidden, + // A hidden built-in preset stays listed so it can be shown again, and + // only its text and icon are dimmed: the tile is not disabled, since + // its star and switch stay usable. + textColor: isHidden ? Theme.of(context).disabledColor : null, leading: Icon( MdiIcons.divingScubaTank, color: isHidden @@ -214,11 +215,9 @@ class TankPresetsPage extends ConsumerWidget { child: Switch( key: ValueKey('tank-preset-visible-${preset.name}'), value: !isHidden, - onChanged: isDefault - ? null - : (visible) => ref - .read(settingsProvider.notifier) - .setTankPresetHidden(preset.name, !visible), + onChanged: (visible) => ref + .read(settingsProvider.notifier) + .setTankPresetHidden(preset.name, !visible), ), ), ), diff --git a/lib/features/tank_presets/presentation/providers/tank_preset_providers.dart b/lib/features/tank_presets/presentation/providers/tank_preset_providers.dart index ac75146c90..95a4a965d0 100644 --- a/lib/features/tank_presets/presentation/providers/tank_preset_providers.dart +++ b/lib/features/tank_presets/presentation/providers/tank_preset_providers.dart @@ -25,9 +25,17 @@ final tankPresetRepositoryProvider = Provider((ref) { /// a cached one-shot snapshot. final tankPresetsProvider = FutureProvider>((ref) async { final repository = ref.watch(tankPresetRepositoryProvider); - final hidden = ref.watch( - settingsProvider.select((s) => s.hiddenTankPresetIds), - ); + // Selected by content, not by the Set instance: every settings reload + // decodes a fresh Set, which must not re-run this provider on its own. + final hidden = ref + .watch( + settingsProvider.select( + (s) => (s.hiddenTankPresetIds.toList()..sort()).join('\n'), + ), + ) + .split('\n') + .where((name) => name.isNotEmpty) + .toSet(); final defaultPresetName = ref.watch( settingsProvider.select((s) => s.defaultTankPreset), ); diff --git a/lib/features/weight_planner/presentation/widgets/rig_composer.dart b/lib/features/weight_planner/presentation/widgets/rig_composer.dart index 30c55441b8..c847653cf3 100644 --- a/lib/features/weight_planner/presentation/widgets/rig_composer.dart +++ b/lib/features/weight_planner/presentation/widgets/rig_composer.dart @@ -123,11 +123,6 @@ class RigComposer extends ConsumerWidget { final theme = Theme.of(context); final visiblePresets = ref.watch(tankPresetsProvider).valueOrNull ?? const []; - // A tank already in the rig keeps its preset in the dropdown even after - // the diver hides it (issue #2305); adding a tank offers visible ones. - final presets = withKeptTankPresets(visiblePresets, [ - for (final tank in tanks) tank.name, - ]); return Card( child: Padding( @@ -213,7 +208,13 @@ class RigComposer extends ConsumerWidget { ), ], ), - for (var i = 0; i < tanks.length; i++) + // Each tank keeps its own preset in its dropdown even after the + // diver hides it (issue #2305), without offering it to the + // other tanks; adding a tank offers visible presets only. + for (final (i, presets) in [ + for (var i = 0; i < tanks.length; i++) + (i, withKeptTankPresets(visiblePresets, [tanks[i].name])), + ]) Row( children: [ Expanded( diff --git a/test/features/tank_presets/presentation/pages/tank_presets_page_test.dart b/test/features/tank_presets/presentation/pages/tank_presets_page_test.dart index beff8ecf62..425602acc7 100644 --- a/test/features/tank_presets/presentation/pages/tank_presets_page_test.dart +++ b/test/features/tank_presets/presentation/pages/tank_presets_page_test.dart @@ -288,7 +288,9 @@ void main() { final tile = tester.widget( find.ancestor(of: find.text('HP80'), matching: find.byType(ListTile)), ); - expect(tile.enabled, isFalse); + // Dimmed, but not disabled: its star and switch stay usable. + expect(tile.enabled, isTrue); + expect(tile.textColor, isNotNull); await tester.tap(switchFor('hp80')); await tester.pumpAndSettle(); diff --git a/test/helpers/mock_providers.dart b/test/helpers/mock_providers.dart index c111e172ac..6c067ee308 100644 --- a/test/helpers/mock_providers.dart +++ b/test/helpers/mock_providers.dart @@ -159,9 +159,9 @@ class MockSettingsNotifier extends StateNotifier state = state.copyWith( defaultTankPreset: presetName, clearDefaultTankPreset: presetName == null, - hiddenTankPresetIds: presetName == null - ? null - : ({...state.hiddenTankPresetIds}..remove(presetName)), + hiddenTankPresetIds: state.hiddenTankPresetIds.contains(presetName) + ? ({...state.hiddenTankPresetIds}..remove(presetName)) + : null, ); @override Future setTankPresetHidden(String presetName, bool hidden) async { From 0e3d95b3f18e5f1d5f1dcc4010d2a1eba9c27d3e Mon Sep 17 00:00:00 2001 From: alpheios-one <275321969+alpheios-one@users.noreply.github.com> Date: Wed, 23 Sep 2026 23:57:17 +0200 Subject: [PATCH 3/5] fix(tank-presets): address second review on hidden tank presets - setDefaultTankPreset also drops a stale hidden entry for the outgoing default, so a synced row that hid the default cannot make it vanish the moment the diver picks another default. - tankPresetsProvider selects the effectively hidden set (hidden minus the default) in one content-compared key, so starring a visible preset does not re-query the presets. - The tank editor and transmitter preset dropdowns keep their current value on reload instead of flashing a progress bar. - Test doubles guard the default preset like the real notifier. Refs #2305 --- .../presentation/widgets/tank_editor.dart | 3 ++ .../providers/settings_providers.dart | 16 +++++++-- .../providers/tank_preset_providers.dart | 26 ++++++++------ .../pages/transmitter_edit_page.dart | 1 + .../pages/settings_page_shared_data_test.dart | 1 + .../pages/settings_page_test.dart | 1 + .../presentation/pages/records_page_test.dart | 1 + .../providers/tank_preset_providers_test.dart | 35 +++++++++++++++++++ test/helpers/mock_providers.dart | 7 ++-- 9 files changed, 74 insertions(+), 17 deletions(-) diff --git a/lib/features/dive_log/presentation/widgets/tank_editor.dart b/lib/features/dive_log/presentation/widgets/tank_editor.dart index 138e098f30..2f6240e478 100644 --- a/lib/features/dive_log/presentation/widgets/tank_editor.dart +++ b/lib/features/dive_log/presentation/widgets/tank_editor.dart @@ -473,6 +473,9 @@ class _TankEditorState extends ConsumerState { // Tank preset dropdown Expanded( child: presetsAsync.when( + // A reload (a synced settings change, a preset edit) keeps the + // dropdown in place instead of swapping it for a progress bar. + skipLoadingOnReload: true, loading: () => const LinearProgressIndicator(), error: (e, st) => Text('Error: $e'), data: (visiblePresets) { diff --git a/lib/features/settings/presentation/providers/settings_providers.dart b/lib/features/settings/presentation/providers/settings_providers.dart index b6eeb977c9..a0fc83586d 100644 --- a/lib/features/settings/presentation/providers/settings_providers.dart +++ b/lib/features/settings/presentation/providers/settings_providers.dart @@ -1539,13 +1539,23 @@ class SettingsNotifier extends StateNotifier { } /// Also shows [presetName] again if it was hidden: the default preset is - /// always offered in the pickers (issue #2305). + /// always offered in the pickers (issue #2305). The outgoing default is + /// dropped from the hidden set too, since a stale entry for it (a synced + /// row can carry one) would otherwise hide it the moment it stops being + /// the default, without the diver ever having switched it off. Future setDefaultTankPreset(String? presetName) async { + final hidden = state.hiddenTankPresetIds; + final previous = state.defaultTankPreset; + final touchesHidden = + hidden.contains(presetName) || hidden.contains(previous); state = state.copyWith( defaultTankPreset: presetName, clearDefaultTankPreset: presetName == null, - hiddenTankPresetIds: state.hiddenTankPresetIds.contains(presetName) - ? ({...state.hiddenTankPresetIds}..remove(presetName)) + hiddenTankPresetIds: touchesHidden + ? { + for (final name in hidden) + if (name != presetName && name != previous) name, + } : null, ); await _saveSettings(); diff --git a/lib/features/tank_presets/presentation/providers/tank_preset_providers.dart b/lib/features/tank_presets/presentation/providers/tank_preset_providers.dart index 95a4a965d0..bfe84efa8e 100644 --- a/lib/features/tank_presets/presentation/providers/tank_preset_providers.dart +++ b/lib/features/tank_presets/presentation/providers/tank_preset_providers.dart @@ -25,28 +25,32 @@ final tankPresetRepositoryProvider = Provider((ref) { /// a cached one-shot snapshot. final tankPresetsProvider = FutureProvider>((ref) async { final repository = ref.watch(tankPresetRepositoryProvider); - // Selected by content, not by the Set instance: every settings reload - // decodes a fresh Set, which must not re-run this provider on its own. + // Selects the presets that are effectively hidden (the default is always + // offered), by content rather than by Set instance: a settings reload + // decodes a fresh Set, and starring a preset that was never hidden must + // not re-run this provider either. final hidden = ref - .watch( - settingsProvider.select( - (s) => (s.hiddenTankPresetIds.toList()..sort()).join('\n'), - ), - ) + .watch(settingsProvider.select(_effectivelyHiddenKey)) .split('\n') .where((name) => name.isNotEmpty) .toSet(); - final defaultPresetName = ref.watch( - settingsProvider.select((s) => s.defaultTankPreset), - ); final validatedDiverId = await ref.watch( validatedCurrentDiverIdProvider.future, ); ref.invalidateSelfWhen(repository.watchTankPresetsChanges()); final all = await repository.getAllPresets(diverId: validatedDiverId); - return visibleTankPresets(all, hidden, defaultPresetName: defaultPresetName); + return visibleTankPresets(all, hidden); }); +/// The hidden built-in preset slugs minus the default preset, sorted and +/// newline-joined so equal contents compare equal. +String _effectivelyHiddenKey(AppSettings settings) => + (settings.hiddenTankPresetIds + .where((name) => name != settings.defaultTankPreset) + .toList() + ..sort()) + .join('\n'); + /// Custom (user-defined) tank presets only for the current diver final customTankPresetsProvider = FutureProvider>(( ref, diff --git a/lib/features/transmitters/presentation/pages/transmitter_edit_page.dart b/lib/features/transmitters/presentation/pages/transmitter_edit_page.dart index ed51403f59..83ad29a013 100644 --- a/lib/features/transmitters/presentation/pages/transmitter_edit_page.dart +++ b/lib/features/transmitters/presentation/pages/transmitter_edit_page.dart @@ -509,6 +509,7 @@ class _TransmitterEditPageState extends ConsumerState { ), const SizedBox(height: 8), presets.when( + skipLoadingOnReload: true, loading: () => const LinearProgressIndicator(), error: (e, st) => Text('${l10n.common_label_error}: $e'), data: (visibleList) { diff --git a/test/features/settings/presentation/pages/settings_page_shared_data_test.dart b/test/features/settings/presentation/pages/settings_page_shared_data_test.dart index 5528a9b5ba..292124abfb 100644 --- a/test/features/settings/presentation/pages/settings_page_shared_data_test.dart +++ b/test/features/settings/presentation/pages/settings_page_shared_data_test.dart @@ -207,6 +207,7 @@ class _MockSettingsNotifier extends StateNotifier @override Future setTankPresetHidden(String presetName, bool hidden) async { + if (hidden && presetName == state.defaultTankPreset) return; final ids = {...state.hiddenTankPresetIds}; if (hidden) { ids.add(presetName); diff --git a/test/features/settings/presentation/pages/settings_page_test.dart b/test/features/settings/presentation/pages/settings_page_test.dart index 3c75e67a12..54f8569fde 100644 --- a/test/features/settings/presentation/pages/settings_page_test.dart +++ b/test/features/settings/presentation/pages/settings_page_test.dart @@ -128,6 +128,7 @@ class _MockSettingsNotifier extends StateNotifier @override Future setTankPresetHidden(String presetName, bool hidden) async { + if (hidden && presetName == state.defaultTankPreset) return; final ids = {...state.hiddenTankPresetIds}; if (hidden) { ids.add(presetName); diff --git a/test/features/statistics/presentation/pages/records_page_test.dart b/test/features/statistics/presentation/pages/records_page_test.dart index 17399da835..f8f9f3c54f 100644 --- a/test/features/statistics/presentation/pages/records_page_test.dart +++ b/test/features/statistics/presentation/pages/records_page_test.dart @@ -97,6 +97,7 @@ class _MockSettingsNotifier extends StateNotifier @override Future setTankPresetHidden(String presetName, bool hidden) async { + if (hidden && presetName == state.defaultTankPreset) return; final ids = {...state.hiddenTankPresetIds}; if (hidden) { ids.add(presetName); diff --git a/test/features/tank_presets/presentation/providers/tank_preset_providers_test.dart b/test/features/tank_presets/presentation/providers/tank_preset_providers_test.dart index 6afc502aed..a033617c25 100644 --- a/test/features/tank_presets/presentation/providers/tank_preset_providers_test.dart +++ b/test/features/tank_presets/presentation/providers/tank_preset_providers_test.dart @@ -6,6 +6,7 @@ import 'package:submersion/core/providers/provider.dart'; import 'package:submersion/features/divers/data/repositories/diver_repository.dart'; import 'package:submersion/features/divers/domain/entities/diver.dart'; import 'package:submersion/features/divers/presentation/providers/diver_providers.dart'; +import 'package:submersion/features/settings/data/repositories/diver_settings_repository.dart'; import 'package:submersion/features/settings/presentation/providers/settings_providers.dart'; import 'package:submersion/features/tank_presets/data/repositories/tank_preset_repository.dart'; import 'package:submersion/features/tank_presets/domain/entities/tank_preset_entity.dart'; @@ -173,6 +174,40 @@ void main() { ); }); + test('a synced row that hides the default: the default stays offered, ' + 'and moving the default does not hide it', () async { + // The notifier never writes the default into the hidden set, but a + // synced diver_settings row can carry one. + final diverId = await seedCurrentDiver(); + await DiverSettingsRepository().getOrCreateSettingsForDiver(diverId); + await DiverSettingsRepository().updateSettingsForDiver( + diverId, + const AppSettings( + defaultTankPreset: 'hp100', + hiddenTankPresetIds: {'hp100', 'lp85'}, + ), + ); + final container = makeContainer(); + addTearDown(container.dispose); + final sub = container.listen(tankPresetsProvider, (_, _) {}); + addTearDown(sub.close); + final notifier = container.read(settingsProvider.notifier); + await notifier.initialLoad; + + var names = (await container.read( + tankPresetsProvider.future, + )).map((p) => p.name); + expect(names, contains('hp100')); + expect(names, isNot(contains('lp85'))); + + await notifier.setDefaultTankPreset('al80'); + expect(container.read(settingsProvider).hiddenTankPresetIds, {'lp85'}); + names = (await container.read( + tankPresetsProvider.future, + )).map((p) => p.name); + expect(names, contains('hp100')); + }); + test('the settings page list keeps every preset', () async { await seedCurrentDiver(); final container = makeHidingContainer(); diff --git a/test/helpers/mock_providers.dart b/test/helpers/mock_providers.dart index 6c067ee308..1de637b5c5 100644 --- a/test/helpers/mock_providers.dart +++ b/test/helpers/mock_providers.dart @@ -159,9 +159,10 @@ class MockSettingsNotifier extends StateNotifier state = state.copyWith( defaultTankPreset: presetName, clearDefaultTankPreset: presetName == null, - hiddenTankPresetIds: state.hiddenTankPresetIds.contains(presetName) - ? ({...state.hiddenTankPresetIds}..remove(presetName)) - : null, + hiddenTankPresetIds: { + for (final name in state.hiddenTankPresetIds) + if (name != presetName && name != state.defaultTankPreset) name, + }, ); @override Future setTankPresetHidden(String presetName, bool hidden) async { From 351ad837eb531f0a91d4899fe096aa39e437c297 Mon Sep 17 00:00:00 2001 From: alpheios-one <275321969+alpheios-one@users.noreply.github.com> Date: Fri, 25 Sep 2026 06:31:56 +0200 Subject: [PATCH 4/5] fix(weight-planner): keep the tank presets while hiding one reloads them tankPresetsProvider watches the hidden-preset setting, so hiding or showing a preset reloads it through a dependency. valueOrNull drops the previous list during that reload, which briefly disabled Add tank and emptied the rig's tank dropdowns; value keeps it until the filtered list lands. Refs #2305 --- .../presentation/widgets/rig_composer.dart | 6 +- .../rig_composer_reload_test.dart | 101 ++++++++++++++++++ 2 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 test/features/weight_planner/presentation/rig_composer_reload_test.dart diff --git a/lib/features/weight_planner/presentation/widgets/rig_composer.dart b/lib/features/weight_planner/presentation/widgets/rig_composer.dart index c847653cf3..7b6163cfa6 100644 --- a/lib/features/weight_planner/presentation/widgets/rig_composer.dart +++ b/lib/features/weight_planner/presentation/widgets/rig_composer.dart @@ -121,8 +121,10 @@ class RigComposer extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); - final visiblePresets = - ref.watch(tankPresetsProvider).valueOrNull ?? const []; + // `value`, not `valueOrNull`: hiding a preset reloads tankPresetsProvider + // through its settings dependency, and only `value` keeps the previous + // list until the filtered one lands (see async_value_reload_test.dart). + final visiblePresets = ref.watch(tankPresetsProvider).value ?? const []; return Card( child: Padding( diff --git a/test/features/weight_planner/presentation/rig_composer_reload_test.dart b/test/features/weight_planner/presentation/rig_composer_reload_test.dart new file mode 100644 index 0000000000..ee97facd1f --- /dev/null +++ b/test/features/weight_planner/presentation/rig_composer_reload_test.dart @@ -0,0 +1,101 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:submersion/core/constants/enums.dart'; +import 'package:submersion/core/constants/tank_presets.dart'; +import 'package:submersion/core/providers/provider.dart'; +import 'package:submersion/core/utils/unit_formatter.dart'; +import 'package:submersion/features/settings/presentation/providers/settings_providers.dart'; +import 'package:submersion/features/tank_presets/domain/entities/tank_preset_entity.dart'; +import 'package:submersion/features/tank_presets/presentation/providers/tank_preset_providers.dart'; +import 'package:submersion/features/weight_planner/presentation/widgets/rig_composer.dart'; +import 'package:submersion/l10n/arb/app_localizations.dart'; + +/// Stands in for the settings change (a preset hidden or shown) that +/// tankPresetsProvider watches since issue #2305. +final _reloadTrigger = StateProvider((ref) => 0); + +void main() { + testWidgets('keeps the presets while a settings change reloads them', ( + tester, + ) async { + final presets = TankPresets.all.map(TankPresetEntity.fromBuiltIn).toList(); + var next = Completer>()..complete(presets); + final controllers = List.generate(4, (_) => TextEditingController()); + addTearDown(() { + for (final c in controllers) { + c.dispose(); + } + }); + + final container = ProviderContainer( + overrides: [ + tankPresetsProvider.overrideWith((ref) { + ref.watch(_reloadTrigger); + return next.future; + }), + ], + ); + addTearDown(container.dispose); + + await tester.pumpWidget( + UncontrolledProviderScope( + container: container, + child: MaterialApp( + locale: const Locale('en'), + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: Scaffold( + body: SingleChildScrollView( + child: RigComposer( + gear: const [], + tanks: [presets.firstWhere((p) => p.name == 'al80')], + waterType: WaterType.salt, + bodyWeightController: controllers[0], + heightCmController: controllers[1], + heightFeetController: controllers[2], + heightInchesController: controllers[3], + units: UnitFormatter(const AppSettings()), + showSaveBodyWeight: false, + onGearAdded: (_) {}, + onGearSetAdded: (_, _) {}, + onGearRemoved: (_) {}, + onTankAdded: (_) {}, + onTankRemoved: (_) {}, + onTankChanged: (_, _) {}, + onWaterChanged: (_) {}, + onSaveBodyWeight: () {}, + onChanged: () {}, + ), + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + // TextButton.icon builds a TextButton subclass, so match by predicate. + Finder addTank() => find.ancestor( + of: find.text('Add tank'), + matching: find.byWidgetPredicate((w) => w is TextButton), + ); + expect(tester.widget(addTank()).onPressed, isNotNull); + + // A dependency-driven reload: the new list has not arrived yet. + next = Completer>(); + container.read(_reloadTrigger.notifier).state++; + await tester.pump(); + + expect(container.read(tankPresetsProvider).isLoading, isTrue); + expect( + tester.widget(addTank()).onPressed, + isNotNull, + reason: 'the previous list stays usable until the reload lands', + ); + expect(find.text('AL80'), findsOneWidget); + + next.complete(presets); + await tester.pumpAndSettle(); + }); +} From a391f97b32e94b54ae8bcbbc3c36c860e45e8dad Mon Sep 17 00:00:00 2001 From: alpheios-one <275321969+alpheios-one@users.noreply.github.com> Date: Fri, 25 Sep 2026 06:35:42 +0200 Subject: [PATCH 5/5] test(weight-planner): const UnitFormatter in the rig composer reload test Refs #2305 --- .../weight_planner/presentation/rig_composer_reload_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/features/weight_planner/presentation/rig_composer_reload_test.dart b/test/features/weight_planner/presentation/rig_composer_reload_test.dart index ee97facd1f..2921b205a4 100644 --- a/test/features/weight_planner/presentation/rig_composer_reload_test.dart +++ b/test/features/weight_planner/presentation/rig_composer_reload_test.dart @@ -56,7 +56,7 @@ void main() { heightCmController: controllers[1], heightFeetController: controllers[2], heightInchesController: controllers[3], - units: UnitFormatter(const AppSettings()), + units: const UnitFormatter(AppSettings()), showSaveBodyWeight: false, onGearAdded: (_) {}, onGearSetAdded: (_, _) {},