From a6b8b5b5778bbf90927bfb0013cfe091f052c11c Mon Sep 17 00:00:00 2001 From: Eric Griffin Date: Sun, 5 Jul 2026 16:11:46 -0400 Subject: [PATCH 1/8] docs: implementation plan for unified Cylinders card --- .../plans/2026-07-05-cylinders-card.md | 920 ++++++++++++++++++ 1 file changed, 920 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-05-cylinders-card.md diff --git a/docs/superpowers/plans/2026-07-05-cylinders-card.md b/docs/superpowers/plans/2026-07-05-cylinders-card.md new file mode 100644 index 000000000..4e431ba5f --- /dev/null +++ b/docs/superpowers/plans/2026-07-05-cylinders-card.md @@ -0,0 +1,920 @@ +# Unified Cylinders Card Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace the dive-detail page's separate "Tanks" card and "SAC by Cylinder" block with one always-expanded "Cylinders" card that shows identity, pressures, MOD/MND, and per-tank SAC for every tank. + +**Architecture:** A new `CylindersCard` `ConsumerWidget` joins `dive.tanks` with `cylinderSacProvider` results by `tankId` and takes over the existing `DiveDetailSectionId.tanks` slot. The old inline builders in `dive_detail_page.dart` and the cylinder-SAC sub-block of the SAC-segments section are deleted, along with the never-used `cylinder_sac_card.dart`. + +**Tech Stack:** Flutter 3.x / Material 3, Riverpod (StateNotifier + FutureProvider.family), flutter gen-l10n (ARB, 11 locales), flutter_test widget tests. + +**Spec:** `docs/superpowers/specs/2026-07-05-cylinders-card-design.md` + +## Global Constraints + +- `DiveDetailSectionId.tanks` enum value is NEVER renamed (persisted section config round-trips through `id.name`). +- All strings displayed to users come from l10n; new/changed keys must be updated in ALL 11 ARB files (`lib/l10n/arb/app_{en,ar,de,es,fr,he,hu,it,nl,pt,zh}.arb`), then regenerate with `flutter gen-l10n`. +- All displayed values go through `UnitFormatter` (respect active unit settings). +- `dart format .` must produce no changes before each commit; `flutter analyze` (whole project) must report no issues. +- Run specific test files, not broad directories (Bash timeouts). +- No emojis in code, comments, or docs. Commits contain no Co-Authored-By lines. +- Behavior preservation: hardcoded English " used" suffixes in pressure/volume strings are pre-existing behavior — keep them verbatim, do not localize in this plan. + +## File Structure + +| File | Action | Responsibility | +| --- | --- | --- | +| `lib/features/dive_log/presentation/widgets/cylinders_card.dart` | Create | The unified card widget (rows, chip, trailing SAC block, pressure resolution) | +| `test/features/dive_log/presentation/widgets/cylinders_card_test.dart` | Create | Widget tests for the card | +| `lib/l10n/arb/app_*.arb` (11 files) | Modify | Add `diveLog_detail_section_cylinders`; update section name/description strings; later remove obsolete keys | +| `lib/core/constants/dive_detail_sections.dart` | Modify | English fallback `displayName`/`description` for `tanks` and `sacSegments` | +| `lib/features/dive_log/presentation/pages/dive_detail_page.dart` | Modify | Swap `tanks` section builder to `CylindersCard`; delete old builders and the cylinder-SAC sub-block | +| `lib/features/dive_log/presentation/providers/gas_analysis_providers.dart` | Modify | Remove `cylinderSacExpandedProvider` | +| `lib/features/dive_log/presentation/widgets/cylinder_sac_card.dart` | Delete | Dead code (no references anywhere) | +| `test/core/constants/dive_detail_sections_test.dart`, `test/features/settings/presentation/pages/dive_detail_sections_page_test.dart` | Modify | 'Tanks' -> 'Cylinders' expectations | + +--- + +### Task 1: l10n strings and section metadata + +**Files:** +- Modify: `lib/l10n/arb/app_en.arb`, `app_ar.arb`, `app_de.arb`, `app_es.arb`, `app_fr.arb`, `app_he.arb`, `app_hu.arb`, `app_it.arb`, `app_nl.arb`, `app_pt.arb`, `app_zh.arb` +- Modify: `lib/core/constants/dive_detail_sections.dart` +- Modify: `test/core/constants/dive_detail_sections_test.dart:402,535` +- Modify: `test/features/settings/presentation/pages/dive_detail_sections_page_test.dart:326` + +**Interfaces:** +- Produces: l10n getter `context.l10n.diveLog_detail_section_cylinders` (type `String`), used by Task 2's widget. Old keys (`diveLog_detail_section_tanks`, `diveLog_detail_section_sacByCylinder`, `diveLog_detail_tankCount`) stay in place until Task 4 so the page keeps compiling. + +- [ ] **Step 1: Add the new key and update changed strings in `app_en.arb`** + +Next to the existing `"diveLog_detail_section_tanks": "Tanks",` line (line ~1900), add: + +```json + "diveLog_detail_section_cylinders": "Cylinders", +``` + +Change these existing values (do NOT remove any keys in this task): + +```json + "diveDetailSection_tanks_name": "Cylinders", + "diveDetailSection_tanks_description": "Cylinder list, gas mixes, pressures, MOD/MND, per-tank SAC", + "diveDetailSection_sacSegments_description": "Phase/time SAC segmentation", +``` + +- [ ] **Step 2: Update the 10 non-English ARB files** + +Every locale already translates "Tanks" with its cylinder/bottle word, so for each locale: (a) add `diveLog_detail_section_cylinders` with the SAME value as that locale's existing `diveLog_detail_section_tanks`; (b) replace `diveDetailSection_sacSegments_description` with the trimmed value below; (c) leave `diveDetailSection_tanks_name` and `diveDetailSection_tanks_description` unchanged (already cylinder-worded). + +| File | `diveLog_detail_section_cylinders` (copy of existing tanks value) | New `diveDetailSection_sacSegments_description` | +| --- | --- | --- | +| app_ar.arb | `الأسطوانات` | `تقسيم SAC حسب المراحل/الوقت` | +| app_de.arb | `Flaschen` | `SAC-Segmentierung nach Phase/Zeit` | +| app_es.arb | `Tanques` | `Segmentacion SAC por fase/tiempo` | +| app_fr.arb | `Blocs` | `Segmentation SAC par phase/temps` | +| app_he.arb | `בלונים` | `פילוח SAC לפי שלב/זמן` | +| app_hu.arb | `Palackok` | `SAC szegmentálás fázis/idő szerint` | +| app_it.arb | `Bombole` | `Segmentazione SAC per fase/tempo` | +| app_nl.arb | `Flessen` | `SAC-segmentatie per fase/tijd` | +| app_pt.arb | `Cilindros` | `Segmentacao SAC por fase/tempo` | +| app_zh.arb | `气瓶` | `按阶段/时间的SAC分段` | + +(es keeps `Tanques` — that is its existing value; consistency with the rest of the es locale wins over the English rename.) + +- [ ] **Step 3: Update the English fallbacks in `lib/core/constants/dive_detail_sections.dart`** + +In the `displayName` switch: `tanks => 'Tanks',` becomes `tanks => 'Cylinders',`. +In the `description` switch: +`sacSegments => 'Phase/time segmentation, cylinder breakdown',` becomes `sacSegments => 'Phase/time SAC segmentation',` and +`tanks => 'Tank list, gas mixes, pressures, per-tank SAC',` becomes `tanks => 'Cylinder list, gas mixes, pressures, MOD/MND, per-tank SAC',`. + +- [ ] **Step 4: Regenerate localizations** + +Run: `flutter gen-l10n` +Expected: exits 0; `lib/l10n/arb/app_localizations.dart` gains `String get diveLog_detail_section_cylinders;`. + +- [ ] **Step 5: Update test expectations** + +- `test/core/constants/dive_detail_sections_test.dart:402`: `expect(DiveDetailSectionId.tanks.displayName, 'Tanks');` -> `'Cylinders'`. +- `test/core/constants/dive_detail_sections_test.dart:535`: `expect(DiveDetailSectionId.tanks.localizedDisplayName(l10n), 'Tanks');` -> `'Cylinders'`. +- `test/features/settings/presentation/pages/dive_detail_sections_page_test.dart:326`: `expect(displayNames.first, 'Tanks');` -> `'Cylinders'`. +- Then grep the two files for any other assertions on the old description strings (`grep -n "Tank list\|cylinder breakdown" test/core/constants/dive_detail_sections_test.dart test/features/settings/presentation/pages/dive_detail_sections_page_test.dart`) and update them to the new values from Step 3. + +- [ ] **Step 6: Run the affected tests** + +Run: `flutter test test/core/constants/dive_detail_sections_test.dart test/features/settings/presentation/pages/dive_detail_sections_page_test.dart` +Expected: all pass. + +- [ ] **Step 7: Analyze, format, commit** + +```bash +flutter analyze # expect: No issues found! +dart format . # expect: no changed files +git add -A +git commit -m "feat(l10n): add Cylinders section strings, trim SAC-segments description" +``` + +--- + +### Task 2: CylindersCard widget (TDD) + +**Files:** +- Create: `test/features/dive_log/presentation/widgets/cylinders_card_test.dart` +- Create: `lib/features/dive_log/presentation/widgets/cylinders_card.dart` + +**Interfaces:** +- Consumes: `context.l10n.diveLog_detail_section_cylinders` (Task 1); existing providers `cylinderSacProvider` / `tankPressuresProvider` (`FutureProvider.family<..., String>` in `gas_analysis_providers.dart` / `dive_providers.dart`), `diveDataSourcesProvider`; entities `Dive`, `DiveTank`, `TankPressurePoint`, `CylinderSac`; helpers `UnitFormatter`, `TankPresets.byName`, `resolveSourceName`/`SourceNameLabels`, `FieldAttributionBadge`. +- Produces: `CylindersCard({required Dive dive, required UnitFormatter units, required AppSettings settings, required SacUnit sacUnit})` — a `ConsumerWidget`. Task 3 constructs it exactly with those four named parameters. Settings-derived values are constructor-injected so tests never need a `SettingsNotifier` mock (see memory: settings-notifier-mocks). + +- [ ] **Step 1: Write the failing test** + +Create `test/features/dive_log/presentation/widgets/cylinders_card_test.dart`: + +```dart +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:submersion/core/constants/enums.dart'; +import 'package:submersion/core/constants/units.dart'; +import 'package:submersion/core/utils/unit_formatter.dart'; +import 'package:submersion/features/dive_log/domain/entities/cylinder_sac.dart'; +import 'package:submersion/features/dive_log/domain/entities/dive.dart'; +import 'package:submersion/features/dive_log/domain/entities/dive_data_source.dart'; +import 'package:submersion/features/dive_log/presentation/providers/dive_providers.dart'; +import 'package:submersion/features/dive_log/presentation/providers/gas_analysis_providers.dart'; +import 'package:submersion/features/dive_log/presentation/widgets/cylinders_card.dart'; +import 'package:submersion/features/dive_log/presentation/widgets/field_attribution_badge.dart'; +import 'package:submersion/features/settings/presentation/providers/settings_providers.dart'; + +import '../../../../helpers/test_app.dart'; + +const _settings = AppSettings(); +const _units = UnitFormatter(_settings); + +DiveTank _makeTank({ + String id = 'tank-1', + String? name, + double? volume = 11.1, + double? startPressure = 200, + double? endPressure = 50, + GasMix gasMix = const GasMix(o2: 32), + String? computerId, +}) { + return DiveTank( + id: id, + name: name, + volume: volume, + startPressure: startPressure, + endPressure: endPressure, + gasMix: gasMix, + computerId: computerId, + ); +} + +Dive _makeDive(List tanks) { + return Dive( + id: 'dive-1', + diveNumber: 1, + dateTime: DateTime(2026, 6, 1, 10, 0), + maxDepth: 30.0, + avgDepth: 18.0, + bottomTime: const Duration(minutes: 45), + tanks: tanks, + ); +} + +CylinderSac _makeSac({ + String tankId = 'tank-1', + double? sacRate = 2.0, + double? tankVolume = 11.1, + double? startPressure = 200, + double? endPressure = 50, +}) { + return CylinderSac( + tankId: tankId, + gasMix: const GasMix(o2: 32), + role: TankRole.backGas, + tankVolume: tankVolume, + sacRate: sacRate, + startPressure: startPressure, + endPressure: endPressure, + ); +} + +DiveDataSource _makeSource({ + required String id, + String? computerId, + bool isPrimary = false, + String? computerModel, +}) { + final now = DateTime(2026, 6, 1, 10, 0); + return DiveDataSource( + id: id, + diveId: 'dive-1', + computerId: computerId, + isPrimary: isPrimary, + computerModel: computerModel, + entryTime: now, + exitTime: now.add(const Duration(minutes: 45)), + importedAt: now, + createdAt: now, + ); +} + +Widget _buildCard({ + required Dive dive, + List cylinderSacs = const [], + List dataSources = const [], + UnitFormatter units = _units, + AppSettings settings = _settings, + SacUnit sacUnit = SacUnit.pressurePerMin, +}) { + return testApp( + overrides: [ + cylinderSacProvider.overrideWith((ref, id) async => cylinderSacs), + tankPressuresProvider.overrideWith( + (ref, id) async => >{}, + ), + diveDataSourcesProvider.overrideWith((ref, id) async => dataSources), + ], + child: SingleChildScrollView( + child: CylindersCard( + dive: dive, + units: units, + settings: settings, + sacUnit: sacUnit, + ), + ), + ); +} + +void main() { + group('CylindersCard', () { + testWidgets('renders title, tank identity, pressures, and MOD/MND', ( + tester, + ) async { + await tester.pumpWidget( + _buildCard(dive: _makeDive([_makeTank()]), cylinderSacs: [_makeSac()]), + ); + await tester.pumpAndSettle(); + + expect(find.text('Cylinders'), findsOneWidget); + expect(find.textContaining('Tank 1 (EAN32)'), findsOneWidget); + expect( + find.textContaining('200 bar → 50 bar (150 bar used)'), + findsOneWidget, + ); + expect(find.textContaining('MOD:'), findsOneWidget); + expect(find.textContaining('MND:'), findsOneWidget); + }); + + testWidgets('shows SAC and gas used on a single-tank dive', ( + tester, + ) async { + await tester.pumpWidget( + _buildCard(dive: _makeDive([_makeTank()]), cylinderSacs: [_makeSac()]), + ); + await tester.pumpAndSettle(); + + // sacRate 2.0 bar/min, pressurePerMin mode, metric. + expect(find.text('2.0 bar/min'), findsOneWidget); + // gasUsedLiters = (200 - 50) * 11.1 = 1665 L. + expect(find.text('1665 L used'), findsOneWidget); + }); + + testWidgets('omits the SAC block when SAC is not computable', ( + tester, + ) async { + await tester.pumpWidget( + _buildCard( + dive: _makeDive([_makeTank(startPressure: null, endPressure: null)]), + cylinderSacs: [ + _makeSac(sacRate: null, startPressure: null, endPressure: null), + ], + ), + ); + await tester.pumpAndSettle(); + + expect(find.textContaining('Tank 1'), findsOneWidget); + expect(find.textContaining('/min'), findsNothing); + expect(find.textContaining('used'), findsNothing); + }); + + testWidgets('shows one row with distinct SAC per tank on multi-tank dive', ( + tester, + ) async { + await tester.pumpWidget( + _buildCard( + dive: _makeDive([ + _makeTank(), + _makeTank( + id: 'tank-2', + name: 'Deco O2', + volume: 5.7, + startPressure: 200, + endPressure: 140, + gasMix: const GasMix(o2: 100), + ), + ]), + cylinderSacs: [ + _makeSac(), + _makeSac( + tankId: 'tank-2', + sacRate: 1.2, + tankVolume: 5.7, + startPressure: 200, + endPressure: 140, + ), + ], + ), + ); + await tester.pumpAndSettle(); + + expect(find.text('2.0 bar/min'), findsOneWidget); + expect(find.text('1.2 bar/min'), findsOneWidget); + expect(find.textContaining('Deco O2'), findsOneWidget); + }); + + testWidgets('formats SAC as L/min when unit is litersPerMin', ( + tester, + ) async { + await tester.pumpWidget( + _buildCard( + dive: _makeDive([_makeTank()]), + cylinderSacs: [_makeSac()], + sacUnit: SacUnit.litersPerMin, + ), + ); + await tester.pumpAndSettle(); + + // sacVolume = 2.0 * 11.1 / 1.01325 = 21.909... -> '21.9 L/min'. + expect(find.text('21.9 L/min'), findsOneWidget); + }); + + testWidgets('formats pressures and SAC in imperial units', ( + tester, + ) async { + const imperialSettings = AppSettings( + pressureUnit: PressureUnit.psi, + volumeUnit: VolumeUnit.cubicFeet, + depthUnit: DepthUnit.feet, + ); + await tester.pumpWidget( + _buildCard( + dive: _makeDive([_makeTank()]), + cylinderSacs: [_makeSac()], + units: const UnitFormatter(imperialSettings), + settings: imperialSettings, + ), + ); + await tester.pumpAndSettle(); + + // 2.0 bar/min * 14.5038 = 29.0076 -> '29.0 psi/min'. + expect(find.text('29.0 psi/min'), findsOneWidget); + // Pressure line rendered in psi. + expect(find.textContaining('psi →'), findsOneWidget); + }); + + testWidgets('shows source badge only with two or more data sources', ( + tester, + ) async { + final dive = _makeDive([_makeTank(computerId: 'comp-1')]); + + await tester.pumpWidget( + _buildCard( + dive: dive, + cylinderSacs: [_makeSac()], + dataSources: [ + _makeSource( + id: 'src-1', + computerId: 'comp-1', + isPrimary: true, + computerModel: 'Perdix 2', + ), + ], + ), + ); + await tester.pumpAndSettle(); + expect(find.byType(FieldAttributionBadge), findsNothing); + + await tester.pumpWidget( + _buildCard( + dive: dive, + cylinderSacs: [_makeSac()], + dataSources: [ + _makeSource( + id: 'src-1', + computerId: 'comp-1', + isPrimary: true, + computerModel: 'Perdix 2', + ), + _makeSource( + id: 'src-2', + computerId: 'comp-2', + computerModel: 'Teric', + ), + ], + ), + ); + await tester.pumpAndSettle(); + expect(find.text('Perdix 2'), findsOneWidget); + }); + }); +} +``` + +Note: if `Dive`, `DiveTank`, or `DiveDataSource` constructors require parameters not listed here, mirror the fixtures in `test/features/dive_log/presentation/pages/dive_detail_page_test.dart:588` (`makeDiveWithTanksAndProfile`) and `test/features/dive_log/presentation/widgets/data_sources_section_test.dart` (`_makeSource`) — do not change the assertions. + +- [ ] **Step 2: Run test to verify it fails** + +Run: `flutter test test/features/dive_log/presentation/widgets/cylinders_card_test.dart` +Expected: FAIL — compile error, `cylinders_card.dart` does not exist. + +- [ ] **Step 3: Implement the widget** + +Create `lib/features/dive_log/presentation/widgets/cylinders_card.dart`: + +```dart +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; + +import 'package:submersion/core/constants/tank_presets.dart'; +import 'package:submersion/core/constants/units.dart'; +import 'package:submersion/core/utils/unit_formatter.dart'; +import 'package:submersion/features/dive_log/domain/entities/cylinder_sac.dart'; +import 'package:submersion/features/dive_log/domain/entities/dive.dart'; +import 'package:submersion/features/dive_log/domain/entities/dive_data_source.dart'; +import 'package:submersion/features/dive_log/domain/services/source_name_resolver.dart'; +import 'package:submersion/features/dive_log/presentation/providers/dive_providers.dart'; +import 'package:submersion/features/dive_log/presentation/providers/gas_analysis_providers.dart'; +import 'package:submersion/features/dive_log/presentation/widgets/field_attribution_badge.dart'; +import 'package:submersion/features/settings/presentation/providers/settings_providers.dart'; +import 'package:submersion/l10n/l10n_extension.dart'; + +/// Unified card showing every cylinder on a dive: identity (name, gas mix, +/// volume), start/end pressures, MOD/MND, and per-tank SAC. +/// +/// Replaces the former Tanks card and SAC by Cylinder block. Occupies the +/// [DiveDetailSectionId.tanks] slot on the dive detail page. Per-tank SAC +/// is shown whenever it is computable, regardless of tank count; the +/// trailing block is omitted entirely when it is not. +class CylindersCard extends ConsumerWidget { + const CylindersCard({ + super.key, + required this.dive, + required this.units, + required this.settings, + required this.sacUnit, + }); + + final Dive dive; + final UnitFormatter units; + final AppSettings settings; + final SacUnit sacUnit; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final tankPressures = ref + .watch(tankPressuresProvider(dive.id)) + .valueOrNull; + final cylinderSacs = + ref.watch(cylinderSacProvider(dive.id)).valueOrNull ?? + const []; + final sacByTankId = {for (final c in cylinderSacs) c.tankId: c}; + final dataSources = + ref.watch(diveDataSourcesProvider(dive.id)).valueOrNull ?? + const []; + // Only badge tanks once there's more than one source to disambiguate — + // a single-source dive never needs attribution. + final showSourceBadges = dataSources.length >= 2; + final computerNames = _computerDisplayNames(context, dataSources); + + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.diveLog_detail_section_cylinders, + style: Theme.of(context).textTheme.titleMedium, + ), + const Divider(), + ...dive.tanks.asMap().entries.map( + (entry) => _tankRow( + context, + index: entry.key, + tank: entry.value, + cylinderSac: sacByTankId[entry.value.id], + tankPressures: tankPressures, + sourceName: + showSourceBadges && entry.value.computerId != null + ? computerNames[entry.value.computerId] + : null, + ), + ), + ], + ), + ), + ); + } + + Widget _tankRow( + BuildContext context, { + required int index, + required DiveTank tank, + required CylinderSac? cylinderSac, + required Map>? tankPressures, + required String? sourceName, + }) { + final theme = Theme.of(context); + + final pressures = _resolveTankPressures( + tank: tank, + tankPressures: tankPressures, + ); + final startP = units.formatPressureValue(pressures.$1); + final endP = units.formatPressureValue(pressures.$2); + final pressureUsed = pressures.$1 != null && pressures.$2 != null + ? pressures.$1! - pressures.$2! + : null; + final used = pressureUsed != null && pressureUsed > 0 + ? ' (${units.formatPressure(pressureUsed)} used)' + : ''; + + // Preset display name, falling back to formatted volume. + final preset = tank.presetName != null + ? TankPresets.byName(tank.presetName!) + : null; + final tankLabel = + preset?.displayName ?? + (tank.volume != null + ? units.formatTankVolume( + tank.volume, + tank.workingPressure, + decimals: 1, + ) + : null); + final tankTitle = tank.name != null && tank.name!.isNotEmpty + ? tank.name! + : context.l10n.diveLog_tank_title(index + 1); + + final modDepth = units.formatDepth(tank.gasMix.mod(), decimals: 0); + final mndValue = tank.gasMix.mnd( + endLimit: settings.endLimit, + o2Narcotic: settings.o2Narcotic, + ); + final mndDepth = mndValue.isFinite + ? units.formatDepth(mndValue, decimals: 0) + : '--'; + final modMndText = context.l10n.diveLog_tank_modMndInfo( + modDepth, + mndDepth, + ); + + return ListTile( + contentPadding: EdgeInsets.zero, + leading: const Icon(MdiIcons.divingScubaTank), + title: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible(child: Text('$tankTitle (${tank.gasMix.name})')), + if (tankLabel != null) _volumeChip(theme, tankLabel), + ], + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '$startP ${units.pressureSymbol} → ' + '$endP ${units.pressureSymbol}$used', + ), + Text( + modMndText, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.tertiary, + ), + ), + ], + ), + trailing: _trailingBlock(theme, cylinderSac, sourceName), + ); + } + + /// Small outlined chip carrying the preset/volume label (e.g. "AL80"). + Widget _volumeChip(ThemeData theme, String label) { + return Container( + margin: const EdgeInsetsDirectional.only(start: 6), + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1), + decoration: BoxDecoration( + border: Border.all(color: theme.colorScheme.outlineVariant), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + label, + style: theme.textTheme.labelSmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ); + } + + /// Trailing column: attribution badge, SAC rate, gas used in liters. + /// Returns null when there is nothing to show so the tile keeps its + /// natural width. + Widget? _trailingBlock( + ThemeData theme, + CylinderSac? cylinderSac, + String? sourceName, + ) { + final hasSac = cylinderSac != null && cylinderSac.hasValidSac; + if (!hasSac && sourceName == null) return null; + return Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (sourceName != null) FieldAttributionBadge(sourceName: sourceName), + if (hasSac) ...[ + Text( + _formatSac(cylinderSac), + style: theme.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.bold, + color: theme.colorScheme.primary, + ), + ), + if (cylinderSac.gasUsedLiters != null) + Text( + '${units.convertVolume(cylinderSac.gasUsedLiters!).round()} ' + '${units.volumeSymbol} used', + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ], + ], + ); + } + + /// Formats the SAC value per the diver's SAC unit preference. L/min needs + /// a tank volume; otherwise falls back to pressure-drop per minute. + /// Only called when [CylinderSac.hasValidSac] is true. + String _formatSac(CylinderSac cylinder) { + if (sacUnit == SacUnit.litersPerMin && cylinder.sacVolume != null) { + final value = units.convertVolume(cylinder.sacVolume!); + return '${value.toStringAsFixed(1)} ${units.volumeSymbol}/min'; + } + final value = units.convertPressure(cylinder.sacRate!); + return '${value.toStringAsFixed(1)} ${units.pressureSymbol}/min'; + } + + /// Resolves start/end pressure: stored tank metadata wins, per-tank + /// time-series fills any nulls. + (double?, double?) _resolveTankPressures({ + required DiveTank tank, + required Map>? tankPressures, + }) { + if (tankPressures != null && tankPressures.containsKey(tank.id)) { + final points = tankPressures[tank.id]!; + if (points.isNotEmpty) { + return ( + tank.startPressure ?? points.first.pressure, + tank.endPressure ?? points.last.pressure, + ); + } + } + return (tank.startPressure, tank.endPressure); + } + + /// computerId -> display name via the shared source-name resolver. + Map _computerDisplayNames( + BuildContext context, + List dataSources, + ) { + final labels = SourceNameLabels( + unknownComputer: context.l10n.diveLog_sources_unknownComputer, + manualEntry: context.l10n.diveLog_sources_manualEntry, + importedFile: context.l10n.diveLog_sources_importedFile, + editedSuffix: context.l10n.diveLog_sources_editedSuffix, + ); + return { + for (final source in dataSources) + if (source.computerId != null) + source.computerId!: resolveSourceName(source, labels), + }; + } +} +``` + +If `GasMix(o2: 32).name` renders something other than `EAN32`, or `UnitFormatter` is not a const constructor, adjust the TEST fixtures/expectations to the actual values — the widget code stays. + +- [ ] **Step 4: Run test to verify it passes** + +Run: `flutter test test/features/dive_log/presentation/widgets/cylinders_card_test.dart` +Expected: all 7 tests PASS. + +- [ ] **Step 5: Analyze, format, commit** + +```bash +flutter analyze # expect: No issues found! +dart format . # expect: no changed files +git add lib/features/dive_log/presentation/widgets/cylinders_card.dart test/features/dive_log/presentation/widgets/cylinders_card_test.dart +git commit -m "feat(dive-detail): add unified CylindersCard widget" +``` + +--- + +### Task 3: Wire CylindersCard into the dive detail page, remove old builders + +**Files:** +- Modify: `lib/features/dive_log/presentation/pages/dive_detail_page.dart` (line refs are pre-change positions: section builder ~336; SAC-segments watches ~1683-1707; SAC-segments sub-block ~1917-1928; `_buildCylinderSacSection` ~2044-2149; `_formatCylinderSac` ~2152-2165; `_buildTanksSection` ~3825-3951; `_resolveTankPressures` ~3953-3981) + +**Interfaces:** +- Consumes: `CylindersCard({required dive, required units, required settings, required sacUnit})` from Task 2; `sacUnitProvider` from `settings_providers.dart`. +- Produces: no new interfaces. The `sacSegments` section, with no segments available, now renders nothing (`SizedBox.shrink`) — the Cylinders card always carries per-tank SAC. + +- [ ] **Step 1: Swap the `tanks` section builder** + +In `_sectionBuilders`, replace: + +```dart + DiveDetailSectionId.tanks: () { + if (dive.tanks.isEmpty) return []; + return [ + const SizedBox(height: 24), + _buildTanksSection(context, ref, dive, units), + ]; + }, +``` + +with: + +```dart + DiveDetailSectionId.tanks: () { + if (dive.tanks.isEmpty) return []; + return [ + const SizedBox(height: 24), + CylindersCard( + dive: dive, + units: units, + settings: settings, + sacUnit: ref.watch(sacUnitProvider), + ), + ]; + }, +``` + +Add the import `package:submersion/features/dive_log/presentation/widgets/cylinders_card.dart` (grouped with the other widget imports). + +- [ ] **Step 2: Strip the cylinder-SAC pieces out of `_buildSacSegmentsSection`** + +(a) Delete the `isMultiTank` watch and the `cylinderSacAsync` watch: + +```dart + final isMultiTank = + ref.watch(isMultiTankDiveProvider(dive.id)).valueOrNull ?? false; + + // Get cylinder SAC data for multi-tank dives + final cylinderSacAsync = ref.watch(cylinderSacProvider(dive.id)); +``` + +(keep the `hasGasSwitches` watch directly above — the segmentation selector still uses it). + +(b) Simplify the no-segments early return from: + +```dart + if (analysis == null || + (analysis.sacSegments == null || analysis.sacSegments!.isEmpty)) { + // Still show cylinder SAC if available + if (isMultiTank && cylinderSacAsync.hasValue) { + return _buildCylinderSacSection( + context, + ref, + dive, + cylinderSacAsync.value!, + units, + sacUnit, + ); + } + return const SizedBox.shrink(); + } +``` + +to: + +```dart + if (analysis == null || + (analysis.sacSegments == null || analysis.sacSegments!.isEmpty)) { + return const SizedBox.shrink(); + } +``` + +(c) Delete the trailing sub-block near the end of the method (the `const SizedBox(height: 24),` after it stays): + +```dart + // Cylinder SAC subsection for multi-tank dives + if (isMultiTank && cylinderSacAsync.hasValue) ...[ + const SizedBox(height: 16), + _buildCylinderSacSection( + context, + ref, + dive, + cylinderSacAsync.value!, + units, + sacUnit, + ), + ], +``` + +- [ ] **Step 3: Delete the four dead methods** + +Remove in full from `dive_detail_page.dart`: +- `Widget _buildCylinderSacSection(...)` (the whole method) +- `String _formatCylinderSac(...)` +- `Widget _buildTanksSection(...)` +- `(double?, double?) _resolveTankPressures(...)` (including its doc comment) + +- [ ] **Step 4: Analyze and remove newly-unused imports** + +Run: `flutter analyze` +Expected: `unused_import` warnings for anything only the deleted methods used (likely the `CylinderSac` entity import; possibly `TankPresets`). Remove exactly the flagged imports, re-run, expect: No issues found! Do NOT remove `gas_analysis_providers.dart` (the page still uses `cylinderSacProvider`? No — after this task the page no longer watches `cylinderSacProvider`, but it still uses `isMultiTankDiveProvider`? No — also removed. The page still imports that file for `selectedSegmentationProvider`/`hasGasSwitchesProvider`/etc.; let analyze be the judge — remove only what it flags). + +- [ ] **Step 5: Run the page test suites** + +Run: `flutter test test/features/dive_log/presentation/pages/dive_detail_page_test.dart test/features/dive_log/presentation/pages/dive_detail_page_section_config_test.dart test/features/dive_log/presentation/pages/dive_detail_multi_source_test.dart` +Expected: all pass. (The section-config "renders tanks section" test only asserts the page renders; `CylindersCard` tolerates erroring providers via `valueOrNull`.) If a test fails on missing text like `Tanks`, update the expectation to `Cylinders`. + +- [ ] **Step 6: Format, commit** + +```bash +dart format . # expect: no changed files +git add -A +git commit -m "feat(dive-detail): replace Tanks and SAC by Cylinder cards with CylindersCard" +``` + +--- + +### Task 4: Cleanup — dead widget, dead provider, obsolete l10n keys + +**Files:** +- Delete: `lib/features/dive_log/presentation/widgets/cylinder_sac_card.dart` +- Modify: `lib/features/dive_log/presentation/providers/gas_analysis_providers.dart` (remove `cylinderSacExpandedProvider`) +- Modify: all 11 `lib/l10n/arb/app_*.arb` files (remove 3 obsolete keys) + +**Interfaces:** +- Consumes: Task 3 must be complete (nothing may reference the removed symbols/keys). +- Produces: nothing — pure deletion. + +- [ ] **Step 1: Verify the symbols are unreferenced** + +```bash +grep -rn "CylinderSacCard\|CylinderSacList" lib/ test/ | grep -v "widgets/cylinder_sac_card.dart" +grep -rn "cylinderSacExpandedProvider" lib/ test/ | grep -v "gas_analysis_providers.dart" +grep -rn "diveLog_detail_section_sacByCylinder\|diveLog_detail_section_tanks\b\|diveLog_detail_tankCount" lib/ test/ | grep -v "lib/l10n" +``` + +Expected: all three commands print nothing. If any prints a hit, STOP — Task 3 missed a reference; fix that first. + +- [ ] **Step 2: Delete the dead code** + +```bash +git rm lib/features/dive_log/presentation/widgets/cylinder_sac_card.dart +``` + +In `gas_analysis_providers.dart`, delete the `cylinderSacExpandedProvider` declaration (a one-line `StateProvider((ref) => true);` plus its doc comment if any). + +- [ ] **Step 3: Remove obsolete l10n keys from all 11 ARB files** + +From every `lib/l10n/arb/app_*.arb`, remove these entries (and the `"@diveLog_detail_tankCount"` metadata block in `app_en.arb`): +- `diveLog_detail_section_sacByCylinder` +- `diveLog_detail_section_tanks` +- `diveLog_detail_tankCount` + +- [ ] **Step 4: Regenerate localizations and verify nothing broke** + +```bash +flutter gen-l10n # expect: exit 0 +flutter analyze # expect: No issues found! (any missing-getter error means a straggler reference) +``` + +- [ ] **Step 5: Run the affected test files** + +Run: `flutter test test/features/dive_log/presentation/widgets/cylinders_card_test.dart test/features/dive_log/presentation/pages/dive_detail_page_test.dart test/features/dive_log/presentation/pages/dive_detail_page_section_config_test.dart test/core/constants/dive_detail_sections_test.dart` +Expected: all pass. + +- [ ] **Step 6: Format, commit** + +```bash +dart format . # expect: no changed files +git add -A +git commit -m "chore(dive-detail): remove dead cylinder SAC card code and obsolete l10n keys" +``` + +--- + +## Final Verification (after all tasks) + +- [ ] `flutter analyze` — No issues found! +- [ ] `dart format .` — no changed files +- [ ] Targeted suites green: `flutter test test/features/dive_log/presentation/widgets/cylinders_card_test.dart test/features/dive_log/presentation/pages/dive_detail_page_test.dart test/features/dive_log/presentation/pages/dive_detail_page_section_config_test.dart test/features/settings/presentation/pages/dive_detail_sections_page_test.dart test/core/constants/dive_detail_sections_test.dart` +- [ ] Manual smoke (optional, macOS): open a multi-tank dive — one Cylinders card with per-tank SAC in trailing position, no separate SAC by Cylinder block; open a single-tank dive — SAC visible on the row; SAC Rate by Segment section unchanged above. From a05b97d5a81ced069a68d1a84da4f545e9fe38a6 Mon Sep 17 00:00:00 2001 From: Eric Griffin Date: Sun, 5 Jul 2026 16:32:27 -0400 Subject: [PATCH 2/8] feat(l10n): add Cylinders section strings, trim SAC-segments description --- lib/core/constants/dive_detail_sections.dart | 6 +++--- lib/l10n/arb/app_ar.arb | 3 ++- lib/l10n/arb/app_de.arb | 3 ++- lib/l10n/arb/app_en.arb | 7 ++++--- lib/l10n/arb/app_es.arb | 3 ++- lib/l10n/arb/app_fr.arb | 3 ++- lib/l10n/arb/app_he.arb | 3 ++- lib/l10n/arb/app_hu.arb | 3 ++- lib/l10n/arb/app_it.arb | 3 ++- lib/l10n/arb/app_localizations.dart | 12 +++++++++--- lib/l10n/arb/app_localizations_ar.dart | 5 ++++- lib/l10n/arb/app_localizations_de.dart | 5 ++++- lib/l10n/arb/app_localizations_en.dart | 9 ++++++--- lib/l10n/arb/app_localizations_es.dart | 5 ++++- lib/l10n/arb/app_localizations_fr.dart | 5 ++++- lib/l10n/arb/app_localizations_he.dart | 5 ++++- lib/l10n/arb/app_localizations_hu.dart | 5 ++++- lib/l10n/arb/app_localizations_it.dart | 5 ++++- lib/l10n/arb/app_localizations_nl.dart | 5 ++++- lib/l10n/arb/app_localizations_pt.dart | 5 ++++- lib/l10n/arb/app_localizations_zh.dart | 5 ++++- lib/l10n/arb/app_nl.arb | 3 ++- lib/l10n/arb/app_pt.arb | 3 ++- lib/l10n/arb/app_zh.arb | 3 ++- test/core/constants/dive_detail_sections_test.dart | 12 ++++++------ .../pages/dive_detail_sections_page_test.dart | 4 ++-- 26 files changed, 90 insertions(+), 40 deletions(-) diff --git a/lib/core/constants/dive_detail_sections.dart b/lib/core/constants/dive_detail_sections.dart index e46126614..9bcf27302 100644 --- a/lib/core/constants/dive_detail_sections.dart +++ b/lib/core/constants/dive_detail_sections.dart @@ -37,7 +37,7 @@ enum DiveDetailSectionId { tide => 'Tide', surfaceGps => 'Surface GPS', weights => 'Weights', - tanks => 'Tanks', + tanks => 'Cylinders', buddies => 'Buddies', signatures => 'Signatures', equipment => 'Equipment', @@ -55,14 +55,14 @@ enum DiveDetailSectionId { String get description { return switch (this) { decoO2 => 'NDL, ceiling, tissue heat map, O2 toxicity', - sacSegments => 'Phase/time segmentation, cylinder breakdown', + sacSegments => 'Phase/time SAC segmentation', details => 'Type, location, trip, dive center, interval', environment => 'Air/water temp, visibility, current', altitude => 'Altitude value, category, deco requirement', tide => 'Tide cycle graph and timing', surfaceGps => 'GPS entry/exit points and surface drift', weights => 'Weight breakdown, total weight', - tanks => 'Tank list, gas mixes, pressures, per-tank SAC', + tanks => 'Cylinder list, gas mixes, pressures, MOD/MND, per-tank SAC', buddies => 'Buddy list with roles', signatures => 'Buddy/instructor signature display and capture', equipment => 'Equipment used in dive', diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index 2f4af5fb4..faae0635d 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -825,7 +825,7 @@ "diveDetailSection_media_name": "الوسائط", "diveDetailSection_notes_description": "ملاحظات/وصف الغوصة", "diveDetailSection_notes_name": "الملاحظات", - "diveDetailSection_sacSegments_description": "تقسيم المراحل/الوقت، تفصيل الأسطوانات", + "diveDetailSection_sacSegments_description": "تقسيم SAC حسب المراحل/الوقت", "diveDetailSection_sacSegments_name": "معدل SAC حسب المقطع", "diveDetailSection_sightings_description": "الأنواع المرصودة، تفاصيل المشاهدات", "diveDetailSection_sightings_name": "مشاهدات الحياة البحرية", @@ -1225,6 +1225,7 @@ "diveLog_detail_section_sacByCylinder": "SAC حسب الأسطوانة", "diveLog_detail_section_sacRateBySegment": "معدل SAC حسب القطاع", "diveLog_detail_section_tags": "الوسوم", + "diveLog_detail_section_cylinders": "الأسطوانات", "diveLog_detail_section_tanks": "الأسطوانات", "diveLog_detail_section_tide": "المد والجزر", "diveLog_detail_section_trainingSignature": "توقيع التدريب", diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index e8525ac15..7bd58943b 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -940,7 +940,7 @@ "diveDetailSection_media_name": "Medien", "diveDetailSection_notes_description": "Tauchnotizen und -beschreibung", "diveDetailSection_notes_name": "Notizen", - "diveDetailSection_sacSegments_description": "Phasen-/Zeitsegmentierung, Flaschenaufteilung", + "diveDetailSection_sacSegments_description": "SAC-Segmentierung nach Phase/Zeit", "diveDetailSection_sacSegments_name": "SAC-Rate nach Segment", "diveDetailSection_sightings_description": "Gesichtete Arten, Sichtungsdetails", "diveDetailSection_sightings_name": "Meereslebewesen-Sichtungen", @@ -1225,6 +1225,7 @@ "diveLog_detail_section_sacByCylinder": "SAC nach Flasche", "diveLog_detail_section_sacRateBySegment": "SAC-Rate nach Segment", "diveLog_detail_section_tags": "Tags", + "diveLog_detail_section_cylinders": "Flaschen", "diveLog_detail_section_tanks": "Flaschen", "diveLog_detail_section_tide": "Gezeiten", "diveLog_detail_section_trainingSignature": "Ausbildungsunterschrift", diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 61838968d..ab366707a 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1897,6 +1897,7 @@ "diveLog_detail_section_sacByCylinder": "SAC by Cylinder", "diveLog_detail_section_sacRateBySegment": "SAC Rate by Segment", "diveLog_detail_section_tags": "Tags", + "diveLog_detail_section_cylinders": "Cylinders", "diveLog_detail_section_tanks": "Tanks", "diveLog_detail_section_tide": "Tide", "diveLog_detail_section_trainingSignature": "Training Signature", @@ -5722,7 +5723,7 @@ "diveDetailSection_decoO2_name": "Deco Status / Tissue Loading", "diveDetailSection_decoO2_description": "NDL, ceiling, tissue heat map, O2 toxicity", "diveDetailSection_sacSegments_name": "SAC Rate by Segment", - "diveDetailSection_sacSegments_description": "Phase/time segmentation, cylinder breakdown", + "diveDetailSection_sacSegments_description": "Phase/time SAC segmentation", "diveDetailSection_details_name": "Details", "diveDetailSection_details_description": "Type, location, trip, dive center, interval", "diveDetailSection_environment_name": "Environment", @@ -5745,8 +5746,8 @@ "diveLog_detail_openInMaps": "Open in Maps", "diveDetailSection_weights_name": "Weights", "diveDetailSection_weights_description": "Weight breakdown, total weight", - "diveDetailSection_tanks_name": "Tanks", - "diveDetailSection_tanks_description": "Tank list, gas mixes, pressures, per-tank SAC", + "diveDetailSection_tanks_name": "Cylinders", + "diveDetailSection_tanks_description": "Cylinder list, gas mixes, pressures, MOD/MND, per-tank SAC", "diveDetailSection_buddies_name": "Buddies", "diveDetailSection_buddies_description": "Buddy list with roles", "diveDetailSection_signatures_name": "Signatures", diff --git a/lib/l10n/arb/app_es.arb b/lib/l10n/arb/app_es.arb index 61bb47c7f..4cc0fe5c6 100644 --- a/lib/l10n/arb/app_es.arb +++ b/lib/l10n/arb/app_es.arb @@ -940,7 +940,7 @@ "diveDetailSection_media_name": "Multimedia", "diveDetailSection_notes_description": "Notas/descripcion de la inmersion", "diveDetailSection_notes_name": "Notas", - "diveDetailSection_sacSegments_description": "Segmentacion por fase/tiempo, desglose por cilindro", + "diveDetailSection_sacSegments_description": "Segmentacion SAC por fase/tiempo", "diveDetailSection_sacSegments_name": "SAC Rate por Segmento", "diveDetailSection_sightings_description": "Especies avistadas, detalles del avistamiento", "diveDetailSection_sightings_name": "Avistamientos de Vida Marina", @@ -1225,6 +1225,7 @@ "diveLog_detail_section_sacByCylinder": "SAC por cilindro", "diveLog_detail_section_sacRateBySegment": "Consumo SAC por segmento", "diveLog_detail_section_tags": "Etiquetas", + "diveLog_detail_section_cylinders": "Tanques", "diveLog_detail_section_tanks": "Tanques", "diveLog_detail_section_tide": "Marea", "diveLog_detail_section_trainingSignature": "Firma de entrenamiento", diff --git a/lib/l10n/arb/app_fr.arb b/lib/l10n/arb/app_fr.arb index 36a7d9e3c..3d77c908a 100644 --- a/lib/l10n/arb/app_fr.arb +++ b/lib/l10n/arb/app_fr.arb @@ -1191,6 +1191,7 @@ "diveLog_detail_section_sacByCylinder": "SAC par bloc", "diveLog_detail_section_sacRateBySegment": "Consommation SAC par segment", "diveLog_detail_section_tags": "Tags", + "diveLog_detail_section_cylinders": "Blocs", "diveLog_detail_section_tanks": "Blocs", "diveLog_detail_section_tide": "Maree", "diveLog_detail_section_trainingSignature": "Signature de formation", @@ -4780,7 +4781,7 @@ "diveDetailSection_media_name": "Medias", "diveDetailSection_notes_description": "Notes/description de la plongee", "diveDetailSection_notes_name": "Notes", - "diveDetailSection_sacSegments_description": "Segmentation par phase/temps, repartition par bouteille", + "diveDetailSection_sacSegments_description": "Segmentation SAC par phase/temps", "diveDetailSection_sacSegments_name": "SAC par segment", "diveDetailSection_sightings_description": "Especes observees, details des observations", "diveDetailSection_sightings_name": "Observations de vie marine", diff --git a/lib/l10n/arb/app_he.arb b/lib/l10n/arb/app_he.arb index abc7b71ba..c745f99a0 100644 --- a/lib/l10n/arb/app_he.arb +++ b/lib/l10n/arb/app_he.arb @@ -1191,6 +1191,7 @@ "diveLog_detail_section_sacByCylinder": "SAC לפי בלון", "diveLog_detail_section_sacRateBySegment": "קצב SAC לפי מקטע", "diveLog_detail_section_tags": "תגיות", + "diveLog_detail_section_cylinders": "בלונים", "diveLog_detail_section_tanks": "בלונים", "diveLog_detail_section_tide": "גאות ושפל", "diveLog_detail_section_trainingSignature": "חתימת הכשרה", @@ -3296,7 +3297,7 @@ "diveDetailSection_media_name": "מדיה", "diveDetailSection_notes_description": "הערות/תיאור צלילה", "diveDetailSection_notes_name": "הערות", - "diveDetailSection_sacSegments_description": "פילוח לפי שלב/זמן, פירוט בלונים", + "diveDetailSection_sacSegments_description": "פילוח SAC לפי שלב/זמן", "diveDetailSection_sacSegments_name": "קצב SAC לפי קטע", "diveDetailSection_sightings_description": "מינים שנצפו, פרטי תצפית", "diveDetailSection_sightings_name": "תצפיות חיי ים", diff --git a/lib/l10n/arb/app_hu.arb b/lib/l10n/arb/app_hu.arb index 89b9ecc95..99a75bd3b 100644 --- a/lib/l10n/arb/app_hu.arb +++ b/lib/l10n/arb/app_hu.arb @@ -1191,6 +1191,7 @@ "diveLog_detail_section_sacByCylinder": "SAC palackonkent", "diveLog_detail_section_sacRateBySegment": "SAC ertek szakaszonkent", "diveLog_detail_section_tags": "Cimkek", + "diveLog_detail_section_cylinders": "Palackok", "diveLog_detail_section_tanks": "Palackok", "diveLog_detail_section_tide": "Arapaly", "diveLog_detail_section_trainingSignature": "Kepzesi alairas", @@ -4780,7 +4781,7 @@ "diveDetailSection_media_name": "Média", "diveDetailSection_notes_description": "Merülési jegyzetek/leírás", "diveDetailSection_notes_name": "Jegyzetek", - "diveDetailSection_sacSegments_description": "Fázis/idő szegmentálás, palack lebontás", + "diveDetailSection_sacSegments_description": "SAC szegmentálás fázis/idő szerint", "diveDetailSection_sacSegments_name": "SAC arány szegmensenként", "diveDetailSection_sightings_description": "Megfigyelt fajok, megfigyelési adatok", "diveDetailSection_sightings_name": "Tengeri élőlény megfigyelések", diff --git a/lib/l10n/arb/app_it.arb b/lib/l10n/arb/app_it.arb index 15b4697cb..65a3705e9 100644 --- a/lib/l10n/arb/app_it.arb +++ b/lib/l10n/arb/app_it.arb @@ -1191,6 +1191,7 @@ "diveLog_detail_section_sacByCylinder": "SAC per bombola", "diveLog_detail_section_sacRateBySegment": "Consumo SAC per segmento", "diveLog_detail_section_tags": "Tag", + "diveLog_detail_section_cylinders": "Bombole", "diveLog_detail_section_tanks": "Bombole", "diveLog_detail_section_tide": "Marea", "diveLog_detail_section_trainingSignature": "Firma addestramento", @@ -4819,7 +4820,7 @@ "diveDetailSection_media_name": "Media", "diveDetailSection_notes_description": "Note/descrizione immersione", "diveDetailSection_notes_name": "Note", - "diveDetailSection_sacSegments_description": "Segmentazione per fase/tempo, ripartizione bombole", + "diveDetailSection_sacSegments_description": "Segmentazione SAC per fase/tempo", "diveDetailSection_sacSegments_name": "SAC Rate per segmento", "diveDetailSection_sightings_description": "Specie avvistate, dettagli avvistamento", "diveDetailSection_sightings_name": "Avvistamenti vita marina", diff --git a/lib/l10n/arb/app_localizations.dart b/lib/l10n/arb/app_localizations.dart index 8e99faa79..921a3e92a 100644 --- a/lib/l10n/arb/app_localizations.dart +++ b/lib/l10n/arb/app_localizations.dart @@ -6056,6 +6056,12 @@ abstract class AppLocalizations { /// **'Tags'** String get diveLog_detail_section_tags; + /// No description provided for @diveLog_detail_section_cylinders. + /// + /// In en, this message translates to: + /// **'Cylinders'** + String get diveLog_detail_section_cylinders; + /// No description provided for @diveLog_detail_section_tanks. /// /// In en, this message translates to: @@ -18137,7 +18143,7 @@ abstract class AppLocalizations { /// No description provided for @diveDetailSection_sacSegments_description. /// /// In en, this message translates to: - /// **'Phase/time segmentation, cylinder breakdown'** + /// **'Phase/time SAC segmentation'** String get diveDetailSection_sacSegments_description; /// No description provided for @diveDetailSection_details_name. @@ -18275,13 +18281,13 @@ abstract class AppLocalizations { /// No description provided for @diveDetailSection_tanks_name. /// /// In en, this message translates to: - /// **'Tanks'** + /// **'Cylinders'** String get diveDetailSection_tanks_name; /// No description provided for @diveDetailSection_tanks_description. /// /// In en, this message translates to: - /// **'Tank list, gas mixes, pressures, per-tank SAC'** + /// **'Cylinder list, gas mixes, pressures, MOD/MND, per-tank SAC'** String get diveDetailSection_tanks_description; /// No description provided for @diveDetailSection_buddies_name. diff --git a/lib/l10n/arb/app_localizations_ar.dart b/lib/l10n/arb/app_localizations_ar.dart index dc1c5b5f1..c64b79cdb 100644 --- a/lib/l10n/arb/app_localizations_ar.dart +++ b/lib/l10n/arb/app_localizations_ar.dart @@ -3529,6 +3529,9 @@ class AppLocalizationsAr extends AppLocalizations { @override String get diveLog_detail_section_tags => 'الوسوم'; + @override + String get diveLog_detail_section_cylinders => 'الأسطوانات'; + @override String get diveLog_detail_section_tanks => 'الأسطوانات'; @@ -10443,7 +10446,7 @@ class AppLocalizationsAr extends AppLocalizations { @override String get diveDetailSection_sacSegments_description => - 'تقسيم المراحل/الوقت، تفصيل الأسطوانات'; + 'تقسيم SAC حسب المراحل/الوقت'; @override String get diveDetailSection_details_name => 'التفاصيل'; diff --git a/lib/l10n/arb/app_localizations_de.dart b/lib/l10n/arb/app_localizations_de.dart index b581a4b12..5641854c9 100644 --- a/lib/l10n/arb/app_localizations_de.dart +++ b/lib/l10n/arb/app_localizations_de.dart @@ -3610,6 +3610,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get diveLog_detail_section_tags => 'Tags'; + @override + String get diveLog_detail_section_cylinders => 'Flaschen'; + @override String get diveLog_detail_section_tanks => 'Flaschen'; @@ -10637,7 +10640,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get diveDetailSection_sacSegments_description => - 'Phasen-/Zeitsegmentierung, Flaschenaufteilung'; + 'SAC-Segmentierung nach Phase/Zeit'; @override String get diveDetailSection_details_name => 'Details'; diff --git a/lib/l10n/arb/app_localizations_en.dart b/lib/l10n/arb/app_localizations_en.dart index a142ee770..effd97f0b 100644 --- a/lib/l10n/arb/app_localizations_en.dart +++ b/lib/l10n/arb/app_localizations_en.dart @@ -3543,6 +3543,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get diveLog_detail_section_tags => 'Tags'; + @override + String get diveLog_detail_section_cylinders => 'Cylinders'; + @override String get diveLog_detail_section_tanks => 'Tanks'; @@ -10472,7 +10475,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get diveDetailSection_sacSegments_description => - 'Phase/time segmentation, cylinder breakdown'; + 'Phase/time SAC segmentation'; @override String get diveDetailSection_details_name => 'Details'; @@ -10548,11 +10551,11 @@ class AppLocalizationsEn extends AppLocalizations { 'Weight breakdown, total weight'; @override - String get diveDetailSection_tanks_name => 'Tanks'; + String get diveDetailSection_tanks_name => 'Cylinders'; @override String get diveDetailSection_tanks_description => - 'Tank list, gas mixes, pressures, per-tank SAC'; + 'Cylinder list, gas mixes, pressures, MOD/MND, per-tank SAC'; @override String get diveDetailSection_buddies_name => 'Buddies'; diff --git a/lib/l10n/arb/app_localizations_es.dart b/lib/l10n/arb/app_localizations_es.dart index b70681bf0..0eb8f6e54 100644 --- a/lib/l10n/arb/app_localizations_es.dart +++ b/lib/l10n/arb/app_localizations_es.dart @@ -3604,6 +3604,9 @@ class AppLocalizationsEs extends AppLocalizations { @override String get diveLog_detail_section_tags => 'Etiquetas'; + @override + String get diveLog_detail_section_cylinders => 'Tanques'; + @override String get diveLog_detail_section_tanks => 'Tanques'; @@ -10630,7 +10633,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get diveDetailSection_sacSegments_description => - 'Segmentacion por fase/tiempo, desglose por cilindro'; + 'Segmentacion SAC por fase/tiempo'; @override String get diveDetailSection_details_name => 'Detalles'; diff --git a/lib/l10n/arb/app_localizations_fr.dart b/lib/l10n/arb/app_localizations_fr.dart index 83b6ea6fe..d705652b8 100644 --- a/lib/l10n/arb/app_localizations_fr.dart +++ b/lib/l10n/arb/app_localizations_fr.dart @@ -3620,6 +3620,9 @@ class AppLocalizationsFr extends AppLocalizations { @override String get diveLog_detail_section_tags => 'Tags'; + @override + String get diveLog_detail_section_cylinders => 'Blocs'; + @override String get diveLog_detail_section_tanks => 'Blocs'; @@ -10679,7 +10682,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get diveDetailSection_sacSegments_description => - 'Segmentation par phase/temps, repartition par bouteille'; + 'Segmentation SAC par phase/temps'; @override String get diveDetailSection_details_name => 'Details'; diff --git a/lib/l10n/arb/app_localizations_he.dart b/lib/l10n/arb/app_localizations_he.dart index e603e393d..c918691eb 100644 --- a/lib/l10n/arb/app_localizations_he.dart +++ b/lib/l10n/arb/app_localizations_he.dart @@ -3509,6 +3509,9 @@ class AppLocalizationsHe extends AppLocalizations { @override String get diveLog_detail_section_tags => 'תגיות'; + @override + String get diveLog_detail_section_cylinders => 'בלונים'; + @override String get diveLog_detail_section_tanks => 'בלונים'; @@ -10368,7 +10371,7 @@ class AppLocalizationsHe extends AppLocalizations { @override String get diveDetailSection_sacSegments_description => - 'פילוח לפי שלב/זמן, פירוט בלונים'; + 'פילוח SAC לפי שלב/זמן'; @override String get diveDetailSection_details_name => 'פרטים'; diff --git a/lib/l10n/arb/app_localizations_hu.dart b/lib/l10n/arb/app_localizations_hu.dart index 51a7219c2..e0ca2c647 100644 --- a/lib/l10n/arb/app_localizations_hu.dart +++ b/lib/l10n/arb/app_localizations_hu.dart @@ -3590,6 +3590,9 @@ class AppLocalizationsHu extends AppLocalizations { @override String get diveLog_detail_section_tags => 'Cimkek'; + @override + String get diveLog_detail_section_cylinders => 'Palackok'; + @override String get diveLog_detail_section_tanks => 'Palackok'; @@ -10614,7 +10617,7 @@ class AppLocalizationsHu extends AppLocalizations { @override String get diveDetailSection_sacSegments_description => - 'Fázis/idő szegmentálás, palack lebontás'; + 'SAC szegmentálás fázis/idő szerint'; @override String get diveDetailSection_details_name => 'Részletek'; diff --git a/lib/l10n/arb/app_localizations_it.dart b/lib/l10n/arb/app_localizations_it.dart index f553e8c47..6b23c9c7f 100644 --- a/lib/l10n/arb/app_localizations_it.dart +++ b/lib/l10n/arb/app_localizations_it.dart @@ -3607,6 +3607,9 @@ class AppLocalizationsIt extends AppLocalizations { @override String get diveLog_detail_section_tags => 'Tag'; + @override + String get diveLog_detail_section_cylinders => 'Bombole'; + @override String get diveLog_detail_section_tanks => 'Bombole'; @@ -10643,7 +10646,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get diveDetailSection_sacSegments_description => - 'Segmentazione per fase/tempo, ripartizione bombole'; + 'Segmentazione SAC per fase/tempo'; @override String get diveDetailSection_details_name => 'Dettagli'; diff --git a/lib/l10n/arb/app_localizations_nl.dart b/lib/l10n/arb/app_localizations_nl.dart index f11a1c32b..7948d7842 100644 --- a/lib/l10n/arb/app_localizations_nl.dart +++ b/lib/l10n/arb/app_localizations_nl.dart @@ -3578,6 +3578,9 @@ class AppLocalizationsNl extends AppLocalizations { @override String get diveLog_detail_section_tags => 'Tags'; + @override + String get diveLog_detail_section_cylinders => 'Flessen'; + @override String get diveLog_detail_section_tanks => 'Flessen'; @@ -10564,7 +10567,7 @@ class AppLocalizationsNl extends AppLocalizations { @override String get diveDetailSection_sacSegments_description => - 'Fase-/tijdsegmentatie, flesoverzicht'; + 'SAC-segmentatie per fase/tijd'; @override String get diveDetailSection_details_name => 'Details'; diff --git a/lib/l10n/arb/app_localizations_pt.dart b/lib/l10n/arb/app_localizations_pt.dart index c1b4bd3b6..9b250e170 100644 --- a/lib/l10n/arb/app_localizations_pt.dart +++ b/lib/l10n/arb/app_localizations_pt.dart @@ -3605,6 +3605,9 @@ class AppLocalizationsPt extends AppLocalizations { @override String get diveLog_detail_section_tags => 'Tags'; + @override + String get diveLog_detail_section_cylinders => 'Cilindros'; + @override String get diveLog_detail_section_tanks => 'Cilindros'; @@ -10647,7 +10650,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String get diveDetailSection_sacSegments_description => - 'Segmentacao por fase/tempo, detalhamento de cilindros'; + 'Segmentacao SAC por fase/tempo'; @override String get diveDetailSection_details_name => 'Detalhes'; diff --git a/lib/l10n/arb/app_localizations_zh.dart b/lib/l10n/arb/app_localizations_zh.dart index 1ef3eda51..51d694d69 100644 --- a/lib/l10n/arb/app_localizations_zh.dart +++ b/lib/l10n/arb/app_localizations_zh.dart @@ -3436,6 +3436,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get diveLog_detail_section_tags => '标签'; + @override + String get diveLog_detail_section_cylinders => '气瓶'; + @override String get diveLog_detail_section_tanks => '气瓶'; @@ -10165,7 +10168,7 @@ class AppLocalizationsZh extends AppLocalizations { String get diveDetailSection_sacSegments_name => '分段耗气率'; @override - String get diveDetailSection_sacSegments_description => '阶段/时间分段、气瓶分解'; + String get diveDetailSection_sacSegments_description => '按阶段/时间的SAC分段'; @override String get diveDetailSection_details_name => '详情'; diff --git a/lib/l10n/arb/app_nl.arb b/lib/l10n/arb/app_nl.arb index 3af1aff58..187bdf465 100644 --- a/lib/l10n/arb/app_nl.arb +++ b/lib/l10n/arb/app_nl.arb @@ -940,7 +940,7 @@ "diveDetailSection_media_name": "Media", "diveDetailSection_notes_description": "Duiknotities/omschrijving", "diveDetailSection_notes_name": "Notities", - "diveDetailSection_sacSegments_description": "Fase-/tijdsegmentatie, flesoverzicht", + "diveDetailSection_sacSegments_description": "SAC-segmentatie per fase/tijd", "diveDetailSection_sacSegments_name": "SAC-snelheid per segment", "diveDetailSection_sightings_description": "Waargenomen soorten, waarnemingsdetails", "diveDetailSection_sightings_name": "Mariene levens waarnemingen", @@ -1225,6 +1225,7 @@ "diveLog_detail_section_sacByCylinder": "SAC per fles", "diveLog_detail_section_sacRateBySegment": "SAC-verbruik per segment", "diveLog_detail_section_tags": "Tags", + "diveLog_detail_section_cylinders": "Flessen", "diveLog_detail_section_tanks": "Flessen", "diveLog_detail_section_tide": "Getij", "diveLog_detail_section_trainingSignature": "Trainingshandtekening", diff --git a/lib/l10n/arb/app_pt.arb b/lib/l10n/arb/app_pt.arb index 1f94b6df2..d8b755a2d 100644 --- a/lib/l10n/arb/app_pt.arb +++ b/lib/l10n/arb/app_pt.arb @@ -940,7 +940,7 @@ "diveDetailSection_media_name": "Midia", "diveDetailSection_notes_description": "Notas/descricao do mergulho", "diveDetailSection_notes_name": "Notas", - "diveDetailSection_sacSegments_description": "Segmentacao por fase/tempo, detalhamento de cilindros", + "diveDetailSection_sacSegments_description": "Segmentacao SAC por fase/tempo", "diveDetailSection_sacSegments_name": "Taxa SAC por Segmento", "diveDetailSection_sightings_description": "Especies avistadas, detalhes dos avistamentos", "diveDetailSection_sightings_name": "Avistamentos de Vida Marinha", @@ -1225,6 +1225,7 @@ "diveLog_detail_section_sacByCylinder": "SAC por Cilindro", "diveLog_detail_section_sacRateBySegment": "Taxa SAC por Segmento", "diveLog_detail_section_tags": "Tags", + "diveLog_detail_section_cylinders": "Cilindros", "diveLog_detail_section_tanks": "Cilindros", "diveLog_detail_section_tide": "Mare", "diveLog_detail_section_trainingSignature": "Assinatura de Treinamento", diff --git a/lib/l10n/arb/app_zh.arb b/lib/l10n/arb/app_zh.arb index cd4154b8a..9c0fb6cb2 100644 --- a/lib/l10n/arb/app_zh.arb +++ b/lib/l10n/arb/app_zh.arb @@ -1032,7 +1032,7 @@ "diveDetailSection_media_name": "媒体", "diveDetailSection_notes_description": "潜水备注/描述", "diveDetailSection_notes_name": "备注", - "diveDetailSection_sacSegments_description": "阶段/时间分段、气瓶分解", + "diveDetailSection_sacSegments_description": "按阶段/时间的SAC分段", "diveDetailSection_sacSegments_name": "分段耗气率", "diveDetailSection_sightings_description": "观察到的物种、目击详情", "diveDetailSection_sightings_name": "海洋生物目击", @@ -1332,6 +1332,7 @@ "diveLog_detail_section_sacByCylinder": "按气瓶的气体消耗率", "diveLog_detail_section_sacRateBySegment": "按分段的气体消耗率", "diveLog_detail_section_tags": "标签", + "diveLog_detail_section_cylinders": "气瓶", "diveLog_detail_section_tanks": "气瓶", "diveLog_detail_section_tide": "潮汐", "diveLog_detail_section_trainingSignature": "培训签名", diff --git a/test/core/constants/dive_detail_sections_test.dart b/test/core/constants/dive_detail_sections_test.dart index 0f5e589b2..fd6a06df6 100644 --- a/test/core/constants/dive_detail_sections_test.dart +++ b/test/core/constants/dive_detail_sections_test.dart @@ -399,7 +399,7 @@ void main() { expect(DiveDetailSectionId.altitude.displayName, 'Altitude'); expect(DiveDetailSectionId.tide.displayName, 'Tide'); expect(DiveDetailSectionId.weights.displayName, 'Weights'); - expect(DiveDetailSectionId.tanks.displayName, 'Tanks'); + expect(DiveDetailSectionId.tanks.displayName, 'Cylinders'); expect(DiveDetailSectionId.buddies.displayName, 'Buddies'); expect(DiveDetailSectionId.signatures.displayName, 'Signatures'); expect(DiveDetailSectionId.equipment.displayName, 'Equipment'); @@ -421,7 +421,7 @@ void main() { ); expect( DiveDetailSectionId.sacSegments.description, - 'Phase/time segmentation, cylinder breakdown', + 'Phase/time SAC segmentation', ); expect( DiveDetailSectionId.details.description, @@ -445,7 +445,7 @@ void main() { ); expect( DiveDetailSectionId.tanks.description, - 'Tank list, gas mixes, pressures, per-tank SAC', + 'Cylinder list, gas mixes, pressures, MOD/MND, per-tank SAC', ); expect(DiveDetailSectionId.buddies.description, 'Buddy list with roles'); expect( @@ -532,7 +532,7 @@ void main() { ); expect(DiveDetailSectionId.tide.localizedDisplayName(l10n), 'Tide'); expect(DiveDetailSectionId.weights.localizedDisplayName(l10n), 'Weights'); - expect(DiveDetailSectionId.tanks.localizedDisplayName(l10n), 'Tanks'); + expect(DiveDetailSectionId.tanks.localizedDisplayName(l10n), 'Cylinders'); expect(DiveDetailSectionId.buddies.localizedDisplayName(l10n), 'Buddies'); expect( DiveDetailSectionId.signatures.localizedDisplayName(l10n), @@ -566,7 +566,7 @@ void main() { ); expect( DiveDetailSectionId.sacSegments.localizedDescription(l10n), - 'Phase/time segmentation, cylinder breakdown', + 'Phase/time SAC segmentation', ); expect( DiveDetailSectionId.details.localizedDescription(l10n), @@ -590,7 +590,7 @@ void main() { ); expect( DiveDetailSectionId.tanks.localizedDescription(l10n), - 'Tank list, gas mixes, pressures, per-tank SAC', + 'Cylinder list, gas mixes, pressures, MOD/MND, per-tank SAC', ); expect( DiveDetailSectionId.buddies.localizedDescription(l10n), diff --git a/test/features/settings/presentation/pages/dive_detail_sections_page_test.dart b/test/features/settings/presentation/pages/dive_detail_sections_page_test.dart index f6a03a647..24ed018d0 100644 --- a/test/features/settings/presentation/pages/dive_detail_sections_page_test.dart +++ b/test/features/settings/presentation/pages/dive_detail_sections_page_test.dart @@ -310,7 +310,7 @@ void main() { ); await tester.pumpAndSettle(); - // First section should be Tanks, second should be Deco Status + // First section should be Cylinders (tanks id), second Deco Status final titles = tester.widgetList( find.descendant(of: find.byType(ListTile), matching: find.byType(Text)), ); @@ -323,7 +323,7 @@ void main() { DiveDetailSectionId.values.any((id) => id.displayName == d), ) .toList(); - expect(displayNames.first, 'Tanks'); + expect(displayNames.first, 'Cylinders'); }); testWidgets('shows app bar title', (tester) async { From 964f532ae65c5ba6c2c71a263241762322759bfb Mon Sep 17 00:00:00 2001 From: Eric Griffin Date: Sun, 5 Jul 2026 16:36:11 -0400 Subject: [PATCH 3/8] feat(dive-detail): add unified CylindersCard widget --- .../presentation/widgets/cylinders_card.dart | 265 ++++++++++++++++ .../widgets/cylinders_card_test.dart | 288 ++++++++++++++++++ 2 files changed, 553 insertions(+) create mode 100644 lib/features/dive_log/presentation/widgets/cylinders_card.dart create mode 100644 test/features/dive_log/presentation/widgets/cylinders_card_test.dart diff --git a/lib/features/dive_log/presentation/widgets/cylinders_card.dart b/lib/features/dive_log/presentation/widgets/cylinders_card.dart new file mode 100644 index 000000000..c61c49cbe --- /dev/null +++ b/lib/features/dive_log/presentation/widgets/cylinders_card.dart @@ -0,0 +1,265 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:submersion/core/constants/tank_presets.dart'; +import 'package:submersion/core/constants/units.dart'; +import 'package:submersion/core/icons/mdi_icons.dart'; +import 'package:submersion/core/providers/async_value_extensions.dart'; +import 'package:submersion/core/utils/unit_formatter.dart'; +import 'package:submersion/features/dive_log/domain/entities/cylinder_sac.dart'; +import 'package:submersion/features/dive_log/domain/entities/dive.dart'; +import 'package:submersion/features/dive_log/domain/entities/dive_data_source.dart'; +import 'package:submersion/features/dive_log/domain/services/source_name_resolver.dart'; +import 'package:submersion/features/dive_log/presentation/providers/dive_providers.dart'; +import 'package:submersion/features/dive_log/presentation/providers/gas_analysis_providers.dart'; +import 'package:submersion/features/dive_log/presentation/widgets/field_attribution_badge.dart'; +import 'package:submersion/features/settings/presentation/providers/settings_providers.dart'; +import 'package:submersion/l10n/l10n_extension.dart'; + +/// Unified card showing every cylinder on a dive: identity (name, gas mix, +/// volume), start/end pressures, MOD/MND, and per-tank SAC. +/// +/// Replaces the former Tanks card and SAC by Cylinder block. Occupies the +/// [DiveDetailSectionId.tanks] slot on the dive detail page. Per-tank SAC +/// is shown whenever it is computable, regardless of tank count; the +/// trailing block is omitted entirely when it is not. +class CylindersCard extends ConsumerWidget { + const CylindersCard({ + super.key, + required this.dive, + required this.units, + required this.settings, + required this.sacUnit, + }); + + final Dive dive; + final UnitFormatter units; + final AppSettings settings; + final SacUnit sacUnit; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final tankPressures = ref.watch(tankPressuresProvider(dive.id)).valueOrNull; + final cylinderSacs = + ref.watch(cylinderSacProvider(dive.id)).valueOrNull ?? + const []; + final sacByTankId = {for (final c in cylinderSacs) c.tankId: c}; + final dataSources = + ref.watch(diveDataSourcesProvider(dive.id)).valueOrNull ?? + const []; + // Only badge tanks once there's more than one source to disambiguate — + // a single-source dive never needs attribution. + final showSourceBadges = dataSources.length >= 2; + final computerNames = _computerDisplayNames(context, dataSources); + + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.diveLog_detail_section_cylinders, + style: Theme.of(context).textTheme.titleMedium, + ), + const Divider(), + ...dive.tanks.asMap().entries.map( + (entry) => _tankRow( + context, + index: entry.key, + tank: entry.value, + cylinderSac: sacByTankId[entry.value.id], + tankPressures: tankPressures, + sourceName: showSourceBadges && entry.value.computerId != null + ? computerNames[entry.value.computerId] + : null, + ), + ), + ], + ), + ), + ); + } + + Widget _tankRow( + BuildContext context, { + required int index, + required DiveTank tank, + required CylinderSac? cylinderSac, + required Map>? tankPressures, + required String? sourceName, + }) { + final theme = Theme.of(context); + + final pressures = _resolveTankPressures( + tank: tank, + tankPressures: tankPressures, + ); + final startP = units.formatPressureValue(pressures.$1); + final endP = units.formatPressureValue(pressures.$2); + final pressureUsed = pressures.$1 != null && pressures.$2 != null + ? pressures.$1! - pressures.$2! + : null; + final used = pressureUsed != null && pressureUsed > 0 + ? ' (${units.formatPressure(pressureUsed)} used)' + : ''; + + // Preset display name, falling back to formatted volume. + final preset = tank.presetName != null + ? TankPresets.byName(tank.presetName!) + : null; + final tankLabel = + preset?.displayName ?? + (tank.volume != null + ? units.formatTankVolume( + tank.volume, + tank.workingPressure, + decimals: 1, + ) + : null); + final tankTitle = tank.name != null && tank.name!.isNotEmpty + ? tank.name! + : context.l10n.diveLog_tank_title(index + 1); + + final modDepth = units.formatDepth(tank.gasMix.mod(), decimals: 0); + final mndValue = tank.gasMix.mnd( + endLimit: settings.endLimit, + o2Narcotic: settings.o2Narcotic, + ); + final mndDepth = mndValue.isFinite + ? units.formatDepth(mndValue, decimals: 0) + : '--'; + final modMndText = context.l10n.diveLog_tank_modMndInfo(modDepth, mndDepth); + + return ListTile( + contentPadding: EdgeInsets.zero, + leading: const Icon(MdiIcons.divingScubaTank), + title: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible(child: Text('$tankTitle (${tank.gasMix.name})')), + if (tankLabel != null) _volumeChip(theme, tankLabel), + ], + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '$startP ${units.pressureSymbol} → ' + '$endP ${units.pressureSymbol}$used', + ), + Text( + modMndText, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.tertiary, + ), + ), + ], + ), + trailing: _trailingBlock(theme, cylinderSac, sourceName), + ); + } + + /// Small outlined chip carrying the preset/volume label (e.g. "AL80"). + Widget _volumeChip(ThemeData theme, String label) { + return Container( + margin: const EdgeInsetsDirectional.only(start: 6), + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1), + decoration: BoxDecoration( + border: Border.all(color: theme.colorScheme.outlineVariant), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + label, + style: theme.textTheme.labelSmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ); + } + + /// Trailing column: attribution badge, SAC rate, gas used in liters. + /// Returns null when there is nothing to show so the tile keeps its + /// natural width. + Widget? _trailingBlock( + ThemeData theme, + CylinderSac? cylinderSac, + String? sourceName, + ) { + final hasSac = cylinderSac != null && cylinderSac.hasValidSac; + if (!hasSac && sourceName == null) return null; + return Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (sourceName != null) FieldAttributionBadge(sourceName: sourceName), + if (hasSac) ...[ + Text( + _formatSac(cylinderSac), + style: theme.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.bold, + color: theme.colorScheme.primary, + ), + ), + if (cylinderSac.gasUsedLiters != null) + Text( + '${units.convertVolume(cylinderSac.gasUsedLiters!).round()} ' + '${units.volumeSymbol} used', + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ], + ], + ); + } + + /// Formats the SAC value per the diver's SAC unit preference. L/min needs + /// a tank volume; otherwise falls back to pressure-drop per minute. + /// Only called when [CylinderSac.hasValidSac] is true. + String _formatSac(CylinderSac cylinder) { + if (sacUnit == SacUnit.litersPerMin && cylinder.sacVolume != null) { + final value = units.convertVolume(cylinder.sacVolume!); + return '${value.toStringAsFixed(1)} ${units.volumeSymbol}/min'; + } + final value = units.convertPressure(cylinder.sacRate!); + return '${value.toStringAsFixed(1)} ${units.pressureSymbol}/min'; + } + + /// Resolves start/end pressure: stored tank metadata wins, per-tank + /// time-series fills any nulls. + (double?, double?) _resolveTankPressures({ + required DiveTank tank, + required Map>? tankPressures, + }) { + if (tankPressures != null && tankPressures.containsKey(tank.id)) { + final points = tankPressures[tank.id]!; + if (points.isNotEmpty) { + return ( + tank.startPressure ?? points.first.pressure, + tank.endPressure ?? points.last.pressure, + ); + } + } + return (tank.startPressure, tank.endPressure); + } + + /// computerId -> display name via the shared source-name resolver. + Map _computerDisplayNames( + BuildContext context, + List dataSources, + ) { + final labels = SourceNameLabels( + unknownComputer: context.l10n.diveLog_sources_unknownComputer, + manualEntry: context.l10n.diveLog_sources_manualEntry, + importedFile: context.l10n.diveLog_sources_importedFile, + editedSuffix: context.l10n.diveLog_sources_editedSuffix, + ); + return { + for (final source in dataSources) + if (source.computerId != null) + source.computerId!: resolveSourceName(source, labels), + }; + } +} diff --git a/test/features/dive_log/presentation/widgets/cylinders_card_test.dart b/test/features/dive_log/presentation/widgets/cylinders_card_test.dart new file mode 100644 index 000000000..bda4adb7b --- /dev/null +++ b/test/features/dive_log/presentation/widgets/cylinders_card_test.dart @@ -0,0 +1,288 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:submersion/core/constants/enums.dart'; +import 'package:submersion/core/constants/units.dart'; +import 'package:submersion/core/utils/unit_formatter.dart'; +import 'package:submersion/features/dive_log/domain/entities/cylinder_sac.dart'; +import 'package:submersion/features/dive_log/domain/entities/dive.dart'; +import 'package:submersion/features/dive_log/domain/entities/dive_data_source.dart'; +import 'package:submersion/features/dive_log/presentation/providers/dive_providers.dart'; +import 'package:submersion/features/dive_log/presentation/providers/gas_analysis_providers.dart'; +import 'package:submersion/features/dive_log/presentation/widgets/cylinders_card.dart'; +import 'package:submersion/features/dive_log/presentation/widgets/field_attribution_badge.dart'; +import 'package:submersion/features/settings/presentation/providers/settings_providers.dart'; + +import '../../../../helpers/test_app.dart'; + +const _settings = AppSettings(); +const _units = UnitFormatter(_settings); + +DiveTank _makeTank({ + String id = 'tank-1', + String? name, + double? volume = 11.1, + double? startPressure = 200, + double? endPressure = 50, + GasMix gasMix = const GasMix(o2: 32), + String? computerId, +}) { + return DiveTank( + id: id, + name: name, + volume: volume, + startPressure: startPressure, + endPressure: endPressure, + gasMix: gasMix, + computerId: computerId, + ); +} + +Dive _makeDive(List tanks) { + return Dive( + id: 'dive-1', + diveNumber: 1, + dateTime: DateTime(2026, 6, 1, 10, 0), + maxDepth: 30.0, + avgDepth: 18.0, + bottomTime: const Duration(minutes: 45), + tanks: tanks, + ); +} + +CylinderSac _makeSac({ + String tankId = 'tank-1', + double? sacRate = 2.0, + double? tankVolume = 11.1, + double? startPressure = 200, + double? endPressure = 50, +}) { + return CylinderSac( + tankId: tankId, + gasMix: const GasMix(o2: 32), + role: TankRole.backGas, + tankVolume: tankVolume, + sacRate: sacRate, + startPressure: startPressure, + endPressure: endPressure, + ); +} + +DiveDataSource _makeSource({ + required String id, + String? computerId, + bool isPrimary = false, + String? computerModel, +}) { + final now = DateTime(2026, 6, 1, 10, 0); + return DiveDataSource( + id: id, + diveId: 'dive-1', + computerId: computerId, + isPrimary: isPrimary, + computerModel: computerModel, + entryTime: now, + exitTime: now.add(const Duration(minutes: 45)), + importedAt: now, + createdAt: now, + ); +} + +Widget _buildCard({ + required Dive dive, + List cylinderSacs = const [], + List dataSources = const [], + UnitFormatter units = _units, + AppSettings settings = _settings, + SacUnit sacUnit = SacUnit.pressurePerMin, +}) { + return testApp( + overrides: [ + cylinderSacProvider.overrideWith((ref, id) async => cylinderSacs), + tankPressuresProvider.overrideWith( + (ref, id) async => >{}, + ), + diveDataSourcesProvider.overrideWith((ref, id) async => dataSources), + ], + child: SingleChildScrollView( + child: CylindersCard( + dive: dive, + units: units, + settings: settings, + sacUnit: sacUnit, + ), + ), + ); +} + +void main() { + group('CylindersCard', () { + testWidgets('renders title, tank identity, pressures, and MOD/MND', ( + tester, + ) async { + await tester.pumpWidget( + _buildCard(dive: _makeDive([_makeTank()]), cylinderSacs: [_makeSac()]), + ); + await tester.pumpAndSettle(); + + expect(find.text('Cylinders'), findsOneWidget); + expect(find.textContaining('Tank 1 (EAN32)'), findsOneWidget); + expect( + find.textContaining('200 bar → 50 bar (150 bar used)'), + findsOneWidget, + ); + expect(find.textContaining('MOD:'), findsOneWidget); + expect(find.textContaining('MND:'), findsOneWidget); + }); + + testWidgets('shows SAC and gas used on a single-tank dive', (tester) async { + await tester.pumpWidget( + _buildCard(dive: _makeDive([_makeTank()]), cylinderSacs: [_makeSac()]), + ); + await tester.pumpAndSettle(); + + // sacRate 2.0 bar/min, pressurePerMin mode, metric. + expect(find.text('2.0 bar/min'), findsOneWidget); + // gasUsedLiters = (200 - 50) * 11.1 = 1665 L. + expect(find.text('1665 L used'), findsOneWidget); + }); + + testWidgets('omits the SAC block when SAC is not computable', ( + tester, + ) async { + await tester.pumpWidget( + _buildCard( + dive: _makeDive([_makeTank(startPressure: null, endPressure: null)]), + cylinderSacs: [ + _makeSac(sacRate: null, startPressure: null, endPressure: null), + ], + ), + ); + await tester.pumpAndSettle(); + + expect(find.textContaining('Tank 1'), findsOneWidget); + expect(find.textContaining('/min'), findsNothing); + expect(find.textContaining('used'), findsNothing); + }); + + testWidgets('shows one row with distinct SAC per tank on multi-tank dive', ( + tester, + ) async { + await tester.pumpWidget( + _buildCard( + dive: _makeDive([ + _makeTank(), + _makeTank( + id: 'tank-2', + name: 'Deco O2', + volume: 5.7, + startPressure: 200, + endPressure: 140, + gasMix: const GasMix(o2: 100), + ), + ]), + cylinderSacs: [ + _makeSac(), + _makeSac( + tankId: 'tank-2', + sacRate: 1.2, + tankVolume: 5.7, + startPressure: 200, + endPressure: 140, + ), + ], + ), + ); + await tester.pumpAndSettle(); + + expect(find.text('2.0 bar/min'), findsOneWidget); + expect(find.text('1.2 bar/min'), findsOneWidget); + expect(find.textContaining('Deco O2'), findsOneWidget); + }); + + testWidgets('formats SAC as L/min when unit is litersPerMin', ( + tester, + ) async { + await tester.pumpWidget( + _buildCard( + dive: _makeDive([_makeTank()]), + cylinderSacs: [_makeSac()], + sacUnit: SacUnit.litersPerMin, + ), + ); + await tester.pumpAndSettle(); + + // sacVolume = 2.0 * 11.1 / 1.01325 = 21.909... -> '21.9 L/min'. + expect(find.text('21.9 L/min'), findsOneWidget); + }); + + testWidgets('formats pressures and SAC in imperial units', (tester) async { + const imperialSettings = AppSettings( + pressureUnit: PressureUnit.psi, + volumeUnit: VolumeUnit.cubicFeet, + depthUnit: DepthUnit.feet, + ); + await tester.pumpWidget( + _buildCard( + dive: _makeDive([_makeTank()]), + cylinderSacs: [_makeSac()], + units: const UnitFormatter(imperialSettings), + settings: imperialSettings, + ), + ); + await tester.pumpAndSettle(); + + // 2.0 bar/min * 14.5038 = 29.0076 -> '29.0 psi/min'. + expect(find.text('29.0 psi/min'), findsOneWidget); + // Pressure line rendered in psi. + expect(find.textContaining('psi →'), findsOneWidget); + }); + + testWidgets('hides source badge with a single data source', (tester) async { + // Riverpod ignores override changes on an in-place ProviderScope + // rebuild, so the single-source and multi-source cases live in + // separate tests with fresh scopes. + await tester.pumpWidget( + _buildCard( + dive: _makeDive([_makeTank(computerId: 'comp-1')]), + cylinderSacs: [_makeSac()], + dataSources: [ + _makeSource( + id: 'src-1', + computerId: 'comp-1', + isPrimary: true, + computerModel: 'Perdix 2', + ), + ], + ), + ); + await tester.pumpAndSettle(); + expect(find.byType(FieldAttributionBadge), findsNothing); + }); + + testWidgets('shows source badge with two or more data sources', ( + tester, + ) async { + await tester.pumpWidget( + _buildCard( + dive: _makeDive([_makeTank(computerId: 'comp-1')]), + cylinderSacs: [_makeSac()], + dataSources: [ + _makeSource( + id: 'src-1', + computerId: 'comp-1', + isPrimary: true, + computerModel: 'Perdix 2', + ), + _makeSource( + id: 'src-2', + computerId: 'comp-2', + computerModel: 'Teric', + ), + ], + ), + ); + await tester.pumpAndSettle(); + expect(find.text('Perdix 2'), findsOneWidget); + }); + }); +} From 1a6a6966ed056d5d3ae8b4cfd7f7d2f21252e9af Mon Sep 17 00:00:00 2001 From: Eric Griffin Date: Sun, 5 Jul 2026 16:38:59 -0400 Subject: [PATCH 4/8] feat(dive-detail): replace Tanks and SAC by Cylinder cards with CylindersCard --- .../presentation/pages/dive_detail_page.dart | 323 +----------------- 1 file changed, 8 insertions(+), 315 deletions(-) diff --git a/lib/features/dive_log/presentation/pages/dive_detail_page.dart b/lib/features/dive_log/presentation/pages/dive_detail_page.dart index 3c7fe1d8a..54e575f83 100644 --- a/lib/features/dive_log/presentation/pages/dive_detail_page.dart +++ b/lib/features/dive_log/presentation/pages/dive_detail_page.dart @@ -12,7 +12,6 @@ import 'package:submersion/core/constants/dive_detail_sections.dart'; import 'package:submersion/core/constants/enums.dart'; import 'package:submersion/features/marine_life/presentation/utils/species_category_icon.dart'; import 'package:submersion/core/constants/list_view_mode.dart'; -import 'package:submersion/core/constants/tank_presets.dart'; import 'package:submersion/core/constants/units.dart'; import 'package:submersion/core/deco/altitude_calculator.dart'; import 'package:submersion/features/dive_log/presentation/widgets/o2_toxicity_card.dart'; @@ -27,7 +26,6 @@ import 'package:submersion/features/settings/presentation/providers/settings_pro import 'package:submersion/features/dive_log/data/services/gas_usage_segments_service.dart'; import 'package:submersion/features/dive_log/data/services/profile_analysis_service.dart'; import 'package:submersion/features/dive_log/data/services/profile_markers_service.dart'; -import 'package:submersion/features/dive_log/domain/entities/cylinder_sac.dart'; import 'package:submersion/features/dive_log/domain/entities/dive_data_source.dart'; import 'package:submersion/features/dive_log/domain/entities/dive.dart'; import 'package:submersion/features/dive_log/domain/entities/dive_computer.dart'; @@ -47,7 +45,6 @@ import 'package:submersion/features/dive_log/presentation/widgets/collapsible_se import 'package:submersion/features/dive_log/presentation/widgets/dive_locations_map.dart'; import 'package:submersion/features/dive_log/presentation/widgets/surface_gps_section.dart'; import 'package:submersion/features/dive_log/presentation/widgets/data_sources_section.dart'; -import 'package:submersion/features/dive_log/presentation/widgets/field_attribution_badge.dart'; import 'package:submersion/features/dive_log/presentation/widgets/dive_detail_row.dart'; import 'package:submersion/features/dive_log/domain/entities/source_profile.dart'; import 'package:submersion/features/dive_log/domain/services/field_attribution_service.dart'; @@ -56,6 +53,7 @@ import 'package:submersion/features/dive_log/presentation/providers/active_sourc import 'package:submersion/features/dive_log/presentation/widgets/source_bar.dart'; import 'package:submersion/features/dive_log/presentation/widgets/compact_deco_status_card.dart'; import 'package:submersion/features/dive_log/presentation/widgets/compact_tissue_loading_card.dart'; +import 'package:submersion/features/dive_log/presentation/widgets/cylinders_card.dart'; import 'package:submersion/features/dive_log/presentation/widgets/dive_profile_chart.dart'; import 'package:submersion/features/dive_log/presentation/widgets/photo_marker_layout.dart'; import 'package:submersion/features/dive_log/presentation/widgets/playback_controls.dart'; @@ -337,7 +335,12 @@ class _DiveDetailPageState extends ConsumerState { if (dive.tanks.isEmpty) return []; return [ const SizedBox(height: 24), - _buildTanksSection(context, ref, dive, units), + CylindersCard( + dive: dive, + units: units, + settings: settings, + sacUnit: ref.watch(sacUnitProvider), + ), ]; }, DiveDetailSectionId.buddies: () { @@ -1682,27 +1685,12 @@ class _DiveDetailPageState extends ConsumerState { // Check availability of different segmentation methods final hasGasSwitches = ref.watch(hasGasSwitchesProvider(dive.id)).valueOrNull ?? false; - final isMultiTank = - ref.watch(isMultiTankDiveProvider(dive.id)).valueOrNull ?? false; - - // Get cylinder SAC data for multi-tank dives - final cylinderSacAsync = ref.watch(cylinderSacProvider(dive.id)); // Don't show if no segments are, or ever were, available for this dive // (the last-good fallback above keeps a transient null from collapsing it). + // Per-tank SAC lives on the Cylinders card, which renders regardless. if (analysis == null || (analysis.sacSegments == null || analysis.sacSegments!.isEmpty)) { - // Still show cylinder SAC if available - if (isMultiTank && cylinderSacAsync.hasValue) { - return _buildCylinderSacSection( - context, - ref, - dive, - cylinderSacAsync.value!, - units, - sacUnit, - ); - } return const SizedBox.shrink(); } @@ -1914,19 +1902,6 @@ class _DiveDetailPageState extends ConsumerState { ), ), - // Cylinder SAC subsection for multi-tank dives - if (isMultiTank && cylinderSacAsync.hasValue) ...[ - const SizedBox(height: 16), - _buildCylinderSacSection( - context, - ref, - dive, - cylinderSacAsync.value!, - units, - sacUnit, - ), - ], - const SizedBox(height: 24), ], ); @@ -2040,130 +2015,6 @@ class _DiveDetailPageState extends ConsumerState { }; } - /// Build cylinder SAC section for multi-tank dives - Widget _buildCylinderSacSection( - BuildContext context, - WidgetRef ref, - Dive dive, - List cylinderSacs, - UnitFormatter units, - SacUnit sacUnit, - ) { - if (cylinderSacs.isEmpty) return const SizedBox.shrink(); - - final isExpanded = ref.watch(cylinderSacExpandedProvider); - final colorScheme = Theme.of(context).colorScheme; - final textTheme = Theme.of(context).textTheme; - - return CollapsibleCardSection( - title: context.l10n.diveLog_detail_section_sacByCylinder, - icon: MdiIcons.divingScubaTank, - collapsedSubtitle: context.l10n.diveLog_detail_tankCount( - cylinderSacs.length, - ), - isExpanded: isExpanded, - onToggle: (expanded) { - ref.read(cylinderSacExpandedProvider.notifier).state = expanded; - }, - contentBuilder: (context) => Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: cylinderSacs.map((cylinder) { - return Padding( - padding: const EdgeInsets.only(bottom: 12), - child: Row( - children: [ - // Tank icon with role color - Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: colorScheme.primaryContainer.withValues( - alpha: 0.3, - ), - borderRadius: BorderRadius.circular(8), - ), - child: Icon( - MdiIcons.divingScubaTank, - size: 16, - color: colorScheme.primary, - ), - ), - const SizedBox(width: 12), - // Tank info - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - cylinder.displayLabel, - style: textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w500, - ), - ), - Text( - '${cylinder.gasMix.name} • ${cylinder.role.displayName}', - style: textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ), - // SAC value - if (cylinder.hasValidSac) ...[ - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - _formatCylinderSac(cylinder, units, sacUnit), - style: textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.bold, - color: colorScheme.primary, - ), - ), - if (cylinder.gasUsedBar != null) - Text( - '${units.convertPressure(cylinder.gasUsedBar!).round()} ${units.pressureSymbol} used', - style: textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ] else ...[ - Text( - '--', - style: textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - ], - ], - ), - ); - }).toList(), - ), - ), - ); - } - - /// Format cylinder SAC value - String _formatCylinderSac( - CylinderSac cylinder, - UnitFormatter units, - SacUnit sacUnit, - ) { - if (sacUnit == SacUnit.litersPerMin && cylinder.sacVolume != null) { - final value = units.convertVolume(cylinder.sacVolume!); - return '${value.toStringAsFixed(1)} ${units.volumeSymbol}/min'; - } else if (cylinder.sacRate != null) { - final value = units.convertPressure(cylinder.sacRate!); - return '${value.toStringAsFixed(1)} ${units.pressureSymbol}/min'; - } - return '--'; - } - Widget _buildSacBar(BuildContext context, double sacRate, double maxSac) { final colorScheme = Theme.of(context).colorScheme; final percentage = maxSac > 0 ? (sacRate / maxSac).clamp(0.0, 1.0) : 0.0; @@ -3822,164 +3673,6 @@ class _DiveDetailPageState extends ConsumerState { ); } - Widget _buildTanksSection( - BuildContext context, - WidgetRef ref, - Dive dive, - UnitFormatter units, - ) { - final tankPressuresAsync = ref.watch(tankPressuresProvider(dive.id)); - final tankPressures = tankPressuresAsync.valueOrNull; - final dataSources = - ref.watch(diveDataSourcesProvider(dive.id)).valueOrNull ?? const []; - final computerNames = _computerDisplayNames(context, dataSources); - // Only badge tanks once there's more than one source to disambiguate — - // a single-source dive never needs attribution. - final showTankSourceBadges = dataSources.length >= 2; - - return Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - context.l10n.diveLog_detail_section_tanks, - style: Theme.of(context).textTheme.titleMedium, - ), - const Divider(), - ...dive.tanks.asMap().entries.map((entry) { - final index = entry.key; - final tank = entry.value; - - // Derive start/end pressure from time-series data when available, - // falling back to stored tank metadata only as a last resort. - final pressures = _resolveTankPressures( - tank: tank, - tankPressures: tankPressures, - profile: dive.profile, - ); - final startP = units.formatPressureValue(pressures.$1); - final endP = units.formatPressureValue(pressures.$2); - - final pressureUsed = pressures.$1 != null && pressures.$2 != null - ? pressures.$1! - pressures.$2! - : null; - final used = pressureUsed != null && pressureUsed > 0 - ? ' (${units.formatPressure(pressureUsed)} used)' - : ''; - // Get preset display name if available - final preset = tank.presetName != null - ? TankPresets.byName(tank.presetName!) - : null; - final tankLabel = - preset?.displayName ?? - (tank.volume != null - ? units.formatTankVolume( - tank.volume, - tank.workingPressure, - decimals: 1, - ) - : null); - // Use same label as profile chart: dive computer name or "Tank N" - final tankTitle = tank.name != null && tank.name!.isNotEmpty - ? tank.name! - : context.l10n.diveLog_tank_title(index + 1); - // MOD/MND info - final settings = ref.watch(settingsProvider); - final modDepth = units.formatDepth( - tank.gasMix.mod(), - decimals: 0, - ); - final mndValue = tank.gasMix.mnd( - endLimit: settings.endLimit, - o2Narcotic: settings.o2Narcotic, - ); - final mndDepth = mndValue.isFinite - ? units.formatDepth(mndValue, decimals: 0) - : '--'; - final modMndText = context.l10n.diveLog_tank_modMndInfo( - modDepth, - mndDepth, - ); - // Shown for every computer-attributed tank on a - // multi-source dive (the primary's included), so divers can - // tell which instrument's pressure log a tank's numbers came - // from. Manually added tanks carry no attribution. - final showSourceBadge = - showTankSourceBadges && tank.computerId != null; - final trailingChildren = [ - if (showSourceBadge) - Padding( - padding: const EdgeInsets.only(right: 6), - child: FieldAttributionBadge( - sourceName: computerNames[tank.computerId], - ), - ), - if (tankLabel != null) Text(tankLabel), - ]; - return ListTile( - contentPadding: EdgeInsets.zero, - leading: const Icon(MdiIcons.divingScubaTank), - title: Text('$tankTitle (${tank.gasMix.name})'), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '$startP ${units.pressureSymbol} → $endP ${units.pressureSymbol}$used', - ), - Text( - modMndText, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.tertiary, - ), - ), - ], - ), - trailing: trailingChildren.isEmpty - ? null - : Row( - mainAxisSize: MainAxisSize.min, - children: trailingChildren, - ), - ); - }), - ], - ), - ), - ); - } - - /// Resolves the actual start and end pressure for a tank by checking - /// time-series data sources before falling back to stored metadata. - /// - /// Priority: - /// 1. Stored tank metadata (DiveTank.startPressure/endPressure) when non-null - /// 2. Per-tank pressure time-series (TankPressurePoint) — potentially more accurate - /// but maybe not the value preferred by user if they manually entered pressures - (double?, double?) _resolveTankPressures({ - required DiveTank tank, - required Map>? tankPressures, - required List profile, - }) { - // 1. Per-tank pressure time-series - if (tankPressures != null && tankPressures.containsKey(tank.id)) { - final points = tankPressures[tank.id]!; - if (points.isNotEmpty) { - final startPressure = - tank.startPressure ?? - points.first.pressure; // Only use time series if metadata is null - final endPressure = - tank.endPressure ?? - points.last.pressure; // Only use time series if metadata is null - return (startPressure, endPressure); - } - } - - // 2. Stored tank metadata (fallback) - return (tank.startPressure, tank.endPressure); - } - Widget _buildEquipmentSection( BuildContext context, WidgetRef ref, From b437e78de2c85f5610885fa2dc6ecfeddcfeecd5 Mon Sep 17 00:00:00 2001 From: Eric Griffin Date: Sun, 5 Jul 2026 16:41:19 -0400 Subject: [PATCH 5/8] chore(dive-detail): remove dead cylinder SAC card code and obsolete l10n keys --- .../providers/gas_analysis_providers.dart | 3 - .../widgets/cylinder_sac_card.dart | 367 ------------------ .../widgets/dive_profile_legend.dart | 2 +- lib/l10n/arb/app_ar.arb | 3 - lib/l10n/arb/app_de.arb | 3 - lib/l10n/arb/app_en.arb | 10 - lib/l10n/arb/app_es.arb | 3 - lib/l10n/arb/app_fr.arb | 3 - lib/l10n/arb/app_he.arb | 3 - lib/l10n/arb/app_hu.arb | 3 - lib/l10n/arb/app_it.arb | 3 - lib/l10n/arb/app_localizations.dart | 18 - lib/l10n/arb/app_localizations_ar.dart | 17 - lib/l10n/arb/app_localizations_de.dart | 17 - lib/l10n/arb/app_localizations_en.dart | 17 - lib/l10n/arb/app_localizations_es.dart | 17 - lib/l10n/arb/app_localizations_fr.dart | 17 - lib/l10n/arb/app_localizations_he.dart | 17 - lib/l10n/arb/app_localizations_hu.dart | 17 - lib/l10n/arb/app_localizations_it.dart | 17 - lib/l10n/arb/app_localizations_nl.dart | 17 - lib/l10n/arb/app_localizations_pt.dart | 17 - lib/l10n/arb/app_localizations_zh.dart | 17 - lib/l10n/arb/app_nl.arb | 3 - lib/l10n/arb/app_pt.arb | 3 - lib/l10n/arb/app_zh.arb | 3 - .../widgets/dive_profile_legend_test.dart | 2 +- 27 files changed, 2 insertions(+), 617 deletions(-) delete mode 100644 lib/features/dive_log/presentation/widgets/cylinder_sac_card.dart diff --git a/lib/features/dive_log/presentation/providers/gas_analysis_providers.dart b/lib/features/dive_log/presentation/providers/gas_analysis_providers.dart index 158b59029..2e289eba7 100644 --- a/lib/features/dive_log/presentation/providers/gas_analysis_providers.dart +++ b/lib/features/dive_log/presentation/providers/gas_analysis_providers.dart @@ -131,6 +131,3 @@ final isMultiTankDiveProvider = FutureProvider.family(( /// Expanded state for the Gas Analysis section in dive details final gasAnalysisSectionExpandedProvider = StateProvider((ref) => false); - -/// Expanded state for the Cylinder SAC subsection -final cylinderSacExpandedProvider = StateProvider((ref) => true); diff --git a/lib/features/dive_log/presentation/widgets/cylinder_sac_card.dart b/lib/features/dive_log/presentation/widgets/cylinder_sac_card.dart deleted file mode 100644 index 63d3a6a78..000000000 --- a/lib/features/dive_log/presentation/widgets/cylinder_sac_card.dart +++ /dev/null @@ -1,367 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:submersion/core/icons/mdi_icons.dart'; - -import 'package:submersion/core/accessibility/semantic_helpers.dart'; -import 'package:submersion/core/constants/units.dart'; -import 'package:submersion/core/utils/unit_formatter.dart'; -import 'package:submersion/features/dive_log/domain/entities/cylinder_sac.dart'; -import 'package:submersion/l10n/l10n_extension.dart'; - -/// Card displaying per-cylinder SAC (Surface Air Consumption) metrics. -/// -/// Shows tank name, gas mix, SAC rate, pressure usage, and data quality indicator. -class CylinderSacCard extends StatelessWidget { - /// The cylinder SAC data to display - final CylinderSac cylinderSac; - - /// Unit formatter for converting values to user preferences - final UnitFormatter units; - - /// Which SAC unit to display (L/min or bar/min) - final SacUnit sacUnit; - - /// Whether this card is selected/highlighted - final bool isSelected; - - /// Callback when card is tapped - final VoidCallback? onTap; - - const CylinderSacCard({ - super.key, - required this.cylinderSac, - required this.units, - required this.sacUnit, - this.isSelected = false, - this.onTap, - }); - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final textTheme = Theme.of(context).textTheme; - - return Card( - color: isSelected - ? colorScheme.primaryContainer.withValues(alpha: 0.3) - : null, - child: Semantics( - button: onTap != null, - label: listItemLabel( - title: cylinderSac.displayLabel, - subtitle: cylinderSac.gasMix.name, - status: cylinderSac.hasValidSac ? 'SAC: ${_formatSacValue()}' : null, - ), - child: InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(12), - child: Padding( - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header row: Tank name + role badge - Row( - children: [ - // Tank icon - ExcludeSemantics( - child: Icon( - MdiIcons.divingScubaTank, - size: 18, - color: colorScheme.primary, - ), - ), - const SizedBox(width: 8), - // Tank name - Expanded( - child: Text( - cylinderSac.displayLabel, - style: textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.w600, - ), - overflow: TextOverflow.ellipsis, - ), - ), - // Role badge - Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, - vertical: 2, - ), - decoration: BoxDecoration( - color: _getRoleColor( - colorScheme, - ).withValues(alpha: 0.2), - borderRadius: BorderRadius.circular(4), - ), - child: Text( - cylinderSac.role.displayName, - style: textTheme.labelSmall?.copyWith( - color: _getRoleColor(colorScheme), - fontWeight: FontWeight.w500, - ), - ), - ), - ], - ), - - const SizedBox(height: 8), - - // Gas mix - Text( - cylinderSac.gasMix.name, - style: textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - - const SizedBox(height: 12), - - // SAC rate (prominent display) - if (cylinderSac.hasValidSac) ...[ - Row( - children: [ - Text( - _formatSacValue(), - style: textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.bold, - color: colorScheme.primary, - ), - ), - const Spacer(), - // Data quality indicator - _buildDataQualityBadge(context), - ], - ), - ] else ...[ - Text( - context.l10n.diveLog_cylinderSac_noSac, - style: textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - ], - - const SizedBox(height: 8), - - // Pressure usage - if (cylinderSac.startPressure != null && - cylinderSac.endPressure != null) ...[ - Row( - children: [ - ExcludeSemantics( - child: Icon( - Icons.trending_down, - size: 14, - color: colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(width: 4), - Text( - _formatPressureRange(), - style: textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - const Spacer(), - // Usage duration - if (cylinderSac.usageDuration != null) ...[ - ExcludeSemantics( - child: Icon( - Icons.timer_outlined, - size: 14, - color: colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(width: 4), - Text( - cylinderSac.durationFormatted, - style: textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - ], - ], - ), - ], - - // Average depth during use - if (cylinderSac.avgDepthDuringUse != null) ...[ - const SizedBox(height: 4), - Row( - children: [ - ExcludeSemantics( - child: Icon( - Icons.straighten, - size: 14, - color: colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(width: 4), - Text( - context.l10n.diveLog_cylinderSac_avgDepth( - units.formatDepth(cylinderSac.avgDepthDuringUse!), - ), - style: textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ], - ], - ), - ), - ), - ), - ); - } - - /// Format SAC value based on unit setting - String _formatSacValue() { - if (sacUnit == SacUnit.litersPerMin && cylinderSac.sacVolume != null) { - final value = units.convertVolume(cylinderSac.sacVolume!); - return '${value.toStringAsFixed(1)} ${units.volumeSymbol}/min'; - } else if (cylinderSac.sacRate != null) { - final value = units.convertPressure(cylinderSac.sacRate!); - return '${value.toStringAsFixed(1)} ${units.pressureSymbol}/min'; - } - return '--'; - } - - /// Format pressure range (start → end) - String _formatPressureRange() { - final start = units.convertPressure(cylinderSac.startPressure!); - final end = units.convertPressure(cylinderSac.endPressure!); - final used = start - end; - return '${start.toStringAsFixed(0)} → ${end.toStringAsFixed(0)} (${used.toStringAsFixed(0)} ${units.pressureSymbol})'; - } - - /// Build data quality badge (enhanced vs basic) - Widget _buildDataQualityBadge(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final textTheme = Theme.of(context).textTheme; - - if (cylinderSac.hasTimeSeriesData) { - return Tooltip( - message: context.l10n.diveLog_cylinderSac_tooltip_aiData, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: Colors.green.withValues(alpha: 0.2), - borderRadius: BorderRadius.circular(4), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.verified, size: 12, color: Colors.green.shade700), - const SizedBox(width: 2), - Text( - context.l10n.diveLog_cylinderSac_badge_ai, - style: textTheme.labelSmall?.copyWith( - color: Colors.green.shade700, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - ), - ); - } else { - return Tooltip( - message: context.l10n.diveLog_cylinderSac_tooltip_basicData, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(4), - ), - child: Text( - context.l10n.diveLog_cylinderSac_badge_basic, - style: textTheme.labelSmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - ), - ); - } - } - - /// Get color for tank role badge - Color _getRoleColor(ColorScheme colorScheme) { - return switch (cylinderSac.role.name) { - 'backGas' => colorScheme.primary, - 'stage' => Colors.orange, - 'deco' => Colors.purple, - 'bailout' => Colors.red, - 'sidemountLeft' || 'sidemountRight' => colorScheme.secondary, - 'pony' => Colors.teal, - 'diluent' || 'oxygenSupply' => Colors.blue, - _ => colorScheme.primary, - }; - } -} - -/// A list of cylinder SAC cards for multi-tank dives -class CylinderSacList extends StatelessWidget { - /// List of cylinder SAC data - final List cylinders; - - /// Unit formatter - final UnitFormatter units; - - /// Which SAC unit to display - final SacUnit sacUnit; - - /// Whether to show in a horizontal scroll view - final bool horizontal; - - const CylinderSacList({ - super.key, - required this.cylinders, - required this.units, - required this.sacUnit, - this.horizontal = true, - }); - - @override - Widget build(BuildContext context) { - if (cylinders.isEmpty) { - return const SizedBox.shrink(); - } - - if (horizontal) { - return SizedBox( - height: 160, - child: ListView.separated( - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric(horizontal: 16), - itemCount: cylinders.length, - separatorBuilder: (_, _) => const SizedBox(width: 8), - itemBuilder: (context, index) { - return SizedBox( - width: 200, - child: CylinderSacCard( - cylinderSac: cylinders[index], - units: units, - sacUnit: sacUnit, - ), - ); - }, - ), - ); - } - - return Column( - children: cylinders.map((cylinder) { - return Padding( - padding: const EdgeInsets.only(bottom: 8), - child: CylinderSacCard( - cylinderSac: cylinder, - units: units, - sacUnit: sacUnit, - ), - ); - }).toList(), - ); - } -} diff --git a/lib/features/dive_log/presentation/widgets/dive_profile_legend.dart b/lib/features/dive_log/presentation/widgets/dive_profile_legend.dart index 0c2f5e866..dd8fd0b89 100644 --- a/lib/features/dive_log/presentation/widgets/dive_profile_legend.dart +++ b/lib/features/dive_log/presentation/widgets/dive_profile_legend.dart @@ -568,7 +568,7 @@ class _ChartOptionsDialog extends StatelessWidget { _buildSection( context, key: 'tanks', - title: context.l10n.diveLog_detail_section_tanks, + title: context.l10n.diveLog_detail_section_cylinders, legendState: legendState, legendNotifier: legendNotifier, children: tankItems, diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index faae0635d..e014b39df 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -1222,11 +1222,9 @@ "diveLog_detail_section_marineLife": "الحياة البحرية", "diveLog_detail_section_notes": "الملاحظات", "diveLog_detail_section_oxygenToxicity": "سمية الأكسجين", - "diveLog_detail_section_sacByCylinder": "SAC حسب الأسطوانة", "diveLog_detail_section_sacRateBySegment": "معدل SAC حسب القطاع", "diveLog_detail_section_tags": "الوسوم", "diveLog_detail_section_cylinders": "الأسطوانات", - "diveLog_detail_section_tanks": "الأسطوانات", "diveLog_detail_section_tide": "المد والجزر", "diveLog_detail_section_trainingSignature": "توقيع التدريب", "diveLog_detail_section_weight": "الأثقال", @@ -1238,7 +1236,6 @@ "diveLog_detail_stat_runtime": "وقت التشغيل", "diveLog_detail_stat_waterTemp": "درجة حرارة الماء", "diveLog_detail_tagCount": "{count} {count, plural, =1{وسم} other{وسوم}}", - "diveLog_detail_tankCount": "{count} {count, plural, =1{أسطوانة} other{أسطوانات}}", "diveLog_detail_tideCalculated": "محسوب من نموذج المد والجزر", "diveLog_detail_tooltip_addToFavorites": "إضافة إلى المفضلة", "diveLog_detail_tooltip_edit": "تعديل", diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index 7bd58943b..f9598cb55 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -1222,11 +1222,9 @@ "diveLog_detail_section_marineLife": "Meeresfauna", "diveLog_detail_section_notes": "Notizen", "diveLog_detail_section_oxygenToxicity": "Sauerstofftoxizität", - "diveLog_detail_section_sacByCylinder": "SAC nach Flasche", "diveLog_detail_section_sacRateBySegment": "SAC-Rate nach Segment", "diveLog_detail_section_tags": "Tags", "diveLog_detail_section_cylinders": "Flaschen", - "diveLog_detail_section_tanks": "Flaschen", "diveLog_detail_section_tide": "Gezeiten", "diveLog_detail_section_trainingSignature": "Ausbildungsunterschrift", "diveLog_detail_section_weight": "Gewicht", @@ -1238,7 +1236,6 @@ "diveLog_detail_stat_runtime": "Laufzeit", "diveLog_detail_stat_waterTemp": "Wassertemp.", "diveLog_detail_tagCount": "{count} {count, plural, =1{Tag} other{Tags}}", - "diveLog_detail_tankCount": "{count} {count, plural, =1{Flasche} other{Flaschen}}", "diveLog_detail_tideCalculated": "Berechnet aus Gezeitenmodell", "diveLog_detail_tooltip_addToFavorites": "Zu Favoriten hinzufügen", "diveLog_detail_tooltip_edit": "Bearbeiten", diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index ab366707a..d4eeacc40 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1894,11 +1894,9 @@ "diveLog_detail_section_marineLife": "Marine Life", "diveLog_detail_section_notes": "Notes", "diveLog_detail_section_oxygenToxicity": "Oxygen Toxicity", - "diveLog_detail_section_sacByCylinder": "SAC by Cylinder", "diveLog_detail_section_sacRateBySegment": "SAC Rate by Segment", "diveLog_detail_section_tags": "Tags", "diveLog_detail_section_cylinders": "Cylinders", - "diveLog_detail_section_tanks": "Tanks", "diveLog_detail_section_tide": "Tide", "diveLog_detail_section_trainingSignature": "Training Signature", "diveLog_detail_section_weight": "Weight", @@ -1910,7 +1908,6 @@ "diveLog_detail_stat_runtime": "Runtime", "diveLog_detail_stat_waterTemp": "Water Temp", "diveLog_detail_tagCount": "{count} {count, plural, =1{tag} other{tags}}", - "diveLog_detail_tankCount": "{count} {count, plural, =1{tank} other{tanks}}", "diveLog_detail_tideCalculated": "Calculated from tide model", "diveLog_detail_tooltip_addToFavorites": "Add to favorites", "diveLog_detail_tooltip_edit": "Edit", @@ -2782,13 +2779,6 @@ } } }, - "@diveLog_detail_tankCount": { - "placeholders": { - "count": { - "type": "int" - } - } - }, "@diveLog_editSighting_removeConfirm": { "placeholders": { "name": { diff --git a/lib/l10n/arb/app_es.arb b/lib/l10n/arb/app_es.arb index 4cc0fe5c6..cedbfb86a 100644 --- a/lib/l10n/arb/app_es.arb +++ b/lib/l10n/arb/app_es.arb @@ -1222,11 +1222,9 @@ "diveLog_detail_section_marineLife": "Vida marina", "diveLog_detail_section_notes": "Notas", "diveLog_detail_section_oxygenToxicity": "Toxicidad del oxígeno", - "diveLog_detail_section_sacByCylinder": "SAC por cilindro", "diveLog_detail_section_sacRateBySegment": "Consumo SAC por segmento", "diveLog_detail_section_tags": "Etiquetas", "diveLog_detail_section_cylinders": "Tanques", - "diveLog_detail_section_tanks": "Tanques", "diveLog_detail_section_tide": "Marea", "diveLog_detail_section_trainingSignature": "Firma de entrenamiento", "diveLog_detail_section_weight": "Lastre", @@ -1238,7 +1236,6 @@ "diveLog_detail_stat_runtime": "Tiempo total", "diveLog_detail_stat_waterTemp": "Temp. del agua", "diveLog_detail_tagCount": "{count} {count, plural, =1{etiqueta} other{etiquetas}}", - "diveLog_detail_tankCount": "{count} {count, plural, =1{tanque} other{tanques}}", "diveLog_detail_tideCalculated": "Calculado a partir del modelo de mareas", "diveLog_detail_tooltip_addToFavorites": "Agregar a favoritos", "diveLog_detail_tooltip_edit": "Editar", diff --git a/lib/l10n/arb/app_fr.arb b/lib/l10n/arb/app_fr.arb index 3d77c908a..56e040f71 100644 --- a/lib/l10n/arb/app_fr.arb +++ b/lib/l10n/arb/app_fr.arb @@ -1188,11 +1188,9 @@ "diveLog_detail_section_marineLife": "Vie marine", "diveLog_detail_section_notes": "Notes", "diveLog_detail_section_oxygenToxicity": "Toxicite de l'oxygene", - "diveLog_detail_section_sacByCylinder": "SAC par bloc", "diveLog_detail_section_sacRateBySegment": "Consommation SAC par segment", "diveLog_detail_section_tags": "Tags", "diveLog_detail_section_cylinders": "Blocs", - "diveLog_detail_section_tanks": "Blocs", "diveLog_detail_section_tide": "Maree", "diveLog_detail_section_trainingSignature": "Signature de formation", "diveLog_detail_section_weight": "Lestage", @@ -1204,7 +1202,6 @@ "diveLog_detail_stat_runtime": "Duree totale", "diveLog_detail_stat_waterTemp": "Temp eau", "diveLog_detail_tagCount": "{count} {count, plural, =1{tag} other{tags}}", - "diveLog_detail_tankCount": "{count} {count, plural, =1{bloc} other{blocs}}", "diveLog_detail_tideCalculated": "Calcule a partir du modele de maree", "diveLog_detail_tooltip_addToFavorites": "Ajouter aux favoris", "diveLog_detail_tooltip_edit": "Modifier", diff --git a/lib/l10n/arb/app_he.arb b/lib/l10n/arb/app_he.arb index c745f99a0..10b50dd9d 100644 --- a/lib/l10n/arb/app_he.arb +++ b/lib/l10n/arb/app_he.arb @@ -1188,11 +1188,9 @@ "diveLog_detail_section_marineLife": "חיים ימיים", "diveLog_detail_section_notes": "הערות", "diveLog_detail_section_oxygenToxicity": "רעילות חמצן", - "diveLog_detail_section_sacByCylinder": "SAC לפי בלון", "diveLog_detail_section_sacRateBySegment": "קצב SAC לפי מקטע", "diveLog_detail_section_tags": "תגיות", "diveLog_detail_section_cylinders": "בלונים", - "diveLog_detail_section_tanks": "בלונים", "diveLog_detail_section_tide": "גאות ושפל", "diveLog_detail_section_trainingSignature": "חתימת הכשרה", "diveLog_detail_section_weight": "משקולות", @@ -1204,7 +1202,6 @@ "diveLog_detail_stat_runtime": "זמן ריצה", "diveLog_detail_stat_waterTemp": "טמפ' מים", "diveLog_detail_tagCount": "{count} {count, plural, =1{תגית} other{תגיות}}", - "diveLog_detail_tankCount": "{count} {count, plural, =1{בלון} other{בלונים}}", "diveLog_detail_tideCalculated": "חושב ממודל גאות ושפל", "diveLog_detail_tooltip_addToFavorites": "הוספה למועדפים", "diveLog_detail_tooltip_edit": "עריכה", diff --git a/lib/l10n/arb/app_hu.arb b/lib/l10n/arb/app_hu.arb index 99a75bd3b..9ea21f82f 100644 --- a/lib/l10n/arb/app_hu.arb +++ b/lib/l10n/arb/app_hu.arb @@ -1188,11 +1188,9 @@ "diveLog_detail_section_marineLife": "Tengeri elet", "diveLog_detail_section_notes": "Jegyzetek", "diveLog_detail_section_oxygenToxicity": "Oxigen toxicitas", - "diveLog_detail_section_sacByCylinder": "SAC palackonkent", "diveLog_detail_section_sacRateBySegment": "SAC ertek szakaszonkent", "diveLog_detail_section_tags": "Cimkek", "diveLog_detail_section_cylinders": "Palackok", - "diveLog_detail_section_tanks": "Palackok", "diveLog_detail_section_tide": "Arapaly", "diveLog_detail_section_trainingSignature": "Kepzesi alairas", "diveLog_detail_section_weight": "Suly", @@ -1204,7 +1202,6 @@ "diveLog_detail_stat_runtime": "Futasido", "diveLog_detail_stat_waterTemp": "Viz hom.", "diveLog_detail_tagCount": "{count} {count, plural, =1{cimke} other{cimke}}", - "diveLog_detail_tankCount": "{count} {count, plural, =1{palack} other{palack}}", "diveLog_detail_tideCalculated": "Arapaly modellbol szamitva", "diveLog_detail_tooltip_addToFavorites": "Hozzaadas a kedvencekhez", "diveLog_detail_tooltip_edit": "Szerkesztes", diff --git a/lib/l10n/arb/app_it.arb b/lib/l10n/arb/app_it.arb index 65a3705e9..f478960c7 100644 --- a/lib/l10n/arb/app_it.arb +++ b/lib/l10n/arb/app_it.arb @@ -1188,11 +1188,9 @@ "diveLog_detail_section_marineLife": "Vita marina", "diveLog_detail_section_notes": "Note", "diveLog_detail_section_oxygenToxicity": "Tossicita dell'ossigeno", - "diveLog_detail_section_sacByCylinder": "SAC per bombola", "diveLog_detail_section_sacRateBySegment": "Consumo SAC per segmento", "diveLog_detail_section_tags": "Tag", "diveLog_detail_section_cylinders": "Bombole", - "diveLog_detail_section_tanks": "Bombole", "diveLog_detail_section_tide": "Marea", "diveLog_detail_section_trainingSignature": "Firma addestramento", "diveLog_detail_section_weight": "Zavorra", @@ -1204,7 +1202,6 @@ "diveLog_detail_stat_runtime": "Tempo totale", "diveLog_detail_stat_waterTemp": "Temp. acqua", "diveLog_detail_tagCount": "{count} {count, plural, =1{tag} other{tag}}", - "diveLog_detail_tankCount": "{count} {count, plural, =1{bombola} other{bombole}}", "diveLog_detail_tideCalculated": "Calcolato dal modello di marea", "diveLog_detail_tooltip_addToFavorites": "Aggiungi ai preferiti", "diveLog_detail_tooltip_edit": "Modifica", diff --git a/lib/l10n/arb/app_localizations.dart b/lib/l10n/arb/app_localizations.dart index 921a3e92a..e75117324 100644 --- a/lib/l10n/arb/app_localizations.dart +++ b/lib/l10n/arb/app_localizations.dart @@ -6038,12 +6038,6 @@ abstract class AppLocalizations { /// **'Oxygen Toxicity'** String get diveLog_detail_section_oxygenToxicity; - /// No description provided for @diveLog_detail_section_sacByCylinder. - /// - /// In en, this message translates to: - /// **'SAC by Cylinder'** - String get diveLog_detail_section_sacByCylinder; - /// No description provided for @diveLog_detail_section_sacRateBySegment. /// /// In en, this message translates to: @@ -6062,12 +6056,6 @@ abstract class AppLocalizations { /// **'Cylinders'** String get diveLog_detail_section_cylinders; - /// No description provided for @diveLog_detail_section_tanks. - /// - /// In en, this message translates to: - /// **'Tanks'** - String get diveLog_detail_section_tanks; - /// No description provided for @diveLog_detail_section_tide. /// /// In en, this message translates to: @@ -6134,12 +6122,6 @@ abstract class AppLocalizations { /// **'{count} {count, plural, =1{tag} other{tags}}'** String diveLog_detail_tagCount(int count); - /// No description provided for @diveLog_detail_tankCount. - /// - /// In en, this message translates to: - /// **'{count} {count, plural, =1{tank} other{tanks}}'** - String diveLog_detail_tankCount(int count); - /// No description provided for @diveLog_detail_tideCalculated. /// /// 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 c64b79cdb..b9fc19ad4 100644 --- a/lib/l10n/arb/app_localizations_ar.dart +++ b/lib/l10n/arb/app_localizations_ar.dart @@ -3520,9 +3520,6 @@ class AppLocalizationsAr extends AppLocalizations { @override String get diveLog_detail_section_oxygenToxicity => 'سمية الأكسجين'; - @override - String get diveLog_detail_section_sacByCylinder => 'SAC حسب الأسطوانة'; - @override String get diveLog_detail_section_sacRateBySegment => 'معدل SAC حسب القطاع'; @@ -3532,9 +3529,6 @@ class AppLocalizationsAr extends AppLocalizations { @override String get diveLog_detail_section_cylinders => 'الأسطوانات'; - @override - String get diveLog_detail_section_tanks => 'الأسطوانات'; - @override String get diveLog_detail_section_tide => 'المد والجزر'; @@ -3579,17 +3573,6 @@ class AppLocalizationsAr extends AppLocalizations { return '$count $_temp0'; } - @override - String diveLog_detail_tankCount(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'أسطوانات', - one: 'أسطوانة', - ); - return '$count $_temp0'; - } - @override String get diveLog_detail_tideCalculated => 'محسوب من نموذج المد والجزر'; diff --git a/lib/l10n/arb/app_localizations_de.dart b/lib/l10n/arb/app_localizations_de.dart index 5641854c9..5197bc4c9 100644 --- a/lib/l10n/arb/app_localizations_de.dart +++ b/lib/l10n/arb/app_localizations_de.dart @@ -3601,9 +3601,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get diveLog_detail_section_oxygenToxicity => 'Sauerstofftoxizität'; - @override - String get diveLog_detail_section_sacByCylinder => 'SAC nach Flasche'; - @override String get diveLog_detail_section_sacRateBySegment => 'SAC-Rate nach Segment'; @@ -3613,9 +3610,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get diveLog_detail_section_cylinders => 'Flaschen'; - @override - String get diveLog_detail_section_tanks => 'Flaschen'; - @override String get diveLog_detail_section_tide => 'Gezeiten'; @@ -3662,17 +3656,6 @@ class AppLocalizationsDe extends AppLocalizations { return '$count $_temp0'; } - @override - String diveLog_detail_tankCount(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Flaschen', - one: 'Flasche', - ); - return '$count $_temp0'; - } - @override String get diveLog_detail_tideCalculated => 'Berechnet aus Gezeitenmodell'; diff --git a/lib/l10n/arb/app_localizations_en.dart b/lib/l10n/arb/app_localizations_en.dart index effd97f0b..58b425aaf 100644 --- a/lib/l10n/arb/app_localizations_en.dart +++ b/lib/l10n/arb/app_localizations_en.dart @@ -3534,9 +3534,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get diveLog_detail_section_oxygenToxicity => 'Oxygen Toxicity'; - @override - String get diveLog_detail_section_sacByCylinder => 'SAC by Cylinder'; - @override String get diveLog_detail_section_sacRateBySegment => 'SAC Rate by Segment'; @@ -3546,9 +3543,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get diveLog_detail_section_cylinders => 'Cylinders'; - @override - String get diveLog_detail_section_tanks => 'Tanks'; - @override String get diveLog_detail_section_tide => 'Tide'; @@ -3593,17 +3587,6 @@ class AppLocalizationsEn extends AppLocalizations { return '$count $_temp0'; } - @override - String diveLog_detail_tankCount(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'tanks', - one: 'tank', - ); - return '$count $_temp0'; - } - @override String get diveLog_detail_tideCalculated => 'Calculated from tide model'; diff --git a/lib/l10n/arb/app_localizations_es.dart b/lib/l10n/arb/app_localizations_es.dart index 0eb8f6e54..7582c5cbe 100644 --- a/lib/l10n/arb/app_localizations_es.dart +++ b/lib/l10n/arb/app_localizations_es.dart @@ -3594,9 +3594,6 @@ class AppLocalizationsEs extends AppLocalizations { @override String get diveLog_detail_section_oxygenToxicity => 'Toxicidad del oxígeno'; - @override - String get diveLog_detail_section_sacByCylinder => 'SAC por cilindro'; - @override String get diveLog_detail_section_sacRateBySegment => 'Consumo SAC por segmento'; @@ -3607,9 +3604,6 @@ class AppLocalizationsEs extends AppLocalizations { @override String get diveLog_detail_section_cylinders => 'Tanques'; - @override - String get diveLog_detail_section_tanks => 'Tanques'; - @override String get diveLog_detail_section_tide => 'Marea'; @@ -3656,17 +3650,6 @@ class AppLocalizationsEs extends AppLocalizations { return '$count $_temp0'; } - @override - String diveLog_detail_tankCount(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'tanques', - one: 'tanque', - ); - return '$count $_temp0'; - } - @override String get diveLog_detail_tideCalculated => 'Calculado a partir del modelo de mareas'; diff --git a/lib/l10n/arb/app_localizations_fr.dart b/lib/l10n/arb/app_localizations_fr.dart index d705652b8..aa5cd406b 100644 --- a/lib/l10n/arb/app_localizations_fr.dart +++ b/lib/l10n/arb/app_localizations_fr.dart @@ -3610,9 +3610,6 @@ class AppLocalizationsFr extends AppLocalizations { @override String get diveLog_detail_section_oxygenToxicity => 'Toxicite de l\'oxygene'; - @override - String get diveLog_detail_section_sacByCylinder => 'SAC par bloc'; - @override String get diveLog_detail_section_sacRateBySegment => 'Consommation SAC par segment'; @@ -3623,9 +3620,6 @@ class AppLocalizationsFr extends AppLocalizations { @override String get diveLog_detail_section_cylinders => 'Blocs'; - @override - String get diveLog_detail_section_tanks => 'Blocs'; - @override String get diveLog_detail_section_tide => 'Maree'; @@ -3672,17 +3666,6 @@ class AppLocalizationsFr extends AppLocalizations { return '$count $_temp0'; } - @override - String diveLog_detail_tankCount(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'blocs', - one: 'bloc', - ); - return '$count $_temp0'; - } - @override String get diveLog_detail_tideCalculated => 'Calcule a partir du modele de maree'; diff --git a/lib/l10n/arb/app_localizations_he.dart b/lib/l10n/arb/app_localizations_he.dart index c918691eb..4c53408c8 100644 --- a/lib/l10n/arb/app_localizations_he.dart +++ b/lib/l10n/arb/app_localizations_he.dart @@ -3500,9 +3500,6 @@ class AppLocalizationsHe extends AppLocalizations { @override String get diveLog_detail_section_oxygenToxicity => 'רעילות חמצן'; - @override - String get diveLog_detail_section_sacByCylinder => 'SAC לפי בלון'; - @override String get diveLog_detail_section_sacRateBySegment => 'קצב SAC לפי מקטע'; @@ -3512,9 +3509,6 @@ class AppLocalizationsHe extends AppLocalizations { @override String get diveLog_detail_section_cylinders => 'בלונים'; - @override - String get diveLog_detail_section_tanks => 'בלונים'; - @override String get diveLog_detail_section_tide => 'גאות ושפל'; @@ -3559,17 +3553,6 @@ class AppLocalizationsHe extends AppLocalizations { return '$count $_temp0'; } - @override - String diveLog_detail_tankCount(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'בלונים', - one: 'בלון', - ); - return '$count $_temp0'; - } - @override String get diveLog_detail_tideCalculated => 'חושב ממודל גאות ושפל'; diff --git a/lib/l10n/arb/app_localizations_hu.dart b/lib/l10n/arb/app_localizations_hu.dart index e0ca2c647..cb72f35fb 100644 --- a/lib/l10n/arb/app_localizations_hu.dart +++ b/lib/l10n/arb/app_localizations_hu.dart @@ -3580,9 +3580,6 @@ class AppLocalizationsHu extends AppLocalizations { @override String get diveLog_detail_section_oxygenToxicity => 'Oxigen toxicitas'; - @override - String get diveLog_detail_section_sacByCylinder => 'SAC palackonkent'; - @override String get diveLog_detail_section_sacRateBySegment => 'SAC ertek szakaszonkent'; @@ -3593,9 +3590,6 @@ class AppLocalizationsHu extends AppLocalizations { @override String get diveLog_detail_section_cylinders => 'Palackok'; - @override - String get diveLog_detail_section_tanks => 'Palackok'; - @override String get diveLog_detail_section_tide => 'Arapaly'; @@ -3641,17 +3635,6 @@ class AppLocalizationsHu extends AppLocalizations { return '$count $_temp0'; } - @override - String diveLog_detail_tankCount(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'palack', - one: 'palack', - ); - return '$count $_temp0'; - } - @override String get diveLog_detail_tideCalculated => 'Arapaly modellbol szamitva'; diff --git a/lib/l10n/arb/app_localizations_it.dart b/lib/l10n/arb/app_localizations_it.dart index 6b23c9c7f..a1b1786d8 100644 --- a/lib/l10n/arb/app_localizations_it.dart +++ b/lib/l10n/arb/app_localizations_it.dart @@ -3597,9 +3597,6 @@ class AppLocalizationsIt extends AppLocalizations { String get diveLog_detail_section_oxygenToxicity => 'Tossicita dell\'ossigeno'; - @override - String get diveLog_detail_section_sacByCylinder => 'SAC per bombola'; - @override String get diveLog_detail_section_sacRateBySegment => 'Consumo SAC per segmento'; @@ -3610,9 +3607,6 @@ class AppLocalizationsIt extends AppLocalizations { @override String get diveLog_detail_section_cylinders => 'Bombole'; - @override - String get diveLog_detail_section_tanks => 'Bombole'; - @override String get diveLog_detail_section_tide => 'Marea'; @@ -3658,17 +3652,6 @@ class AppLocalizationsIt extends AppLocalizations { return '$count $_temp0'; } - @override - String diveLog_detail_tankCount(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'bombole', - one: 'bombola', - ); - return '$count $_temp0'; - } - @override String get diveLog_detail_tideCalculated => 'Calcolato dal modello di marea'; diff --git a/lib/l10n/arb/app_localizations_nl.dart b/lib/l10n/arb/app_localizations_nl.dart index 7948d7842..cc989b061 100644 --- a/lib/l10n/arb/app_localizations_nl.dart +++ b/lib/l10n/arb/app_localizations_nl.dart @@ -3568,9 +3568,6 @@ class AppLocalizationsNl extends AppLocalizations { @override String get diveLog_detail_section_oxygenToxicity => 'Zuurstoftoxiciteit'; - @override - String get diveLog_detail_section_sacByCylinder => 'SAC per fles'; - @override String get diveLog_detail_section_sacRateBySegment => 'SAC-verbruik per segment'; @@ -3581,9 +3578,6 @@ class AppLocalizationsNl extends AppLocalizations { @override String get diveLog_detail_section_cylinders => 'Flessen'; - @override - String get diveLog_detail_section_tanks => 'Flessen'; - @override String get diveLog_detail_section_tide => 'Getij'; @@ -3630,17 +3624,6 @@ class AppLocalizationsNl extends AppLocalizations { return '$count $_temp0'; } - @override - String diveLog_detail_tankCount(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'flessen', - one: 'fles', - ); - return '$count $_temp0'; - } - @override String get diveLog_detail_tideCalculated => 'Berekend op basis van getijmodel'; diff --git a/lib/l10n/arb/app_localizations_pt.dart b/lib/l10n/arb/app_localizations_pt.dart index 9b250e170..e17fb4122 100644 --- a/lib/l10n/arb/app_localizations_pt.dart +++ b/lib/l10n/arb/app_localizations_pt.dart @@ -3596,9 +3596,6 @@ class AppLocalizationsPt extends AppLocalizations { @override String get diveLog_detail_section_oxygenToxicity => 'Toxicidade de Oxigenio'; - @override - String get diveLog_detail_section_sacByCylinder => 'SAC por Cilindro'; - @override String get diveLog_detail_section_sacRateBySegment => 'Taxa SAC por Segmento'; @@ -3608,9 +3605,6 @@ class AppLocalizationsPt extends AppLocalizations { @override String get diveLog_detail_section_cylinders => 'Cilindros'; - @override - String get diveLog_detail_section_tanks => 'Cilindros'; - @override String get diveLog_detail_section_tide => 'Mare'; @@ -3657,17 +3651,6 @@ class AppLocalizationsPt extends AppLocalizations { return '$count $_temp0'; } - @override - String diveLog_detail_tankCount(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'cilindros', - one: 'cilindro', - ); - return '$count $_temp0'; - } - @override String get diveLog_detail_tideCalculated => 'Calculado a partir do modelo de mares'; diff --git a/lib/l10n/arb/app_localizations_zh.dart b/lib/l10n/arb/app_localizations_zh.dart index 51d694d69..7cd96d524 100644 --- a/lib/l10n/arb/app_localizations_zh.dart +++ b/lib/l10n/arb/app_localizations_zh.dart @@ -3427,9 +3427,6 @@ class AppLocalizationsZh extends AppLocalizations { @override String get diveLog_detail_section_oxygenToxicity => '氧中毒'; - @override - String get diveLog_detail_section_sacByCylinder => '按气瓶的气体消耗率'; - @override String get diveLog_detail_section_sacRateBySegment => '按分段的气体消耗率'; @@ -3439,9 +3436,6 @@ class AppLocalizationsZh extends AppLocalizations { @override String get diveLog_detail_section_cylinders => '气瓶'; - @override - String get diveLog_detail_section_tanks => '气瓶'; - @override String get diveLog_detail_section_tide => '潮汐'; @@ -3485,17 +3479,6 @@ class AppLocalizationsZh extends AppLocalizations { return '$count $_temp0'; } - @override - String diveLog_detail_tankCount(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '个气瓶', - one: '个气瓶', - ); - return '$count $_temp0'; - } - @override String get diveLog_detail_tideCalculated => '根据潮汐模型计算'; diff --git a/lib/l10n/arb/app_nl.arb b/lib/l10n/arb/app_nl.arb index 187bdf465..9e9b8737a 100644 --- a/lib/l10n/arb/app_nl.arb +++ b/lib/l10n/arb/app_nl.arb @@ -1222,11 +1222,9 @@ "diveLog_detail_section_marineLife": "Onderwaterleven", "diveLog_detail_section_notes": "Notities", "diveLog_detail_section_oxygenToxicity": "Zuurstoftoxiciteit", - "diveLog_detail_section_sacByCylinder": "SAC per fles", "diveLog_detail_section_sacRateBySegment": "SAC-verbruik per segment", "diveLog_detail_section_tags": "Tags", "diveLog_detail_section_cylinders": "Flessen", - "diveLog_detail_section_tanks": "Flessen", "diveLog_detail_section_tide": "Getij", "diveLog_detail_section_trainingSignature": "Trainingshandtekening", "diveLog_detail_section_weight": "Gewicht", @@ -1238,7 +1236,6 @@ "diveLog_detail_stat_runtime": "Looptijd", "diveLog_detail_stat_waterTemp": "Watertemp.", "diveLog_detail_tagCount": "{count} {count, plural, =1{tag} other{tags}}", - "diveLog_detail_tankCount": "{count} {count, plural, =1{fles} other{flessen}}", "diveLog_detail_tideCalculated": "Berekend op basis van getijmodel", "diveLog_detail_tooltip_addToFavorites": "Aan favorieten toevoegen", "diveLog_detail_tooltip_edit": "Bewerken", diff --git a/lib/l10n/arb/app_pt.arb b/lib/l10n/arb/app_pt.arb index d8b755a2d..9f8d42f63 100644 --- a/lib/l10n/arb/app_pt.arb +++ b/lib/l10n/arb/app_pt.arb @@ -1222,11 +1222,9 @@ "diveLog_detail_section_marineLife": "Vida Marinha", "diveLog_detail_section_notes": "Anotacoes", "diveLog_detail_section_oxygenToxicity": "Toxicidade de Oxigenio", - "diveLog_detail_section_sacByCylinder": "SAC por Cilindro", "diveLog_detail_section_sacRateBySegment": "Taxa SAC por Segmento", "diveLog_detail_section_tags": "Tags", "diveLog_detail_section_cylinders": "Cilindros", - "diveLog_detail_section_tanks": "Cilindros", "diveLog_detail_section_tide": "Mare", "diveLog_detail_section_trainingSignature": "Assinatura de Treinamento", "diveLog_detail_section_weight": "Lastro", @@ -1238,7 +1236,6 @@ "diveLog_detail_stat_runtime": "Tempo Total", "diveLog_detail_stat_waterTemp": "Temp da Agua", "diveLog_detail_tagCount": "{count} {count, plural, =1{tag} other{tags}}", - "diveLog_detail_tankCount": "{count} {count, plural, =1{cilindro} other{cilindros}}", "diveLog_detail_tideCalculated": "Calculado a partir do modelo de mares", "diveLog_detail_tooltip_addToFavorites": "Adicionar aos favoritos", "diveLog_detail_tooltip_edit": "Editar", diff --git a/lib/l10n/arb/app_zh.arb b/lib/l10n/arb/app_zh.arb index 9c0fb6cb2..a1699e3ce 100644 --- a/lib/l10n/arb/app_zh.arb +++ b/lib/l10n/arb/app_zh.arb @@ -1329,11 +1329,9 @@ "diveLog_detail_section_marineLife": "海洋生物", "diveLog_detail_section_notes": "备注", "diveLog_detail_section_oxygenToxicity": "氧中毒", - "diveLog_detail_section_sacByCylinder": "按气瓶的气体消耗率", "diveLog_detail_section_sacRateBySegment": "按分段的气体消耗率", "diveLog_detail_section_tags": "标签", "diveLog_detail_section_cylinders": "气瓶", - "diveLog_detail_section_tanks": "气瓶", "diveLog_detail_section_tide": "潮汐", "diveLog_detail_section_trainingSignature": "培训签名", "diveLog_detail_section_weight": "配重", @@ -1347,7 +1345,6 @@ "diveLog_detail_subsection_diveConditions": "潜水条件", "diveLog_detail_subsection_weather": "天气", "diveLog_detail_tagCount": "{count} {count, plural, =1{个标签} other{个标签}}", - "diveLog_detail_tankCount": "{count} {count, plural, =1{个气瓶} other{个气瓶}}", "diveLog_detail_tideCalculated": "根据潮汐模型计算", "diveLog_detail_tooltip_addToFavorites": "添加到收藏", "diveLog_detail_tooltip_edit": "编辑", diff --git a/test/features/dive_log/presentation/widgets/dive_profile_legend_test.dart b/test/features/dive_log/presentation/widgets/dive_profile_legend_test.dart index d1095ceeb..417a5aab8 100644 --- a/test/features/dive_log/presentation/widgets/dive_profile_legend_test.dart +++ b/test/features/dive_log/presentation/widgets/dive_profile_legend_test.dart @@ -229,7 +229,7 @@ void main() { await tester.tap(find.byIcon(Icons.tune), warnIfMissed: false); await tester.pumpAndSettle(); - expect(find.text('Tanks'), findsOneWidget); + expect(find.text('Cylinders'), findsOneWidget); expect(find.text('D80 (Air)'), findsOneWidget); expect(find.text('AL80 (EAN50)'), findsOneWidget); expect(find.text('Tank Pressures'), findsNothing); From 8cd340a5281c53459eb9ba14a64ed2f20ddfdd3f Mon Sep 17 00:00:00 2001 From: Eric Griffin Date: Sun, 5 Jul 2026 17:23:45 -0400 Subject: [PATCH 6/8] test(dive-detail): cover preset chip and time-series pressure fallback --- .../widgets/cylinders_card_test.dart | 56 ++++++++++++++++++- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/test/features/dive_log/presentation/widgets/cylinders_card_test.dart b/test/features/dive_log/presentation/widgets/cylinders_card_test.dart index bda4adb7b..a0c362888 100644 --- a/test/features/dive_log/presentation/widgets/cylinders_card_test.dart +++ b/test/features/dive_log/presentation/widgets/cylinders_card_test.dart @@ -91,6 +91,7 @@ Widget _buildCard({ required Dive dive, List cylinderSacs = const [], List dataSources = const [], + Map> tankPressures = const {}, UnitFormatter units = _units, AppSettings settings = _settings, SacUnit sacUnit = SacUnit.pressurePerMin, @@ -98,9 +99,7 @@ Widget _buildCard({ return testApp( overrides: [ cylinderSacProvider.overrideWith((ref, id) async => cylinderSacs), - tankPressuresProvider.overrideWith( - (ref, id) async => >{}, - ), + tankPressuresProvider.overrideWith((ref, id) async => tankPressures), diveDataSourcesProvider.overrideWith((ref, id) async => dataSources), ], child: SingleChildScrollView( @@ -237,6 +236,57 @@ void main() { expect(find.textContaining('psi →'), findsOneWidget); }); + testWidgets('shows preset display name as the volume chip', (tester) async { + await tester.pumpWidget( + _buildCard( + dive: _makeDive([ + DiveTank( + id: 'tank-1', + presetName: 'al80', + startPressure: 200, + endPressure: 50, + gasMix: const GasMix(o2: 32), + ), + ]), + ), + ); + await tester.pumpAndSettle(); + + expect(find.text('AL80'), findsOneWidget); + }); + + testWidgets('falls back to time-series pressures when metadata is null', ( + tester, + ) async { + await tester.pumpWidget( + _buildCard( + dive: _makeDive([_makeTank(startPressure: null, endPressure: null)]), + tankPressures: const { + 'tank-1': [ + TankPressurePoint( + id: 'tp-1', + tankId: 'tank-1', + timestamp: 0, + pressure: 200, + ), + TankPressurePoint( + id: 'tp-2', + tankId: 'tank-1', + timestamp: 2700, + pressure: 60, + ), + ], + }, + ), + ); + await tester.pumpAndSettle(); + + expect( + find.textContaining('200 bar → 60 bar (140 bar used)'), + findsOneWidget, + ); + }); + testWidgets('hides source badge with a single data source', (tester) async { // Riverpod ignores override changes on an in-place ProviderScope // rebuild, so the single-source and multi-source cases live in From f672e44d2de4a5b007f84da8e5839070328f67e5 Mon Sep 17 00:00:00 2001 From: Eric Griffin Date: Sun, 5 Jul 2026 17:24:52 -0400 Subject: [PATCH 7/8] style: const-construct preset test fixture --- .../dive_log/presentation/widgets/cylinders_card_test.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/features/dive_log/presentation/widgets/cylinders_card_test.dart b/test/features/dive_log/presentation/widgets/cylinders_card_test.dart index a0c362888..7afb4b719 100644 --- a/test/features/dive_log/presentation/widgets/cylinders_card_test.dart +++ b/test/features/dive_log/presentation/widgets/cylinders_card_test.dart @@ -240,12 +240,12 @@ void main() { await tester.pumpWidget( _buildCard( dive: _makeDive([ - DiveTank( + const DiveTank( id: 'tank-1', presetName: 'al80', startPressure: 200, endPressure: 50, - gasMix: const GasMix(o2: 32), + gasMix: GasMix(o2: 32), ), ]), ), From 4137000fdbcf03f044b9c0d56b34802fdb32c66c Mon Sep 17 00:00:00 2001 From: Eric Griffin Date: Sun, 5 Jul 2026 22:54:38 -0400 Subject: [PATCH 8/8] docs: trailing block shows gas used in the diver's volume unit --- .../dive_log/presentation/widgets/cylinders_card.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/features/dive_log/presentation/widgets/cylinders_card.dart b/lib/features/dive_log/presentation/widgets/cylinders_card.dart index c61c49cbe..48aac5ec2 100644 --- a/lib/features/dive_log/presentation/widgets/cylinders_card.dart +++ b/lib/features/dive_log/presentation/widgets/cylinders_card.dart @@ -178,9 +178,9 @@ class CylindersCard extends ConsumerWidget { ); } - /// Trailing column: attribution badge, SAC rate, gas used in liters. - /// Returns null when there is nothing to show so the tile keeps its - /// natural width. + /// Trailing column: attribution badge, SAC rate, gas used (converted to + /// the diver's volume unit). Returns null when there is nothing to show + /// so the tile keeps its natural width. Widget? _trailingBlock( ThemeData theme, CylinderSac? cylinderSac,