diff --git a/lib/core/database/database.dart b/lib/core/database/database.dart index a41b6dc1ab..b5da5fb595 100644 --- a/lib/core/database/database.dart +++ b/lib/core/database/database.dart @@ -2130,6 +2130,23 @@ class DiveTypes extends Table { /// (nullable: rows written before HLC rollout fall back to updatedAt). TextColumn get hlc => text().nullable()(); + /// Abbreviated display form for a custom type (v173). Built-in types never + /// set this -- they use the fixed translated abbreviation in + /// builtInDiveTypeShortName instead. Null means the diver hasn't set one. + TextColumn get shortName => text().nullable()(); + + /// Whether this type's badge appears in the dive detail header's type-badge + /// row (v174). Defaults to shown, so existing dives keep their current + /// badges after the upgrade. + BoolColumn get showInDetailHeader => + boolean().withDefault(const Constant(true))(); + + /// Whether this type's badge appears in the dive list card's type-badge + /// row (v174). Independent of [showInDetailHeader] -- a diver may want a + /// type visible in the detail header but not cluttering every list row. + BoolColumn get showInListView => + boolean().withDefault(const Constant(true))(); + @override Set get primaryKey => {id}; } @@ -3253,7 +3270,7 @@ class AppDatabase extends _$AppDatabase { /// The current schema version as a static constant so that pre-open checks /// (e.g. version-mismatch guard) can reference it without an instance. - static const int currentSchemaVersion = 171; + static const int currentSchemaVersion = 174; /// The oldest schema whose reader can apply this build's sync payloads /// without loss or misinterpretation (the compatibility floor). @@ -3592,6 +3609,17 @@ class AppDatabase extends _$AppDatabase { // PR #1320 to 175. This ladder is non-contiguous by design; the audit // asserts monotonic, unique, and scalar == max, never contiguous. 171, + // v173: dive_types.short_name, an optional diver-set abbreviation for + // custom dive types (mirrors the fixed built-in abbreviations). Issue + // #1269 (this PR). Renumbered up from 167, then 171, as main kept + // landing past this branch's claim while it was open; main's own v171 + // comment above already reserves 173 for this PR, so that's the number + // landed here directly. + 173, + // v174: dive_types.show_in_detail_header and dive_types.show_in_list_view, + // per-type toggles for which badge rows a diver's types appear in. + // Issue #1269 follow-up. + 174, ]; /// Idempotent DDL for the v106 connector-suggestion columns (Lightroom @@ -5389,6 +5417,47 @@ class AppDatabase extends _$AppDatabase { ); } + /// Idempotent DDL for the v173 dive_types.short_name column: an optional + /// abbreviation a diver can set on a custom dive type (built-ins use the + /// fixed translated abbreviation in builtInDiveTypeShortName instead). + /// Called from the v173 onUpgrade step and the beforeOpen backstop, + /// matching the _assertTripReturnFlightColumn pattern so a schema-version + /// collision cannot strand a database without it. Self-guarding when the + /// table is absent (minimal migration-test fixtures). + Future _assertDiveTypeShortNameColumn() async { + final cols = await customSelect("PRAGMA table_info('dive_types')").get(); + if (cols.isEmpty) return; + final names = cols.map((c) => c.read('name')).toSet(); + if (names.contains('short_name')) return; + await customStatement('ALTER TABLE dive_types ADD COLUMN short_name TEXT'); + } + + /// Idempotent DDL for the v174 dive_types.show_in_detail_header and + /// dive_types.show_in_list_view columns: per-type toggles for which + /// badge rows a diver's types appear in (issue #1269 follow-up). Both + /// default to shown (1) so existing dives keep their current badges. + /// Called from the v174 onUpgrade step and the beforeOpen backstop, + /// matching the _assertDiveTypeShortNameColumn pattern so a schema-version + /// collision cannot strand a database without them. Self-guarding when the + /// table is absent (minimal migration-test fixtures). + Future _assertDiveTypeVisibilityColumns() async { + final cols = await customSelect("PRAGMA table_info('dive_types')").get(); + if (cols.isEmpty) return; + final names = cols.map((c) => c.read('name')).toSet(); + if (!names.contains('show_in_detail_header')) { + await customStatement( + 'ALTER TABLE dive_types ADD COLUMN show_in_detail_header ' + 'INTEGER NOT NULL DEFAULT 1 CHECK (show_in_detail_header IN (0, 1))', + ); + } + if (!names.contains('show_in_list_view')) { + await customStatement( + 'ALTER TABLE dive_types ADD COLUMN show_in_list_view ' + 'INTEGER NOT NULL DEFAULT 1 CHECK (show_in_list_view IN (0, 1))', + ); + } + } + /// One-time attribution of existing dive_profiles rows to their owning /// dive_data_sources row (issue #1149). /// @@ -8914,6 +8983,18 @@ class AppDatabase extends _$AppDatabase { await _assertTripDayWeatherSchema(); } if (from < 171) await reportProgress(); + // v173: dive_types.short_name, an optional diver-set abbreviation + // for custom dive types. + if (from < 173) { + await _assertDiveTypeShortNameColumn(); + } + if (from < 173) await reportProgress(); + // v174: dive_types.show_in_detail_header and + // dive_types.show_in_list_view, per-type badge-row visibility. + if (from < 174) { + await _assertDiveTypeVisibilityColumns(); + } + if (from < 174) await reportProgress(); }, beforeOpen: (details) async { // Enable foreign keys @@ -9135,6 +9216,15 @@ class AppDatabase extends _$AppDatabase { // every open after the first. await _assertTripDayWeatherSchema(); + // v173 backstop: re-assert dive_types.short_name (same + // parallel-branch version-collision self-heal). + await _assertDiveTypeShortNameColumn(); + + // v174 backstop: re-assert dive_types.show_in_detail_header and + // dive_types.show_in_list_view (same parallel-branch + // version-collision self-heal). + await _assertDiveTypeVisibilityColumns(); + // v145 backstop: re-assert the gps_tracks provenance and trim columns. await _assertGpsTrackColumns(); diff --git a/lib/features/dashboard/presentation/widgets/recent_dives_card.dart b/lib/features/dashboard/presentation/widgets/recent_dives_card.dart index da70ac5e6d..75d7978927 100644 --- a/lib/features/dashboard/presentation/widgets/recent_dives_card.dart +++ b/lib/features/dashboard/presentation/widgets/recent_dives_card.dart @@ -91,6 +91,12 @@ class RecentDivesCard extends ConsumerWidget { ref, context.l10n, ); + final diveTypeShortLabelResolver = watchDiveTypeShortLabelResolver( + ref, + context.l10n, + ); + final diveTypeListVisibilityPredicate = + watchDiveTypeListVisibilityPredicate(ref); final list = Column( children: dives.asMap().entries.map((entry) { @@ -103,6 +109,9 @@ class RecentDivesCard extends ConsumerWidget { return DiveListItem( summary: DiveSummary.fromDive(dive), diveTypeLabelResolver: diveTypeLabelResolver, + diveTypeShortLabelResolver: diveTypeShortLabelResolver, + diveTypeListVisibilityPredicate: + diveTypeListVisibilityPredicate, fullDive: dive, diveNumber: dive.diveNumber ?? index + 1, colorValue: getCardColorValueFromDive(dive, colorAttribute), diff --git a/lib/features/dive_log/presentation/formatters/dive_type_label.dart b/lib/features/dive_log/presentation/formatters/dive_type_label.dart index 2033e851ac..e58c8dbc3b 100644 --- a/lib/features/dive_log/presentation/formatters/dive_type_label.dart +++ b/lib/features/dive_log/presentation/formatters/dive_type_label.dart @@ -39,3 +39,26 @@ String diveTypeLabels( Iterable ids, { Map? typesById, }) => ids.map((id) => diveTypeLabel(l10n, id, typesById: typesById)).join(', '); + +/// Short-form counterpart to [diveTypeLabel], for space-constrained surfaces +/// like the dive detail header's type badges. +/// +/// A built-in type uses the fixed translated abbreviation (see +/// [builtInDiveTypeShortName]). A custom type -- including a diver's own row +/// sitting on a built-in slug (see [diveTypeLabel]'s tier 1) -- uses +/// [DiveTypeEntity.shortName] if the diver set one, else falls through to +/// its full [diveTypeLabel]. +String diveTypeShortLabel( + AppLocalizations l10n, + String id, { + Map? typesById, +}) { + final loaded = typesById?[id]; + if (loaded != null && !loaded.isBuiltIn) { + final shortName = loaded.shortName; + if (shortName != null && shortName.trim().isNotEmpty) return shortName; + return diveTypeLabel(l10n, id, typesById: typesById); + } + return builtInDiveTypeShortName(l10n, id) ?? + diveTypeLabel(l10n, id, typesById: typesById); +} diff --git a/lib/features/dive_log/presentation/formatters/dive_type_label_resolver.dart b/lib/features/dive_log/presentation/formatters/dive_type_label_resolver.dart index 6096d9951c..8c629b0034 100644 --- a/lib/features/dive_log/presentation/formatters/dive_type_label_resolver.dart +++ b/lib/features/dive_log/presentation/formatters/dive_type_label_resolver.dart @@ -35,3 +35,41 @@ DiveTypeLabelResolver watchDiveTypeLabelResolver( }; return (id) => diveTypeLabel(l10n, id, typesById: typesById); } + +/// Short-form counterpart to [watchDiveTypeLabelResolver], for space +/// -constrained surfaces like list-row type badges. Same call-once-per-list +/// contract and empty-map-before-load fallback. +DiveTypeLabelResolver watchDiveTypeShortLabelResolver( + WidgetRef ref, + AppLocalizations l10n, +) { + final typesById = { + for (final t + in ref.watch(diveTypesProvider).value ?? const []) + t.id: t, + }; + return (id) => diveTypeShortLabel(l10n, id, typesById: typesById); +} + +/// Whether a dive-type slug's badge should appear in the dive list's +/// type-badge row (issue #1269 follow-up). Independent of the header's own +/// visibility toggle -- a diver may want a type in one badge row but not +/// the other. +typedef DiveTypeListVisibilityPredicate = bool Function(String id); + +/// Builds a [DiveTypeListVisibilityPredicate] from the currently loaded dive +/// types. Same call-once-per-list contract as [watchDiveTypeLabelResolver]. +/// +/// A slug absent from the loaded types (not yet loaded, or deleted out from +/// under a still-referencing dive) resolves to visible -- unknown is not the +/// same as explicitly hidden. +DiveTypeListVisibilityPredicate watchDiveTypeListVisibilityPredicate( + WidgetRef ref, +) { + final typesById = { + for (final t + in ref.watch(diveTypesProvider).value ?? const []) + t.id: t, + }; + return (id) => typesById[id]?.showInListView ?? true; +} 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 cf15b14832..f722026938 100644 --- a/lib/features/dive_log/presentation/pages/dive_detail_page.dart +++ b/lib/features/dive_log/presentation/pages/dive_detail_page.dart @@ -40,6 +40,7 @@ import 'package:submersion/features/dive_log/presentation/providers/dive_compute import 'package:submersion/features/dive_log/presentation/providers/dive_detail_ui_providers.dart'; import 'package:submersion/features/dive_log/presentation/providers/dive_providers.dart'; import 'package:submersion/features/dive_log/presentation/widgets/dive_mode_badge.dart'; +import 'package:submersion/features/dive_log/presentation/widgets/dive_type_badge_row.dart'; import 'package:submersion/shared/utils/ink_centered_text_style.dart'; import 'package:submersion/features/dive_log/presentation/widgets/dive_nav_buttons.dart'; import 'package:submersion/features/dive_log/presentation/providers/gas_analysis_providers.dart'; @@ -1225,130 +1226,183 @@ class _DiveDetailPageState extends ConsumerState { final hasLocation = siteLoc != null || hasGps; final colorScheme = Theme.of(context).colorScheme; final cardColor = Theme.of(context).cardColor; + final diveTypesById = { + for (final t + in ref.watch(diveTypesProvider).value ?? const []) + t.id: t, + }; + // A type absent from diveTypesById (not yet loaded, or deleted out from + // under a still-referencing dive) stays shown -- unknown is not the same + // as explicitly hidden. + final visibleHeaderTypeIds = dive.diveTypeIds + .where((id) => diveTypesById[id]?.showInDetailHeader ?? true) + .toList(); final content = Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - CircleAvatar( - radius: 24, - backgroundColor: colorScheme.primaryContainer, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: FittedBox( - fit: BoxFit.scaleDown, - child: Text( - '#${dive.diveNumber ?? '-'}', - maxLines: 1, - style: TextStyle( - color: colorScheme.onPrimaryContainer, - fontWeight: FontWeight.bold, - // Pin the pre-scale size (CircleAvatar's implicit - // titleMedium default) so FittedBox scales from a - // theme-independent baseline. - fontSize: 16, + LayoutBuilder( + builder: (context, headerConstraints) { + // Scales with the header's own width instead of a flat cap, so + // a wide detail pane can spell out more type badges before + // collapsing to "+N" while a narrow one still reserves enough + // room for the site name/dates column on the left. + final badgeMaxWidth = (headerConstraints.maxWidth * 0.35).clamp( + 120.0, + 280.0, + ); + return Row( + children: [ + CircleAvatar( + radius: 24, + backgroundColor: colorScheme.primaryContainer, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + '#${dive.diveNumber ?? '-'}', + maxLines: 1, + style: TextStyle( + color: colorScheme.onPrimaryContainer, + fontWeight: FontWeight.bold, + // Pin the pre-scale size (CircleAvatar's implicit + // titleMedium default) so FittedBox scales from a + // theme-independent baseline. + fontSize: 16, + ), + ), ), ), ), - ), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - dive.effectiveName ?? - dive.site?.name ?? - context.l10n.diveLog_listPage_unknownSite, - style: Theme.of(context).textTheme.titleLarge, + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + dive.effectiveName ?? + dive.site?.name ?? + context.l10n.diveLog_listPage_unknownSite, + style: Theme.of(context).textTheme.titleLarge, + ), + Consumer( + builder: (context, ref, _) { + final count = + ref + .watch( + diveOpenFindingsCountProvider(dive.id), + ) + .value ?? + 0; + if (count == 0) return const SizedBox.shrink(); + return Padding( + padding: const EdgeInsets.only(top: 4), + child: ActionChip( + avatar: Icon( + Icons.rule, + size: 16, + color: colorScheme.tertiary, + ), + label: Text( + context.l10n.dataQuality_detail_chipCount( + count, + ), + ), + onPressed: () => context.push( + '/dives/quality?dive=${dive.id}', + ), + ), + ); + }, + ), + if (dive.effectiveName != null && dive.site != null) + Text( + dive.site!.name, + style: Theme.of(context).textTheme.bodyMedium + ?.copyWith(color: colorScheme.onSurfaceVariant), + ), + if (dive.site?.locationString.isNotEmpty == true) + Text( + dive.site!.locationString, + style: Theme.of(context).textTheme.bodyMedium + ?.copyWith(color: colorScheme.onSurfaceVariant), + ), + Text( + '${context.l10n.diveLog_detail_label_entry} ${units.formatDateTimeBullet(dive.effectiveEntryTime)}', + style: Theme.of(context).textTheme.bodyMedium + ?.copyWith(color: colorScheme.onSurfaceVariant), + ), + if (dive.exitTime != null) + Text( + '${context.l10n.diveLog_detail_label_exit} ${units.formatDateTimeBullet(dive.exitTime!)}', + style: Theme.of(context).textTheme.bodyMedium + ?.copyWith(color: colorScheme.onSurfaceVariant), + ), + ], ), - Consumer( - builder: (context, ref, _) { - final count = - ref - .watch(diveOpenFindingsCountProvider(dive.id)) - .value ?? - 0; - if (count == 0) return const SizedBox.shrink(); - return Padding( - padding: const EdgeInsets.only(top: 4), - child: ActionChip( - avatar: Icon( - Icons.rule, - size: 16, - color: colorScheme.tertiary, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + if (dive.rating != null) ...[ + ExcludeSemantics( + child: Icon( + Icons.star, + color: Colors.amber.shade600, + size: 20, + ), ), - label: Text( - context.l10n.dataQuality_detail_chipCount(count), + const SizedBox(width: 4), + Text( + '${dive.rating}', + // Same ink-centering fix as DiveModeBadge: without + // it this number's default line leading isn't + // split evenly around its own glyph, so it + // doesn't sit on the same visual line as the star + // icon next to it. + style: Theme.of( + context, + ).textTheme.titleMedium?.inkCentered, + textHeightBehavior: inkCenteredTextHeightBehavior, ), - onPressed: () => - context.push('/dives/quality?dive=${dive.id}'), - ), - ); - }, - ), - if (dive.effectiveName != null && dive.site != null) - Text( - dive.site!.name, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - if (dive.site?.locationString.isNotEmpty == true) - Text( - dive.site!.locationString, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - Text( - '${context.l10n.diveLog_detail_label_entry} ${units.formatDateTimeBullet(dive.effectiveEntryTime)}', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, + const SizedBox(width: 8), + ], + DiveModeBadge(mode: dive.diveMode), + ], ), - ), - if (dive.exitTime != null) - Text( - '${context.l10n.diveLog_detail_label_exit} ${units.formatDateTimeBullet(dive.exitTime!)}', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, + if (visibleHeaderTypeIds.isNotEmpty) ...[ + const SizedBox(height: 8), + // Capped rather than left unbounded: Row hands a + // non-flex child unbounded width, which would let a + // long run of type badges grow without limit instead of + // wrapping under the rating/mode row. The cap itself + // scales with the header's width (see badgeMaxWidth) + // rather than a flat constant. + ConstrainedBox( + constraints: BoxConstraints(maxWidth: badgeMaxWidth), + child: DiveTypeBadgeRow( + labels: [ + for (final typeId in visibleHeaderTypeIds) + diveTypeShortLabel( + context.l10n, + typeId, + typesById: diveTypesById, + ), + ], + ), ), - ), - ], - ), - ), - Row( - children: [ - if (dive.rating != null) ...[ - ExcludeSemantics( - child: Icon( - Icons.star, - color: Colors.amber.shade600, - size: 20, - ), - ), - const SizedBox(width: 4), - Text( - '${dive.rating}', - // Same ink-centering fix as DiveModeBadge: without it - // this number's default line leading isn't split - // evenly around its own glyph, so it doesn't sit on - // the same visual line as the star icon next to it. - style: Theme.of( - context, - ).textTheme.titleMedium?.inkCentered, - textHeightBehavior: inkCenteredTextHeightBehavior, - ), - const SizedBox(width: 8), - ], - DiveModeBadge(mode: dive.diveMode), + ], + ], + ), ], - ), - ], + ); + }, ), const SizedBox(height: 16), Row( diff --git a/lib/features/dive_log/presentation/pages/dive_list_page.dart b/lib/features/dive_log/presentation/pages/dive_list_page.dart index 695e203a0e..e5fa8c2b15 100644 --- a/lib/features/dive_log/presentation/pages/dive_list_page.dart +++ b/lib/features/dive_log/presentation/pages/dive_list_page.dart @@ -34,6 +34,7 @@ import 'package:submersion/features/dive_log/presentation/widgets/dive_numbering import 'package:submersion/features/dive_log/presentation/widgets/dive_profile_chart.dart'; import 'package:submersion/features/dive_log/presentation/widgets/dive_profile_panel.dart'; import 'package:submersion/features/dive_log/presentation/widgets/dive_summary_widget.dart'; +import 'package:submersion/features/dive_log/presentation/widgets/dive_type_badge_row.dart'; import 'package:submersion/features/dive_log/presentation/widgets/table_column_picker.dart'; import 'package:submersion/features/media/presentation/providers/lightroom_providers.dart'; import 'package:submersion/features/settings/presentation/providers/settings_providers.dart'; @@ -738,6 +739,15 @@ class DiveListTile extends ConsumerWidget { /// English slug capitalization, matching the locale-independent export path. final DiveTypeLabelResolver? diveTypeLabelResolver; + /// Resolves a dive-type slug to its short-form abbreviation, for the badge + /// row on the stat line (mirrors the dive detail header's type badges). + /// When omitted, badges fall back to the slug's capitalization. + final DiveTypeLabelResolver? diveTypeShortLabelResolver; + + /// Whether a dive-type slug's badge should appear in the badge row + /// (issue #1269 follow-up). When omitted, every type is shown. + final DiveTypeListVisibilityPredicate? diveTypeListVisibilityPredicate; + const DiveListTile({ super.key, required this.diveId, @@ -767,6 +777,8 @@ class DiveListTile extends ConsumerWidget { this.summary, this.fullDive, this.diveTypeLabelResolver, + this.diveTypeShortLabelResolver, + this.diveTypeListVisibilityPredicate, }); /// Calculate background color based on the active color attribute @@ -853,6 +865,12 @@ class DiveListTile extends ConsumerWidget { final titleField = slotField('title', DiveField.siteName); final dateField = slotField('date', DiveField.dateTime); + final diveTypeLabels = [ + for (final id in summary?.diveTypeIds ?? const []) + if (diveTypeListVisibilityPredicate?.call(id) ?? true) + (diveTypeShortLabelResolver ?? Dive.diveTypeDisplayName)(id), + ]; + // Resolve the title and date lines from their slot assignments, keeping // the legacy rendering when the slot holds its default field (mirrors // CompactDiveListTile). @@ -1061,15 +1079,16 @@ class DiveListTile extends ConsumerWidget { ], ), const SizedBox(height: 6), - // Stats row plus tags. Tags flow onto the same line as the - // stats (in the space under the mini chart) when they fit, and - // wrap to the next line otherwise, keeping cards compact. + // Stat row (protected: sized to its own content first) plus + // the dive-type badges, right-aligned on the same line. Tags + // get their own line below instead of sharing this one, so + // neither can squeeze the other -- DiveTypeBadgeRow still + // collapses into a single "+N" badge if the stat row alone + // leaves it little room. Padding( padding: const EdgeInsetsDirectional.only(start: 52), - child: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - spacing: 16, - runSpacing: 6, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, children: [ Row( mainAxisSize: MainAxisSize.min, @@ -1093,11 +1112,32 @@ class DiveListTile extends ConsumerWidget { ), ], ), - if (tags.isNotEmpty && detailedConfig.showTags) - TagChips(tags: tags, maxTags: 3), + if (diveTypeLabels.isNotEmpty) ...[ + const SizedBox(width: 8), + Expanded( + child: Align( + alignment: AlignmentDirectional.centerEnd, + child: DiveTypeBadgeRow( + labels: diveTypeLabels, + dense: true, + ), + ), + ), + ] else + const Spacer(), ], ), ), + // Tags, on their own line so a long tag name never competes + // with the stat row or the type badges for width (each chip + // is itself capped with an ellipsis as a defensive backstop). + if (tags.isNotEmpty && detailedConfig.showTags) ...[ + const SizedBox(height: 4), + Padding( + padding: const EdgeInsetsDirectional.only(start: 52), + child: TagChips(tags: tags, maxTags: 3), + ), + ], // Extra configurable fields area if (extraFields.isNotEmpty && (fullDive != null || summary != null)) ...[ diff --git a/lib/features/dive_log/presentation/widgets/compact_dive_list_tile.dart b/lib/features/dive_log/presentation/widgets/compact_dive_list_tile.dart index a83296ff06..4b8186ed7b 100644 --- a/lib/features/dive_log/presentation/widgets/compact_dive_list_tile.dart +++ b/lib/features/dive_log/presentation/widgets/compact_dive_list_tile.dart @@ -5,9 +5,11 @@ import 'package:submersion/core/constants/dive_field.dart'; import 'package:submersion/core/constants/enums.dart'; import 'package:submersion/core/providers/provider.dart'; import 'package:submersion/core/utils/unit_formatter.dart'; +import 'package:submersion/features/dive_log/domain/entities/dive.dart'; import 'package:submersion/features/dive_log/domain/entities/dive_summary.dart'; import 'package:submersion/features/dive_log/presentation/formatters/dive_type_label_resolver.dart'; import 'package:submersion/features/dive_log/presentation/widgets/dive_mode_badge.dart'; +import 'package:submersion/features/dive_log/presentation/widgets/dive_type_badge_row.dart'; import 'package:submersion/features/settings/presentation/providers/settings_providers.dart'; import 'package:submersion/l10n/l10n_extension.dart'; import 'package:submersion/shared/selection/selection_leading.dart'; @@ -58,6 +60,15 @@ class CompactDiveListTile extends ConsumerWidget { /// capitalization, matching the locale-independent export path. final DiveTypeLabelResolver? diveTypeLabelResolver; + /// Resolves a dive-type slug to its short-form abbreviation, for the badge + /// row on the stat line (mirrors the dive detail header's type badges). + /// When omitted, badges fall back to the slug's capitalization. + final DiveTypeLabelResolver? diveTypeShortLabelResolver; + + /// Whether a dive-type slug's badge should appear in the badge row + /// (issue #1269 follow-up). When omitted, every type is shown. + final DiveTypeListVisibilityPredicate? diveTypeListVisibilityPredicate; + const CompactDiveListTile({ super.key, required this.diveId, @@ -82,6 +93,8 @@ class CompactDiveListTile extends ConsumerWidget { this.stat1Field = DiveField.maxDepth, this.stat2Field = DiveField.bottomTime, this.diveTypeLabelResolver, + this.diveTypeShortLabelResolver, + this.diveTypeListVisibilityPredicate, }); Color? _getAttributeBackgroundColor() { @@ -245,6 +258,12 @@ class CompactDiveListTile extends ConsumerWidget { ? duration != null : maxDepth != null); + final diveTypeLabels = [ + for (final id in summary?.diveTypeIds ?? const []) + if (diveTypeListVisibilityPredicate?.call(id) ?? true) + (diveTypeShortLabelResolver ?? Dive.diveTypeDisplayName)(id), + ]; + // The highlight is the fill above, not an edge stripe -- the key marks the // row for tests without decorating it. return Container( @@ -366,6 +385,16 @@ class CompactDiveListTile extends ConsumerWidget { stat2HasValue ? accentColor : secondaryTextColor, ), ), + if (diveTypeLabels.isNotEmpty) + Expanded( + child: Align( + alignment: AlignmentDirectional.centerEnd, + child: DiveTypeBadgeRow( + labels: diveTypeLabels, + dense: true, + ), + ), + ), ], ), ), diff --git a/lib/features/dive_log/presentation/widgets/dive_list_content.dart b/lib/features/dive_log/presentation/widgets/dive_list_content.dart index 402452201c..8c03d1017b 100644 --- a/lib/features/dive_log/presentation/widgets/dive_list_content.dart +++ b/lib/features/dive_log/presentation/widgets/dive_list_content.dart @@ -1365,6 +1365,12 @@ class _DiveListContentState extends ConsumerState { // Built once for the whole list, not per row: the lookup map is shared by // every tile and only this widget subscribes to the dive-type list. final diveTypeLabelResolver = watchDiveTypeLabelResolver(ref, context.l10n); + final diveTypeShortLabelResolver = watchDiveTypeShortLabelResolver( + ref, + context.l10n, + ); + final diveTypeListVisibilityPredicate = + watchDiveTypeListVisibilityPredicate(ref); // Check if detailed mode needs full Dive objects for non-summary fields final detailedConfig = ref.watch(detailedCardConfigProvider); @@ -1418,6 +1424,9 @@ class _DiveListContentState extends ConsumerState { return DiveListItem( summary: dive, diveTypeLabelResolver: diveTypeLabelResolver, + diveTypeShortLabelResolver: diveTypeShortLabelResolver, + diveTypeListVisibilityPredicate: + diveTypeListVisibilityPredicate, fullDive: fullDiveLookup[dive.id], diveNumber: dive.diveNumber ?? index + 1, colorValue: getCardColorValue(dive, colorAttribute), diff --git a/lib/features/dive_log/presentation/widgets/dive_list_item.dart b/lib/features/dive_log/presentation/widgets/dive_list_item.dart index 8ec92ae8fc..b2773e6a1a 100644 --- a/lib/features/dive_log/presentation/widgets/dive_list_item.dart +++ b/lib/features/dive_log/presentation/widgets/dive_list_item.dart @@ -59,11 +59,25 @@ class DiveListItem extends ConsumerWidget { /// names, which is the bug #643 fixed. final DiveTypeLabelResolver diveTypeLabelResolver; + /// Resolves a dive-type slug to its short-form abbreviation, for the + /// compact and detailed cards' type-badge row (issue #1269 follow-up). + /// Required for the same reason as [diveTypeLabelResolver]: a badge row + /// falling back to English slugs under a non-English locale is the exact + /// bug #643 fixed for the Dive Type slot. + final DiveTypeLabelResolver diveTypeShortLabelResolver; + + /// Whether a dive-type slug's badge should appear in the compact and + /// detailed cards' type-badge row (issue #1269 follow-up). Required for + /// the same reason as the resolvers above. + final DiveTypeListVisibilityPredicate diveTypeListVisibilityPredicate; + const DiveListItem({ super.key, required this.summary, required this.diveNumber, required this.diveTypeLabelResolver, + required this.diveTypeShortLabelResolver, + required this.diveTypeListVisibilityPredicate, this.fullDive, this.colorValue, this.minValueInList, @@ -118,6 +132,8 @@ class DiveListItem extends ConsumerWidget { stat1Field: _slotField(slots, 'stat1', DiveField.maxDepth), stat2Field: _slotField(slots, 'stat2', DiveField.bottomTime), diveTypeLabelResolver: diveTypeLabelResolver, + diveTypeShortLabelResolver: diveTypeShortLabelResolver, + diveTypeListVisibilityPredicate: diveTypeListVisibilityPredicate, onTap: onTap, ); case ListViewMode.detailed: @@ -150,6 +166,8 @@ class DiveListItem extends ConsumerWidget { summary: summary, fullDive: fullDive, diveTypeLabelResolver: diveTypeLabelResolver, + diveTypeShortLabelResolver: diveTypeShortLabelResolver, + diveTypeListVisibilityPredicate: diveTypeListVisibilityPredicate, ); } } diff --git a/lib/features/dive_log/presentation/widgets/dive_type_badge.dart b/lib/features/dive_log/presentation/widgets/dive_type_badge.dart new file mode 100644 index 0000000000..f818ac5be6 --- /dev/null +++ b/lib/features/dive_log/presentation/widgets/dive_type_badge.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; + +import 'package:submersion/shared/utils/ink_centered_text_style.dart'; + +/// Badge for one of a dive's types (e.g. Wreck, Night, Drift). +/// +/// Shares [DiveModeBadge]'s bordered-box shape, type scale, and font weight +/// so the two read as one family in the dive detail header, but sits a step +/// quieter: the border uses the lighter outlineVariant tone rather than the +/// mode badge's stronger outline, since a run of several type badges +/// shouldn't compete with the single OC/CCR indicator for attention. The +/// text splits the difference between outlineVariant (too low-contrast to +/// read as text) and onSurfaceVariant (the mode badge's full-strength +/// label color, too bright once several of these sit in a row) -- see +/// [_textColor]. Renders whatever [label] the caller passes in -- callers +/// typically resolve a short-form abbreviation via `diveTypeShortLabel` +/// first, falling back to the full localized name when none is available. +/// +/// Translucent-filled (unlike [DiveModeBadge]'s bare outline): the header +/// can sit this badge over a live map background (see the location card in +/// dive_detail_page.dart), whose gradient scrim is only lightly tinted right +/// where the badge row sits. A fully transparent badge loses all contrast +/// there, but a fully opaque one reads as a heavy, out-of-place block next +/// to the plain-outline mode badge on a flat card -- so this splits the +/// difference with a low-alpha fill: enough of a backing to stay legible +/// over the map, faint enough to disappear into a flat card. +class DiveTypeBadge extends StatelessWidget { + final String label; + + /// Tighter padding and type scale for list rows, matching + /// [DiveModeBadge]'s own `dense` variant so the two badge families read as + /// the same size next to each other in a dive-list card. + final bool dense; + + const DiveTypeBadge({super.key, required this.label, this.dense = false}); + + /// Matches [DiveModeBadge]'s font size for the same [dense] value: close to + /// the header's titleMedium rating number (a touch under it) when not + /// dense, or the fixed 10.0 list-row size when dense. + static double fontSizeOf(BuildContext context, {bool dense = false}) => dense + ? 10.0 + : (Theme.of(context).textTheme.titleMedium?.fontSize ?? 16) - 3; + + /// Midpoint between outlineVariant and onSurfaceVariant: dim enough not to + /// shout next to the mode badge, but still legible as text (outlineVariant + /// alone reads fine as a 1px border, not as a run of glyphs). Derived from + /// the theme tokens rather than a fixed color so it keeps adapting across + /// light/dark and any accent-color theme. + static Color _textColor(ColorScheme colorScheme) => Color.lerp( + colorScheme.outlineVariant, + colorScheme.onSurfaceVariant, + 0.5, + )!; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return Container( + // Matches DiveModeBadge's padding for the same dense value, including + // the asymmetric vertical split that compensates for the ink sitting a + // hair low within the tight ascent/descent box textHeightBehavior + // forces. + padding: EdgeInsets.only( + left: dense ? 3 : 4, + right: dense ? 3 : 4, + top: dense ? 1.5 : 2.5, + bottom: dense ? 2.5 : 3.5, + ), + decoration: BoxDecoration( + color: colorScheme.surface.withValues(alpha: 0.3), + border: Border.all(color: colorScheme.outlineVariant), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + label, + style: Theme.of(context).textTheme.labelSmall + ?.copyWith( + fontSize: fontSizeOf(context, dense: dense), + color: _textColor(colorScheme), + fontWeight: FontWeight.bold, + ) + .inkCentered, + textHeightBehavior: inkCenteredTextHeightBehavior, + ), + ); + } +} diff --git a/lib/features/dive_log/presentation/widgets/dive_type_badge_row.dart b/lib/features/dive_log/presentation/widgets/dive_type_badge_row.dart new file mode 100644 index 0000000000..8878a8473e --- /dev/null +++ b/lib/features/dive_log/presentation/widgets/dive_type_badge_row.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; + +import 'package:submersion/features/dive_log/presentation/widgets/dive_type_badge.dart'; + +/// Single-line run of [DiveTypeBadge]s that collapses into a "+N" badge +/// (with a tooltip listing the hidden types) instead of wrapping when the +/// available width can't fit every label. +/// +/// Sits below the OC/CCR mode badge in the dive detail header, where a +/// second line of type badges would push into the stat row underneath -- so +/// this measures labels against the incoming width and always renders +/// exactly one line. +class DiveTypeBadgeRow extends StatelessWidget { + final List labels; + + /// Renders every badge (including the "+N" overflow badge) in + /// [DiveTypeBadge]'s dense size, matching [DiveModeBadge]'s own `dense` + /// variant for list rows. + final bool dense; + + const DiveTypeBadgeRow({super.key, required this.labels, this.dense = false}); + + static const _spacing = 6.0; + + // DiveTypeBadge's horizontal padding each side (dense: 3, non-dense: 4) + // plus its 1px border each side -- the width a badge adds on top of its + // text. + static double _badgeChrome(bool dense) => (dense ? 3.0 : 4.0) * 2 + 1.0 * 2; + + @override + Widget build(BuildContext context) { + if (labels.isEmpty) return const SizedBox.shrink(); + + final style = Theme.of(context).textTheme.labelSmall?.copyWith( + fontSize: DiveTypeBadge.fontSizeOf(context, dense: dense), + fontWeight: FontWeight.bold, + ); + final direction = Directionality.of(context); + + double badgeWidth(String text) { + final painter = TextPainter( + text: TextSpan(text: text, style: style), + textDirection: direction, + maxLines: 1, + )..layout(); + return painter.width + _badgeChrome(dense); + } + + return LayoutBuilder( + builder: (context, constraints) { + final maxWidth = constraints.maxWidth; + var used = 0.0; + var visibleCount = 0; + + for (var i = 0; i < labels.length; i++) { + final isLast = i == labels.length - 1; + final ownWidth = badgeWidth(labels[i]); + final overflowReserve = isLast + ? 0.0 + : _spacing + badgeWidth('+${labels.length - i - 1}'); + final prefix = visibleCount == 0 ? 0.0 : _spacing; + if (used + prefix + ownWidth + overflowReserve > maxWidth) break; + used += prefix + ownWidth; + visibleCount++; + } + + final hidden = labels.sublist(visibleCount); + + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + for (var i = 0; i < visibleCount; i++) ...[ + if (i > 0) const SizedBox(width: _spacing), + DiveTypeBadge(label: labels[i], dense: dense), + ], + if (hidden.isNotEmpty) ...[ + if (visibleCount > 0) const SizedBox(width: _spacing), + Tooltip( + message: hidden.join(', '), + child: DiveTypeBadge(label: '+${hidden.length}', dense: dense), + ), + ], + ], + ); + }, + ); + } +} diff --git a/lib/features/dive_types/data/repositories/dive_type_repository.dart b/lib/features/dive_types/data/repositories/dive_type_repository.dart index e434f8b719..77457403e3 100644 --- a/lib/features/dive_types/data/repositories/dive_type_repository.dart +++ b/lib/features/dive_types/data/repositories/dive_type_repository.dart @@ -192,6 +192,9 @@ class DiveTypeRepository { ), createdAt: Value(now), updatedAt: Value(now), + shortName: Value(diveType.shortName), + showInDetailHeader: Value(diveType.showInDetailHeader), + showInListView: Value(diveType.showInListView), ), ); @@ -240,6 +243,9 @@ class DiveTypeRepository { name: Value(diveType.name), sortOrder: Value(diveType.sortOrder), updatedAt: Value(now), + shortName: Value(diveType.shortName), + showInDetailHeader: Value(diveType.showInDetailHeader), + showInListView: Value(diveType.showInListView), ), ); await _syncRepository.markRecordPending( @@ -259,6 +265,43 @@ class DiveTypeRepository { } } + /// Set which badge rows a type's badge appears in. Unlike [updateDiveType], + /// this is allowed on built-in types too: badge-row visibility is a + /// per-diver display preference, not part of the type's core definition + /// (name/shortName/sortOrder), which built-ins still protect from edits. + Future setDiveTypeVisibility( + String id, { + required bool showInDetailHeader, + required bool showInListView, + }) async { + try { + _log.info('Setting dive type visibility: $id'); + final now = DateTime.now().millisecondsSinceEpoch; + + await (_db.update(_db.diveTypes)..where((t) => t.id.equals(id))).write( + DiveTypesCompanion( + updatedAt: Value(now), + showInDetailHeader: Value(showInDetailHeader), + showInListView: Value(showInListView), + ), + ); + await _syncRepository.markRecordPending( + entityType: 'diveTypes', + recordId: id, + localUpdatedAt: now, + ); + SyncEventBus.notifyLocalChange(); + _log.info('Set dive type visibility: $id'); + } catch (e, stackTrace) { + _log.error( + 'Failed to set dive type visibility: $id', + error: e, + stackTrace: stackTrace, + ); + rethrow; + } + } + /// Delete a custom dive type (built-in types cannot be deleted) Future deleteDiveType(String id) async { try { @@ -343,6 +386,10 @@ class DiveTypeRepository { updatedAt: DateTime.fromMillisecondsSinceEpoch( row.data['updated_at'] as int, ), + shortName: row.data['short_name'] as String?, + showInDetailHeader: + (row.data['show_in_detail_header'] as int) == 1, + showInListView: (row.data['show_in_list_view'] as int) == 1, ), diveCount: row.data['dive_count'] as int, ), @@ -400,6 +447,9 @@ class DiveTypeRepository { sortOrder: row.sortOrder, createdAt: DateTime.fromMillisecondsSinceEpoch(row.createdAt), updatedAt: DateTime.fromMillisecondsSinceEpoch(row.updatedAt), + shortName: row.shortName, + showInDetailHeader: row.showInDetailHeader, + showInListView: row.showInListView, ); } } diff --git a/lib/features/dive_types/domain/entities/dive_type_entity.dart b/lib/features/dive_types/domain/entities/dive_type_entity.dart index 302e20d2fe..c6f4a23891 100644 --- a/lib/features/dive_types/domain/entities/dive_type_entity.dart +++ b/lib/features/dive_types/domain/entities/dive_type_entity.dart @@ -10,6 +10,21 @@ class DiveTypeEntity extends Equatable { final DateTime createdAt; final DateTime updatedAt; + /// Abbreviated form for space-constrained surfaces like the dive detail + /// header's type badges (e.g. "Wreck" -> "Wreck", "Search & Recovery" -> + /// "S&R"). Only settable on custom types -- built-ins use the fixed + /// translated abbreviation in [builtInDiveTypeShortName] instead. Null + /// means the diver hasn't set one, so callers fall back to [name]. + final String? shortName; + + /// Whether this type's badge appears in the dive detail header's + /// type-badge row. Defaults to shown. + final bool showInDetailHeader; + + /// Whether this type's badge appears in the dive list card's type-badge + /// row. Independent of [showInDetailHeader]. Defaults to shown. + final bool showInListView; + const DiveTypeEntity({ required this.id, this.diverId, @@ -18,6 +33,9 @@ class DiveTypeEntity extends Equatable { this.sortOrder = 0, required this.createdAt, required this.updatedAt, + this.shortName, + this.showInDetailHeader = true, + this.showInListView = true, }); /// Create a new custom dive type @@ -26,6 +44,9 @@ class DiveTypeEntity extends Equatable { required String name, String? diverId, int sortOrder = 0, + String? shortName, + bool showInDetailHeader = true, + bool showInListView = true, }) { final now = DateTime.now(); return DiveTypeEntity( @@ -36,6 +57,9 @@ class DiveTypeEntity extends Equatable { sortOrder: sortOrder, createdAt: now, updatedAt: now, + shortName: shortName, + showInDetailHeader: showInDetailHeader, + showInListView: showInListView, ); } @@ -48,6 +72,16 @@ class DiveTypeEntity extends Equatable { .replaceAll(RegExp(r'\s+'), '_'); } + /// Sentinel marking a `copyWith` parameter as "not provided". Lets callers + /// distinguish omitting [shortName] (keep the current value) from passing + /// `null` (clear it) -- plain `value ?? this.value` cannot express a clear, + /// which broke the edit dialog's "remove an existing short name" flow. + /// Mirrors [Diver.copyWith]'s `_unset`/`_resolve` pattern. + static const Object _unset = Object(); + + static T _resolve(Object? value, T current) => + identical(value, _unset) ? current : value as T; + DiveTypeEntity copyWith({ String? id, String? diverId, @@ -56,6 +90,9 @@ class DiveTypeEntity extends Equatable { int? sortOrder, DateTime? createdAt, DateTime? updatedAt, + Object? shortName = _unset, + bool? showInDetailHeader, + bool? showInListView, }) { return DiveTypeEntity( id: id ?? this.id, @@ -65,6 +102,9 @@ class DiveTypeEntity extends Equatable { sortOrder: sortOrder ?? this.sortOrder, createdAt: createdAt ?? this.createdAt, updatedAt: updatedAt ?? this.updatedAt, + shortName: _resolve(shortName, this.shortName), + showInDetailHeader: showInDetailHeader ?? this.showInDetailHeader, + showInListView: showInListView ?? this.showInListView, ); } @@ -77,5 +117,8 @@ class DiveTypeEntity extends Equatable { sortOrder, createdAt, updatedAt, + shortName, + showInDetailHeader, + showInListView, ]; } diff --git a/lib/features/dive_types/presentation/dive_type_display.dart b/lib/features/dive_types/presentation/dive_type_display.dart index b49fb43971..dff6b5f74a 100644 --- a/lib/features/dive_types/presentation/dive_type_display.dart +++ b/lib/features/dive_types/presentation/dive_type_display.dart @@ -31,6 +31,33 @@ String? builtInDiveTypeName(AppLocalizations l10n, String id) => switch (id) { _ => null, }; +/// Short-form localized name for a built-in dive type (e.g. "Wreck" -> "Wreck", +/// "Technisches Tauchen" -> "Tec"), for space-constrained surfaces like the +/// dive detail header's type badges. +/// +/// Returns null for anything that is not a known built-in slug; callers should +/// handle custom dive types separately (e.g. `DiveTypeEntity.shortName` or the +/// full display name). +String? builtInDiveTypeShortName(AppLocalizations l10n, String id) => + switch (id) { + 'recreational' => l10n.diveType_builtin_recreational_short, + 'technical' => l10n.diveType_builtin_technical_short, + 'freedive' => l10n.diveType_builtin_freedive_short, + 'training' => l10n.diveType_builtin_training_short, + 'wreck' => l10n.diveType_builtin_wreck_short, + 'cave' => l10n.diveType_builtin_cave_short, + 'ice' => l10n.diveType_builtin_ice_short, + 'night' => l10n.diveType_builtin_night_short, + 'drift' => l10n.diveType_builtin_drift_short, + 'deep' => l10n.diveType_builtin_deep_short, + 'altitude' => l10n.diveType_builtin_altitude_short, + 'shore' => l10n.diveType_builtin_shore_short, + 'boat' => l10n.diveType_builtin_boat_short, + 'liveaboard' => l10n.diveType_builtin_liveaboard_short, + 'cavern' => l10n.diveType_builtin_cavern_short, + _ => null, + }; + extension DiveTypeDisplay on DiveTypeEntity { /// Localized name for built-in types; the stored name for custom types. /// diff --git a/lib/features/dive_types/presentation/pages/dive_types_page.dart b/lib/features/dive_types/presentation/pages/dive_types_page.dart index f8c373055d..1c3649dabb 100644 --- a/lib/features/dive_types/presentation/pages/dive_types_page.dart +++ b/lib/features/dive_types/presentation/pages/dive_types_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:submersion/core/providers/provider.dart'; import 'package:go_router/go_router.dart'; +import 'package:submersion/features/dive_log/presentation/widgets/dive_type_badge.dart'; import 'package:submersion/features/dive_types/domain/entities/dive_type_entity.dart'; import 'package:submersion/features/dive_types/presentation/dive_type_display.dart'; import 'package:submersion/features/dive_types/presentation/providers/dive_type_providers.dart'; @@ -83,6 +84,21 @@ class DiveTypesPage extends ConsumerWidget { DiveTypeEntity diveType, { required bool canDelete, }) { + // Previewed here so a diver knows what a header badge collapses their + // selection to: the fixed translated abbreviation for a built-in type + // (see builtInDiveTypeShortName), or the diver's own short name for a + // custom one. Hidden when there's nothing to preview -- no short name + // set, or (for a built-in) identical to the full name (e.g. English + // "Wreck"). + final fullName = diveType.localizedName(context.l10n); + final builtInShort = canDelete + ? null + : builtInDiveTypeShortName(context.l10n, diveType.id); + final customShort = canDelete ? diveType.shortName?.trim() : null; + final shortName = canDelete + ? (customShort?.isNotEmpty == true ? customShort : null) + : (builtInShort == fullName ? null : builtInShort); + return ListTile( leading: Icon( canDelete ? Icons.label_outline : Icons.label, @@ -90,17 +106,28 @@ class DiveTypesPage extends ConsumerWidget { ? Theme.of(context).colorScheme.secondary : Theme.of(context).colorScheme.primary, ), - title: Text(diveType.localizedName(context.l10n)), - subtitle: canDelete - ? Text(context.l10n.diveTypes_custom) - : Text(context.l10n.diveTypes_builtIn), - trailing: canDelete - ? IconButton( + title: Text(fullName), + subtitle: Text( + canDelete + ? context.l10n.diveTypes_custom + : context.l10n.diveTypes_builtIn, + ), + onTap: () => _showEditDiveTypeDialog(context, ref, diveType), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (shortName != null) ...[ + DiveTypeBadge(label: shortName), + if (canDelete) const SizedBox(width: 8), + ], + if (canDelete) + IconButton( icon: const Icon(Icons.delete_outline), onPressed: () => _confirmDelete(context, ref, diveType), tooltip: context.l10n.diveTypes_deleteTooltip, - ) - : null, + ), + ], + ), ); } @@ -109,28 +136,50 @@ class DiveTypesPage extends ConsumerWidget { WidgetRef ref, ) async { final nameController = TextEditingController(); + final shortNameController = TextEditingController(); final formKey = GlobalKey(); - final result = await showDialog( + final result = await showDialog<({String name, String? shortName})>( context: context, builder: (dialogContext) => AlertDialog( title: Text(dialogContext.l10n.diveTypes_addDialog_title), content: Form( key: formKey, - child: TextFormField( - controller: nameController, - autofocus: true, - decoration: InputDecoration( - labelText: dialogContext.l10n.diveTypes_addDialog_nameLabel, - hintText: dialogContext.l10n.diveTypes_addDialog_nameHint, - ), - textCapitalization: TextCapitalization.words, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return dialogContext.l10n.diveTypes_addDialog_nameValidation; - } - return null; - }, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + controller: nameController, + autofocus: true, + decoration: InputDecoration( + labelText: dialogContext.l10n.diveTypes_addDialog_nameLabel, + hintText: dialogContext.l10n.diveTypes_addDialog_nameHint, + ), + textCapitalization: TextCapitalization.words, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return dialogContext + .l10n + .diveTypes_addDialog_nameValidation; + } + return null; + }, + ), + const SizedBox(height: 12), + TextFormField( + controller: shortNameController, + decoration: InputDecoration( + labelText: + dialogContext.l10n.diveTypes_addDialog_shortNameLabel, + hintText: + dialogContext.l10n.diveTypes_addDialog_shortNameHint, + helperText: + dialogContext.l10n.diveTypes_addDialog_shortNameHelper, + helperMaxLines: 2, + ), + textCapitalization: TextCapitalization.words, + ), + ], ), ), actions: [ @@ -141,7 +190,12 @@ class DiveTypesPage extends ConsumerWidget { FilledButton( onPressed: () { if (formKey.currentState!.validate()) { - Navigator.of(dialogContext).pop(nameController.text.trim()); + Navigator.of(dialogContext).pop(( + name: nameController.text.trim(), + shortName: shortNameController.text.trim().isEmpty + ? null + : shortNameController.text.trim(), + )); } }, child: Text(dialogContext.l10n.diveTypes_addDialog_addButton), @@ -150,14 +204,17 @@ class DiveTypesPage extends ConsumerWidget { ), ); - if (result != null && result.isNotEmpty) { + if (result != null && result.name.isNotEmpty) { try { final notifier = ref.read(diveTypeListNotifierProvider.notifier); - await notifier.addDiveTypeByName(result); + await notifier.addDiveTypeByName( + result.name, + shortName: result.shortName, + ); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(context.l10n.diveTypes_snackbar_added(result)), + content: Text(context.l10n.diveTypes_snackbar_added(result.name)), ), ); } @@ -176,6 +233,192 @@ class DiveTypesPage extends ConsumerWidget { } } + /// Edit an existing dive type: name and short name for custom types (the + /// name field is disabled for built-ins, whose protected core definition + /// updateDiveType refuses to touch), plus the two badge-row visibility + /// checkboxes, which are editable on every type regardless of isBuiltIn -- + /// visibility is a per-diver display preference, not part of that + /// protected definition. + Future _showEditDiveTypeDialog( + BuildContext context, + WidgetRef ref, + DiveTypeEntity diveType, + ) async { + final canEditName = !diveType.isBuiltIn; + // Built-in names are disabled for editing, so show the localized name + // (matching the list tile) rather than the seeded English DB value -- + // otherwise a German-locale diver would see "Wreck" instead of + // "Wracktauchen" in a field they can't even change. + final nameController = TextEditingController( + text: canEditName ? diveType.name : diveType.localizedName(context.l10n), + ); + final shortNameController = TextEditingController( + text: diveType.shortName ?? '', + ); + final formKey = GlobalKey(); + var showInDetailHeader = diveType.showInDetailHeader; + var showInListView = diveType.showInListView; + + final result = + await showDialog< + ({ + String name, + String? shortName, + bool showInDetailHeader, + bool showInListView, + }) + >( + context: context, + builder: (dialogContext) => StatefulBuilder( + builder: (dialogContext, setState) => AlertDialog( + title: Text(dialogContext.l10n.diveTypes_editDialog_title), + content: Form( + key: formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextFormField( + controller: nameController, + enabled: canEditName, + autofocus: canEditName, + decoration: InputDecoration( + labelText: + dialogContext.l10n.diveTypes_addDialog_nameLabel, + helperText: canEditName + ? null + : dialogContext + .l10n + .diveTypes_editDialog_builtInNameHelper, + helperMaxLines: 2, + ), + textCapitalization: TextCapitalization.words, + validator: canEditName + ? (value) { + if (value == null || value.trim().isEmpty) { + return dialogContext + .l10n + .diveTypes_addDialog_nameValidation; + } + return null; + } + : null, + ), + if (canEditName) ...[ + const SizedBox(height: 12), + TextFormField( + controller: shortNameController, + decoration: InputDecoration( + labelText: dialogContext + .l10n + .diveTypes_addDialog_shortNameLabel, + hintText: dialogContext + .l10n + .diveTypes_addDialog_shortNameHint, + helperText: dialogContext + .l10n + .diveTypes_addDialog_shortNameHelper, + helperMaxLines: 2, + ), + textCapitalization: TextCapitalization.words, + ), + ], + const SizedBox(height: 8), + CheckboxListTile( + value: showInDetailHeader, + controlAffinity: ListTileControlAffinity.leading, + contentPadding: EdgeInsets.zero, + title: Text( + dialogContext.l10n.diveTypes_showInHeaderLabel, + ), + subtitle: Text( + dialogContext.l10n.diveTypes_showInHeaderTooltip, + ), + onChanged: (value) => + setState(() => showInDetailHeader = value ?? true), + ), + CheckboxListTile( + value: showInListView, + controlAffinity: ListTileControlAffinity.leading, + contentPadding: EdgeInsets.zero, + title: Text(dialogContext.l10n.diveTypes_showInListLabel), + subtitle: Text( + dialogContext.l10n.diveTypes_showInListTooltip, + ), + onChanged: (value) => + setState(() => showInListView = value ?? true), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(dialogContext).pop(), + child: Text(dialogContext.l10n.common_action_cancel), + ), + FilledButton( + onPressed: () { + if (!canEditName || formKey.currentState!.validate()) { + Navigator.of(dialogContext).pop(( + name: nameController.text.trim(), + shortName: shortNameController.text.trim().isEmpty + ? null + : shortNameController.text.trim(), + showInDetailHeader: showInDetailHeader, + showInListView: showInListView, + )); + } + }, + child: Text( + dialogContext.l10n.diveTypes_editDialog_saveButton, + ), + ), + ], + ), + ), + ); + + if (result == null) return; + + try { + final notifier = ref.read(diveTypeListNotifierProvider.notifier); + if (canEditName) { + await notifier.updateDiveType( + diveType.copyWith( + name: result.name, + shortName: result.shortName, + showInDetailHeader: result.showInDetailHeader, + showInListView: result.showInListView, + ), + ); + } else { + await notifier.setDiveTypeVisibility( + diveType.id, + showInDetailHeader: result.showInDetailHeader, + showInListView: result.showInListView, + ); + } + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.diveTypes_snackbar_updated(result.name)), + ), + ); + } + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + context.l10n.diveTypes_snackbar_errorUpdating(e.toString()), + ), + backgroundColor: Theme.of(context).colorScheme.error, + ), + ); + } + } + } + Future _confirmDelete( BuildContext context, WidgetRef ref, diff --git a/lib/features/dive_types/presentation/providers/dive_type_providers.dart b/lib/features/dive_types/presentation/providers/dive_type_providers.dart index c9abb2dc5b..05cc201cb0 100644 --- a/lib/features/dive_types/presentation/providers/dive_type_providers.dart +++ b/lib/features/dive_types/presentation/providers/dive_type_providers.dart @@ -179,7 +179,10 @@ class DiveTypeListNotifier /// Add a custom dive type by name (generates ID automatically) /// Throws if no valid diver profile exists - Future addDiveTypeByName(String name) async { + Future addDiveTypeByName( + String name, { + String? shortName, + }) async { // Get fresh validated diver ID before creating final validatedId = await _ref.read(validatedCurrentDiverIdProvider.future); @@ -187,10 +190,12 @@ class DiveTypeListNotifier throw Exception('Cannot create custom dive type without a diver profile'); } + final trimmedShortName = shortName?.trim(); final diveType = DiveTypeEntity.create( id: DiveTypeEntity.generateSlug(name), name: name.trim(), diverId: validatedId, + shortName: trimmedShortName?.isNotEmpty == true ? trimmedShortName : null, ); return addDiveType(diveType); } @@ -204,6 +209,25 @@ class DiveTypeListNotifier _ref.invalidate(customDiveTypesProvider); } + /// Set which badge rows a type's badge appears in. Allowed on built-in + /// types too, unlike [updateDiveType] (see + /// [DiveTypeRepository.setDiveTypeVisibility]). + Future setDiveTypeVisibility( + String id, { + required bool showInDetailHeader, + required bool showInListView, + }) async { + await _repository.setDiveTypeVisibility( + id, + showInDetailHeader: showInDetailHeader, + showInListView: showInListView, + ); + await _loadDiveTypes(); + _ref.invalidate(diveTypesProvider); + _ref.invalidate(diveTypeStatisticsProvider); + _ref.invalidate(customDiveTypesProvider); + } + /// Delete a custom dive type (built-in types cannot be deleted) Future deleteDiveType(String id) async { await _repository.deleteDiveType(id); diff --git a/lib/features/trips/presentation/widgets/story/trip_story_day_card.dart b/lib/features/trips/presentation/widgets/story/trip_story_day_card.dart index 410140d779..c7f7ceac1f 100644 --- a/lib/features/trips/presentation/widgets/story/trip_story_day_card.dart +++ b/lib/features/trips/presentation/widgets/story/trip_story_day_card.dart @@ -36,6 +36,12 @@ class TripStoryDayCard extends ConsumerWidget { // Built once for the day's dives rather than per row. final diveTypeLabelResolver = watchDiveTypeLabelResolver(ref, context.l10n); + final diveTypeShortLabelResolver = watchDiveTypeShortLabelResolver( + ref, + context.l10n, + ); + final diveTypeListVisibilityPredicate = + watchDiveTypeListVisibilityPredicate(ref); // The day title, subtitle, and Planned chip live in the sticky // TripStoryDayHeader above this card; the card is body-only. A planned @@ -84,6 +90,9 @@ class TripStoryDayCard extends ConsumerWidget { (index, dive) => DiveListItem( summary: DiveSummary.fromDive(dive), diveTypeLabelResolver: diveTypeLabelResolver, + diveTypeShortLabelResolver: diveTypeShortLabelResolver, + diveTypeListVisibilityPredicate: + diveTypeListVisibilityPredicate, // The story already holds the full Dive; pass it so the // configurable card can resolve fields absent from the // summary (tanks, SAC, buddies, weights). diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index 959b84d371..9c1294d51e 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -2959,24 +2959,42 @@ "diveSites_summary_stat_totalSites": "إجمالي المواقع", "diveSites_summary_stat_withGps": "مع GPS", "diveType_builtin_altitude": "ارتفاع", + "diveType_builtin_altitude_short": "ارتفاع", "diveType_builtin_boat": "من القارب", + "diveType_builtin_boat_short": "قارب", "diveType_builtin_cave": "كهف", + "diveType_builtin_cave_short": "كهف", "diveType_builtin_cavern": "كهف ضحل", + "diveType_builtin_cavern_short": "كهف ضحل", "diveType_builtin_deep": "عميق", + "diveType_builtin_deep_short": "عميق", "diveType_builtin_drift": "انجراف", + "diveType_builtin_drift_short": "انجراف", "diveType_builtin_freedive": "غطس حر", + "diveType_builtin_freedive_short": "غطس حر", "diveType_builtin_ice": "جليد", + "diveType_builtin_ice_short": "جليد", "diveType_builtin_liveaboard": "رحلة غوص بحرية", + "diveType_builtin_liveaboard_short": "رحلة غوص", "diveType_builtin_night": "ليلي", + "diveType_builtin_night_short": "ليلي", "diveType_builtin_recreational": "ترفيهي", + "diveType_builtin_recreational_short": "ترفيه", "diveType_builtin_shore": "من الشاطئ", + "diveType_builtin_shore_short": "شاطئ", "diveType_builtin_technical": "تقني", + "diveType_builtin_technical_short": "تقني", "diveType_builtin_training": "تدريب", + "diveType_builtin_training_short": "تدريب", "diveType_builtin_wreck": "حطام", + "diveType_builtin_wreck_short": "حطام", "diveTypes_addDialog_addButton": "إضافة", "diveTypes_addDialog_nameHint": "مثال: البحث والإنقاذ", "diveTypes_addDialog_nameLabel": "اسم نوع الغوص", "diveTypes_addDialog_nameValidation": "الرجاء إدخال اسم", + "diveTypes_addDialog_shortNameHelper": "يظهر في رأس تفاصيل الغطسة عند ضيق المساحة", + "diveTypes_addDialog_shortNameHint": "مثال: ب.إ", + "diveTypes_addDialog_shortNameLabel": "اسم مختصر (اختياري)", "diveTypes_addDialog_title": "إضافة نوع غوص مخصص", "diveTypes_addTooltip": "إضافة نوع غوص", "diveTypes_appBar_title": "أنواع الغوص", @@ -2987,6 +3005,15 @@ "diveTypes_deleteDialog_content": "هل أنت متأكد من حذف \"{name}\"؟", "diveTypes_deleteDialog_title": "حذف نوع الغوص؟", "diveTypes_deleteTooltip": "حذف نوع الغوص", + "diveTypes_editDialog_builtInNameHelper": "لا يمكن تغيير الأسماء المدمجة", + "diveTypes_editDialog_saveButton": "حفظ", + "diveTypes_editDialog_title": "تعديل نوع الغوص", + "diveTypes_showInHeaderLabel": "الترويسة", + "diveTypes_showInHeaderTooltip": "إظهار شارة هذا النوع في ترويسة تفاصيل الغطسة", + "diveTypes_showInListLabel": "القائمة", + "diveTypes_showInListTooltip": "إظهار شارة هذا النوع في قائمة الغطسات", + "diveTypes_snackbar_errorUpdating": "خطأ في تحديث نوع الغوص: {error}", + "diveTypes_snackbar_updated": "تم تحديث \"{name}\"", "diveTypes_snackbar_added": "تمت إضافة نوع الغوص: {name}", "diveTypes_snackbar_cannotDelete": "لا يمكن حذف \"{name}\" - مستخدم في غطسات موجودة", "diveTypes_snackbar_deleted": "تم حذف \"{name}\"", diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index e84df0a8be..cf068f5300 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -2959,24 +2959,42 @@ "diveSites_summary_stat_totalSites": "Tauchplätze gesamt", "diveSites_summary_stat_withGps": "Mit GPS", "diveType_builtin_altitude": "Bergseetauchen", + "diveType_builtin_altitude_short": "Bergsee", "diveType_builtin_boat": "Bootstauchgang", + "diveType_builtin_boat_short": "Boot", "diveType_builtin_cave": "Höhlentauchen", + "diveType_builtin_cave_short": "Höhle", "diveType_builtin_cavern": "Cavern", + "diveType_builtin_cavern_short": "Cavern", "diveType_builtin_deep": "Tieftauchen", + "diveType_builtin_deep_short": "Tief", "diveType_builtin_drift": "Strömungstauchen", + "diveType_builtin_drift_short": "Strömung", "diveType_builtin_freedive": "Apnoetauchen", + "diveType_builtin_freedive_short": "Apnoe", "diveType_builtin_ice": "Eistauchen", + "diveType_builtin_ice_short": "Eis", "diveType_builtin_liveaboard": "Tauchsafari", + "diveType_builtin_liveaboard_short": "Safari", "diveType_builtin_night": "Nachttauchen", + "diveType_builtin_night_short": "Nacht", "diveType_builtin_recreational": "Sporttauchen", + "diveType_builtin_recreational_short": "Rec", "diveType_builtin_shore": "Ufertauchgang", + "diveType_builtin_shore_short": "Ufer", "diveType_builtin_technical": "Technisches Tauchen", + "diveType_builtin_technical_short": "Tec", "diveType_builtin_training": "Ausbildung", + "diveType_builtin_training_short": "Kurs", "diveType_builtin_wreck": "Wracktauchen", + "diveType_builtin_wreck_short": "Wrack", "diveTypes_addDialog_addButton": "Hinzufügen", "diveTypes_addDialog_nameHint": "z.B. Suche & Bergung", "diveTypes_addDialog_nameLabel": "Tauchgangstyp-Name", "diveTypes_addDialog_nameValidation": "Bitte geben Sie einen Namen ein", + "diveTypes_addDialog_shortNameHelper": "Wird im Tauchgang-Header angezeigt, wenn der Platz knapp ist", + "diveTypes_addDialog_shortNameHint": "z.B. S&B", + "diveTypes_addDialog_shortNameLabel": "Kurzname (optional)", "diveTypes_addDialog_title": "Benutzerdefinierten Tauchgangstyp hinzufügen", "diveTypes_addTooltip": "Tauchgangstyp hinzufügen", "diveTypes_appBar_title": "Tauchgangstypen", @@ -2987,11 +3005,20 @@ "diveTypes_deleteDialog_content": "Möchten Sie \"{name}\" wirklich löschen?", "diveTypes_deleteDialog_title": "Tauchgangstyp löschen?", "diveTypes_deleteTooltip": "Tauchgangstyp löschen", + "diveTypes_editDialog_builtInNameHelper": "Interne Namen können nicht geändert werden", + "diveTypes_editDialog_saveButton": "Speichern", + "diveTypes_editDialog_title": "Tauchgangstyp bearbeiten", + "diveTypes_showInHeaderLabel": "Kopfzeile", + "diveTypes_showInHeaderTooltip": "Badge dieses Typs in der Tauchgang-Kopfzeile anzeigen", + "diveTypes_showInListLabel": "Liste", + "diveTypes_showInListTooltip": "Badge dieses Typs in der Tauchgangsliste anzeigen", "diveTypes_snackbar_added": "Tauchgangstyp hinzugefügt: {name}", "diveTypes_snackbar_cannotDelete": "\"{name}\" kann nicht gelöscht werden - wird von vorhandenen Tauchgängen verwendet", "diveTypes_snackbar_deleted": "\"{name}\" gelöscht", "diveTypes_snackbar_errorAdding": "Fehler beim Hinzufügen des Tauchgangstyps: {error}", "diveTypes_snackbar_errorDeleting": "Fehler beim Löschen des Tauchgangstyps: {error}", + "diveTypes_snackbar_errorUpdating": "Fehler beim Aktualisieren des Tauchgangstyps: {error}", + "diveTypes_snackbar_updated": "\"{name}\" aktualisiert", "divers_detail_activeDiver": "Aktiver Taucher", "divers_detail_allergiesLabel": "Allergien", "divers_detail_appBarTitle": "Taucher", diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 9f5f0603c7..4bfc17885e 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -5440,24 +5440,42 @@ } }, "diveType_builtin_altitude": "Altitude", + "diveType_builtin_altitude_short": "Alt", "diveType_builtin_boat": "Boat", + "diveType_builtin_boat_short": "Boat", "diveType_builtin_cave": "Cave", + "diveType_builtin_cave_short": "Cave", "diveType_builtin_cavern": "Cavern", + "diveType_builtin_cavern_short": "Cavern", "diveType_builtin_deep": "Deep", + "diveType_builtin_deep_short": "Deep", "diveType_builtin_drift": "Drift", + "diveType_builtin_drift_short": "Drift", "diveType_builtin_freedive": "Freedive", + "diveType_builtin_freedive_short": "Free", "diveType_builtin_ice": "Ice", + "diveType_builtin_ice_short": "Ice", "diveType_builtin_liveaboard": "Liveaboard", + "diveType_builtin_liveaboard_short": "Live", "diveType_builtin_night": "Night", + "diveType_builtin_night_short": "Night", "diveType_builtin_recreational": "Recreational", + "diveType_builtin_recreational_short": "Rec", "diveType_builtin_shore": "Shore", + "diveType_builtin_shore_short": "Shore", "diveType_builtin_technical": "Technical", + "diveType_builtin_technical_short": "Tec", "diveType_builtin_training": "Training", + "diveType_builtin_training_short": "Training", "diveType_builtin_wreck": "Wreck", + "diveType_builtin_wreck_short": "Wreck", "diveTypes_addDialog_addButton": "Add", "diveTypes_addDialog_nameHint": "e.g., Search & Recovery", "diveTypes_addDialog_nameLabel": "Dive Type Name", "diveTypes_addDialog_nameValidation": "Please enter a name", + "diveTypes_addDialog_shortNameHelper": "Shown on the dive detail header when space is tight", + "diveTypes_addDialog_shortNameHint": "e.g., S&R", + "diveTypes_addDialog_shortNameLabel": "Short name (optional)", "diveTypes_addDialog_title": "Add Custom Dive Type", "diveTypes_addTooltip": "Add dive type", "diveTypes_appBar_title": "Dive Types", @@ -5468,11 +5486,34 @@ "diveTypes_deleteDialog_content": "Are you sure you want to delete \"{name}\"?", "diveTypes_deleteDialog_title": "Delete Dive Type?", "diveTypes_deleteTooltip": "Delete dive type", + "diveTypes_editDialog_builtInNameHelper": "Built-in names can't be changed", + "diveTypes_editDialog_saveButton": "Save", + "diveTypes_editDialog_title": "Edit Dive Type", + "diveTypes_showInHeaderLabel": "Header", + "diveTypes_showInHeaderTooltip": "Show this type's badge in the dive detail header", + "diveTypes_showInListLabel": "List", + "diveTypes_showInListTooltip": "Show this type's badge in the dive list", "diveTypes_snackbar_added": "Added dive type: {name}", "diveTypes_snackbar_cannotDelete": "Cannot delete \"{name}\" - it is used by existing dives", "diveTypes_snackbar_deleted": "Deleted \"{name}\"", "diveTypes_snackbar_errorAdding": "Error adding dive type: {error}", "diveTypes_snackbar_errorDeleting": "Error deleting dive type: {error}", + "diveTypes_snackbar_errorUpdating": "Error updating dive type: {error}", + "diveTypes_snackbar_updated": "Updated \"{name}\"", + "@diveTypes_snackbar_errorUpdating": { + "placeholders": { + "error": { + "type": "Object" + } + } + }, + "@diveTypes_snackbar_updated": { + "placeholders": { + "name": { + "type": "Object" + } + } + }, "@diveTypes_deleteDialog_content": { "placeholders": { "name": { diff --git a/lib/l10n/arb/app_es.arb b/lib/l10n/arb/app_es.arb index 526fd3e0fa..bec0b421e1 100644 --- a/lib/l10n/arb/app_es.arb +++ b/lib/l10n/arb/app_es.arb @@ -2959,24 +2959,42 @@ "diveSites_summary_stat_totalSites": "Total de sitios", "diveSites_summary_stat_withGps": "Con GPS", "diveType_builtin_altitude": "Altitud", + "diveType_builtin_altitude_short": "Altitud", "diveType_builtin_boat": "Desde barco", + "diveType_builtin_boat_short": "Barco", "diveType_builtin_cave": "Cueva", + "diveType_builtin_cave_short": "Cueva", "diveType_builtin_cavern": "Caverna", + "diveType_builtin_cavern_short": "Caverna", "diveType_builtin_deep": "Profunda", + "diveType_builtin_deep_short": "Profunda", "diveType_builtin_drift": "Deriva", + "diveType_builtin_drift_short": "Deriva", "diveType_builtin_freedive": "Apnea", + "diveType_builtin_freedive_short": "Apnea", "diveType_builtin_ice": "Hielo", + "diveType_builtin_ice_short": "Hielo", "diveType_builtin_liveaboard": "Crucero de buceo", + "diveType_builtin_liveaboard_short": "Crucero", "diveType_builtin_night": "Nocturna", + "diveType_builtin_night_short": "Nocturna", "diveType_builtin_recreational": "Recreativa", + "diveType_builtin_recreational_short": "Rec", "diveType_builtin_shore": "Desde costa", + "diveType_builtin_shore_short": "Costa", "diveType_builtin_technical": "Técnica", + "diveType_builtin_technical_short": "Tec", "diveType_builtin_training": "Formación", + "diveType_builtin_training_short": "Formación", "diveType_builtin_wreck": "Pecio", + "diveType_builtin_wreck_short": "Pecio", "diveTypes_addDialog_addButton": "Agregar", "diveTypes_addDialog_nameHint": "ej., Búsqueda y Recuperación", "diveTypes_addDialog_nameLabel": "Nombre del Tipo de Inmersión", "diveTypes_addDialog_nameValidation": "Por favor ingresa un nombre", + "diveTypes_addDialog_shortNameHelper": "Se muestra en el encabezado del detalle de inmersión cuando hay poco espacio", + "diveTypes_addDialog_shortNameHint": "ej., ByR", + "diveTypes_addDialog_shortNameLabel": "Nombre corto (opcional)", "diveTypes_addDialog_title": "Agregar Tipo de Inmersión Personalizado", "diveTypes_addTooltip": "Agregar tipo de inmersión", "diveTypes_appBar_title": "Tipos de Inmersión", @@ -2987,6 +3005,15 @@ "diveTypes_deleteDialog_content": "¿Estás seguro de que deseas eliminar \"{name}\"?", "diveTypes_deleteDialog_title": "¿Eliminar Tipo de Inmersión?", "diveTypes_deleteTooltip": "Eliminar tipo de inmersión", + "diveTypes_editDialog_builtInNameHelper": "Los nombres integrados no se pueden cambiar", + "diveTypes_editDialog_saveButton": "Guardar", + "diveTypes_editDialog_title": "Editar tipo de inmersión", + "diveTypes_showInHeaderLabel": "Encabezado", + "diveTypes_showInHeaderTooltip": "Mostrar la insignia de este tipo en el encabezado de detalles de la inmersión", + "diveTypes_showInListLabel": "Lista", + "diveTypes_showInListTooltip": "Mostrar la insignia de este tipo en la lista de inmersiones", + "diveTypes_snackbar_errorUpdating": "Error al actualizar el tipo de inmersión: {error}", + "diveTypes_snackbar_updated": "\"{name}\" actualizado", "diveTypes_snackbar_added": "Tipo de inmersión agregado: {name}", "diveTypes_snackbar_cannotDelete": "No se puede eliminar \"{name}\" - está siendo usado por inmersiones existentes", "diveTypes_snackbar_deleted": "Eliminado \"{name}\"", diff --git a/lib/l10n/arb/app_fr.arb b/lib/l10n/arb/app_fr.arb index abb02ed6aa..9682bb3689 100644 --- a/lib/l10n/arb/app_fr.arb +++ b/lib/l10n/arb/app_fr.arb @@ -2886,24 +2886,42 @@ "diveSites_summary_stat_totalSites": "Total sites", "diveSites_summary_stat_withGps": "Avec GPS", "diveType_builtin_altitude": "Altitude", + "diveType_builtin_altitude_short": "Altitude", "diveType_builtin_boat": "Depuis un bateau", + "diveType_builtin_boat_short": "Bateau", "diveType_builtin_cave": "Grotte", + "diveType_builtin_cave_short": "Grotte", "diveType_builtin_cavern": "Caverne", + "diveType_builtin_cavern_short": "Caverne", "diveType_builtin_deep": "Profonde", + "diveType_builtin_deep_short": "Profonde", "diveType_builtin_drift": "Dérive", + "diveType_builtin_drift_short": "Dérive", "diveType_builtin_freedive": "Apnée", + "diveType_builtin_freedive_short": "Apnée", "diveType_builtin_ice": "Sous glace", + "diveType_builtin_ice_short": "Glace", "diveType_builtin_liveaboard": "Croisière plongée", + "diveType_builtin_liveaboard_short": "Croisière", "diveType_builtin_night": "Nuit", + "diveType_builtin_night_short": "Nuit", "diveType_builtin_recreational": "Loisir", + "diveType_builtin_recreational_short": "Rec", "diveType_builtin_shore": "Depuis la côte", + "diveType_builtin_shore_short": "Côte", "diveType_builtin_technical": "Technique", + "diveType_builtin_technical_short": "Tec", "diveType_builtin_training": "Formation", + "diveType_builtin_training_short": "Formation", "diveType_builtin_wreck": "Épave", + "diveType_builtin_wreck_short": "Épave", "diveTypes_addDialog_addButton": "Ajouter", "diveTypes_addDialog_nameHint": "ex. Recherche et récupération", "diveTypes_addDialog_nameLabel": "Nom du type de plongée", "diveTypes_addDialog_nameValidation": "Veuillez entrer un nom", + "diveTypes_addDialog_shortNameHelper": "Affiché dans l'en-tête du détail de plongée quand la place manque", + "diveTypes_addDialog_shortNameHint": "ex. RR", + "diveTypes_addDialog_shortNameLabel": "Nom court (facultatif)", "diveTypes_addDialog_title": "Ajouter un type de plongée personnalisé", "diveTypes_addTooltip": "Ajouter un type de plongée", "diveTypes_appBar_title": "Types de plongée", @@ -2914,6 +2932,15 @@ "diveTypes_deleteDialog_content": "Voulez-vous vraiment supprimer « {name} » ?", "diveTypes_deleteDialog_title": "Supprimer le type de plongée ?", "diveTypes_deleteTooltip": "Supprimer le type de plongée", + "diveTypes_editDialog_builtInNameHelper": "Les noms intégrés ne peuvent pas être modifiés", + "diveTypes_editDialog_saveButton": "Enregistrer", + "diveTypes_editDialog_title": "Modifier le type de plongée", + "diveTypes_showInHeaderLabel": "En-tête", + "diveTypes_showInHeaderTooltip": "Afficher le badge de ce type dans l'en-tête des détails de la plongée", + "diveTypes_showInListLabel": "Liste", + "diveTypes_showInListTooltip": "Afficher le badge de ce type dans la liste des plongées", + "diveTypes_snackbar_errorUpdating": "Erreur lors de la mise à jour du type de plongée : {error}", + "diveTypes_snackbar_updated": "\"{name}\" mis à jour", "diveTypes_snackbar_added": "Type de plongée ajouté : {name}", "diveTypes_snackbar_cannotDelete": "Impossible de supprimer « {name} » - il est utilisé par des plongées existantes", "diveTypes_snackbar_deleted": "« {name} » supprimé", diff --git a/lib/l10n/arb/app_he.arb b/lib/l10n/arb/app_he.arb index 15dcf9310c..e214eec86b 100644 --- a/lib/l10n/arb/app_he.arb +++ b/lib/l10n/arb/app_he.arb @@ -2886,24 +2886,42 @@ "diveSites_summary_stat_totalSites": "סה\"כ אתרים", "diveSites_summary_stat_withGps": "עם GPS", "diveType_builtin_altitude": "גובה רב", + "diveType_builtin_altitude_short": "גובה", "diveType_builtin_boat": "מסירה", + "diveType_builtin_boat_short": "מסירה", "diveType_builtin_cave": "מערה", + "diveType_builtin_cave_short": "מערה", "diveType_builtin_cavern": "מערה פתוחה", + "diveType_builtin_cavern_short": "מערה פתוחה", "diveType_builtin_deep": "עמוקה", + "diveType_builtin_deep_short": "עמוקה", "diveType_builtin_drift": "סחף", + "diveType_builtin_drift_short": "סחף", "diveType_builtin_freedive": "צלילה חופשית", + "diveType_builtin_freedive_short": "חופשית", "diveType_builtin_ice": "קרח", + "diveType_builtin_ice_short": "קרח", "diveType_builtin_liveaboard": "שייט צלילה", + "diveType_builtin_liveaboard_short": "שייט", "diveType_builtin_night": "לילה", + "diveType_builtin_night_short": "לילה", "diveType_builtin_recreational": "ספורטיבי", + "diveType_builtin_recreational_short": "ספורט", "diveType_builtin_shore": "מהחוף", + "diveType_builtin_shore_short": "מהחוף", "diveType_builtin_technical": "טכני", + "diveType_builtin_technical_short": "טכני", "diveType_builtin_training": "הכשרה", + "diveType_builtin_training_short": "הכשרה", "diveType_builtin_wreck": "ספינה טבועה", + "diveType_builtin_wreck_short": "טבועה", "diveTypes_addDialog_addButton": "הוסף", "diveTypes_addDialog_nameHint": "לדוגמה: חיפוש ושחזור", "diveTypes_addDialog_nameLabel": "שם סוג צלילה", "diveTypes_addDialog_nameValidation": "נא להזין שם", + "diveTypes_addDialog_shortNameHelper": "מוצג בכותרת פרטי הצלילה כשאין הרבה מקום", + "diveTypes_addDialog_shortNameHint": "לדוגמה: ח.ש", + "diveTypes_addDialog_shortNameLabel": "שם קצר (אופציונלי)", "diveTypes_addDialog_title": "הוסף סוג צלילה מותאם", "diveTypes_addTooltip": "הוסף סוג צלילה", "diveTypes_appBar_title": "סוגי צלילה", @@ -2914,6 +2932,15 @@ "diveTypes_deleteDialog_content": "האם אתה בטוח שברצונך למחוק את \"{name}\"?", "diveTypes_deleteDialog_title": "למחוק סוג צלילה?", "diveTypes_deleteTooltip": "מחק סוג צלילה", + "diveTypes_editDialog_builtInNameHelper": "לא ניתן לשנות שמות מובנים", + "diveTypes_editDialog_saveButton": "שמירה", + "diveTypes_editDialog_title": "עריכת סוג צלילה", + "diveTypes_showInHeaderLabel": "כותרת", + "diveTypes_showInHeaderTooltip": "הצג את התג של סוג זה בכותרת פרטי הצלילה", + "diveTypes_showInListLabel": "רשימה", + "diveTypes_showInListTooltip": "הצג את התג של סוג זה ברשימת הצלילות", + "diveTypes_snackbar_errorUpdating": "שגיאה בעדכון סוג הצלילה: {error}", + "diveTypes_snackbar_updated": "\"{name}\" עודכן", "diveTypes_snackbar_added": "סוג צלילה נוסף: {name}", "diveTypes_snackbar_cannotDelete": "לא ניתן למחוק את \"{name}\" - הוא משמש צלילות קיימות", "diveTypes_snackbar_deleted": "נמחק \"{name}\"", diff --git a/lib/l10n/arb/app_hu.arb b/lib/l10n/arb/app_hu.arb index 9b4d9434f3..d43699d696 100644 --- a/lib/l10n/arb/app_hu.arb +++ b/lib/l10n/arb/app_hu.arb @@ -2886,24 +2886,42 @@ "diveSites_summary_stat_totalSites": "Osszes helyszin", "diveSites_summary_stat_withGps": "GPS-szel", "diveType_builtin_altitude": "Magaslati", + "diveType_builtin_altitude_short": "Magaslati", "diveType_builtin_boat": "Hajóról", + "diveType_builtin_boat_short": "Hajó", "diveType_builtin_cave": "Barlang", + "diveType_builtin_cave_short": "Barlang", "diveType_builtin_cavern": "Barlangbejárat", + "diveType_builtin_cavern_short": "Bejárat", "diveType_builtin_deep": "Mély", + "diveType_builtin_deep_short": "Mély", "diveType_builtin_drift": "Sodrásos", + "diveType_builtin_drift_short": "Sodrásos", "diveType_builtin_freedive": "Szabadtüdős", + "diveType_builtin_freedive_short": "Szabad", "diveType_builtin_ice": "Jég", + "diveType_builtin_ice_short": "Jég", "diveType_builtin_liveaboard": "Búvárhajós", + "diveType_builtin_liveaboard_short": "Szafari", "diveType_builtin_night": "Éjszakai", + "diveType_builtin_night_short": "Éjszakai", "diveType_builtin_recreational": "Szabadidős", + "diveType_builtin_recreational_short": "Rec", "diveType_builtin_shore": "Partról", + "diveType_builtin_shore_short": "Part", "diveType_builtin_technical": "Technikai", + "diveType_builtin_technical_short": "Tec", "diveType_builtin_training": "Képzés", + "diveType_builtin_training_short": "Képzés", "diveType_builtin_wreck": "Roncs", + "diveType_builtin_wreck_short": "Roncs", "diveTypes_addDialog_addButton": "Hozzáadás", "diveTypes_addDialog_nameHint": "pl. Kutatás és mentés", "diveTypes_addDialog_nameLabel": "Merülés típus neve", "diveTypes_addDialog_nameValidation": "Adj meg egy nevet", + "diveTypes_addDialog_shortNameHelper": "A merülés részletek fejlécében jelenik meg, ha kevés a hely", + "diveTypes_addDialog_shortNameHint": "pl. K&M", + "diveTypes_addDialog_shortNameLabel": "Rövid név (opcionális)", "diveTypes_addDialog_title": "Egyedi merülés típus hozzáadása", "diveTypes_addTooltip": "Merülés típus hozzáadása", "diveTypes_appBar_title": "Merülés típusok", @@ -2914,6 +2932,15 @@ "diveTypes_deleteDialog_content": "Biztosan törölni szeretnéd: \"{name}\"?", "diveTypes_deleteDialog_title": "Merülés típus törlése?", "diveTypes_deleteTooltip": "Merülés típus törlése", + "diveTypes_editDialog_builtInNameHelper": "A beépített nevek nem módosíthatók", + "diveTypes_editDialog_saveButton": "Mentés", + "diveTypes_editDialog_title": "Merülési típus szerkesztése", + "diveTypes_showInHeaderLabel": "Fejléc", + "diveTypes_showInHeaderTooltip": "Ezen típus jelvényének megjelenítése a merülés részleteinek fejlécében", + "diveTypes_showInListLabel": "Lista", + "diveTypes_showInListTooltip": "Ezen típus jelvényének megjelenítése a merülési listában", + "diveTypes_snackbar_errorUpdating": "Hiba a merülési típus frissítésekor: {error}", + "diveTypes_snackbar_updated": "\"{name}\" frissítve", "diveTypes_snackbar_added": "Merülés típus hozzáadva: {name}", "diveTypes_snackbar_cannotDelete": "Nem lehet törölni \"{name}\" - meglévő merülések használják", "diveTypes_snackbar_deleted": "Törölve: \"{name}\"", diff --git a/lib/l10n/arb/app_it.arb b/lib/l10n/arb/app_it.arb index 035b4a5986..f7700ab65f 100644 --- a/lib/l10n/arb/app_it.arb +++ b/lib/l10n/arb/app_it.arb @@ -2886,24 +2886,42 @@ "diveSites_summary_stat_totalSites": "Siti totali", "diveSites_summary_stat_withGps": "Con GPS", "diveType_builtin_altitude": "Altitudine", + "diveType_builtin_altitude_short": "Altitudine", "diveType_builtin_boat": "Da barca", + "diveType_builtin_boat_short": "Barca", "diveType_builtin_cave": "Grotta", + "diveType_builtin_cave_short": "Grotta", "diveType_builtin_cavern": "Caverna", + "diveType_builtin_cavern_short": "Caverna", "diveType_builtin_deep": "Profonda", + "diveType_builtin_deep_short": "Profonda", "diveType_builtin_drift": "Corrente", + "diveType_builtin_drift_short": "Corrente", "diveType_builtin_freedive": "Apnea", + "diveType_builtin_freedive_short": "Apnea", "diveType_builtin_ice": "Sotto ghiaccio", + "diveType_builtin_ice_short": "Ghiaccio", "diveType_builtin_liveaboard": "Crociera subacquea", + "diveType_builtin_liveaboard_short": "Crociera", "diveType_builtin_night": "Notturna", + "diveType_builtin_night_short": "Notturna", "diveType_builtin_recreational": "Ricreativa", + "diveType_builtin_recreational_short": "Rec", "diveType_builtin_shore": "Da riva", + "diveType_builtin_shore_short": "Riva", "diveType_builtin_technical": "Tecnica", + "diveType_builtin_technical_short": "Tec", "diveType_builtin_training": "Addestramento", + "diveType_builtin_training_short": "Addestramento", "diveType_builtin_wreck": "Relitto", + "diveType_builtin_wreck_short": "Relitto", "diveTypes_addDialog_addButton": "Aggiungi", "diveTypes_addDialog_nameHint": "es., Ricerca e Recupero", "diveTypes_addDialog_nameLabel": "Nome Tipo Immersione", "diveTypes_addDialog_nameValidation": "Inserisci un nome", + "diveTypes_addDialog_shortNameHelper": "Mostrato nell'intestazione del dettaglio immersione quando lo spazio è limitato", + "diveTypes_addDialog_shortNameHint": "es. RR", + "diveTypes_addDialog_shortNameLabel": "Nome breve (opzionale)", "diveTypes_addDialog_title": "Aggiungi Tipo Immersione Personalizzato", "diveTypes_addTooltip": "Aggiungi tipo immersione", "diveTypes_appBar_title": "Tipi di Immersione", @@ -2914,6 +2932,15 @@ "diveTypes_deleteDialog_content": "Sei sicuro di voler eliminare \"{name}\"?", "diveTypes_deleteDialog_title": "Eliminare Tipo Immersione?", "diveTypes_deleteTooltip": "Elimina tipo immersione", + "diveTypes_editDialog_builtInNameHelper": "I nomi predefiniti non possono essere modificati", + "diveTypes_editDialog_saveButton": "Salva", + "diveTypes_editDialog_title": "Modifica tipo di immersione", + "diveTypes_showInHeaderLabel": "Intestazione", + "diveTypes_showInHeaderTooltip": "Mostra il badge di questo tipo nell'intestazione dei dettagli dell'immersione", + "diveTypes_showInListLabel": "Elenco", + "diveTypes_showInListTooltip": "Mostra il badge di questo tipo nell'elenco delle immersioni", + "diveTypes_snackbar_errorUpdating": "Errore durante l'aggiornamento del tipo di immersione: {error}", + "diveTypes_snackbar_updated": "\"{name}\" aggiornato", "diveTypes_snackbar_added": "Tipo immersione aggiunto: {name}", "diveTypes_snackbar_cannotDelete": "Impossibile eliminare \"{name}\" - è usato da immersioni esistenti", "diveTypes_snackbar_deleted": "Eliminato \"{name}\"", diff --git a/lib/l10n/arb/app_localizations.dart b/lib/l10n/arb/app_localizations.dart index 16eb04d905..b5a0482554 100644 --- a/lib/l10n/arb/app_localizations.dart +++ b/lib/l10n/arb/app_localizations.dart @@ -15350,90 +15350,180 @@ abstract class AppLocalizations { /// **'Altitude'** String get diveType_builtin_altitude; + /// No description provided for @diveType_builtin_altitude_short. + /// + /// In en, this message translates to: + /// **'Alt'** + String get diveType_builtin_altitude_short; + /// No description provided for @diveType_builtin_boat. /// /// In en, this message translates to: /// **'Boat'** String get diveType_builtin_boat; + /// No description provided for @diveType_builtin_boat_short. + /// + /// In en, this message translates to: + /// **'Boat'** + String get diveType_builtin_boat_short; + /// No description provided for @diveType_builtin_cave. /// /// In en, this message translates to: /// **'Cave'** String get diveType_builtin_cave; + /// No description provided for @diveType_builtin_cave_short. + /// + /// In en, this message translates to: + /// **'Cave'** + String get diveType_builtin_cave_short; + /// No description provided for @diveType_builtin_cavern. /// /// In en, this message translates to: /// **'Cavern'** String get diveType_builtin_cavern; + /// No description provided for @diveType_builtin_cavern_short. + /// + /// In en, this message translates to: + /// **'Cavern'** + String get diveType_builtin_cavern_short; + /// No description provided for @diveType_builtin_deep. /// /// In en, this message translates to: /// **'Deep'** String get diveType_builtin_deep; + /// No description provided for @diveType_builtin_deep_short. + /// + /// In en, this message translates to: + /// **'Deep'** + String get diveType_builtin_deep_short; + /// No description provided for @diveType_builtin_drift. /// /// In en, this message translates to: /// **'Drift'** String get diveType_builtin_drift; + /// No description provided for @diveType_builtin_drift_short. + /// + /// In en, this message translates to: + /// **'Drift'** + String get diveType_builtin_drift_short; + /// No description provided for @diveType_builtin_freedive. /// /// In en, this message translates to: /// **'Freedive'** String get diveType_builtin_freedive; + /// No description provided for @diveType_builtin_freedive_short. + /// + /// In en, this message translates to: + /// **'Free'** + String get diveType_builtin_freedive_short; + /// No description provided for @diveType_builtin_ice. /// /// In en, this message translates to: /// **'Ice'** String get diveType_builtin_ice; + /// No description provided for @diveType_builtin_ice_short. + /// + /// In en, this message translates to: + /// **'Ice'** + String get diveType_builtin_ice_short; + /// No description provided for @diveType_builtin_liveaboard. /// /// In en, this message translates to: /// **'Liveaboard'** String get diveType_builtin_liveaboard; + /// No description provided for @diveType_builtin_liveaboard_short. + /// + /// In en, this message translates to: + /// **'Live'** + String get diveType_builtin_liveaboard_short; + /// No description provided for @diveType_builtin_night. /// /// In en, this message translates to: /// **'Night'** String get diveType_builtin_night; + /// No description provided for @diveType_builtin_night_short. + /// + /// In en, this message translates to: + /// **'Night'** + String get diveType_builtin_night_short; + /// No description provided for @diveType_builtin_recreational. /// /// In en, this message translates to: /// **'Recreational'** String get diveType_builtin_recreational; + /// No description provided for @diveType_builtin_recreational_short. + /// + /// In en, this message translates to: + /// **'Rec'** + String get diveType_builtin_recreational_short; + /// No description provided for @diveType_builtin_shore. /// /// In en, this message translates to: /// **'Shore'** String get diveType_builtin_shore; + /// No description provided for @diveType_builtin_shore_short. + /// + /// In en, this message translates to: + /// **'Shore'** + String get diveType_builtin_shore_short; + /// No description provided for @diveType_builtin_technical. /// /// In en, this message translates to: /// **'Technical'** String get diveType_builtin_technical; + /// No description provided for @diveType_builtin_technical_short. + /// + /// In en, this message translates to: + /// **'Tec'** + String get diveType_builtin_technical_short; + /// No description provided for @diveType_builtin_training. /// /// In en, this message translates to: /// **'Training'** String get diveType_builtin_training; + /// No description provided for @diveType_builtin_training_short. + /// + /// In en, this message translates to: + /// **'Training'** + String get diveType_builtin_training_short; + /// No description provided for @diveType_builtin_wreck. /// /// In en, this message translates to: /// **'Wreck'** String get diveType_builtin_wreck; + /// No description provided for @diveType_builtin_wreck_short. + /// + /// In en, this message translates to: + /// **'Wreck'** + String get diveType_builtin_wreck_short; + /// No description provided for @diveTypes_addDialog_addButton. /// /// In en, this message translates to: @@ -15458,6 +15548,24 @@ abstract class AppLocalizations { /// **'Please enter a name'** String get diveTypes_addDialog_nameValidation; + /// No description provided for @diveTypes_addDialog_shortNameHelper. + /// + /// In en, this message translates to: + /// **'Shown on the dive detail header when space is tight'** + String get diveTypes_addDialog_shortNameHelper; + + /// No description provided for @diveTypes_addDialog_shortNameHint. + /// + /// In en, this message translates to: + /// **'e.g., S&R'** + String get diveTypes_addDialog_shortNameHint; + + /// No description provided for @diveTypes_addDialog_shortNameLabel. + /// + /// In en, this message translates to: + /// **'Short name (optional)'** + String get diveTypes_addDialog_shortNameLabel; + /// No description provided for @diveTypes_addDialog_title. /// /// In en, this message translates to: @@ -15518,6 +15626,48 @@ abstract class AppLocalizations { /// **'Delete dive type'** String get diveTypes_deleteTooltip; + /// No description provided for @diveTypes_editDialog_builtInNameHelper. + /// + /// In en, this message translates to: + /// **'Built-in names can\'t be changed'** + String get diveTypes_editDialog_builtInNameHelper; + + /// No description provided for @diveTypes_editDialog_saveButton. + /// + /// In en, this message translates to: + /// **'Save'** + String get diveTypes_editDialog_saveButton; + + /// No description provided for @diveTypes_editDialog_title. + /// + /// In en, this message translates to: + /// **'Edit Dive Type'** + String get diveTypes_editDialog_title; + + /// No description provided for @diveTypes_showInHeaderLabel. + /// + /// In en, this message translates to: + /// **'Header'** + String get diveTypes_showInHeaderLabel; + + /// No description provided for @diveTypes_showInHeaderTooltip. + /// + /// In en, this message translates to: + /// **'Show this type\'s badge in the dive detail header'** + String get diveTypes_showInHeaderTooltip; + + /// No description provided for @diveTypes_showInListLabel. + /// + /// In en, this message translates to: + /// **'List'** + String get diveTypes_showInListLabel; + + /// No description provided for @diveTypes_showInListTooltip. + /// + /// In en, this message translates to: + /// **'Show this type\'s badge in the dive list'** + String get diveTypes_showInListTooltip; + /// No description provided for @diveTypes_snackbar_added. /// /// In en, this message translates to: @@ -15548,6 +15698,18 @@ abstract class AppLocalizations { /// **'Error deleting dive type: {error}'** String diveTypes_snackbar_errorDeleting(Object error); + /// No description provided for @diveTypes_snackbar_errorUpdating. + /// + /// In en, this message translates to: + /// **'Error updating dive type: {error}'** + String diveTypes_snackbar_errorUpdating(Object error); + + /// No description provided for @diveTypes_snackbar_updated. + /// + /// In en, this message translates to: + /// **'Updated \"{name}\"'** + String diveTypes_snackbar_updated(Object name); + /// No description provided for @divers_detail_activeDiver. /// /// 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 e9063e094d..4a70e2a617 100644 --- a/lib/l10n/arb/app_localizations_ar.dart +++ b/lib/l10n/arb/app_localizations_ar.dart @@ -9016,48 +9016,93 @@ class AppLocalizationsAr extends AppLocalizations { @override String get diveType_builtin_altitude => 'ارتفاع'; + @override + String get diveType_builtin_altitude_short => 'ارتفاع'; + @override String get diveType_builtin_boat => 'من القارب'; + @override + String get diveType_builtin_boat_short => 'قارب'; + @override String get diveType_builtin_cave => 'كهف'; + @override + String get diveType_builtin_cave_short => 'كهف'; + @override String get diveType_builtin_cavern => 'كهف ضحل'; + @override + String get diveType_builtin_cavern_short => 'كهف ضحل'; + @override String get diveType_builtin_deep => 'عميق'; + @override + String get diveType_builtin_deep_short => 'عميق'; + @override String get diveType_builtin_drift => 'انجراف'; + @override + String get diveType_builtin_drift_short => 'انجراف'; + @override String get diveType_builtin_freedive => 'غطس حر'; + @override + String get diveType_builtin_freedive_short => 'غطس حر'; + @override String get diveType_builtin_ice => 'جليد'; + @override + String get diveType_builtin_ice_short => 'جليد'; + @override String get diveType_builtin_liveaboard => 'رحلة غوص بحرية'; + @override + String get diveType_builtin_liveaboard_short => 'رحلة غوص'; + @override String get diveType_builtin_night => 'ليلي'; + @override + String get diveType_builtin_night_short => 'ليلي'; + @override String get diveType_builtin_recreational => 'ترفيهي'; + @override + String get diveType_builtin_recreational_short => 'ترفيه'; + @override String get diveType_builtin_shore => 'من الشاطئ'; + @override + String get diveType_builtin_shore_short => 'شاطئ'; + @override String get diveType_builtin_technical => 'تقني'; + @override + String get diveType_builtin_technical_short => 'تقني'; + @override String get diveType_builtin_training => 'تدريب'; + @override + String get diveType_builtin_training_short => 'تدريب'; + @override String get diveType_builtin_wreck => 'حطام'; + @override + String get diveType_builtin_wreck_short => 'حطام'; + @override String get diveTypes_addDialog_addButton => 'إضافة'; @@ -9070,6 +9115,16 @@ class AppLocalizationsAr extends AppLocalizations { @override String get diveTypes_addDialog_nameValidation => 'الرجاء إدخال اسم'; + @override + String get diveTypes_addDialog_shortNameHelper => + 'يظهر في رأس تفاصيل الغطسة عند ضيق المساحة'; + + @override + String get diveTypes_addDialog_shortNameHint => 'مثال: ب.إ'; + + @override + String get diveTypes_addDialog_shortNameLabel => 'اسم مختصر (اختياري)'; + @override String get diveTypes_addDialog_title => 'إضافة نوع غوص مخصص'; @@ -9102,6 +9157,30 @@ class AppLocalizationsAr extends AppLocalizations { @override String get diveTypes_deleteTooltip => 'حذف نوع الغوص'; + @override + String get diveTypes_editDialog_builtInNameHelper => + 'لا يمكن تغيير الأسماء المدمجة'; + + @override + String get diveTypes_editDialog_saveButton => 'حفظ'; + + @override + String get diveTypes_editDialog_title => 'تعديل نوع الغوص'; + + @override + String get diveTypes_showInHeaderLabel => 'الترويسة'; + + @override + String get diveTypes_showInHeaderTooltip => + 'إظهار شارة هذا النوع في ترويسة تفاصيل الغطسة'; + + @override + String get diveTypes_showInListLabel => 'القائمة'; + + @override + String get diveTypes_showInListTooltip => + 'إظهار شارة هذا النوع في قائمة الغطسات'; + @override String diveTypes_snackbar_added(Object name) { return 'تمت إضافة نوع الغوص: $name'; @@ -9127,6 +9206,16 @@ class AppLocalizationsAr extends AppLocalizations { return 'خطأ في حذف نوع الغوص: $error'; } + @override + String diveTypes_snackbar_errorUpdating(Object error) { + return 'خطأ في تحديث نوع الغوص: $error'; + } + + @override + String diveTypes_snackbar_updated(Object name) { + return 'تم تحديث \"$name\"'; + } + @override String get divers_detail_activeDiver => 'الغواص النشط'; diff --git a/lib/l10n/arb/app_localizations_de.dart b/lib/l10n/arb/app_localizations_de.dart index 23f700fea2..9c6c7042cc 100644 --- a/lib/l10n/arb/app_localizations_de.dart +++ b/lib/l10n/arb/app_localizations_de.dart @@ -9187,48 +9187,93 @@ class AppLocalizationsDe extends AppLocalizations { @override String get diveType_builtin_altitude => 'Bergseetauchen'; + @override + String get diveType_builtin_altitude_short => 'Bergsee'; + @override String get diveType_builtin_boat => 'Bootstauchgang'; + @override + String get diveType_builtin_boat_short => 'Boot'; + @override String get diveType_builtin_cave => 'Höhlentauchen'; + @override + String get diveType_builtin_cave_short => 'Höhle'; + @override String get diveType_builtin_cavern => 'Cavern'; + @override + String get diveType_builtin_cavern_short => 'Cavern'; + @override String get diveType_builtin_deep => 'Tieftauchen'; + @override + String get diveType_builtin_deep_short => 'Tief'; + @override String get diveType_builtin_drift => 'Strömungstauchen'; + @override + String get diveType_builtin_drift_short => 'Strömung'; + @override String get diveType_builtin_freedive => 'Apnoetauchen'; + @override + String get diveType_builtin_freedive_short => 'Apnoe'; + @override String get diveType_builtin_ice => 'Eistauchen'; + @override + String get diveType_builtin_ice_short => 'Eis'; + @override String get diveType_builtin_liveaboard => 'Tauchsafari'; + @override + String get diveType_builtin_liveaboard_short => 'Safari'; + @override String get diveType_builtin_night => 'Nachttauchen'; + @override + String get diveType_builtin_night_short => 'Nacht'; + @override String get diveType_builtin_recreational => 'Sporttauchen'; + @override + String get diveType_builtin_recreational_short => 'Rec'; + @override String get diveType_builtin_shore => 'Ufertauchgang'; + @override + String get diveType_builtin_shore_short => 'Ufer'; + @override String get diveType_builtin_technical => 'Technisches Tauchen'; + @override + String get diveType_builtin_technical_short => 'Tec'; + @override String get diveType_builtin_training => 'Ausbildung'; + @override + String get diveType_builtin_training_short => 'Kurs'; + @override String get diveType_builtin_wreck => 'Wracktauchen'; + @override + String get diveType_builtin_wreck_short => 'Wrack'; + @override String get diveTypes_addDialog_addButton => 'Hinzufügen'; @@ -9242,6 +9287,16 @@ class AppLocalizationsDe extends AppLocalizations { String get diveTypes_addDialog_nameValidation => 'Bitte geben Sie einen Namen ein'; + @override + String get diveTypes_addDialog_shortNameHelper => + 'Wird im Tauchgang-Header angezeigt, wenn der Platz knapp ist'; + + @override + String get diveTypes_addDialog_shortNameHint => 'z.B. S&B'; + + @override + String get diveTypes_addDialog_shortNameLabel => 'Kurzname (optional)'; + @override String get diveTypes_addDialog_title => 'Benutzerdefinierten Tauchgangstyp hinzufügen'; @@ -9275,6 +9330,30 @@ class AppLocalizationsDe extends AppLocalizations { @override String get diveTypes_deleteTooltip => 'Tauchgangstyp löschen'; + @override + String get diveTypes_editDialog_builtInNameHelper => + 'Interne Namen können nicht geändert werden'; + + @override + String get diveTypes_editDialog_saveButton => 'Speichern'; + + @override + String get diveTypes_editDialog_title => 'Tauchgangstyp bearbeiten'; + + @override + String get diveTypes_showInHeaderLabel => 'Kopfzeile'; + + @override + String get diveTypes_showInHeaderTooltip => + 'Badge dieses Typs in der Tauchgang-Kopfzeile anzeigen'; + + @override + String get diveTypes_showInListLabel => 'Liste'; + + @override + String get diveTypes_showInListTooltip => + 'Badge dieses Typs in der Tauchgangsliste anzeigen'; + @override String diveTypes_snackbar_added(Object name) { return 'Tauchgangstyp hinzugefügt: $name'; @@ -9300,6 +9379,16 @@ class AppLocalizationsDe extends AppLocalizations { return 'Fehler beim Löschen des Tauchgangstyps: $error'; } + @override + String diveTypes_snackbar_errorUpdating(Object error) { + return 'Fehler beim Aktualisieren des Tauchgangstyps: $error'; + } + + @override + String diveTypes_snackbar_updated(Object name) { + return '\"$name\" aktualisiert'; + } + @override String get divers_detail_activeDiver => 'Aktiver Taucher'; diff --git a/lib/l10n/arb/app_localizations_en.dart b/lib/l10n/arb/app_localizations_en.dart index c618e3a65f..de6a28db6b 100644 --- a/lib/l10n/arb/app_localizations_en.dart +++ b/lib/l10n/arb/app_localizations_en.dart @@ -9034,48 +9034,93 @@ class AppLocalizationsEn extends AppLocalizations { @override String get diveType_builtin_altitude => 'Altitude'; + @override + String get diveType_builtin_altitude_short => 'Alt'; + @override String get diveType_builtin_boat => 'Boat'; + @override + String get diveType_builtin_boat_short => 'Boat'; + @override String get diveType_builtin_cave => 'Cave'; + @override + String get diveType_builtin_cave_short => 'Cave'; + @override String get diveType_builtin_cavern => 'Cavern'; + @override + String get diveType_builtin_cavern_short => 'Cavern'; + @override String get diveType_builtin_deep => 'Deep'; + @override + String get diveType_builtin_deep_short => 'Deep'; + @override String get diveType_builtin_drift => 'Drift'; + @override + String get diveType_builtin_drift_short => 'Drift'; + @override String get diveType_builtin_freedive => 'Freedive'; + @override + String get diveType_builtin_freedive_short => 'Free'; + @override String get diveType_builtin_ice => 'Ice'; + @override + String get diveType_builtin_ice_short => 'Ice'; + @override String get diveType_builtin_liveaboard => 'Liveaboard'; + @override + String get diveType_builtin_liveaboard_short => 'Live'; + @override String get diveType_builtin_night => 'Night'; + @override + String get diveType_builtin_night_short => 'Night'; + @override String get diveType_builtin_recreational => 'Recreational'; + @override + String get diveType_builtin_recreational_short => 'Rec'; + @override String get diveType_builtin_shore => 'Shore'; + @override + String get diveType_builtin_shore_short => 'Shore'; + @override String get diveType_builtin_technical => 'Technical'; + @override + String get diveType_builtin_technical_short => 'Tec'; + @override String get diveType_builtin_training => 'Training'; + @override + String get diveType_builtin_training_short => 'Training'; + @override String get diveType_builtin_wreck => 'Wreck'; + @override + String get diveType_builtin_wreck_short => 'Wreck'; + @override String get diveTypes_addDialog_addButton => 'Add'; @@ -9088,6 +9133,16 @@ class AppLocalizationsEn extends AppLocalizations { @override String get diveTypes_addDialog_nameValidation => 'Please enter a name'; + @override + String get diveTypes_addDialog_shortNameHelper => + 'Shown on the dive detail header when space is tight'; + + @override + String get diveTypes_addDialog_shortNameHint => 'e.g., S&R'; + + @override + String get diveTypes_addDialog_shortNameLabel => 'Short name (optional)'; + @override String get diveTypes_addDialog_title => 'Add Custom Dive Type'; @@ -9120,6 +9175,30 @@ class AppLocalizationsEn extends AppLocalizations { @override String get diveTypes_deleteTooltip => 'Delete dive type'; + @override + String get diveTypes_editDialog_builtInNameHelper => + 'Built-in names can\'t be changed'; + + @override + String get diveTypes_editDialog_saveButton => 'Save'; + + @override + String get diveTypes_editDialog_title => 'Edit Dive Type'; + + @override + String get diveTypes_showInHeaderLabel => 'Header'; + + @override + String get diveTypes_showInHeaderTooltip => + 'Show this type\'s badge in the dive detail header'; + + @override + String get diveTypes_showInListLabel => 'List'; + + @override + String get diveTypes_showInListTooltip => + 'Show this type\'s badge in the dive list'; + @override String diveTypes_snackbar_added(Object name) { return 'Added dive type: $name'; @@ -9145,6 +9224,16 @@ class AppLocalizationsEn extends AppLocalizations { return 'Error deleting dive type: $error'; } + @override + String diveTypes_snackbar_errorUpdating(Object error) { + return 'Error updating dive type: $error'; + } + + @override + String diveTypes_snackbar_updated(Object name) { + return 'Updated \"$name\"'; + } + @override String get divers_detail_activeDiver => 'Active Diver'; diff --git a/lib/l10n/arb/app_localizations_es.dart b/lib/l10n/arb/app_localizations_es.dart index e1b5510924..d78efbbf9e 100644 --- a/lib/l10n/arb/app_localizations_es.dart +++ b/lib/l10n/arb/app_localizations_es.dart @@ -9187,48 +9187,93 @@ class AppLocalizationsEs extends AppLocalizations { @override String get diveType_builtin_altitude => 'Altitud'; + @override + String get diveType_builtin_altitude_short => 'Altitud'; + @override String get diveType_builtin_boat => 'Desde barco'; + @override + String get diveType_builtin_boat_short => 'Barco'; + @override String get diveType_builtin_cave => 'Cueva'; + @override + String get diveType_builtin_cave_short => 'Cueva'; + @override String get diveType_builtin_cavern => 'Caverna'; + @override + String get diveType_builtin_cavern_short => 'Caverna'; + @override String get diveType_builtin_deep => 'Profunda'; + @override + String get diveType_builtin_deep_short => 'Profunda'; + @override String get diveType_builtin_drift => 'Deriva'; + @override + String get diveType_builtin_drift_short => 'Deriva'; + @override String get diveType_builtin_freedive => 'Apnea'; + @override + String get diveType_builtin_freedive_short => 'Apnea'; + @override String get diveType_builtin_ice => 'Hielo'; + @override + String get diveType_builtin_ice_short => 'Hielo'; + @override String get diveType_builtin_liveaboard => 'Crucero de buceo'; + @override + String get diveType_builtin_liveaboard_short => 'Crucero'; + @override String get diveType_builtin_night => 'Nocturna'; + @override + String get diveType_builtin_night_short => 'Nocturna'; + @override String get diveType_builtin_recreational => 'Recreativa'; + @override + String get diveType_builtin_recreational_short => 'Rec'; + @override String get diveType_builtin_shore => 'Desde costa'; + @override + String get diveType_builtin_shore_short => 'Costa'; + @override String get diveType_builtin_technical => 'Técnica'; + @override + String get diveType_builtin_technical_short => 'Tec'; + @override String get diveType_builtin_training => 'Formación'; + @override + String get diveType_builtin_training_short => 'Formación'; + @override String get diveType_builtin_wreck => 'Pecio'; + @override + String get diveType_builtin_wreck_short => 'Pecio'; + @override String get diveTypes_addDialog_addButton => 'Agregar'; @@ -9242,6 +9287,16 @@ class AppLocalizationsEs extends AppLocalizations { String get diveTypes_addDialog_nameValidation => 'Por favor ingresa un nombre'; + @override + String get diveTypes_addDialog_shortNameHelper => + 'Se muestra en el encabezado del detalle de inmersión cuando hay poco espacio'; + + @override + String get diveTypes_addDialog_shortNameHint => 'ej., ByR'; + + @override + String get diveTypes_addDialog_shortNameLabel => 'Nombre corto (opcional)'; + @override String get diveTypes_addDialog_title => 'Agregar Tipo de Inmersión Personalizado'; @@ -9275,6 +9330,30 @@ class AppLocalizationsEs extends AppLocalizations { @override String get diveTypes_deleteTooltip => 'Eliminar tipo de inmersión'; + @override + String get diveTypes_editDialog_builtInNameHelper => + 'Los nombres integrados no se pueden cambiar'; + + @override + String get diveTypes_editDialog_saveButton => 'Guardar'; + + @override + String get diveTypes_editDialog_title => 'Editar tipo de inmersión'; + + @override + String get diveTypes_showInHeaderLabel => 'Encabezado'; + + @override + String get diveTypes_showInHeaderTooltip => + 'Mostrar la insignia de este tipo en el encabezado de detalles de la inmersión'; + + @override + String get diveTypes_showInListLabel => 'Lista'; + + @override + String get diveTypes_showInListTooltip => + 'Mostrar la insignia de este tipo en la lista de inmersiones'; + @override String diveTypes_snackbar_added(Object name) { return 'Tipo de inmersión agregado: $name'; @@ -9300,6 +9379,16 @@ class AppLocalizationsEs extends AppLocalizations { return 'Error al eliminar tipo de inmersión: $error'; } + @override + String diveTypes_snackbar_errorUpdating(Object error) { + return 'Error al actualizar el tipo de inmersión: $error'; + } + + @override + String diveTypes_snackbar_updated(Object name) { + return '\"$name\" actualizado'; + } + @override String get divers_detail_activeDiver => 'Buceador activo'; diff --git a/lib/l10n/arb/app_localizations_fr.dart b/lib/l10n/arb/app_localizations_fr.dart index be387b69c7..b79eb77102 100644 --- a/lib/l10n/arb/app_localizations_fr.dart +++ b/lib/l10n/arb/app_localizations_fr.dart @@ -9221,48 +9221,93 @@ class AppLocalizationsFr extends AppLocalizations { @override String get diveType_builtin_altitude => 'Altitude'; + @override + String get diveType_builtin_altitude_short => 'Altitude'; + @override String get diveType_builtin_boat => 'Depuis un bateau'; + @override + String get diveType_builtin_boat_short => 'Bateau'; + @override String get diveType_builtin_cave => 'Grotte'; + @override + String get diveType_builtin_cave_short => 'Grotte'; + @override String get diveType_builtin_cavern => 'Caverne'; + @override + String get diveType_builtin_cavern_short => 'Caverne'; + @override String get diveType_builtin_deep => 'Profonde'; + @override + String get diveType_builtin_deep_short => 'Profonde'; + @override String get diveType_builtin_drift => 'Dérive'; + @override + String get diveType_builtin_drift_short => 'Dérive'; + @override String get diveType_builtin_freedive => 'Apnée'; + @override + String get diveType_builtin_freedive_short => 'Apnée'; + @override String get diveType_builtin_ice => 'Sous glace'; + @override + String get diveType_builtin_ice_short => 'Glace'; + @override String get diveType_builtin_liveaboard => 'Croisière plongée'; + @override + String get diveType_builtin_liveaboard_short => 'Croisière'; + @override String get diveType_builtin_night => 'Nuit'; + @override + String get diveType_builtin_night_short => 'Nuit'; + @override String get diveType_builtin_recreational => 'Loisir'; + @override + String get diveType_builtin_recreational_short => 'Rec'; + @override String get diveType_builtin_shore => 'Depuis la côte'; + @override + String get diveType_builtin_shore_short => 'Côte'; + @override String get diveType_builtin_technical => 'Technique'; + @override + String get diveType_builtin_technical_short => 'Tec'; + @override String get diveType_builtin_training => 'Formation'; + @override + String get diveType_builtin_training_short => 'Formation'; + @override String get diveType_builtin_wreck => 'Épave'; + @override + String get diveType_builtin_wreck_short => 'Épave'; + @override String get diveTypes_addDialog_addButton => 'Ajouter'; @@ -9275,6 +9320,16 @@ class AppLocalizationsFr extends AppLocalizations { @override String get diveTypes_addDialog_nameValidation => 'Veuillez entrer un nom'; + @override + String get diveTypes_addDialog_shortNameHelper => + 'Affiché dans l\'en-tête du détail de plongée quand la place manque'; + + @override + String get diveTypes_addDialog_shortNameHint => 'ex. RR'; + + @override + String get diveTypes_addDialog_shortNameLabel => 'Nom court (facultatif)'; + @override String get diveTypes_addDialog_title => 'Ajouter un type de plongée personnalisé'; @@ -9308,6 +9363,30 @@ class AppLocalizationsFr extends AppLocalizations { @override String get diveTypes_deleteTooltip => 'Supprimer le type de plongée'; + @override + String get diveTypes_editDialog_builtInNameHelper => + 'Les noms intégrés ne peuvent pas être modifiés'; + + @override + String get diveTypes_editDialog_saveButton => 'Enregistrer'; + + @override + String get diveTypes_editDialog_title => 'Modifier le type de plongée'; + + @override + String get diveTypes_showInHeaderLabel => 'En-tête'; + + @override + String get diveTypes_showInHeaderTooltip => + 'Afficher le badge de ce type dans l\'en-tête des détails de la plongée'; + + @override + String get diveTypes_showInListLabel => 'Liste'; + + @override + String get diveTypes_showInListTooltip => + 'Afficher le badge de ce type dans la liste des plongées'; + @override String diveTypes_snackbar_added(Object name) { return 'Type de plongée ajouté : $name'; @@ -9333,6 +9412,16 @@ class AppLocalizationsFr extends AppLocalizations { return 'Erreur lors de la suppression du type de plongée : $error'; } + @override + String diveTypes_snackbar_errorUpdating(Object error) { + return 'Erreur lors de la mise à jour du type de plongée : $error'; + } + + @override + String diveTypes_snackbar_updated(Object name) { + return '\"$name\" mis à jour'; + } + @override String get divers_detail_activeDiver => 'Plongeur actif'; diff --git a/lib/l10n/arb/app_localizations_he.dart b/lib/l10n/arb/app_localizations_he.dart index 53506352f1..58ffa5b8b4 100644 --- a/lib/l10n/arb/app_localizations_he.dart +++ b/lib/l10n/arb/app_localizations_he.dart @@ -8958,48 +8958,93 @@ class AppLocalizationsHe extends AppLocalizations { @override String get diveType_builtin_altitude => 'גובה רב'; + @override + String get diveType_builtin_altitude_short => 'גובה'; + @override String get diveType_builtin_boat => 'מסירה'; + @override + String get diveType_builtin_boat_short => 'מסירה'; + @override String get diveType_builtin_cave => 'מערה'; + @override + String get diveType_builtin_cave_short => 'מערה'; + @override String get diveType_builtin_cavern => 'מערה פתוחה'; + @override + String get diveType_builtin_cavern_short => 'מערה פתוחה'; + @override String get diveType_builtin_deep => 'עמוקה'; + @override + String get diveType_builtin_deep_short => 'עמוקה'; + @override String get diveType_builtin_drift => 'סחף'; + @override + String get diveType_builtin_drift_short => 'סחף'; + @override String get diveType_builtin_freedive => 'צלילה חופשית'; + @override + String get diveType_builtin_freedive_short => 'חופשית'; + @override String get diveType_builtin_ice => 'קרח'; + @override + String get diveType_builtin_ice_short => 'קרח'; + @override String get diveType_builtin_liveaboard => 'שייט צלילה'; + @override + String get diveType_builtin_liveaboard_short => 'שייט'; + @override String get diveType_builtin_night => 'לילה'; + @override + String get diveType_builtin_night_short => 'לילה'; + @override String get diveType_builtin_recreational => 'ספורטיבי'; + @override + String get diveType_builtin_recreational_short => 'ספורט'; + @override String get diveType_builtin_shore => 'מהחוף'; + @override + String get diveType_builtin_shore_short => 'מהחוף'; + @override String get diveType_builtin_technical => 'טכני'; + @override + String get diveType_builtin_technical_short => 'טכני'; + @override String get diveType_builtin_training => 'הכשרה'; + @override + String get diveType_builtin_training_short => 'הכשרה'; + @override String get diveType_builtin_wreck => 'ספינה טבועה'; + @override + String get diveType_builtin_wreck_short => 'טבועה'; + @override String get diveTypes_addDialog_addButton => 'הוסף'; @@ -9012,6 +9057,16 @@ class AppLocalizationsHe extends AppLocalizations { @override String get diveTypes_addDialog_nameValidation => 'נא להזין שם'; + @override + String get diveTypes_addDialog_shortNameHelper => + 'מוצג בכותרת פרטי הצלילה כשאין הרבה מקום'; + + @override + String get diveTypes_addDialog_shortNameHint => 'לדוגמה: ח.ש'; + + @override + String get diveTypes_addDialog_shortNameLabel => 'שם קצר (אופציונלי)'; + @override String get diveTypes_addDialog_title => 'הוסף סוג צלילה מותאם'; @@ -9044,6 +9099,30 @@ class AppLocalizationsHe extends AppLocalizations { @override String get diveTypes_deleteTooltip => 'מחק סוג צלילה'; + @override + String get diveTypes_editDialog_builtInNameHelper => + 'לא ניתן לשנות שמות מובנים'; + + @override + String get diveTypes_editDialog_saveButton => 'שמירה'; + + @override + String get diveTypes_editDialog_title => 'עריכת סוג צלילה'; + + @override + String get diveTypes_showInHeaderLabel => 'כותרת'; + + @override + String get diveTypes_showInHeaderTooltip => + 'הצג את התג של סוג זה בכותרת פרטי הצלילה'; + + @override + String get diveTypes_showInListLabel => 'רשימה'; + + @override + String get diveTypes_showInListTooltip => + 'הצג את התג של סוג זה ברשימת הצלילות'; + @override String diveTypes_snackbar_added(Object name) { return 'סוג צלילה נוסף: $name'; @@ -9069,6 +9148,16 @@ class AppLocalizationsHe extends AppLocalizations { return 'שגיאה במחיקת סוג צלילה: $error'; } + @override + String diveTypes_snackbar_errorUpdating(Object error) { + return 'שגיאה בעדכון סוג הצלילה: $error'; + } + + @override + String diveTypes_snackbar_updated(Object name) { + return '\"$name\" עודכן'; + } + @override String get divers_detail_activeDiver => 'צולל פעיל'; diff --git a/lib/l10n/arb/app_localizations_hu.dart b/lib/l10n/arb/app_localizations_hu.dart index 4c51596901..67db729f8e 100644 --- a/lib/l10n/arb/app_localizations_hu.dart +++ b/lib/l10n/arb/app_localizations_hu.dart @@ -9162,48 +9162,93 @@ class AppLocalizationsHu extends AppLocalizations { @override String get diveType_builtin_altitude => 'Magaslati'; + @override + String get diveType_builtin_altitude_short => 'Magaslati'; + @override String get diveType_builtin_boat => 'Hajóról'; + @override + String get diveType_builtin_boat_short => 'Hajó'; + @override String get diveType_builtin_cave => 'Barlang'; + @override + String get diveType_builtin_cave_short => 'Barlang'; + @override String get diveType_builtin_cavern => 'Barlangbejárat'; + @override + String get diveType_builtin_cavern_short => 'Bejárat'; + @override String get diveType_builtin_deep => 'Mély'; + @override + String get diveType_builtin_deep_short => 'Mély'; + @override String get diveType_builtin_drift => 'Sodrásos'; + @override + String get diveType_builtin_drift_short => 'Sodrásos'; + @override String get diveType_builtin_freedive => 'Szabadtüdős'; + @override + String get diveType_builtin_freedive_short => 'Szabad'; + @override String get diveType_builtin_ice => 'Jég'; + @override + String get diveType_builtin_ice_short => 'Jég'; + @override String get diveType_builtin_liveaboard => 'Búvárhajós'; + @override + String get diveType_builtin_liveaboard_short => 'Szafari'; + @override String get diveType_builtin_night => 'Éjszakai'; + @override + String get diveType_builtin_night_short => 'Éjszakai'; + @override String get diveType_builtin_recreational => 'Szabadidős'; + @override + String get diveType_builtin_recreational_short => 'Rec'; + @override String get diveType_builtin_shore => 'Partról'; + @override + String get diveType_builtin_shore_short => 'Part'; + @override String get diveType_builtin_technical => 'Technikai'; + @override + String get diveType_builtin_technical_short => 'Tec'; + @override String get diveType_builtin_training => 'Képzés'; + @override + String get diveType_builtin_training_short => 'Képzés'; + @override String get diveType_builtin_wreck => 'Roncs'; + @override + String get diveType_builtin_wreck_short => 'Roncs'; + @override String get diveTypes_addDialog_addButton => 'Hozzáadás'; @@ -9216,6 +9261,16 @@ class AppLocalizationsHu extends AppLocalizations { @override String get diveTypes_addDialog_nameValidation => 'Adj meg egy nevet'; + @override + String get diveTypes_addDialog_shortNameHelper => + 'A merülés részletek fejlécében jelenik meg, ha kevés a hely'; + + @override + String get diveTypes_addDialog_shortNameHint => 'pl. K&M'; + + @override + String get diveTypes_addDialog_shortNameLabel => 'Rövid név (opcionális)'; + @override String get diveTypes_addDialog_title => 'Egyedi merülés típus hozzáadása'; @@ -9248,6 +9303,30 @@ class AppLocalizationsHu extends AppLocalizations { @override String get diveTypes_deleteTooltip => 'Merülés típus törlése'; + @override + String get diveTypes_editDialog_builtInNameHelper => + 'A beépített nevek nem módosíthatók'; + + @override + String get diveTypes_editDialog_saveButton => 'Mentés'; + + @override + String get diveTypes_editDialog_title => 'Merülési típus szerkesztése'; + + @override + String get diveTypes_showInHeaderLabel => 'Fejléc'; + + @override + String get diveTypes_showInHeaderTooltip => + 'Ezen típus jelvényének megjelenítése a merülés részleteinek fejlécében'; + + @override + String get diveTypes_showInListLabel => 'Lista'; + + @override + String get diveTypes_showInListTooltip => + 'Ezen típus jelvényének megjelenítése a merülési listában'; + @override String diveTypes_snackbar_added(Object name) { return 'Merülés típus hozzáadva: $name'; @@ -9273,6 +9352,16 @@ class AppLocalizationsHu extends AppLocalizations { return 'Hiba a merülés típus törlésekor: $error'; } + @override + String diveTypes_snackbar_errorUpdating(Object error) { + return 'Hiba a merülési típus frissítésekor: $error'; + } + + @override + String diveTypes_snackbar_updated(Object name) { + return '\"$name\" frissítve'; + } + @override String get divers_detail_activeDiver => 'Aktiv merülo'; diff --git a/lib/l10n/arb/app_localizations_it.dart b/lib/l10n/arb/app_localizations_it.dart index e5531df98e..ed595b90d2 100644 --- a/lib/l10n/arb/app_localizations_it.dart +++ b/lib/l10n/arb/app_localizations_it.dart @@ -9187,48 +9187,93 @@ class AppLocalizationsIt extends AppLocalizations { @override String get diveType_builtin_altitude => 'Altitudine'; + @override + String get diveType_builtin_altitude_short => 'Altitudine'; + @override String get diveType_builtin_boat => 'Da barca'; + @override + String get diveType_builtin_boat_short => 'Barca'; + @override String get diveType_builtin_cave => 'Grotta'; + @override + String get diveType_builtin_cave_short => 'Grotta'; + @override String get diveType_builtin_cavern => 'Caverna'; + @override + String get diveType_builtin_cavern_short => 'Caverna'; + @override String get diveType_builtin_deep => 'Profonda'; + @override + String get diveType_builtin_deep_short => 'Profonda'; + @override String get diveType_builtin_drift => 'Corrente'; + @override + String get diveType_builtin_drift_short => 'Corrente'; + @override String get diveType_builtin_freedive => 'Apnea'; + @override + String get diveType_builtin_freedive_short => 'Apnea'; + @override String get diveType_builtin_ice => 'Sotto ghiaccio'; + @override + String get diveType_builtin_ice_short => 'Ghiaccio'; + @override String get diveType_builtin_liveaboard => 'Crociera subacquea'; + @override + String get diveType_builtin_liveaboard_short => 'Crociera'; + @override String get diveType_builtin_night => 'Notturna'; + @override + String get diveType_builtin_night_short => 'Notturna'; + @override String get diveType_builtin_recreational => 'Ricreativa'; + @override + String get diveType_builtin_recreational_short => 'Rec'; + @override String get diveType_builtin_shore => 'Da riva'; + @override + String get diveType_builtin_shore_short => 'Riva'; + @override String get diveType_builtin_technical => 'Tecnica'; + @override + String get diveType_builtin_technical_short => 'Tec'; + @override String get diveType_builtin_training => 'Addestramento'; + @override + String get diveType_builtin_training_short => 'Addestramento'; + @override String get diveType_builtin_wreck => 'Relitto'; + @override + String get diveType_builtin_wreck_short => 'Relitto'; + @override String get diveTypes_addDialog_addButton => 'Aggiungi'; @@ -9241,6 +9286,16 @@ class AppLocalizationsIt extends AppLocalizations { @override String get diveTypes_addDialog_nameValidation => 'Inserisci un nome'; + @override + String get diveTypes_addDialog_shortNameHelper => + 'Mostrato nell\'intestazione del dettaglio immersione quando lo spazio è limitato'; + + @override + String get diveTypes_addDialog_shortNameHint => 'es. RR'; + + @override + String get diveTypes_addDialog_shortNameLabel => 'Nome breve (opzionale)'; + @override String get diveTypes_addDialog_title => 'Aggiungi Tipo Immersione Personalizzato'; @@ -9274,6 +9329,30 @@ class AppLocalizationsIt extends AppLocalizations { @override String get diveTypes_deleteTooltip => 'Elimina tipo immersione'; + @override + String get diveTypes_editDialog_builtInNameHelper => + 'I nomi predefiniti non possono essere modificati'; + + @override + String get diveTypes_editDialog_saveButton => 'Salva'; + + @override + String get diveTypes_editDialog_title => 'Modifica tipo di immersione'; + + @override + String get diveTypes_showInHeaderLabel => 'Intestazione'; + + @override + String get diveTypes_showInHeaderTooltip => + 'Mostra il badge di questo tipo nell\'intestazione dei dettagli dell\'immersione'; + + @override + String get diveTypes_showInListLabel => 'Elenco'; + + @override + String get diveTypes_showInListTooltip => + 'Mostra il badge di questo tipo nell\'elenco delle immersioni'; + @override String diveTypes_snackbar_added(Object name) { return 'Tipo immersione aggiunto: $name'; @@ -9299,6 +9378,16 @@ class AppLocalizationsIt extends AppLocalizations { return 'Errore durante l\'eliminazione del tipo immersione: $error'; } + @override + String diveTypes_snackbar_errorUpdating(Object error) { + return 'Errore durante l\'aggiornamento del tipo di immersione: $error'; + } + + @override + String diveTypes_snackbar_updated(Object name) { + return '\"$name\" aggiornato'; + } + @override String get divers_detail_activeDiver => 'Subacqueo attivo'; diff --git a/lib/l10n/arb/app_localizations_nl.dart b/lib/l10n/arb/app_localizations_nl.dart index 45b2eed352..93398351f9 100644 --- a/lib/l10n/arb/app_localizations_nl.dart +++ b/lib/l10n/arb/app_localizations_nl.dart @@ -9116,48 +9116,93 @@ class AppLocalizationsNl extends AppLocalizations { @override String get diveType_builtin_altitude => 'Hoogte'; + @override + String get diveType_builtin_altitude_short => 'Hoogte'; + @override String get diveType_builtin_boat => 'Vanaf boot'; + @override + String get diveType_builtin_boat_short => 'Boot'; + @override String get diveType_builtin_cave => 'Grot'; + @override + String get diveType_builtin_cave_short => 'Grot'; + @override String get diveType_builtin_cavern => 'Cavern'; + @override + String get diveType_builtin_cavern_short => 'Cavern'; + @override String get diveType_builtin_deep => 'Diep'; + @override + String get diveType_builtin_deep_short => 'Diep'; + @override String get diveType_builtin_drift => 'Stroming'; + @override + String get diveType_builtin_drift_short => 'Stroming'; + @override String get diveType_builtin_freedive => 'Vrijduiken'; + @override + String get diveType_builtin_freedive_short => 'Vrij'; + @override String get diveType_builtin_ice => 'IJs'; + @override + String get diveType_builtin_ice_short => 'IJs'; + @override String get diveType_builtin_liveaboard => 'Liveaboard'; + @override + String get diveType_builtin_liveaboard_short => 'Liveaboard'; + @override String get diveType_builtin_night => 'Nacht'; + @override + String get diveType_builtin_night_short => 'Nacht'; + @override String get diveType_builtin_recreational => 'Recreatief'; + @override + String get diveType_builtin_recreational_short => 'Rec'; + @override String get diveType_builtin_shore => 'Vanaf de kant'; + @override + String get diveType_builtin_shore_short => 'Kant'; + @override String get diveType_builtin_technical => 'Technisch'; + @override + String get diveType_builtin_technical_short => 'Tec'; + @override String get diveType_builtin_training => 'Opleiding'; + @override + String get diveType_builtin_training_short => 'Opleiding'; + @override String get diveType_builtin_wreck => 'Wrak'; + @override + String get diveType_builtin_wreck_short => 'Wrak'; + @override String get diveTypes_addDialog_addButton => 'Toevoegen'; @@ -9170,6 +9215,16 @@ class AppLocalizationsNl extends AppLocalizations { @override String get diveTypes_addDialog_nameValidation => 'Voer een naam in'; + @override + String get diveTypes_addDialog_shortNameHelper => + 'Wordt getoond in de duik-header wanneer er weinig ruimte is'; + + @override + String get diveTypes_addDialog_shortNameHint => 'bijv. Z&B'; + + @override + String get diveTypes_addDialog_shortNameLabel => 'Korte naam (optioneel)'; + @override String get diveTypes_addDialog_title => 'Aangepast duiktype toevoegen'; @@ -9202,6 +9257,30 @@ class AppLocalizationsNl extends AppLocalizations { @override String get diveTypes_deleteTooltip => 'Duiktype verwijderen'; + @override + String get diveTypes_editDialog_builtInNameHelper => + 'Ingebouwde namen kunnen niet worden gewijzigd'; + + @override + String get diveTypes_editDialog_saveButton => 'Opslaan'; + + @override + String get diveTypes_editDialog_title => 'Duiktype bewerken'; + + @override + String get diveTypes_showInHeaderLabel => 'Koptekst'; + + @override + String get diveTypes_showInHeaderTooltip => + 'Toon de badge van dit type in de duikdetailkop'; + + @override + String get diveTypes_showInListLabel => 'Lijst'; + + @override + String get diveTypes_showInListTooltip => + 'Toon de badge van dit type in de duiklijst'; + @override String diveTypes_snackbar_added(Object name) { return 'Duiktype toegevoegd: $name'; @@ -9227,6 +9306,16 @@ class AppLocalizationsNl extends AppLocalizations { return 'Fout bij verwijderen duiktype: $error'; } + @override + String diveTypes_snackbar_errorUpdating(Object error) { + return 'Fout bij het bijwerken van het duiktype: $error'; + } + + @override + String diveTypes_snackbar_updated(Object name) { + return '\"$name\" bijgewerkt'; + } + @override String get divers_detail_activeDiver => 'Actieve duiker'; diff --git a/lib/l10n/arb/app_localizations_pt.dart b/lib/l10n/arb/app_localizations_pt.dart index d520bdd42e..d2ebcafc90 100644 --- a/lib/l10n/arb/app_localizations_pt.dart +++ b/lib/l10n/arb/app_localizations_pt.dart @@ -9189,48 +9189,93 @@ class AppLocalizationsPt extends AppLocalizations { @override String get diveType_builtin_altitude => 'Altitude'; + @override + String get diveType_builtin_altitude_short => 'Altitude'; + @override String get diveType_builtin_boat => 'A partir de barco'; + @override + String get diveType_builtin_boat_short => 'Barco'; + @override String get diveType_builtin_cave => 'Gruta'; + @override + String get diveType_builtin_cave_short => 'Gruta'; + @override String get diveType_builtin_cavern => 'Caverna'; + @override + String get diveType_builtin_cavern_short => 'Caverna'; + @override String get diveType_builtin_deep => 'Profundo'; + @override + String get diveType_builtin_deep_short => 'Profundo'; + @override String get diveType_builtin_drift => 'Deriva'; + @override + String get diveType_builtin_drift_short => 'Deriva'; + @override String get diveType_builtin_freedive => 'Apneia'; + @override + String get diveType_builtin_freedive_short => 'Apneia'; + @override String get diveType_builtin_ice => 'Gelo'; + @override + String get diveType_builtin_ice_short => 'Gelo'; + @override String get diveType_builtin_liveaboard => 'Cruzeiro de mergulho'; + @override + String get diveType_builtin_liveaboard_short => 'Cruzeiro'; + @override String get diveType_builtin_night => 'Noturno'; + @override + String get diveType_builtin_night_short => 'Noturno'; + @override String get diveType_builtin_recreational => 'Recreativo'; + @override + String get diveType_builtin_recreational_short => 'Rec'; + @override String get diveType_builtin_shore => 'A partir da costa'; + @override + String get diveType_builtin_shore_short => 'Costa'; + @override String get diveType_builtin_technical => 'Técnico'; + @override + String get diveType_builtin_technical_short => 'Tec'; + @override String get diveType_builtin_training => 'Treinamento'; + @override + String get diveType_builtin_training_short => 'Treinamento'; + @override String get diveType_builtin_wreck => 'Naufrágio'; + @override + String get diveType_builtin_wreck_short => 'Naufrágio'; + @override String get diveTypes_addDialog_addButton => 'Adicionar'; @@ -9243,6 +9288,16 @@ class AppLocalizationsPt extends AppLocalizations { @override String get diveTypes_addDialog_nameValidation => 'Digite um nome'; + @override + String get diveTypes_addDialog_shortNameHelper => + 'Exibido no cabeçalho de detalhes do mergulho quando o espaço é limitado'; + + @override + String get diveTypes_addDialog_shortNameHint => 'ex: ByR'; + + @override + String get diveTypes_addDialog_shortNameLabel => 'Nome curto (opcional)'; + @override String get diveTypes_addDialog_title => 'Adicionar Tipo de Mergulho Personalizado'; @@ -9276,6 +9331,30 @@ class AppLocalizationsPt extends AppLocalizations { @override String get diveTypes_deleteTooltip => 'Excluir tipo de mergulho'; + @override + String get diveTypes_editDialog_builtInNameHelper => + 'Os nomes internos não podem ser alterados'; + + @override + String get diveTypes_editDialog_saveButton => 'Salvar'; + + @override + String get diveTypes_editDialog_title => 'Editar tipo de mergulho'; + + @override + String get diveTypes_showInHeaderLabel => 'Cabeçalho'; + + @override + String get diveTypes_showInHeaderTooltip => + 'Mostrar o selo deste tipo no cabeçalho de detalhes do mergulho'; + + @override + String get diveTypes_showInListLabel => 'Lista'; + + @override + String get diveTypes_showInListTooltip => + 'Mostrar o selo deste tipo na lista de mergulhos'; + @override String diveTypes_snackbar_added(Object name) { return 'Tipo de mergulho adicionado: $name'; @@ -9301,6 +9380,16 @@ class AppLocalizationsPt extends AppLocalizations { return 'Erro ao excluir tipo de mergulho: $error'; } + @override + String diveTypes_snackbar_errorUpdating(Object error) { + return 'Erro ao atualizar o tipo de mergulho: $error'; + } + + @override + String diveTypes_snackbar_updated(Object name) { + return '\"$name\" atualizado'; + } + @override String get divers_detail_activeDiver => 'Mergulhador Ativo'; diff --git a/lib/l10n/arb/app_localizations_zh.dart b/lib/l10n/arb/app_localizations_zh.dart index 5c376f850d..b14cd93279 100644 --- a/lib/l10n/arb/app_localizations_zh.dart +++ b/lib/l10n/arb/app_localizations_zh.dart @@ -8737,48 +8737,93 @@ class AppLocalizationsZh extends AppLocalizations { @override String get diveType_builtin_altitude => '高原潜水'; + @override + String get diveType_builtin_altitude_short => '高原'; + @override String get diveType_builtin_boat => '船潜'; + @override + String get diveType_builtin_boat_short => '船潜'; + @override String get diveType_builtin_cave => '洞穴潜水'; + @override + String get diveType_builtin_cave_short => '洞穴'; + @override String get diveType_builtin_cavern => '洞厅潜水'; + @override + String get diveType_builtin_cavern_short => '洞厅'; + @override String get diveType_builtin_deep => '深潜'; + @override + String get diveType_builtin_deep_short => '深潜'; + @override String get diveType_builtin_drift => '流潜'; + @override + String get diveType_builtin_drift_short => '流潜'; + @override String get diveType_builtin_freedive => '自由潜水'; + @override + String get diveType_builtin_freedive_short => '自由'; + @override String get diveType_builtin_ice => '冰潜'; + @override + String get diveType_builtin_ice_short => '冰潜'; + @override String get diveType_builtin_liveaboard => '船宿潜水'; + @override + String get diveType_builtin_liveaboard_short => '船宿'; + @override String get diveType_builtin_night => '夜潜'; + @override + String get diveType_builtin_night_short => '夜潜'; + @override String get diveType_builtin_recreational => '休闲潜水'; + @override + String get diveType_builtin_recreational_short => '休闲'; + @override String get diveType_builtin_shore => '岸潜'; + @override + String get diveType_builtin_shore_short => '岸潜'; + @override String get diveType_builtin_technical => '技术潜水'; + @override + String get diveType_builtin_technical_short => '技术'; + @override String get diveType_builtin_training => '训练潜水'; + @override + String get diveType_builtin_training_short => '训练'; + @override String get diveType_builtin_wreck => '沉船潜水'; + @override + String get diveType_builtin_wreck_short => '沉船'; + @override String get diveTypes_addDialog_addButton => '添加'; @@ -8791,6 +8836,15 @@ class AppLocalizationsZh extends AppLocalizations { @override String get diveTypes_addDialog_nameValidation => '请输入名称'; + @override + String get diveTypes_addDialog_shortNameHelper => '空间不足时显示在潜水详情标题中'; + + @override + String get diveTypes_addDialog_shortNameHint => '例如:搜救'; + + @override + String get diveTypes_addDialog_shortNameLabel => '简称(可选)'; + @override String get diveTypes_addDialog_title => '添加自定义潜水类型'; @@ -8823,6 +8877,27 @@ class AppLocalizationsZh extends AppLocalizations { @override String get diveTypes_deleteTooltip => '删除潜水类型'; + @override + String get diveTypes_editDialog_builtInNameHelper => '内置名称无法更改'; + + @override + String get diveTypes_editDialog_saveButton => '保存'; + + @override + String get diveTypes_editDialog_title => '编辑潜水类型'; + + @override + String get diveTypes_showInHeaderLabel => '标题栏'; + + @override + String get diveTypes_showInHeaderTooltip => '在潜水详情标题栏中显示此类型的徽章'; + + @override + String get diveTypes_showInListLabel => '列表'; + + @override + String get diveTypes_showInListTooltip => '在潜水列表中显示此类型的徽章'; + @override String diveTypes_snackbar_added(Object name) { return '已添加潜水类型:$name'; @@ -8848,6 +8923,16 @@ class AppLocalizationsZh extends AppLocalizations { return '删除出错潜水类型: $error'; } + @override + String diveTypes_snackbar_errorUpdating(Object error) { + return '更新潜水类型时出错:$error'; + } + + @override + String diveTypes_snackbar_updated(Object name) { + return '已更新“$name”'; + } + @override String get divers_detail_activeDiver => '当前潜水员'; diff --git a/lib/l10n/arb/app_nl.arb b/lib/l10n/arb/app_nl.arb index 935958f996..036dcceb65 100644 --- a/lib/l10n/arb/app_nl.arb +++ b/lib/l10n/arb/app_nl.arb @@ -2959,24 +2959,42 @@ "diveSites_summary_stat_totalSites": "Totaal stekken", "diveSites_summary_stat_withGps": "Met GPS", "diveType_builtin_altitude": "Hoogte", + "diveType_builtin_altitude_short": "Hoogte", "diveType_builtin_boat": "Vanaf boot", + "diveType_builtin_boat_short": "Boot", "diveType_builtin_cave": "Grot", + "diveType_builtin_cave_short": "Grot", "diveType_builtin_cavern": "Cavern", + "diveType_builtin_cavern_short": "Cavern", "diveType_builtin_deep": "Diep", + "diveType_builtin_deep_short": "Diep", "diveType_builtin_drift": "Stroming", + "diveType_builtin_drift_short": "Stroming", "diveType_builtin_freedive": "Vrijduiken", + "diveType_builtin_freedive_short": "Vrij", "diveType_builtin_ice": "IJs", + "diveType_builtin_ice_short": "IJs", "diveType_builtin_liveaboard": "Liveaboard", + "diveType_builtin_liveaboard_short": "Liveaboard", "diveType_builtin_night": "Nacht", + "diveType_builtin_night_short": "Nacht", "diveType_builtin_recreational": "Recreatief", + "diveType_builtin_recreational_short": "Rec", "diveType_builtin_shore": "Vanaf de kant", + "diveType_builtin_shore_short": "Kant", "diveType_builtin_technical": "Technisch", + "diveType_builtin_technical_short": "Tec", "diveType_builtin_training": "Opleiding", + "diveType_builtin_training_short": "Opleiding", "diveType_builtin_wreck": "Wrak", + "diveType_builtin_wreck_short": "Wrak", "diveTypes_addDialog_addButton": "Toevoegen", "diveTypes_addDialog_nameHint": "bijv. Zoeken & Bergen", "diveTypes_addDialog_nameLabel": "Duiktype naam", "diveTypes_addDialog_nameValidation": "Voer een naam in", + "diveTypes_addDialog_shortNameHelper": "Wordt getoond in de duik-header wanneer er weinig ruimte is", + "diveTypes_addDialog_shortNameHint": "bijv. Z&B", + "diveTypes_addDialog_shortNameLabel": "Korte naam (optioneel)", "diveTypes_addDialog_title": "Aangepast duiktype toevoegen", "diveTypes_addTooltip": "Duiktype toevoegen", "diveTypes_appBar_title": "Duiktypes", @@ -2987,6 +3005,15 @@ "diveTypes_deleteDialog_content": "Weet je zeker dat je \"{name}\" wilt verwijderen?", "diveTypes_deleteDialog_title": "Duiktype verwijderen?", "diveTypes_deleteTooltip": "Duiktype verwijderen", + "diveTypes_editDialog_builtInNameHelper": "Ingebouwde namen kunnen niet worden gewijzigd", + "diveTypes_editDialog_saveButton": "Opslaan", + "diveTypes_editDialog_title": "Duiktype bewerken", + "diveTypes_showInHeaderLabel": "Koptekst", + "diveTypes_showInHeaderTooltip": "Toon de badge van dit type in de duikdetailkop", + "diveTypes_showInListLabel": "Lijst", + "diveTypes_showInListTooltip": "Toon de badge van dit type in de duiklijst", + "diveTypes_snackbar_errorUpdating": "Fout bij het bijwerken van het duiktype: {error}", + "diveTypes_snackbar_updated": "\"{name}\" bijgewerkt", "diveTypes_snackbar_added": "Duiktype toegevoegd: {name}", "diveTypes_snackbar_cannotDelete": "Kan \"{name}\" niet verwijderen - wordt gebruikt door bestaande duiken", "diveTypes_snackbar_deleted": "\"{name}\" verwijderd", diff --git a/lib/l10n/arb/app_pt.arb b/lib/l10n/arb/app_pt.arb index 68760866cd..f0a687a6d0 100644 --- a/lib/l10n/arb/app_pt.arb +++ b/lib/l10n/arb/app_pt.arb @@ -2959,24 +2959,42 @@ "diveSites_summary_stat_totalSites": "Total de Pontos", "diveSites_summary_stat_withGps": "Com GPS", "diveType_builtin_altitude": "Altitude", + "diveType_builtin_altitude_short": "Altitude", "diveType_builtin_boat": "A partir de barco", + "diveType_builtin_boat_short": "Barco", "diveType_builtin_cave": "Gruta", + "diveType_builtin_cave_short": "Gruta", "diveType_builtin_cavern": "Caverna", + "diveType_builtin_cavern_short": "Caverna", "diveType_builtin_deep": "Profundo", + "diveType_builtin_deep_short": "Profundo", "diveType_builtin_drift": "Deriva", + "diveType_builtin_drift_short": "Deriva", "diveType_builtin_freedive": "Apneia", + "diveType_builtin_freedive_short": "Apneia", "diveType_builtin_ice": "Gelo", + "diveType_builtin_ice_short": "Gelo", "diveType_builtin_liveaboard": "Cruzeiro de mergulho", + "diveType_builtin_liveaboard_short": "Cruzeiro", "diveType_builtin_night": "Noturno", + "diveType_builtin_night_short": "Noturno", "diveType_builtin_recreational": "Recreativo", + "diveType_builtin_recreational_short": "Rec", "diveType_builtin_shore": "A partir da costa", + "diveType_builtin_shore_short": "Costa", "diveType_builtin_technical": "Técnico", + "diveType_builtin_technical_short": "Tec", "diveType_builtin_training": "Treinamento", + "diveType_builtin_training_short": "Treinamento", "diveType_builtin_wreck": "Naufrágio", + "diveType_builtin_wreck_short": "Naufrágio", "diveTypes_addDialog_addButton": "Adicionar", "diveTypes_addDialog_nameHint": "ex: Busca e Recuperação", "diveTypes_addDialog_nameLabel": "Nome do Tipo de Mergulho", "diveTypes_addDialog_nameValidation": "Digite um nome", + "diveTypes_addDialog_shortNameHelper": "Exibido no cabeçalho de detalhes do mergulho quando o espaço é limitado", + "diveTypes_addDialog_shortNameHint": "ex: ByR", + "diveTypes_addDialog_shortNameLabel": "Nome curto (opcional)", "diveTypes_addDialog_title": "Adicionar Tipo de Mergulho Personalizado", "diveTypes_addTooltip": "Adicionar tipo de mergulho", "diveTypes_appBar_title": "Tipos de Mergulho", @@ -2987,6 +3005,15 @@ "diveTypes_deleteDialog_content": "Tem certeza de que deseja excluir \"{name}\"?", "diveTypes_deleteDialog_title": "Excluir Tipo de Mergulho?", "diveTypes_deleteTooltip": "Excluir tipo de mergulho", + "diveTypes_editDialog_builtInNameHelper": "Os nomes internos não podem ser alterados", + "diveTypes_editDialog_saveButton": "Salvar", + "diveTypes_editDialog_title": "Editar tipo de mergulho", + "diveTypes_showInHeaderLabel": "Cabeçalho", + "diveTypes_showInHeaderTooltip": "Mostrar o selo deste tipo no cabeçalho de detalhes do mergulho", + "diveTypes_showInListLabel": "Lista", + "diveTypes_showInListTooltip": "Mostrar o selo deste tipo na lista de mergulhos", + "diveTypes_snackbar_errorUpdating": "Erro ao atualizar o tipo de mergulho: {error}", + "diveTypes_snackbar_updated": "\"{name}\" atualizado", "diveTypes_snackbar_added": "Tipo de mergulho adicionado: {name}", "diveTypes_snackbar_cannotDelete": "Não é possível excluir \"{name}\" - está sendo usado por mergulhos existentes", "diveTypes_snackbar_deleted": "Excluído \"{name}\"", diff --git a/lib/l10n/arb/app_zh.arb b/lib/l10n/arb/app_zh.arb index 7e786149b9..87dc9d5c7e 100644 --- a/lib/l10n/arb/app_zh.arb +++ b/lib/l10n/arb/app_zh.arb @@ -3092,24 +3092,42 @@ "diveSites_summary_stat_totalSites": "总计潜水点", "diveSites_summary_stat_withGps": "与 GPS", "diveType_builtin_altitude": "高原潜水", + "diveType_builtin_altitude_short": "高原", "diveType_builtin_boat": "船潜", + "diveType_builtin_boat_short": "船潜", "diveType_builtin_cave": "洞穴潜水", + "diveType_builtin_cave_short": "洞穴", "diveType_builtin_cavern": "洞厅潜水", + "diveType_builtin_cavern_short": "洞厅", "diveType_builtin_deep": "深潜", + "diveType_builtin_deep_short": "深潜", "diveType_builtin_drift": "流潜", + "diveType_builtin_drift_short": "流潜", "diveType_builtin_freedive": "自由潜水", + "diveType_builtin_freedive_short": "自由", "diveType_builtin_ice": "冰潜", + "diveType_builtin_ice_short": "冰潜", "diveType_builtin_liveaboard": "船宿潜水", + "diveType_builtin_liveaboard_short": "船宿", "diveType_builtin_night": "夜潜", + "diveType_builtin_night_short": "夜潜", "diveType_builtin_recreational": "休闲潜水", + "diveType_builtin_recreational_short": "休闲", "diveType_builtin_shore": "岸潜", + "diveType_builtin_shore_short": "岸潜", "diveType_builtin_technical": "技术潜水", + "diveType_builtin_technical_short": "技术", "diveType_builtin_training": "训练潜水", + "diveType_builtin_training_short": "训练", "diveType_builtin_wreck": "沉船潜水", + "diveType_builtin_wreck_short": "沉船", "diveTypes_addDialog_addButton": "添加", "diveTypes_addDialog_nameHint": "例如:搜索与救援", "diveTypes_addDialog_nameLabel": "潜水类型名称", "diveTypes_addDialog_nameValidation": "请输入名称", + "diveTypes_addDialog_shortNameHelper": "空间不足时显示在潜水详情标题中", + "diveTypes_addDialog_shortNameHint": "例如:搜救", + "diveTypes_addDialog_shortNameLabel": "简称(可选)", "diveTypes_addDialog_title": "添加自定义潜水类型", "diveTypes_addTooltip": "添加潜水类型", "diveTypes_appBar_title": "潜水类型", @@ -3120,6 +3138,15 @@ "diveTypes_deleteDialog_content": "确定要删除 \"{name}\"?", "diveTypes_deleteDialog_title": "删除潜水类型?", "diveTypes_deleteTooltip": "删除潜水类型", + "diveTypes_editDialog_builtInNameHelper": "内置名称无法更改", + "diveTypes_editDialog_saveButton": "保存", + "diveTypes_editDialog_title": "编辑潜水类型", + "diveTypes_showInHeaderLabel": "标题栏", + "diveTypes_showInHeaderTooltip": "在潜水详情标题栏中显示此类型的徽章", + "diveTypes_showInListLabel": "列表", + "diveTypes_showInListTooltip": "在潜水列表中显示此类型的徽章", + "diveTypes_snackbar_errorUpdating": "更新潜水类型时出错:{error}", + "diveTypes_snackbar_updated": "已更新“{name}”", "diveTypes_snackbar_added": "已添加潜水类型:{name}", "diveTypes_snackbar_cannotDelete": "无法删除 \"{name}\" - 已被现有潜水记录使用", "diveTypes_snackbar_deleted": "已删除\"{name}\"", diff --git a/test/core/database/migration_v173_dive_type_short_name_test.dart b/test/core/database/migration_v173_dive_type_short_name_test.dart new file mode 100644 index 0000000000..b741c95dac --- /dev/null +++ b/test/core/database/migration_v173_dive_type_short_name_test.dart @@ -0,0 +1,68 @@ +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:submersion/core/database/database.dart'; + +/// Minimal pre-v173 shape: a dive_types table without the short_name column, +/// stamped at v161 so the 161->173 upgrade runs. +NativeDatabase _dbAt161() { + return NativeDatabase.memory( + setup: (rawDb) { + rawDb.execute('PRAGMA user_version = 161'); + rawDb.execute(''' + CREATE TABLE dive_types ( + id TEXT NOT NULL PRIMARY KEY, + diver_id TEXT, + name TEXT NOT NULL, + is_built_in INTEGER NOT NULL DEFAULT 0, + sort_order INTEGER NOT NULL DEFAULT 0, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + hlc TEXT + ) + '''); + rawDb.execute( + "INSERT INTO dive_types (id, name, is_built_in, created_at, updated_at) " + "VALUES ('wreck', 'Wreck', 1, 1000, 1000)", + ); + }, + ); +} + +void main() { + test('v173 adds dive_types.short_name, preserving existing rows', () async { + final db = AppDatabase(_dbAt161()); + addTearDown(() => db.close()); + + final cols = await db.customSelect("PRAGMA table_info('dive_types')").get(); + final names = cols.map((c) => c.read('name')).toSet(); + expect(names, contains('short_name')); + + final row = await db + .customSelect("SELECT short_name FROM dive_types WHERE id = 'wreck'") + .getSingle(); + expect(row.data['short_name'], isNull); + }); + + test('fresh databases get the dive_types.short_name column', () async { + final db = AppDatabase(NativeDatabase.memory()); + addTearDown(db.close); + final cols = await db.customSelect("PRAGMA table_info('dive_types')").get(); + final names = cols.map((c) => c.read('name')).toSet(); + expect(names, contains('short_name')); + }); + + test('the helper no-ops when dive_types is absent', () async { + final native = NativeDatabase.memory( + setup: (rawDb) => rawDb.execute('PRAGMA user_version = 161'), + ); + final db = AppDatabase(native); + addTearDown(db.close); + + await expectLater(db.customSelect('SELECT 1').get(), completes); + }); + + test('v173 is present in the migration ladder', () { + expect(AppDatabase.currentSchemaVersion, greaterThanOrEqualTo(173)); + expect(AppDatabase.migrationVersions, contains(173)); + }); +} diff --git a/test/core/database/migration_v174_dive_type_visibility_test.dart b/test/core/database/migration_v174_dive_type_visibility_test.dart new file mode 100644 index 0000000000..4dd99b4291 --- /dev/null +++ b/test/core/database/migration_v174_dive_type_visibility_test.dart @@ -0,0 +1,79 @@ +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:submersion/core/database/database.dart'; + +/// Minimal pre-v174 shape: a dive_types table without the two visibility +/// columns, stamped at v161 so the 161->174 upgrade runs. +NativeDatabase _dbAt161() { + return NativeDatabase.memory( + setup: (rawDb) { + rawDb.execute('PRAGMA user_version = 161'); + rawDb.execute(''' + CREATE TABLE dive_types ( + id TEXT NOT NULL PRIMARY KEY, + diver_id TEXT, + name TEXT NOT NULL, + is_built_in INTEGER NOT NULL DEFAULT 0, + sort_order INTEGER NOT NULL DEFAULT 0, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + hlc TEXT + ) + '''); + rawDb.execute( + "INSERT INTO dive_types (id, name, is_built_in, created_at, updated_at) " + "VALUES ('wreck', 'Wreck', 1, 1000, 1000)", + ); + }, + ); +} + +void main() { + test( + 'v174 adds the two visibility columns, defaulting existing rows to shown', + () async { + final db = AppDatabase(_dbAt161()); + addTearDown(() => db.close()); + + final cols = await db + .customSelect("PRAGMA table_info('dive_types')") + .get(); + final names = cols.map((c) => c.read('name')).toSet(); + expect(names, contains('show_in_detail_header')); + expect(names, contains('show_in_list_view')); + + final row = await db + .customSelect( + 'SELECT show_in_detail_header, show_in_list_view ' + "FROM dive_types WHERE id = 'wreck'", + ) + .getSingle(); + expect(row.data['show_in_detail_header'], 1); + expect(row.data['show_in_list_view'], 1); + }, + ); + + test('fresh databases get both visibility columns', () async { + final db = AppDatabase(NativeDatabase.memory()); + addTearDown(db.close); + final cols = await db.customSelect("PRAGMA table_info('dive_types')").get(); + final names = cols.map((c) => c.read('name')).toSet(); + expect(names, contains('show_in_detail_header')); + expect(names, contains('show_in_list_view')); + }); + + test('the helper no-ops when dive_types is absent', () async { + final native = NativeDatabase.memory( + setup: (rawDb) => rawDb.execute('PRAGMA user_version = 161'), + ); + final db = AppDatabase(native); + addTearDown(db.close); + + await expectLater(db.customSelect('SELECT 1').get(), completes); + }); + + test('v174 is present in the migration ladder', () { + expect(AppDatabase.currentSchemaVersion, greaterThanOrEqualTo(174)); + expect(AppDatabase.migrationVersions, contains(174)); + }); +} diff --git a/test/features/dive_log/presentation/formatters/dive_type_label_test.dart b/test/features/dive_log/presentation/formatters/dive_type_label_test.dart index 27a19ece35..3ba4213d82 100644 --- a/test/features/dive_log/presentation/formatters/dive_type_label_test.dart +++ b/test/features/dive_log/presentation/formatters/dive_type_label_test.dart @@ -63,4 +63,29 @@ void main() { expect(diveTypeLabels(en, ['wreck', 'night']), 'Wreck, Night'); expect(diveTypeLabels(en, const []), ''); }); + + test('diveTypeShortLabel uses the built-in abbreviation', () { + expect(diveTypeShortLabel(de, 'technical'), 'Tec'); + expect(diveTypeShortLabel(de, 'recreational'), 'Rec'); + }); + + test('diveTypeShortLabel falls back to the full label for a custom type', () { + final byId = { + 'my-tag': entity('my-tag', 'Suche & Bergung', isBuiltIn: false), + }; + expect( + diveTypeShortLabel(de, 'my-tag', typesById: byId), + 'Suche & Bergung', + ); + }); + + test('diveTypeShortLabel keeps the diver label for a custom row on a ' + 'built-in slug', () { + final byId = {'wreck': entity('wreck', 'Hausriff-Wrack', isBuiltIn: false)}; + expect( + diveTypeShortLabel(de, 'wreck', typesById: byId), + 'Hausriff-Wrack', + reason: 'the entity guard must win over the built-in abbreviation', + ); + }); } diff --git a/test/features/dive_log/presentation/pages/dive_detail_page_test.dart b/test/features/dive_log/presentation/pages/dive_detail_page_test.dart index e438ee020f..e7f4128b21 100644 --- a/test/features/dive_log/presentation/pages/dive_detail_page_test.dart +++ b/test/features/dive_log/presentation/pages/dive_detail_page_test.dart @@ -28,6 +28,9 @@ import 'package:submersion/features/dive_log/presentation/widgets/compact_tissue import 'package:submersion/features/dive_log/domain/entities/source_profile.dart'; import 'package:submersion/features/dive_log/presentation/widgets/source_bar.dart'; import 'package:submersion/features/dive_log/presentation/widgets/dive_profile_chart.dart'; +import 'package:submersion/features/dive_log/presentation/widgets/dive_type_badge.dart'; +import 'package:submersion/features/dive_types/domain/entities/dive_type_entity.dart'; +import 'package:submersion/features/dive_types/presentation/providers/dive_type_providers.dart'; import 'package:submersion/features/dive_log/presentation/widgets/field_attribution_badge.dart'; import 'package:submersion/features/dive_log/presentation/widgets/o2_toxicity_card.dart'; import 'package:submersion/l10n/arb/app_localizations.dart'; @@ -48,6 +51,7 @@ Widget _buildDetailPage(Dive dive, List overrides) { ).overrideWith((ref) async => []), ], child: MaterialApp( + locale: const Locale('en'), localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, home: DiveDetailPage(diveId: dive.id, embedded: true), @@ -1583,4 +1587,68 @@ void main() { expect(find.text('OC'), findsOneWidget); }); }); + + group('DiveDetailPage dive type badges', () { + testWidgets('shows a badge for each of the dive\'s types', (tester) async { + final dive = createTestDiveWithBottomTime().copyWith( + diveTypeIds: ['wreck', 'night'], + ); + await _pumpDetailPage(tester, dive); + + expect(find.text('Wreck'), findsOneWidget); + expect(find.text('Night'), findsOneWidget); + }); + + testWidgets('shows no badges for a dive with no types', (tester) async { + final dive = createTestDiveWithBottomTime().copyWith(diveTypeIds: []); + await _pumpDetailPage(tester, dive); + + expect(find.byType(DiveTypeBadge), findsNothing); + }); + + testWidgets( + 'spells out more badges instead of collapsing when the header is wide', + (tester) async { + // The cap scales with the header's own width (issue #1269 follow-up) + // rather than a flat constant, so a comfortably wide header should + // show several real labels, not immediately fall back to "+N". + final dive = createTestDiveWithBottomTime().copyWith( + diveTypeIds: ['wreck', 'night', 'drift', 'cave'], + ); + await _pumpDetailPage(tester, dive); + + expect(find.text('Wreck'), findsOneWidget); + expect(find.text('Night'), findsOneWidget); + expect(tester.takeException(), isNull); + }, + ); + + testWidgets( + 'hides a type whose showInDetailHeader is false, keeps the rest', + (tester) async { + final dive = createTestDiveWithBottomTime().copyWith( + diveTypeIds: ['wreck', 'night'], + ); + final hiddenWreck = DiveTypeEntity( + id: 'wreck', + name: 'Wreck', + isBuiltIn: true, + createdAt: DateTime(2026), + updatedAt: DateTime(2026), + showInDetailHeader: false, + ); + final overrides = await getBaseOverrides(); + await tester.pumpWidget( + _buildDetailPage(dive, [ + ...overrides, + diveTypesProvider.overrideWith((ref) async => [hiddenWreck]), + ]), + ); + await tester.pumpAndSettle(); + + expect(find.text('Wreck'), findsNothing); + expect(find.text('Night'), findsOneWidget); + }, + ); + }); } diff --git a/test/features/dive_log/presentation/pages/dive_list_tile_dive_type_localization_test.dart b/test/features/dive_log/presentation/pages/dive_list_tile_dive_type_localization_test.dart index 12158d14a4..dabe135c9f 100644 --- a/test/features/dive_log/presentation/pages/dive_list_tile_dive_type_localization_test.dart +++ b/test/features/dive_log/presentation/pages/dive_list_tile_dive_type_localization_test.dart @@ -99,8 +99,8 @@ void main() { ), diveTypesProvider.overrideWith((ref) async => types ?? loadedTypes), ], - // The resolver is built through the production helper, so these cases - // still cover the provider -> label seam the tile no longer owns. + // The resolvers are built through the production helpers, so these + // cases still cover the provider -> label seam the tile no longer owns. child: Consumer( builder: (context, ref, _) => DiveListTile( diveId: 'd1', @@ -112,6 +112,10 @@ void main() { summary: summary, fullDive: fullDive, diveTypeLabelResolver: watchDiveTypeLabelResolver(ref, context.l10n), + diveTypeShortLabelResolver: watchDiveTypeShortLabelResolver( + ref, + context.l10n, + ), ), ), ); @@ -223,7 +227,9 @@ void main() { ); await tester.pumpAndSettle(); - expect(find.text('Muck'), findsOneWidget); + // A custom type has no short form, so the slot and the type-badge row + // both fall back to the diver's own name -- both are expected here. + expect(find.text('Muck'), findsWidgets); }); testWidgets( @@ -238,7 +244,7 @@ void main() { ); await tester.pumpAndSettle(); - expect(find.text('Hausriff-Wrack'), findsOneWidget); + expect(find.text('Hausriff-Wrack'), findsWidgets); expect(find.text('Wracktauchen'), findsNothing); }, ); @@ -267,7 +273,10 @@ void main() { ); await tester.pumpAndSettle(); - expect(find.text('Wreck'), findsOneWidget); + // The slot renders "Wreck" and the new type-badge row (issue #1269) + // renders its own short label alongside it, which for this built-in + // slug happens to be the same English word -- so both are expected. + expect(find.text('Wreck'), findsWidgets); }); }); } diff --git a/test/features/dive_log/presentation/widgets/dive_list_tile_dive_type_localization_test.dart b/test/features/dive_log/presentation/widgets/dive_list_tile_dive_type_localization_test.dart index 46cbb7fc2e..34717f630b 100644 --- a/test/features/dive_log/presentation/widgets/dive_list_tile_dive_type_localization_test.dart +++ b/test/features/dive_log/presentation/widgets/dive_list_tile_dive_type_localization_test.dart @@ -67,7 +67,11 @@ void main() { ); Widget harness({ - required Widget Function(DiveTypeLabelResolver resolve) builder, + required Widget Function( + DiveTypeLabelResolver resolve, + DiveTypeLabelResolver resolveShort, + ) + builder, required Locale locale, List? types, }) { @@ -77,11 +81,13 @@ void main() { settingsProvider.overrideWith((ref) => _TestSettingsNotifier()), diveTypesProvider.overrideWith((ref) async => types ?? loadedTypes), ], - // The resolver is built through the production helper, so these cases - // still cover the provider -> label seam the tiles no longer own. + // The resolvers are built through the production helpers, so these + // cases still cover the provider -> label seam the tiles no longer own. child: Consumer( - builder: (context, ref, _) => - builder(watchDiveTypeLabelResolver(ref, context.l10n)), + builder: (context, ref, _) => builder( + watchDiveTypeLabelResolver(ref, context.l10n), + watchDiveTypeShortLabelResolver(ref, context.l10n), + ), ), ); } @@ -97,7 +103,7 @@ void main() { }) => harness( locale: locale, types: types, - builder: (resolve) => CompactDiveListTile( + builder: (resolve, resolveShort) => CompactDiveListTile( diveId: 'd1', diveNumber: 7, dateTime: DateTime(2026, 3, 15), @@ -110,6 +116,7 @@ void main() { stat1Field: stat1Field, onTap: () {}, diveTypeLabelResolver: resolve, + diveTypeShortLabelResolver: resolveShort, ), ); @@ -178,7 +185,9 @@ void main() { ); await tester.pumpAndSettle(); - expect(find.textContaining('Muck'), findsOneWidget); + // A custom type has no short form, so the slot and the type-badge row + // both fall back to the diver's own name -- both are expected here. + expect(find.textContaining('Muck'), findsWidgets); }); testWidgets( @@ -196,7 +205,9 @@ void main() { ); await tester.pumpAndSettle(); - expect(find.textContaining('Hausriff-Wrack'), findsOneWidget); + // A custom type has no short form, so the slot and the type-badge + // row both fall back to the diver's own name -- both are expected. + expect(find.textContaining('Hausriff-Wrack'), findsWidgets); expect(find.textContaining('Wracktauchen'), findsNothing); }, ); @@ -210,7 +221,7 @@ void main() { harness( locale: const Locale('de'), types: const [], - builder: (resolve) => CompactDiveListTile( + builder: (resolve, resolveShort) => CompactDiveListTile( diveId: 'd1', diveNumber: 7, dateTime: DateTime(2026, 3, 15), @@ -219,6 +230,7 @@ void main() { stat1Field: DiveField.diveTypeName, onTap: () {}, diveTypeLabelResolver: resolve, + diveTypeShortLabelResolver: resolveShort, ), ), ); @@ -235,7 +247,9 @@ void main() { ); await tester.pumpAndSettle(); - expect(find.textContaining('Deep wreck'), findsOneWidget); + // An unknown slug has no short form either, so the slot and the + // type-badge row both fall back to the same slug capitalization. + expect(find.textContaining('Deep wreck'), findsWidgets); }); testWidgets('English still shows the English built-in label', ( @@ -246,7 +260,10 @@ void main() { ); await tester.pumpAndSettle(); - expect(find.textContaining('Wreck'), findsOneWidget); + // The slot renders "Wreck" and the new type-badge row (issue #1269) + // renders its own short label alongside it, which for this built-in + // slug happens to be the same English word -- so both are expected. + expect(find.textContaining('Wreck'), findsWidgets); expect(find.textContaining('Wracktauchen'), findsNothing); }); @@ -259,7 +276,7 @@ void main() { await tester.pumpWidget( harness( locale: const Locale('de'), - builder: (resolve) => CompactDiveListTile( + builder: (resolve, resolveShort) => CompactDiveListTile( diveId: 'd1', diveNumber: 7, dateTime: DateTime(2026, 3, 15), @@ -269,6 +286,7 @@ void main() { stat1Field: DiveField.waterTemp, onTap: () {}, diveTypeLabelResolver: resolve, + diveTypeShortLabelResolver: resolveShort, ), ), ); @@ -289,7 +307,7 @@ void main() { }) => harness( locale: locale, types: types, - builder: (resolve) => DenseDiveListTile( + builder: (resolve, _) => DenseDiveListTile( diveId: 'd1', diveNumber: 7, dateTime: DateTime(2026, 3, 15), @@ -383,7 +401,7 @@ void main() { await tester.pumpWidget( harness( locale: const Locale('de'), - builder: (resolve) => DenseDiveListTile( + builder: (resolve, _) => DenseDiveListTile( diveId: 'd1', diveNumber: 7, dateTime: DateTime(2026, 3, 15), diff --git a/test/features/dive_log/presentation/widgets/dive_list_tile_type_badges_test.dart b/test/features/dive_log/presentation/widgets/dive_list_tile_type_badges_test.dart new file mode 100644 index 0000000000..2aacdf6d1c --- /dev/null +++ b/test/features/dive_log/presentation/widgets/dive_list_tile_type_badges_test.dart @@ -0,0 +1,302 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:submersion/core/constants/list_view_mode.dart'; +import 'package:submersion/core/providers/provider.dart'; +import 'package:submersion/features/dive_log/domain/entities/dive_summary.dart'; +import 'package:submersion/features/dive_log/presentation/formatters/dive_type_label_resolver.dart'; +import 'package:submersion/features/dive_log/presentation/pages/dive_list_page.dart'; +import 'package:submersion/features/dive_log/presentation/providers/view_config_providers.dart'; +import 'package:submersion/features/dive_log/presentation/widgets/compact_dive_list_tile.dart'; +import 'package:submersion/features/dive_log/presentation/widgets/dive_mode_badge.dart'; +import 'package:submersion/features/dive_log/presentation/widgets/dive_type_badge.dart'; +import 'package:submersion/features/dive_log/presentation/widgets/dive_type_badge_row.dart'; +import 'package:submersion/features/dive_types/domain/entities/dive_type_entity.dart'; +import 'package:submersion/features/dive_types/presentation/providers/dive_type_providers.dart'; +import 'package:submersion/features/settings/presentation/providers/settings_providers.dart'; +import 'package:submersion/features/tags/domain/entities/tag.dart'; +import 'package:submersion/features/tags/presentation/widgets/tag_input_widget.dart'; +import 'package:submersion/l10n/l10n_extension.dart'; + +import '../../../../helpers/test_app.dart'; + +/// Issue #1269 follow-up: the dive list's compact and detailed cards show a +/// row of dive-type badges on the same line as the depth stat, right-aligned, +/// matching the badges already shown in the dive detail header. +class _TestSettingsNotifier extends StateNotifier + implements SettingsNotifier { + _TestSettingsNotifier() : super(const AppSettings()); + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class _TestCardConfigNotifier extends CardViewConfigNotifier { + _TestCardConfigNotifier() : super.withMode(ListViewMode.detailed) { + state = CardViewConfig.defaultDetailed(); + } +} + +void main() { + DiveSummary summaryWith(List ids) => DiveSummary( + id: 'd1', + diveNumber: 7, + dateTime: DateTime(2026, 3, 15), + siteName: 'Blue Hole', + maxDepth: 20.0, + bottomTime: const Duration(minutes: 30), + diveTypeIds: ids, + sortTimestamp: 0, + ); + + Widget harness({ + required Widget Function( + DiveTypeLabelResolver resolve, + DiveTypeListVisibilityPredicate isVisible, + ) + builder, + List diveTypes = const [], + }) { + return testApp( + overrides: [ + settingsProvider.overrideWith((ref) => _TestSettingsNotifier()), + diveTypesProvider.overrideWith((ref) async => diveTypes), + detailedCardConfigProvider.overrideWith( + (ref) => _TestCardConfigNotifier(), + ), + ], + child: Consumer( + builder: (context, ref, _) => builder( + watchDiveTypeShortLabelResolver(ref, context.l10n), + watchDiveTypeListVisibilityPredicate(ref), + ), + ), + ); + } + + group('CompactDiveListTile type badges', () { + Widget tile( + List diveTypeIds, { + List diveTypes = const [], + }) => harness( + diveTypes: diveTypes, + builder: (resolve, isVisible) => CompactDiveListTile( + diveId: 'd1', + diveNumber: 7, + dateTime: DateTime(2026, 3, 15), + siteName: 'Blue Hole', + maxDepth: 20.0, + duration: const Duration(minutes: 30), + summary: summaryWith(diveTypeIds), + onTap: () {}, + diveTypeShortLabelResolver: resolve, + diveTypeListVisibilityPredicate: isVisible, + ), + ); + + testWidgets('shows a badge for each of the dive\'s types', (tester) async { + await tester.pumpWidget(tile(['wreck', 'night'])); + await tester.pumpAndSettle(); + + expect(find.byType(DiveTypeBadge), findsNWidgets(2)); + expect(find.text('Wreck'), findsOneWidget); + expect(find.text('Night'), findsOneWidget); + }); + + testWidgets('shows no badges for a dive with no types', (tester) async { + await tester.pumpWidget(tile([])); + await tester.pumpAndSettle(); + + expect(find.byType(DiveTypeBadge), findsNothing); + }); + + testWidgets('the badge row sits right of the stat row midpoint', ( + tester, + ) async { + await tester.pumpWidget(tile(['wreck'])); + await tester.pumpAndSettle(); + + final cardRect = tester.getRect(find.byType(Card)); + final badgeRect = tester.getRect(find.byType(DiveTypeBadge)); + expect(badgeRect.center.dx, greaterThan(cardRect.center.dx)); + }); + + testWidgets('renders at the same size as the dense OC/CCR badge', ( + tester, + ) async { + await tester.pumpWidget(tile(['wreck'])); + await tester.pumpAndSettle(); + + final typeBadgeSize = tester.getSize(find.byType(DiveTypeBadge)); + final modeBadgeSize = tester.getSize(find.byType(DiveModeBadge)); + expect(typeBadgeSize.height, modeBadgeSize.height); + }); + + testWidgets('hides a type whose showInListView is false, keeps the rest', ( + tester, + ) async { + await tester.pumpWidget( + tile( + ['wreck', 'night'], + diveTypes: [ + DiveTypeEntity( + id: 'wreck', + name: 'Wreck', + isBuiltIn: true, + createdAt: DateTime(2026), + updatedAt: DateTime(2026), + showInListView: false, + ), + ], + ), + ); + await tester.pumpAndSettle(); + + expect(find.byType(DiveTypeBadge), findsNWidgets(1)); + expect(find.text('Night'), findsOneWidget); + expect(find.text('Wreck'), findsNothing); + }); + }); + + group('DiveListTile detailed-view type badges', () { + Widget tile( + List diveTypeIds, { + List diveTypes = const [], + }) => harness( + diveTypes: diveTypes, + builder: (resolve, isVisible) => DiveListTile( + diveId: 'd1', + diveNumber: 7, + dateTime: DateTime(2026, 3, 15), + siteName: 'Blue Hole', + maxDepth: 20.0, + duration: const Duration(minutes: 30), + summary: summaryWith(diveTypeIds), + onTap: () {}, + diveTypeShortLabelResolver: resolve, + diveTypeListVisibilityPredicate: isVisible, + ), + ); + + testWidgets('shows a badge for each of the dive\'s types', (tester) async { + await tester.pumpWidget(tile(['wreck', 'night'])); + await tester.pumpAndSettle(); + + expect(find.byType(DiveTypeBadge), findsNWidgets(2)); + expect(find.text('Wreck'), findsOneWidget); + expect(find.text('Night'), findsOneWidget); + }); + + testWidgets('shows no badges for a dive with no types', (tester) async { + await tester.pumpWidget(tile([])); + await tester.pumpAndSettle(); + + expect(find.byType(DiveTypeBadge), findsNothing); + }); + + testWidgets('the badge row sits right of the stat row midpoint', ( + tester, + ) async { + await tester.pumpWidget(tile(['wreck'])); + await tester.pumpAndSettle(); + + final cardRect = tester.getRect(find.byType(Card)); + final badgeRect = tester.getRect(find.byType(DiveTypeBadge)); + expect(badgeRect.center.dx, greaterThan(cardRect.center.dx)); + }); + + testWidgets('renders at the same size as the dense OC/CCR badge', ( + tester, + ) async { + await tester.pumpWidget(tile(['wreck'])); + await tester.pumpAndSettle(); + + final typeBadgeSize = tester.getSize(find.byType(DiveTypeBadge)); + final modeBadgeSize = tester.getSize(find.byType(DiveModeBadge)); + expect(typeBadgeSize.height, modeBadgeSize.height); + }); + + testWidgets('hides a type whose showInListView is false, keeps the rest', ( + tester, + ) async { + await tester.pumpWidget( + tile( + ['wreck', 'night'], + diveTypes: [ + DiveTypeEntity( + id: 'wreck', + name: 'Wreck', + isBuiltIn: true, + createdAt: DateTime(2026), + updatedAt: DateTime(2026), + showInListView: false, + ), + ], + ), + ); + await tester.pumpAndSettle(); + + expect(find.byType(DiveTypeBadge), findsNWidgets(1)); + expect(find.text('Night'), findsOneWidget); + expect(find.text('Wreck'), findsNothing); + }); + + testWidgets('does not overflow with several types and a long tag name', ( + tester, + ) async { + // Regression: a long tag (e.g. an import-source tag) used to render + // unclipped in the stats/tags Wrap. Nothing there checked horizontal + // overflow until the type-badge row added an enclosing Row, which + // surfaced the pre-existing "tag too wide" case as a RenderFlex + // overflow (issue #1269 follow-up). + await tester.pumpWidget( + harness( + builder: (resolve, isVisible) => Align( + alignment: Alignment.topLeft, + // Matches the narrow list panel in the real master-detail + // layout where this overflow was observed -- the default test + // surface is wide enough to hide it. + child: SizedBox( + width: 380, + child: DiveListTile( + diveId: 'd1', + diveNumber: 7, + dateTime: DateTime(2026, 3, 15), + siteName: 'Blue Hole', + maxDepth: 20.0, + duration: const Duration(minutes: 30), + summary: summaryWith([ + 'wreck', + 'night', + 'drift', + 'cave', + 'ice', + ]), + tags: [ + Tag( + id: 't1', + name: + '100_shearwater_cloud_export_with_one_ccr_dive.db.export', + createdAt: DateTime(2026), + updatedAt: DateTime(2026), + ), + ], + onTap: () {}, + diveTypeShortLabelResolver: resolve, + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(tester.takeException(), isNull); + + // Tags render on their own line below the stat/badge row, so a long + // tag name never competes with them for width. + final badgeRect = tester.getRect(find.byType(DiveTypeBadgeRow)); + final tagRect = tester.getRect(find.byType(TagChips)); + expect(tagRect.top, greaterThanOrEqualTo(badgeRect.bottom)); + }); + }); +} diff --git a/test/features/dive_types/data/repositories/dive_type_repository_short_name_test.dart b/test/features/dive_types/data/repositories/dive_type_repository_short_name_test.dart new file mode 100644 index 0000000000..4be9b2f4e8 --- /dev/null +++ b/test/features/dive_types/data/repositories/dive_type_repository_short_name_test.dart @@ -0,0 +1,68 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:submersion/core/services/database_service.dart'; +import 'package:submersion/features/dive_types/data/repositories/dive_type_repository.dart'; +import 'package:submersion/features/dive_types/domain/entities/dive_type_entity.dart'; + +import '../../../../helpers/test_database.dart'; + +void main() { + late DiveTypeRepository repository; + + setUp(() async { + await setUpTestDatabase(); + repository = DiveTypeRepository(); + // Custom dive types carry a diverId FK, so a row must exist for it to + // reference. + await DatabaseService.instance.database.customStatement( + "INSERT INTO divers (id, name, created_at, updated_at) " + "VALUES ('diver-1', 'Test Diver', 1000, 1000)", + ); + }); + + tearDown(() async { + await tearDownTestDatabase(); + }); + + test('createDiveType persists and round-trips a short name', () async { + final created = await repository.createDiveType( + DiveTypeEntity.create( + id: '', + name: 'Search & Recovery', + diverId: 'diver-1', + shortName: 'S&R', + ), + ); + + expect(created.shortName, 'S&R'); + + final reloaded = await repository.getDiveTypeById(created.id); + expect(reloaded?.shortName, 'S&R'); + }); + + test('createDiveType with no short name leaves it null', () async { + final created = await repository.createDiveType( + DiveTypeEntity.create(id: '', name: 'Cenote', diverId: 'diver-1'), + ); + + expect(created.shortName, isNull); + + final reloaded = await repository.getDiveTypeById(created.id); + expect(reloaded?.shortName, isNull); + }); + + test('updateDiveType changes the persisted short name', () async { + final created = await repository.createDiveType( + DiveTypeEntity.create( + id: '', + name: 'Cenote', + diverId: 'diver-1', + shortName: 'Cen', + ), + ); + + await repository.updateDiveType(created.copyWith(shortName: 'CEN')); + + final reloaded = await repository.getDiveTypeById(created.id); + expect(reloaded?.shortName, 'CEN'); + }); +} diff --git a/test/features/dive_types/data/repositories/dive_type_repository_visibility_test.dart b/test/features/dive_types/data/repositories/dive_type_repository_visibility_test.dart new file mode 100644 index 0000000000..195230f1b0 --- /dev/null +++ b/test/features/dive_types/data/repositories/dive_type_repository_visibility_test.dart @@ -0,0 +1,98 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:submersion/core/services/database_service.dart'; +import 'package:submersion/features/dive_types/data/repositories/dive_type_repository.dart'; +import 'package:submersion/features/dive_types/domain/entities/dive_type_entity.dart'; + +import '../../../../helpers/test_database.dart'; + +void main() { + late DiveTypeRepository repository; + + setUp(() async { + await setUpTestDatabase(); + repository = DiveTypeRepository(); + // Custom dive types carry a diverId FK, so a row must exist for it to + // reference. + await DatabaseService.instance.database.customStatement( + "INSERT INTO divers (id, name, created_at, updated_at) " + "VALUES ('diver-1', 'Test Diver', 1000, 1000)", + ); + }); + + tearDown(() async { + await tearDownTestDatabase(); + }); + + test('createDiveType defaults both visibility flags to shown', () async { + final created = await repository.createDiveType( + DiveTypeEntity.create(id: '', name: 'Cenote', diverId: 'diver-1'), + ); + + expect(created.showInDetailHeader, isTrue); + expect(created.showInListView, isTrue); + + final reloaded = await repository.getDiveTypeById(created.id); + expect(reloaded?.showInDetailHeader, isTrue); + expect(reloaded?.showInListView, isTrue); + }); + + test('createDiveType persists explicit visibility flags', () async { + final created = await repository.createDiveType( + DiveTypeEntity.create( + id: '', + name: 'Cenote', + diverId: 'diver-1', + showInDetailHeader: false, + showInListView: true, + ), + ); + + expect(created.showInDetailHeader, isFalse); + expect(created.showInListView, isTrue); + + final reloaded = await repository.getDiveTypeById(created.id); + expect(reloaded?.showInDetailHeader, isFalse); + expect(reloaded?.showInListView, isTrue); + }); + + test( + 'setDiveTypeVisibility updates both flags independently of each other', + () async { + final created = await repository.createDiveType( + DiveTypeEntity.create(id: '', name: 'Cenote', diverId: 'diver-1'), + ); + + await repository.setDiveTypeVisibility( + created.id, + showInDetailHeader: false, + showInListView: true, + ); + + final reloaded = await repository.getDiveTypeById(created.id); + expect(reloaded?.showInDetailHeader, isFalse); + expect(reloaded?.showInListView, isTrue); + }, + ); + + test('setDiveTypeVisibility is allowed on built-in types', () async { + // Built-in types are seeded on a fresh database (see kSeedBuiltInDiveTypesSql); + // 'wreck' is one of the seeded slugs. + final before = await repository.getDiveTypeById('wreck'); + expect(before?.isBuiltIn, isTrue); + + await expectLater( + repository.setDiveTypeVisibility( + 'wreck', + showInDetailHeader: false, + showInListView: false, + ), + completes, + ); + + final reloaded = await repository.getDiveTypeById('wreck'); + expect(reloaded?.showInDetailHeader, isFalse); + expect(reloaded?.showInListView, isFalse); + // The protected core definition is untouched. + expect(reloaded?.name, before?.name); + }); +} diff --git a/test/features/dive_types/domain/entities/dive_type_entity_test.dart b/test/features/dive_types/domain/entities/dive_type_entity_test.dart new file mode 100644 index 0000000000..2e350ba923 --- /dev/null +++ b/test/features/dive_types/domain/entities/dive_type_entity_test.dart @@ -0,0 +1,39 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:submersion/features/dive_types/domain/entities/dive_type_entity.dart'; + +void main() { + DiveTypeEntity entityWith({String? shortName}) => DiveTypeEntity( + id: 'cenote', + name: 'Cenote', + createdAt: DateTime(2026), + updatedAt: DateTime(2026), + shortName: shortName, + ); + + group('DiveTypeEntity.copyWith shortName', () { + test('omitting the parameter keeps the current shortName', () { + final original = entityWith(shortName: 'Cen'); + + final copy = original.copyWith(name: 'Cenote System'); + + expect(copy.shortName, 'Cen'); + }); + + test('passing a new value replaces the current shortName', () { + final original = entityWith(shortName: 'Cen'); + + final copy = original.copyWith(shortName: 'CEN'); + + expect(copy.shortName, 'CEN'); + }); + + test('passing null explicitly clears an existing shortName ' + '(regression: the edit dialog could not remove a short name)', () { + final original = entityWith(shortName: 'Cen'); + + final copy = original.copyWith(shortName: null); + + expect(copy.shortName, isNull); + }); + }); +} diff --git a/test/features/dive_types/presentation/pages/dive_types_page_test.dart b/test/features/dive_types/presentation/pages/dive_types_page_test.dart index 7900b9fe54..87d0649276 100644 --- a/test/features/dive_types/presentation/pages/dive_types_page_test.dart +++ b/test/features/dive_types/presentation/pages/dive_types_page_test.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:submersion/core/services/database_service.dart'; +import 'package:submersion/features/dive_log/presentation/widgets/dive_type_badge.dart'; +import 'package:submersion/features/dive_types/data/repositories/dive_type_repository.dart'; import 'package:submersion/features/dive_types/presentation/pages/dive_types_page.dart'; import 'package:submersion/features/divers/presentation/providers/diver_providers.dart'; import 'package:submersion/l10n/arb/app_localizations.dart'; @@ -67,4 +69,227 @@ void main() { expect(find.text('Wreck'), findsNothing); expect(find.text('Night'), findsNothing); }); + + group('custom dive type short name', () { + testWidgets( + 'adding a custom type with a short name shows both on its tile', + (tester) async { + await tester.pumpWidget( + _buildPage(diverIdNotifier, const Locale('en')), + ); + await tester.pumpAndSettle(); + + await tester.tap(find.byType(FloatingActionButton)); + await tester.pumpAndSettle(); + + await tester.enterText( + find.byType(TextFormField).at(0), + 'Search & Recovery', + ); + await tester.enterText(find.byType(TextFormField).at(1), 'S&R'); + await tester.tap(find.widgetWithText(FilledButton, 'Add')); + await tester.pumpAndSettle(); + + expect(find.text('Search & Recovery'), findsOneWidget); + expect(find.text('S&R'), findsOneWidget); + }, + ); + + testWidgets('a custom type added with no short name shows no badge', ( + tester, + ) async { + await tester.pumpWidget(_buildPage(diverIdNotifier, const Locale('en'))); + await tester.pumpAndSettle(); + + await tester.tap(find.byType(FloatingActionButton)); + await tester.pumpAndSettle(); + + await tester.enterText(find.byType(TextFormField).at(0), 'Cenote'); + await tester.tap(find.widgetWithText(FilledButton, 'Add')); + await tester.pumpAndSettle(); + + final tile = find.ancestor( + of: find.text('Cenote'), + matching: find.byType(ListTile), + ); + expect(tile, findsOneWidget); + // Built-in types elsewhere on the page (Recreational -> "Rec", etc.) + // legitimately carry their own badge, so the check is scoped to this + // tile rather than asserting no DiveTypeBadge exists anywhere. + expect( + find.descendant(of: tile, matching: find.byType(DiveTypeBadge)), + findsNothing, + ); + }); + }); + + group('edit dialog', () { + Future openEditDialogFor(WidgetTester tester, String name) async { + await tester.tap(find.text(name)); + await tester.pumpAndSettle(); + } + + testWidgets('tapping a built-in type opens the edit dialog', ( + tester, + ) async { + await tester.pumpWidget(_buildPage(diverIdNotifier, const Locale('en'))); + await tester.pumpAndSettle(); + + await openEditDialogFor(tester, 'Wreck'); + + expect(find.byType(AlertDialog), findsOneWidget); + expect(find.byType(Checkbox), findsNWidgets(2)); + for (final element in tester.widgetList( + find.byType(Checkbox), + )) { + expect(element.value, isTrue); + } + // The name field exists but is disabled for a built-in type. + final nameField = tester.widget( + find.byType(TextFormField).first, + ); + expect(nameField.enabled, isFalse); + // No short-name field for built-ins -- it's never used for them. + expect(find.byType(TextFormField), findsOneWidget); + }); + + testWidgets( + 'a built-in type\'s disabled name field shows the localized name, ' + 'not the seeded English value', + (tester) async { + await tester.pumpWidget( + _buildPage(diverIdNotifier, const Locale('de')), + ); + await tester.pumpAndSettle(); + + await openEditDialogFor(tester, 'Wracktauchen'); + + final nameField = tester.widget( + find.byType(TextFormField).first, + ); + expect(nameField.controller?.text, 'Wracktauchen'); + expect(nameField.controller?.text, isNot('Wreck')); + }, + ); + + testWidgets( + 'unchecking "Header" and saving persists and survives a rebuild', + (tester) async { + await tester.pumpWidget( + _buildPage(diverIdNotifier, const Locale('en')), + ); + await tester.pumpAndSettle(); + + await openEditDialogFor(tester, 'Wreck'); + await tester.tap(find.byType(Checkbox).first); + await tester.tap(find.text('Save')); + await tester.pumpAndSettle(); + + final updated = await DiveTypeRepository().getDiveTypeById('wreck'); + expect(updated?.showInDetailHeader, isFalse); + expect(updated?.showInListView, isTrue); + + // Rebuilding the page (simulating navigating away and back) must not + // silently revert the toggle. + await tester.pumpWidget( + _buildPage(diverIdNotifier, const Locale('en')), + ); + await tester.pumpAndSettle(); + await openEditDialogFor(tester, 'Wreck'); + final reloadedCheckbox = tester + .widgetList(find.byType(Checkbox)) + .first; + expect(reloadedCheckbox.value, isFalse); + }, + ); + + testWidgets('unchecking "List" and saving leaves "Header" untouched', ( + tester, + ) async { + await tester.pumpWidget(_buildPage(diverIdNotifier, const Locale('en'))); + await tester.pumpAndSettle(); + + await openEditDialogFor(tester, 'Wreck'); + await tester.tap(find.byType(Checkbox).last); + await tester.tap(find.text('Save')); + await tester.pumpAndSettle(); + + final updated = await DiveTypeRepository().getDiveTypeById('wreck'); + expect(updated?.showInListView, isFalse); + expect(updated?.showInDetailHeader, isTrue); + }); + + testWidgets('cancelling the dialog persists nothing', (tester) async { + await tester.pumpWidget(_buildPage(diverIdNotifier, const Locale('en'))); + await tester.pumpAndSettle(); + + await openEditDialogFor(tester, 'Wreck'); + await tester.tap(find.byType(Checkbox).first); + await tester.tap(find.text('Cancel')); + await tester.pumpAndSettle(); + + final updated = await DiveTypeRepository().getDiveTypeById('wreck'); + expect(updated?.showInDetailHeader, isTrue); + }); + + testWidgets('editing a custom type\'s name and short name persists', ( + tester, + ) async { + await tester.pumpWidget(_buildPage(diverIdNotifier, const Locale('en'))); + await tester.pumpAndSettle(); + + await tester.tap(find.byType(FloatingActionButton)); + await tester.pumpAndSettle(); + await tester.enterText(find.byType(TextFormField).at(0), 'Cenote'); + await tester.tap(find.widgetWithText(FilledButton, 'Add')); + await tester.pumpAndSettle(); + + await openEditDialogFor(tester, 'Cenote'); + // The name field is enabled for a custom type, and a short-name field + // is offered too (unlike the built-in dialog above). + expect(find.byType(TextFormField), findsNWidgets(2)); + final nameField = tester.widget( + find.byType(TextFormField).at(0), + ); + expect(nameField.enabled, isTrue); + + await tester.enterText(find.byType(TextFormField).at(0), 'Cave System'); + await tester.enterText(find.byType(TextFormField).at(1), 'Cave'); + await tester.tap(find.text('Save')); + await tester.pumpAndSettle(); + + expect(find.text('Cave System'), findsOneWidget); + expect(find.text('Cenote'), findsNothing); + + final dive = await DiveTypeRepository().getAllDiveTypes( + diverId: 'diver-1', + ); + final saved = dive.firstWhere((t) => t.name == 'Cave System'); + expect(saved.shortName, 'Cave'); + }); + + testWidgets('clearing a custom type\'s short name field removes it ' + '(regression: copyWith could not clear shortName)', (tester) async { + await tester.pumpWidget(_buildPage(diverIdNotifier, const Locale('en'))); + await tester.pumpAndSettle(); + + await tester.tap(find.byType(FloatingActionButton)); + await tester.pumpAndSettle(); + await tester.enterText(find.byType(TextFormField).at(0), 'Cenote'); + await tester.enterText(find.byType(TextFormField).at(1), 'Cen'); + await tester.tap(find.widgetWithText(FilledButton, 'Add')); + await tester.pumpAndSettle(); + + var dive = await DiveTypeRepository().getAllDiveTypes(diverId: 'diver-1'); + expect(dive.firstWhere((t) => t.name == 'Cenote').shortName, 'Cen'); + + await openEditDialogFor(tester, 'Cenote'); + await tester.enterText(find.byType(TextFormField).at(1), ''); + await tester.tap(find.text('Save')); + await tester.pumpAndSettle(); + + dive = await DiveTypeRepository().getAllDiveTypes(diverId: 'diver-1'); + expect(dive.firstWhere((t) => t.name == 'Cenote').shortName, isNull); + }); + }); }