Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion lib/core/database/database.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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))();
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<void> _assertHiddenTankPresetIdsColumn() async {
final cols = await customSelect(
"PRAGMA table_info('diver_settings')",
).get();
if (cols.isEmpty) return;
final names = cols.map((c) => c.read<String>('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.
Expand Down Expand Up @@ -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();

Expand Down
13 changes: 10 additions & 3 deletions lib/features/dive_log/presentation/widgets/tank_editor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -472,17 +473,23 @@ class _TankEditorState extends ConsumerState<TankEditor> {
// 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: (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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> hiddenTankPresetIds;

// Decompression & Safety settings
/// Gradient Factor Low (0-100, typically 30)
final int gfLow;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -752,6 +757,7 @@ class AppSettings {
String? defaultTankPreset,
bool clearDefaultTankPreset = false,
bool? applyDefaultTankToImports,
Set<String>? hiddenTankPresetIds,
int? gfLow,
int? gfHigh,
double? ppO2MaxWorking,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -1531,14 +1538,43 @@ class SettingsNotifier extends StateNotifier<AppSettings> {
await _saveSettings();
}

/// Also shows [presetName] again if it was hidden: the default preset is
/// 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<void> 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: touchesHidden
? {
for (final name in hidden)
if (name != presetName && name != previous) name,
}
: null,
);
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<void> setTankPresetHidden(String presetName, bool hidden) async {
if (hidden && presetName == state.defaultTankPreset) return;
Comment thread
alpheios-one marked this conversation as resolved.
final ids = {...state.hiddenTankPresetIds};
if (hidden) {
ids.add(presetName);
} else {
ids.remove(presetName);
}
state = state.copyWith(hiddenTankPresetIds: ids);
await _saveSettings();
}

Future<void> setApplyDefaultTankToImports(bool value) async {
state = state.copyWith(applyDefaultTankToImports: value);
await _saveSettings();
Expand Down
Original file line number Diff line number Diff line change
@@ -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<TankPresetEntity> visibleTankPresets(
List<TankPresetEntity> all,
Set<String> 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<TankPresetEntity> withKeptTankPresets(
List<TankPresetEntity> visible,
Iterable<String?> 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),
];
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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),
),
),
],
Expand Down Expand Up @@ -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,
Expand All @@ -146,9 +159,15 @@ class TankPresetsPage extends ConsumerWidget {
);

return ListTile(
// 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: canEdit
color: isHidden
? Theme.of(context).disabledColor
: canEdit
? Theme.of(context).colorScheme.secondary
: Theme.of(context).colorScheme.primary,
),
Expand Down Expand Up @@ -182,6 +201,26 @@ 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: (visible) => ref
.read(settingsProvider.notifier)
.setTankPresetHidden(preset.name, !visible),
),
),
),
if (canEdit) ...[
IconButton(
icon: const Icon(Icons.edit_outlined),
Expand Down
Loading
Loading