diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index 611fa8604..07744c557 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -237,7 +237,7 @@ submersion/
│ │ ├── dive_types/ # Custom dive types
│ │ ├── marine_life/ # Species sightings
│ │ ├── tools/ # Calculators
-│ │ └── onboarding/ # First-run experience
+│ │ └── setup_wizard/ # First-run setup wizard + Settings re-entry
│ │
│ └── shared/ # Reusable components
│ ├── constants/
diff --git a/docs/FEATURE_ROADMAP.md b/docs/FEATURE_ROADMAP.md
index 2cc91feb4..b3c165ce3 100644
--- a/docs/FEATURE_ROADMAP.md
+++ b/docs/FEATURE_ROADMAP.md
@@ -1303,6 +1303,13 @@
---
+### Setup Wizard (Completed)
+
+- [x] Setup wizard for new databases (discussion #523): first-run fork
+ (fresh start vs existing data), units with locale-aware preset,
+ appearance, backup schedule and cloud sync connect, restore/adopt
+ paths, feature-discovery finish screen, Settings re-entry
+
## 15.2 Multi-User / Family Support
| Feature | Status | Phase | Notes |
diff --git a/docs/developer/architecture.md b/docs/developer/architecture.md
index b542bb710..40cac3e53 100644
--- a/docs/developer/architecture.md
+++ b/docs/developer/architecture.md
@@ -157,7 +157,7 @@ lib/
│ ├── dive_types/
│ ├── marine_life/
│ ├── tools/
-│ └── onboarding/
+│ └── setup_wizard/
│
└── shared/ # Shared components
├── constants/
diff --git a/docs/developer/navigation.md b/docs/developer/navigation.md
index 9515448fb..aebde5d09 100644
--- a/docs/developer/navigation.md
+++ b/docs/developer/navigation.md
@@ -330,7 +330,8 @@ GoRoute(
| Path | Name | Screen |
|------|------|--------|
-| `/welcome` | welcome | WelcomePage |
+| `/welcome` | welcome | SetupWizardPage (first-run mode) |
+| `/settings/setup-assistant` | setupAssistant | SetupWizardPage (re-entry mode) |
## Navigation Patterns
@@ -394,7 +395,10 @@ This provides instant transitions between main tabs, matching platform conventio
### First-Run Guard
-The router redirects to onboarding if no divers exist:
+The router redirects to the setup wizard if no divers exist. The wizard
+offers a fresh-start path (profile, units, appearance, backup and sync) and
+an existing-data path (restore a backup, connect cloud sync, or open an
+existing storage folder):
```dart
redirect: (context, state) async {
diff --git a/docs/guide/README.md b/docs/guide/README.md
index e49a80467..27d535619 100644
--- a/docs/guide/README.md
+++ b/docs/guide/README.md
@@ -30,7 +30,7 @@ Submersion has five main sections accessible from the bottom navigation bar (mob
### Core Workflow
-1. **Create a diver profile** - On first launch, you'll set up your diver profile
+1. **Run the setup wizard** - On first launch, create your profile and pick units, appearance, and backup options (or restore existing data)
2. **Add dive sites** - Build your site database (or import existing sites)
3. **Log dives** - Manually or download from a dive computer
4. **Track gear** - Add your equipment with service reminders
diff --git a/docs/guide/first-dive.md b/docs/guide/first-dive.md
index e130d708e..541825f6f 100644
--- a/docs/guide/first-dive.md
+++ b/docs/guide/first-dive.md
@@ -2,18 +2,23 @@
Let's log your first dive in Submersion! This quick tutorial will get you familiar with the basics.
-## Step 1: Create Your Diver Profile
+## Step 1: Run the Setup Wizard
-When you first launch Submersion, you'll be asked to create a diver profile.
+When you first launch Submersion, a short setup wizard walks you through
+creating your logbook. If you already have Submersion data, choose
+**I have existing Submersion data** to restore a backup, connect cloud
+sync, or open an existing folder instead.
- Screenshot: Welcome/Onboarding Screen
- The welcome screen where you create your first diver profile
+ Screenshot: Setup Wizard Welcome Screen
+ The welcome screen where setup begins
-1. Enter your **name**
-2. Optionally add your **email** and **phone**
-3. Tap **Create Profile**
+1. Choose **Set up a new logbook**
+2. Enter your **name**
+3. Pick your **units** (Metric or Imperial), appearance, and backup
+ preferences - every step after the name can be skipped
+4. Tap **Start logging**
Tip: You can add more details to your profile later, including emergency contacts, medical info, and insurance details.
diff --git a/docs/superpowers/plans/2026-07-10-setup-wizard.md b/docs/superpowers/plans/2026-07-10-setup-wizard.md
new file mode 100644
index 000000000..81986c089
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-10-setup-wizard.md
@@ -0,0 +1,3620 @@
+# Setup Wizard for New Databases Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Replace the name-only welcome page with a multi-step setup wizard (units, appearance, backup/sync, existing-data restore/adopt paths) per the approved spec `docs/superpowers/specs/2026-07-10-setup-wizard-design.md` (discussion #523).
+
+**Architecture:** New `lib/features/setup_wizard/` feature with a draft-and-apply `StateNotifier` and a small `PageView` shell; `WizardStepDef`/`WizardStepIndicator` move to `lib/shared/widgets/wizard/` and are shared with the import wizard. Existing-data paths re-skin presentation over the existing restore, sync, and storage engines. First-run settings are seeded via `DiverSettingsRepository.createSettingsForDiver(id, settings: draft)` BEFORE `setCurrentDiver` (avoids the SettingsNotifier reload race — this supersedes the spec's `applySettings()` bulk-method idea; amend the spec in Task 5).
+
+**Tech Stack:** Flutter 3 / Material 3, Riverpod (StateNotifier), go_router, Drift (via existing repositories), file_picker, flutter gen-l10n (11 locales).
+
+## Global Constraints
+
+- All user-facing strings via `context.l10n.*`; new keys added to `lib/l10n/arb/app_en.arb` AND translated in all 10 other arb files (ar, de, es, fr, he, hu, it, nl, pt, zh); regenerate with `flutter gen-l10n` (generated `app_localizations*.dart` files are checked in — commit them).
+- No emojis in code/comments/docs. Immutability (copyWith, no mutation). `dart format .` must produce no changes before each commit.
+- Tests: run SPECIFIC test files (never the whole suite — it times out). Widget tests wrap post-pump Drift awaits in `tester.runAsync`; `FormSection` uppercases labels; never dispose controllers in `whenComplete()`.
+- Commit after each task with `git add
` (never `git add -A`; the worktree may contain untracked build dirs). Do not push (pre-push hook runs against the MAIN tree; push is out of scope).
+- Import order: dart, flutter, packages, local. Files 200-400 lines typical, 800 max.
+- Riverpod 3: no provider mutation in `dispose()`; use `Future.microtask` + `mounted` guards where a callback mutates providers.
+- Existing engines are called, never modified, except: `app.dart` gains a preview-locale hook (Task 9) and the router swaps the `/welcome` builder (Task 13).
+
+**Key facts (verified in code — trust these over guesses):**
+- `DiverListNotifier.addDiver(Diver)` returns `Future` with the real id (`diver_providers.dart:251`).
+- `CurrentDiverIdNotifier.setCurrentDiver(String)` triggers an UNAWAITED `SettingsNotifier` reload that resets `_validatedDiverId` and later overwrites state with fresh defaults — settings setters called immediately after `setCurrentDiver` are silently dropped. Sequence: create diver → seed settings row → THEN setCurrentDiver.
+- `DiverSettingsRepository.createSettingsForDiver(String diverId, {AppSettings? settings})` seeds a full row in one insert (`diver_settings_repository.dart:45`); `getSettingsForDiver(diverId)` returns `AppSettings?`; `updateSettingsForDiver(diverId, settings)` updates. Provider: `diverSettingsRepositoryProvider` (settings_providers.dart).
+- `SettingsNotifier` existing setters: `setMetric()`, `setImperial()` (6 units, NOT sacUnit), `setDepthUnit/setTemperatureUnit/setPressureUnit/setVolumeUnit/setWeightUnit/setAltitudeUnit/setSacUnit`, `setTimeFormat`, `setDateFormat`, `setThemeMode(ThemeMode)`, `setThemePresetId(String)`, `setMapStyle(MapStyle)`, `setLocale(String)`. Do NOT add methods to SettingsNotifier (3 hand-written full-implementer mocks would break).
+- `AppSettings` fields used: 7 units + `timeFormat` + `dateFormat` + `themeMode` (Flutter `ThemeMode`) + `themePresetId` (String: submersion|console|tropical|minimalist|deep) + `locale` (String, 'system' or code) + `mapStyle` (`MapStyle{openStreetMap,openTopoMap,esriSatellite}` in `lib/core/constants/map_style.dart`). `UnitPreset{metric,imperial,custom}` lives in settings_providers.dart; `AppSettings.unitPreset` getter checks the 6 core units.
+- Backup: `backupSettingsProvider` → `BackupSettingsNotifier.setEnabled(bool)/setFrequency(BackupFrequency{daily,weekly,monthly})/setCloudBackupEnabled(bool)`; state `BackupSettings{enabled=false, frequency=weekly, cloudBackupEnabled=false}` (SharedPreferences-backed).
+- Restore: `FilePicker.pickFiles(type: FileType.any)` → temp `BackupRecord` → `RestoreConfirmationDialog.show(context, record, currentSchemaVersion: AppDatabase.currentSchemaVersion, offerReplace: ...)` → `RestoreMode?` → `ref.read(backupOperationProvider.notifier).restoreFromFilePath(path, mode: mode)`; listen for `BackupOperationStatus.restoreComplete` → `RestoreCompletePage.show(context)` (its Continue button calls `restartApp()`).
+- Folder adopt: `ref.read(storageConfigNotifierProvider.notifier)`: `pickCustomFolder({chooser})` → `FolderPickResultWithBookmark?`; `checkForExistingDatabase(path)` → `ExistingDatabaseInfo?`; `switchToExistingDatabase(path)` → `MigrationResult` (`.success`). Wizard calls `restartApp()` after success (per spec).
+- Sync activation contract (mirror `CloudSyncPage._selectProvider`): set `selectedCloudProviderTypeProvider.notifier.state = type` → `await cloudProviderInstanceFor(type).authenticate()` (`Future`, throws on failure) → `await ref.read(syncInitializerProvider).saveProvider(type)` → `ref.read(syncStateProvider.notifier).refreshState()`. On failure reset selection to null.
+- iCloud gate: `isApplePlatformProvider` AND `iCloudAvailabilityProvider != ICloudAvailability.unsupported`. Dropbox gate: `dropboxConfiguredProvider`; connect via `showDialog(builder: (_) => DropboxConnectDialog(provider: ref.read(dropboxStorageProviderInstanceProvider)))`. S3: push existing route `/settings/cloud-sync/s3-config` (S3ConfigPage activates S3 itself on save). Google Drive stays hidden.
+- Remote-library detection: `await ref.read(syncInitializerProvider).peerSyncFiles(providerInstance)` → `List`; non-empty = existing library.
+- Pull: `ref.read(syncStateProvider.notifier).performSync()`; on a zero-dive DB it auto-adopts replaced libraries and plain-pulls otherwise. Watch `syncStateProvider` (`SyncState{status: SyncStatus{idle,syncing,success,error,hasConflicts}, progress, message}`). After success call `realignActiveDiverAfterDataReplace(ref.read(sharedPreferencesProvider))` (from diver_providers.dart) before leaving the wizard.
+- Router: `/welcome` is a top-level GoRoute OUTSIDE the ShellRoute (`app_router.dart:169`); redirect gate reads `hasAnyDiversProvider.future` on navigation events only (no refreshListenable — no mid-wizard yank). Settings children are relative-path GoRoutes under `/settings` with plain `builder:`.
+- Test helpers: `testApp({child, overrides})` / `testAppRouter({router, overrides})` in `test/helpers/test_app.dart`; `setUpTestDatabase()`/`tearDownTestDatabase()` in `test/helpers/test_database.dart`; `SharedPreferences.setMockInitialValues({})` then `sharedPreferencesProvider.overrideWithValue(prefs)`; `getBaseOverrides()` in `test/helpers/mock_providers.dart`.
+
+---
+
+### Task 1: Move wizard primitives to lib/shared/widgets/wizard/
+
+**Files:**
+- Move: `lib/features/import_wizard/domain/models/wizard_step_def.dart` → `lib/shared/widgets/wizard/wizard_step_def.dart`
+- Move: `lib/features/import_wizard/presentation/widgets/wizard_step_indicator.dart` → `lib/shared/widgets/wizard/wizard_step_indicator.dart`
+- Modify (import line only): `lib/features/import_wizard/data/adapters/healthkit_adapter.dart:19`, `lib/features/import_wizard/data/adapters/universal_adapter.dart:36`, `lib/features/import_wizard/data/adapters/dive_computer_adapter.dart:28`, `lib/features/import_wizard/domain/services/step_skip_calculator.dart:1`, `lib/features/import_wizard/domain/adapters/import_source_adapter.dart:6`, `lib/features/import_wizard/presentation/pages/unified_import_wizard.dart:20,28`
+- Modify (test imports): `test/features/import_wizard/domain/services/step_skip_calculator_test.dart:5`, `test/features/import_wizard/domain/models/wizard_step_def_test.dart:4`, `test/features/import_wizard/domain/adapters/import_source_adapter_test.dart:8`, `test/features/import_wizard/presentation/pages/unified_import_wizard_test.dart:15`, `test/features/import_wizard/presentation/providers/import_wizard_notifier_test.mocks.dart:22`, `test/features/import_wizard/presentation/widgets/review_step_test.dart:14`, `test/features/import_wizard/presentation/widgets/review_step_pending_test.dart:12`, `test/features/import_wizard/presentation/widgets/import_summary_step_test.dart:14`, `test/features/import_wizard/presentation/widgets/import_progress_step_test.dart:11`, `test/features/import_wizard/presentation/widgets/wizard_step_indicator_test.dart:3`
+
+**Interfaces:**
+- Produces: `package:submersion/shared/widgets/wizard/wizard_step_def.dart` exporting `WizardStepDef` (unchanged API) and `package:submersion/shared/widgets/wizard/wizard_step_indicator.dart` exporting `WizardStepIndicator({required List labels, required int currentStep})`. Later tasks import from these paths.
+
+- [ ] **Step 1: Move files with git mv**
+
+```bash
+mkdir -p lib/shared/widgets/wizard
+git mv lib/features/import_wizard/domain/models/wizard_step_def.dart lib/shared/widgets/wizard/wizard_step_def.dart
+git mv lib/features/import_wizard/presentation/widgets/wizard_step_indicator.dart lib/shared/widgets/wizard/wizard_step_indicator.dart
+```
+
+- [ ] **Step 2: Update every import site**
+
+In each of the 16 files listed above, replace the old import string with the new one (both old strings map to the matching new path):
+
+```dart
+// OLD
+import 'package:submersion/features/import_wizard/domain/models/wizard_step_def.dart';
+// NEW
+import 'package:submersion/shared/widgets/wizard/wizard_step_def.dart';
+
+// OLD
+import 'package:submersion/features/import_wizard/presentation/widgets/wizard_step_indicator.dart';
+// NEW
+import 'package:submersion/shared/widgets/wizard/wizard_step_indicator.dart';
+```
+
+Note `test/features/import_wizard/presentation/providers/import_wizard_notifier_test.mocks.dart` is a GENERATED file but is checked in — edit its import line the same way (build_runner would regenerate identically from the new path).
+
+- [ ] **Step 3: Verify no stale imports remain**
+
+Run: `rg -n "import_wizard/domain/models/wizard_step_def|import_wizard/presentation/widgets/wizard_step_indicator" lib test`
+Expected: no matches.
+
+- [ ] **Step 4: Run the import wizard test suite**
+
+Run: `flutter test test/features/import_wizard/`
+Expected: all tests PASS (move is behavior-neutral).
+
+- [ ] **Step 5: Format and commit**
+
+```bash
+dart format .
+git add lib/shared/widgets/wizard lib/features/import_wizard test/features/import_wizard
+git commit -m "refactor: move wizard step primitives to lib/shared/widgets/wizard"
+```
+
+---
+
+### Task 2: Localization strings for the setup wizard
+
+**Files:**
+- Modify: `lib/l10n/arb/app_en.arb` (add ~65 `setup_*` + 2 `settings_manage_setupAssistant*` keys, alphabetically placed)
+- Modify: `lib/l10n/arb/app_ar.arb`, `app_de.arb`, `app_es.arb`, `app_fr.arb`, `app_he.arb`, `app_hu.arb`, `app_it.arb`, `app_nl.arb`, `app_pt.arb`, `app_zh.arb` (same keys, translated)
+- Regenerate: `lib/l10n/arb/app_localizations*.dart` via `flutter gen-l10n`
+
+**Interfaces:**
+- Produces: `context.l10n.setup_*` getters used by Tasks 6-13. Key names below are load-bearing — later tasks reference them verbatim.
+
+- [ ] **Step 1: Add the English keys to `app_en.arb`**
+
+Insert each key in correct alphabetical position (the file is sorted; `setup_*` sorts after `settings_*`). Placeholder keys need `@`-metadata blocks as shown:
+
+```json
+"settings_manage_setupAssistant": "Setup assistant",
+"settings_manage_setupAssistant_subtitle": "Revisit units, appearance, and backup choices",
+"setup_appearance_language": "Language",
+"setup_appearance_mapStyle": "Map style",
+"setup_appearance_mapStyle_esriSatellite": "Satellite",
+"setup_appearance_mapStyle_openStreetMap": "Street",
+"setup_appearance_mapStyle_openTopoMap": "Topographic",
+"setup_appearance_subtitle": "Make Submersion yours. Everything here can be changed later.",
+"setup_appearance_theme": "Theme",
+"setup_appearance_themeDark": "Dark",
+"setup_appearance_themeLight": "Light",
+"setup_appearance_themePreset": "Color theme",
+"setup_appearance_themeSystem": "System",
+"setup_appearance_title": "Appearance",
+"setup_backup_cloudCopy": "Keep a backup copy in the cloud",
+"setup_backup_frequency": "Frequency",
+"setup_backup_frequency_daily": "Daily",
+"setup_backup_frequency_monthly": "Monthly",
+"setup_backup_frequency_weekly": "Weekly",
+"setup_backup_scheduleSubtitle": "Back up your logbook on a schedule",
+"setup_backup_scheduleToggle": "Automatic backups",
+"setup_backup_subtitle": "Protect your dive log from day one.",
+"setup_backup_title": "Backups & Sync",
+"setup_common_back": "Back",
+"setup_common_next": "Next",
+"setup_common_skip": "Skip",
+"setup_existing_folder_subtitle": "Point Submersion at a folder that already contains a library",
+"setup_existing_folder_title": "Open an existing folder",
+"setup_existing_restore_subtitle": "Pick a backup file exported from Submersion",
+"setup_existing_restore_title": "Restore a backup file",
+"setup_existing_subtitle": "Choose how to load your existing Submersion library",
+"setup_existing_sync_subtitle": "Pull your library from iCloud, Dropbox, or S3",
+"setup_existing_sync_title": "Connect cloud sync",
+"setup_existing_title": "Bring your data",
+"setup_finish_applying": "Setting up...",
+"setup_finish_error": "Could not complete setup: {error}",
+"@setup_finish_error": { "placeholders": { "error": { "type": "Object" } } },
+"setup_finish_feature_diveComputer": "Download dives from your dive computer",
+"setup_finish_feature_gear": "Track gear and service intervals",
+"setup_finish_feature_import": "Import logs from files and other apps",
+"setup_finish_feature_sites": "Map your dive sites",
+"setup_finish_feature_statistics": "Explore statistics about your diving",
+"setup_finish_start": "Start logging",
+"setup_finish_subtitle": "Submersion can also...",
+"setup_finish_title": "You're all set",
+"setup_folder_notFound_message": "The selected folder does not contain a Submersion database.",
+"setup_folder_notFound_title": "No library in that folder",
+"setup_folder_pick": "Choose folder",
+"setup_folder_switching": "Opening library...",
+"setup_folder_title": "Open existing folder",
+"setup_profile_nameHint": "Enter your name",
+"setup_profile_nameLabel": "Your Name",
+"setup_profile_nameValidation": "Please enter your name",
+"setup_profile_subtitle": "Enter your name to get started. You can add more details later in Settings.",
+"setup_profile_title": "Create Your Profile",
+"setup_restore_inProgress": "Restoring...",
+"setup_restore_pick": "Choose backup file",
+"setup_restore_title": "Restore backup",
+"setup_step_appearance": "Appearance",
+"setup_step_backup": "Backup",
+"setup_step_finish": "Done",
+"setup_step_profile": "Profile",
+"setup_step_units": "Units",
+"setup_sync_connectedTo": "Connected to {provider}",
+"@setup_sync_connectedTo": { "placeholders": { "provider": { "type": "String" } } },
+"setup_sync_error": "Could not connect: {error}",
+"@setup_sync_error": { "placeholders": { "error": { "type": "Object" } } },
+"setup_sync_header": "Cloud sync",
+"setup_sync_icloudUnavailable": "iCloud is not available on this device",
+"setup_sync_libraryFound_adopt": "Adopt existing library",
+"setup_sync_libraryFound_keepFresh": "Start fresh",
+"setup_sync_libraryFound_message": "This account already contains a Submersion library. Adopt it instead of starting fresh?",
+"setup_sync_libraryFound_title": "Existing library found",
+"setup_sync_manageInSettings": "Manage in Settings",
+"setup_sync_notConnected": "Not connected",
+"setup_sync_subtitle": "Sync your logbook across devices",
+"setup_syncPull_continue": "Continue",
+"setup_syncPull_noLibrary_message": "No existing Submersion library was found on this account. Start fresh instead? Your connection will be kept.",
+"setup_syncPull_noLibrary_title": "No library found",
+"setup_syncPull_success": "Library adopted",
+"setup_syncPull_syncing": "Pulling your library...",
+"setup_syncPull_title": "Connect and pull",
+"setup_units_advanced": "Fine-tune units",
+"setup_units_altitude": "Altitude",
+"setup_units_dateFormat": "Date format",
+"setup_units_depth": "Depth",
+"setup_units_imperial": "Imperial",
+"setup_units_metric": "Metric",
+"setup_units_pressure": "Pressure",
+"setup_units_sac": "SAC rate",
+"setup_units_subtitle": "Choose how measurements are displayed. You can fine-tune each unit.",
+"setup_units_temperature": "Temperature",
+"setup_units_timeFormat": "Time format",
+"setup_units_title": "Units",
+"setup_units_volume": "Volume",
+"setup_units_weight": "Weight",
+"setup_welcome_existingData_subtitle": "Restore a backup, connect cloud sync, or open an existing folder",
+"setup_welcome_existingData_title": "I have existing Submersion data",
+"setup_welcome_skipSetup": "Skip setup",
+"setup_welcome_startFresh_subtitle": "Create your diver profile and configure the app",
+"setup_welcome_startFresh_title": "Set up a new logbook",
+"setup_welcome_subtitle": "Advanced dive logging and analytics",
+"setup_welcome_title": "Welcome to Submersion"
+```
+
+- [ ] **Step 2: Translate the same keys into the 10 other arb files**
+
+For each of `app_ar.arb, app_de.arb, app_es.arb, app_fr.arb, app_he.arb, app_hu.arb, app_it.arb, app_nl.arb, app_pt.arb, app_zh.arb`: add every key above (same alphabetical placement, same `@`-metadata blocks copied verbatim) with a fluent translation of the English value, matching the tone of each file's existing `onboarding_welcome_*` / `settings_*` translations. Keep the placeholders `{error}` / `{provider}` untranslated. Product name "Submersion", provider names (iCloud, Dropbox, S3), and "SAC" stay untranslated.
+
+- [ ] **Step 3: Regenerate localizations**
+
+Run: `flutter gen-l10n`
+Expected: exits 0; `lib/l10n/arb/app_localizations*.dart` files regenerate with the new getters and no "untranslated message" warnings for the new keys.
+
+- [ ] **Step 4: Verify compilation**
+
+Run: `flutter analyze lib/l10n`
+Expected: No issues found.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add lib/l10n
+git commit -m "feat(l10n): add setup wizard strings in all locales"
+```
+
+---
+
+### Task 3: Domain model — draft, step list, unit-preset detection
+
+**Files:**
+- Create: `lib/features/setup_wizard/domain/setup_wizard_models.dart`
+- Create: `lib/features/setup_wizard/domain/unit_preset_detector.dart`
+- Test: `test/features/setup_wizard/domain/setup_wizard_models_test.dart`
+- Test: `test/features/setup_wizard/domain/unit_preset_detector_test.dart`
+
+**Interfaces:**
+- Consumes: `AppSettings`, `UnitPreset` from `package:submersion/features/settings/presentation/providers/settings_providers.dart`; `BackupFrequency` from `package:submersion/features/backup/domain/entities/backup_settings.dart`; `CloudProviderType` from `package:submersion/core/data/repositories/sync_repository.dart`.
+- Produces (used by Tasks 4-13): `SetupWizardMode{firstRun, settings}`, `SetupPath{undecided, fresh, existingData}`, `ExistingDataSource{none, restoreBackup, cloudSync, openFolder}`, `SetupStepId{welcomeFork, profile, units, appearance, backupSync, finish, existingChoice, restore, syncConnect, openFolder}`, `class SetupWizardDraft` (fields below, `copyWith`), `List computeSteps(SetupWizardDraft draft)`, `UnitPreset presetForLocale(Locale locale)`.
+
+- [ ] **Step 1: Write the failing tests**
+
+`test/features/setup_wizard/domain/setup_wizard_models_test.dart`:
+
+```dart
+import 'package:flutter_test/flutter_test.dart';
+import 'package:submersion/features/setup_wizard/domain/setup_wizard_models.dart';
+
+void main() {
+ group('computeSteps', () {
+ test('first run starts with fork only while path undecided', () {
+ const draft = SetupWizardDraft(mode: SetupWizardMode.firstRun);
+ expect(computeSteps(draft), [SetupStepId.welcomeFork]);
+ });
+
+ test('fresh path shows full first-run flow', () {
+ const draft = SetupWizardDraft(
+ mode: SetupWizardMode.firstRun,
+ path: SetupPath.fresh,
+ );
+ expect(computeSteps(draft), [
+ SetupStepId.welcomeFork,
+ SetupStepId.profile,
+ SetupStepId.units,
+ SetupStepId.appearance,
+ SetupStepId.backupSync,
+ SetupStepId.finish,
+ ]);
+ });
+
+ test('skip setup collapses fresh path to profile then finish', () {
+ const draft = SetupWizardDraft(
+ mode: SetupWizardMode.firstRun,
+ path: SetupPath.fresh,
+ skipRequested: true,
+ );
+ expect(computeSteps(draft), [
+ SetupStepId.welcomeFork,
+ SetupStepId.profile,
+ SetupStepId.finish,
+ ]);
+ });
+
+ test('existing-data path shows choice, then source step', () {
+ const undecided = SetupWizardDraft(
+ mode: SetupWizardMode.firstRun,
+ path: SetupPath.existingData,
+ );
+ expect(computeSteps(undecided), [
+ SetupStepId.welcomeFork,
+ SetupStepId.existingChoice,
+ ]);
+
+ const restore = SetupWizardDraft(
+ mode: SetupWizardMode.firstRun,
+ path: SetupPath.existingData,
+ source: ExistingDataSource.restoreBackup,
+ );
+ expect(computeSteps(restore), [
+ SetupStepId.welcomeFork,
+ SetupStepId.existingChoice,
+ SetupStepId.restore,
+ ]);
+
+ const sync = SetupWizardDraft(
+ mode: SetupWizardMode.firstRun,
+ path: SetupPath.existingData,
+ source: ExistingDataSource.cloudSync,
+ );
+ expect(computeSteps(sync).last, SetupStepId.syncConnect);
+
+ const folder = SetupWizardDraft(
+ mode: SetupWizardMode.firstRun,
+ path: SetupPath.existingData,
+ source: ExistingDataSource.openFolder,
+ );
+ expect(computeSteps(folder).last, SetupStepId.openFolder);
+ });
+
+ test('settings mode hides fork and profile', () {
+ const draft = SetupWizardDraft(mode: SetupWizardMode.settings);
+ expect(computeSteps(draft), [
+ SetupStepId.units,
+ SetupStepId.appearance,
+ SetupStepId.backupSync,
+ SetupStepId.finish,
+ ]);
+ });
+ });
+
+ group('SetupWizardDraft', () {
+ test('applyUnitPreset imperial sets the six core units', () {
+ const draft = SetupWizardDraft(mode: SetupWizardMode.firstRun);
+ final imperial = draft.applyingUnitPreset(UnitPreset.imperial);
+ expect(imperial.settings.depthUnit, DepthUnit.feet);
+ expect(imperial.settings.temperatureUnit, TemperatureUnit.fahrenheit);
+ expect(imperial.settings.pressureUnit, PressureUnit.psi);
+ expect(imperial.settings.volumeUnit, VolumeUnit.cubicFeet);
+ expect(imperial.settings.weightUnit, WeightUnit.pounds);
+ expect(imperial.settings.altitudeUnit, AltitudeUnit.feet);
+ // sacUnit untouched by preset
+ expect(imperial.settings.sacUnit, const AppSettings().sacUnit);
+ // and back to metric
+ final metric = imperial.applyingUnitPreset(UnitPreset.metric);
+ expect(metric.settings.unitPreset, UnitPreset.metric);
+ });
+ });
+}
+```
+
+Add these imports at the top of the test (needed by the assertions): `import 'package:submersion/core/constants/units.dart';` and `import 'package:submersion/features/settings/presentation/providers/settings_providers.dart';`
+
+`test/features/setup_wizard/domain/unit_preset_detector_test.dart`:
+
+```dart
+import 'dart:ui';
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:submersion/features/settings/presentation/providers/settings_providers.dart';
+import 'package:submersion/features/setup_wizard/domain/unit_preset_detector.dart';
+
+void main() {
+ test('US, Liberia, Myanmar map to imperial', () {
+ expect(presetForLocale(const Locale('en', 'US')), UnitPreset.imperial);
+ expect(presetForLocale(const Locale('en', 'LR')), UnitPreset.imperial);
+ expect(presetForLocale(const Locale('my', 'MM')), UnitPreset.imperial);
+ });
+
+ test('everything else maps to metric', () {
+ expect(presetForLocale(const Locale('de', 'DE')), UnitPreset.metric);
+ expect(presetForLocale(const Locale('en', 'GB')), UnitPreset.metric);
+ expect(presetForLocale(const Locale('en')), UnitPreset.metric);
+ });
+}
+```
+
+- [ ] **Step 2: Run tests to verify they fail**
+
+Run: `flutter test test/features/setup_wizard/`
+Expected: FAIL — files under `lib/features/setup_wizard/domain/` do not exist.
+
+- [ ] **Step 3: Implement the domain files**
+
+`lib/features/setup_wizard/domain/setup_wizard_models.dart`:
+
+```dart
+import 'package:submersion/core/constants/units.dart';
+import 'package:submersion/core/data/repositories/sync_repository.dart';
+import 'package:submersion/features/backup/domain/entities/backup_settings.dart';
+import 'package:submersion/features/settings/presentation/providers/settings_providers.dart';
+
+/// Whether the wizard runs at first launch or from Settings.
+enum SetupWizardMode { firstRun, settings }
+
+/// Which top-level branch the user chose on the fork step.
+enum SetupPath { undecided, fresh, existingData }
+
+/// Which existing-data source the user chose.
+enum ExistingDataSource { none, restoreBackup, cloudSync, openFolder }
+
+/// Identity of every wizard step; the shell maps these to widgets.
+enum SetupStepId {
+ welcomeFork,
+ profile,
+ units,
+ appearance,
+ backupSync,
+ finish,
+ existingChoice,
+ restore,
+ syncConnect,
+ openFolder,
+}
+
+/// Immutable wizard draft. Nothing is persisted until Finish applies it.
+class SetupWizardDraft {
+ final SetupWizardMode mode;
+ final SetupPath path;
+ final ExistingDataSource source;
+ final bool skipRequested;
+ final String name;
+ final AppSettings settings;
+ final bool backupEnabled;
+ final BackupFrequency backupFrequency;
+ final bool cloudBackupEnabled;
+ final CloudProviderType? connectedProvider;
+
+ const SetupWizardDraft({
+ required this.mode,
+ this.path = SetupPath.undecided,
+ this.source = ExistingDataSource.none,
+ this.skipRequested = false,
+ this.name = '',
+ this.settings = const AppSettings(),
+ this.backupEnabled = false,
+ this.backupFrequency = BackupFrequency.weekly,
+ this.cloudBackupEnabled = false,
+ this.connectedProvider,
+ });
+
+ SetupWizardDraft copyWith({
+ SetupPath? path,
+ ExistingDataSource? source,
+ bool? skipRequested,
+ String? name,
+ AppSettings? settings,
+ bool? backupEnabled,
+ BackupFrequency? backupFrequency,
+ bool? cloudBackupEnabled,
+ CloudProviderType? connectedProvider,
+ }) {
+ return SetupWizardDraft(
+ mode: mode,
+ path: path ?? this.path,
+ source: source ?? this.source,
+ skipRequested: skipRequested ?? this.skipRequested,
+ name: name ?? this.name,
+ settings: settings ?? this.settings,
+ backupEnabled: backupEnabled ?? this.backupEnabled,
+ backupFrequency: backupFrequency ?? this.backupFrequency,
+ cloudBackupEnabled: cloudBackupEnabled ?? this.cloudBackupEnabled,
+ connectedProvider: connectedProvider ?? this.connectedProvider,
+ );
+ }
+
+ /// Returns a copy with the six core units set to [preset].
+ /// [UnitPreset.custom] returns this draft unchanged.
+ SetupWizardDraft applyingUnitPreset(UnitPreset preset) {
+ switch (preset) {
+ case UnitPreset.metric:
+ return copyWith(
+ settings: settings.copyWith(
+ depthUnit: DepthUnit.meters,
+ temperatureUnit: TemperatureUnit.celsius,
+ pressureUnit: PressureUnit.bar,
+ volumeUnit: VolumeUnit.liters,
+ weightUnit: WeightUnit.kilograms,
+ altitudeUnit: AltitudeUnit.meters,
+ ),
+ );
+ case UnitPreset.imperial:
+ return copyWith(
+ settings: settings.copyWith(
+ depthUnit: DepthUnit.feet,
+ temperatureUnit: TemperatureUnit.fahrenheit,
+ pressureUnit: PressureUnit.psi,
+ volumeUnit: VolumeUnit.cubicFeet,
+ weightUnit: WeightUnit.pounds,
+ altitudeUnit: AltitudeUnit.feet,
+ ),
+ );
+ case UnitPreset.custom:
+ return this;
+ }
+ }
+}
+
+/// Computes the ordered step list for the current draft state.
+List computeSteps(SetupWizardDraft draft) {
+ if (draft.mode == SetupWizardMode.settings) {
+ return const [
+ SetupStepId.units,
+ SetupStepId.appearance,
+ SetupStepId.backupSync,
+ SetupStepId.finish,
+ ];
+ }
+ switch (draft.path) {
+ case SetupPath.undecided:
+ return const [SetupStepId.welcomeFork];
+ case SetupPath.fresh:
+ if (draft.skipRequested) {
+ return const [
+ SetupStepId.welcomeFork,
+ SetupStepId.profile,
+ SetupStepId.finish,
+ ];
+ }
+ return const [
+ SetupStepId.welcomeFork,
+ SetupStepId.profile,
+ SetupStepId.units,
+ SetupStepId.appearance,
+ SetupStepId.backupSync,
+ SetupStepId.finish,
+ ];
+ case SetupPath.existingData:
+ final steps = [SetupStepId.welcomeFork, SetupStepId.existingChoice];
+ switch (draft.source) {
+ case ExistingDataSource.none:
+ break;
+ case ExistingDataSource.restoreBackup:
+ steps.add(SetupStepId.restore);
+ case ExistingDataSource.cloudSync:
+ steps.add(SetupStepId.syncConnect);
+ case ExistingDataSource.openFolder:
+ steps.add(SetupStepId.openFolder);
+ }
+ return steps;
+ }
+}
+```
+
+`lib/features/setup_wizard/domain/unit_preset_detector.dart`:
+
+```dart
+import 'dart:ui';
+
+import 'package:submersion/features/settings/presentation/providers/settings_providers.dart';
+
+/// Countries using imperial-style units for everyday measurement.
+const _imperialCountries = {'US', 'LR', 'MM'};
+
+/// Maps a device locale to the unit preset the wizard preselects.
+UnitPreset presetForLocale(Locale locale) {
+ final country = locale.countryCode?.toUpperCase();
+ if (country != null && _imperialCountries.contains(country)) {
+ return UnitPreset.imperial;
+ }
+ return UnitPreset.metric;
+}
+```
+
+- [ ] **Step 4: Run tests to verify they pass**
+
+Run: `flutter test test/features/setup_wizard/`
+Expected: all PASS.
+
+- [ ] **Step 5: Format and commit**
+
+```bash
+dart format .
+git add lib/features/setup_wizard test/features/setup_wizard
+git commit -m "feat(setup): setup wizard domain model and step computation"
+```
+
+---
+
+### Task 4: SetupWizardNotifier and providers
+
+**Files:**
+- Create: `lib/features/setup_wizard/presentation/providers/setup_wizard_providers.dart`
+- Test: `test/features/setup_wizard/presentation/providers/setup_wizard_providers_test.dart`
+
+**Interfaces:**
+- Consumes: Task 3 domain types.
+- Produces (used by Tasks 5-13):
+ - `final setupWizardProvider = StateNotifierProvider.autoDispose.family(...)`
+ - `SetupWizardNotifier` mutators: `void choosePath(SetupPath path)`, `void chooseSource(ExistingDataSource source)`, `void requestSkip()`, `void setName(String name)`, `void applyUnitPreset(UnitPreset preset)`, `void updateSettings(AppSettings settings)`, `void setBackupEnabled(bool value)`, `void setBackupFrequency(BackupFrequency value)`, `void setCloudBackupEnabled(bool value)`, `void setConnectedProvider(CloudProviderType? type)`
+ - `final previewLocaleProvider = StateProvider((ref) => null);`
+
+- [ ] **Step 1: Write the failing test**
+
+`test/features/setup_wizard/presentation/providers/setup_wizard_providers_test.dart`:
+
+```dart
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:submersion/features/settings/presentation/providers/settings_providers.dart';
+import 'package:submersion/features/setup_wizard/domain/setup_wizard_models.dart';
+import 'package:submersion/features/setup_wizard/presentation/providers/setup_wizard_providers.dart';
+
+void main() {
+ ProviderContainer makeContainer() {
+ final container = ProviderContainer();
+ addTearDown(container.dispose);
+ return container;
+ }
+
+ test('starts undecided in first-run mode, seeded from defaults', () {
+ final container = makeContainer();
+ final draft = container.read(setupWizardProvider(SetupWizardMode.firstRun));
+ expect(draft.path, SetupPath.undecided);
+ expect(draft.settings, const AppSettings());
+ });
+
+ test('mutators update the draft', () {
+ final container = makeContainer();
+ final notifier = container.read(
+ setupWizardProvider(SetupWizardMode.firstRun).notifier,
+ );
+ notifier.choosePath(SetupPath.fresh);
+ notifier.setName(' Eric ');
+ notifier.applyUnitPreset(UnitPreset.imperial);
+ notifier.setBackupEnabled(true);
+
+ final draft = container.read(setupWizardProvider(SetupWizardMode.firstRun));
+ expect(draft.path, SetupPath.fresh);
+ expect(draft.name, 'Eric');
+ expect(draft.settings.unitPreset, UnitPreset.imperial);
+ expect(draft.backupEnabled, isTrue);
+ expect(
+ computeSteps(draft),
+ contains(SetupStepId.backupSync),
+ );
+ });
+
+ test('requestSkip trims the fresh path', () {
+ final container = makeContainer();
+ final notifier = container.read(
+ setupWizardProvider(SetupWizardMode.firstRun).notifier,
+ );
+ notifier.requestSkip();
+ final draft = container.read(setupWizardProvider(SetupWizardMode.firstRun));
+ expect(draft.path, SetupPath.fresh);
+ expect(draft.skipRequested, isTrue);
+ expect(computeSteps(draft), [
+ SetupStepId.welcomeFork,
+ SetupStepId.profile,
+ SetupStepId.finish,
+ ]);
+ });
+}
+```
+
+- [ ] **Step 2: Run test to verify it fails**
+
+Run: `flutter test test/features/setup_wizard/presentation/providers/setup_wizard_providers_test.dart`
+Expected: FAIL — provider file does not exist.
+
+- [ ] **Step 3: Implement the notifier**
+
+`lib/features/setup_wizard/presentation/providers/setup_wizard_providers.dart`:
+
+```dart
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+
+import 'package:submersion/core/constants/units.dart' as units;
+import 'package:submersion/core/data/repositories/sync_repository.dart';
+import 'package:submersion/features/backup/domain/entities/backup_settings.dart';
+import 'package:submersion/features/settings/presentation/providers/settings_providers.dart';
+import 'package:submersion/features/setup_wizard/domain/setup_wizard_models.dart';
+
+/// Locale code the wizard previews before settings are applied at Finish.
+/// Consulted by the app's locale resolution (see app.dart) so a language
+/// choice takes effect immediately while the wizard is open.
+final previewLocaleProvider = StateProvider((ref) => null);
+
+/// Draft state for one wizard session, keyed by mode.
+final setupWizardProvider = StateNotifierProvider.autoDispose
+ .family(
+ (ref, mode) => SetupWizardNotifier(ref, mode),
+ );
+
+class SetupWizardNotifier extends StateNotifier {
+ final Ref _ref;
+
+ SetupWizardNotifier(this._ref, SetupWizardMode mode)
+ : super(SetupWizardDraft(mode: mode)) {
+ if (mode == SetupWizardMode.settings) {
+ // Seed the draft from the live settings so re-entry edits start
+ // from what the diver already has.
+ state = state.copyWith(settings: _ref.read(settingsProvider));
+ }
+ }
+
+ void choosePath(SetupPath path) => state = state.copyWith(path: path);
+
+ void chooseSource(ExistingDataSource source) =>
+ state = state.copyWith(source: source);
+
+ void requestSkip() =>
+ state = state.copyWith(path: SetupPath.fresh, skipRequested: true);
+
+ void setName(String name) => state = state.copyWith(name: name.trim());
+
+ void applyUnitPreset(UnitPreset preset) =>
+ state = state.applyingUnitPreset(preset);
+
+ void updateSettings(AppSettings settings) =>
+ state = state.copyWith(settings: settings);
+
+ void setBackupEnabled(bool value) =>
+ state = state.copyWith(backupEnabled: value);
+
+ void setBackupFrequency(BackupFrequency value) =>
+ state = state.copyWith(backupFrequency: value);
+
+ void setCloudBackupEnabled(bool value) =>
+ state = state.copyWith(cloudBackupEnabled: value);
+
+ void setConnectedProvider(CloudProviderType? type) =>
+ state = state.copyWith(connectedProvider: type);
+}
+```
+
+Note: the `units` import alias is only needed if the analyzer flags an unused import — the draft's preset logic lives in the domain file. Remove the import if unused.
+
+- [ ] **Step 4: Run test to verify it passes**
+
+Run: `flutter test test/features/setup_wizard/presentation/providers/setup_wizard_providers_test.dart`
+Expected: PASS. (The settings-mode seeding path is exercised in Task 5's tests, where settingsProvider is overridable.)
+
+- [ ] **Step 5: Format and commit**
+
+```bash
+dart format .
+git add lib/features/setup_wizard test/features/setup_wizard
+git commit -m "feat(setup): setup wizard draft notifier and preview locale provider"
+```
+
+---
+
+### Task 5: Apply service — persist the draft at Finish
+
+**Files:**
+- Create: `lib/features/setup_wizard/data/setup_apply_service.dart`
+- Test: `test/features/setup_wizard/data/setup_apply_service_test.dart`
+- Modify: `docs/superpowers/specs/2026-07-10-setup-wizard-design.md` (Architecture item 3: replace the `applySettings()` bulk-method sentence with the seed-before-switch approach)
+
+**Interfaces:**
+- Consumes: `diverListNotifierProvider`, `currentDiverIdProvider`, `realignActiveDiverAfterDataReplace` (diver_providers.dart); `diverSettingsRepositoryProvider`, `settingsProvider` (settings_providers.dart); `backupSettingsProvider` (backup_providers.dart); Task 3/4 types.
+- Produces (used by Task 11): `class SetupApplyService { SetupApplyService(this._ref); Future applyFirstRun(SetupWizardDraft draft); Future applySettingsMode(SetupWizardDraft draft); }` and `final setupApplyServiceProvider = Provider(...)`.
+
+- [ ] **Step 1: Write the failing test**
+
+`test/features/setup_wizard/data/setup_apply_service_test.dart`:
+
+```dart
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+import 'package:submersion/core/constants/units.dart';
+import 'package:submersion/core/providers/provider.dart';
+import 'package:submersion/features/backup/domain/entities/backup_settings.dart';
+import 'package:submersion/features/backup/presentation/providers/backup_providers.dart';
+import 'package:submersion/features/divers/data/repositories/diver_repository.dart';
+import 'package:submersion/features/divers/presentation/providers/diver_providers.dart';
+import 'package:submersion/features/settings/data/repositories/diver_settings_repository.dart';
+import 'package:submersion/features/settings/presentation/providers/settings_providers.dart';
+import 'package:submersion/features/setup_wizard/data/setup_apply_service.dart';
+import 'package:submersion/features/setup_wizard/domain/setup_wizard_models.dart';
+
+import '../../../helpers/test_database.dart';
+
+void main() {
+ late SharedPreferences prefs;
+
+ setUp(() async {
+ SharedPreferences.setMockInitialValues({});
+ prefs = await SharedPreferences.getInstance();
+ await setUpTestDatabase();
+ });
+
+ tearDown(() async {
+ await tearDownTestDatabase();
+ });
+
+ ProviderContainer makeContainer() {
+ final container = ProviderContainer(
+ overrides: [sharedPreferencesProvider.overrideWithValue(prefs)],
+ );
+ addTearDown(container.dispose);
+ return container;
+ }
+
+ test('applyFirstRun creates default diver, seeds settings, sets current',
+ () async {
+ final container = makeContainer();
+ final service = container.read(setupApplyServiceProvider);
+
+ const draft = SetupWizardDraft(
+ mode: SetupWizardMode.firstRun,
+ path: SetupPath.fresh,
+ name: 'Eric',
+ backupEnabled: true,
+ backupFrequency: BackupFrequency.daily,
+ );
+ final imperialDraft = draft.applyingUnitPreset(UnitPreset.imperial);
+
+ await service.applyFirstRun(imperialDraft);
+
+ final divers = await DiverRepository().getAllDivers();
+ expect(divers, hasLength(1));
+ expect(divers.single.name, 'Eric');
+ expect(divers.single.isDefault, isTrue);
+
+ // Settings row was seeded with the draft BEFORE the diver switch.
+ final stored =
+ await DiverSettingsRepository().getSettingsForDiver(divers.single.id);
+ expect(stored, isNotNull);
+ expect(stored!.depthUnit, DepthUnit.feet);
+ expect(stored.pressureUnit, PressureUnit.psi);
+ expect(stored.weightUnit, WeightUnit.pounds);
+
+ // Current diver persisted to prefs.
+ expect(prefs.getString(currentDiverIdKey), divers.single.id);
+
+ // Backup schedule applied.
+ final backup = container.read(backupSettingsProvider);
+ expect(backup.enabled, isTrue);
+ expect(backup.frequency, BackupFrequency.daily);
+ });
+
+ test('applyFirstRun with empty name throws ArgumentError and writes nothing',
+ () async {
+ final container = makeContainer();
+ final service = container.read(setupApplyServiceProvider);
+ const draft = SetupWizardDraft(mode: SetupWizardMode.firstRun);
+
+ await expectLater(service.applyFirstRun(draft), throwsArgumentError);
+ expect(await DiverRepository().getAllDivers(), isEmpty);
+ });
+
+ test('applySettingsMode updates the current diver via existing setters',
+ () async {
+ final container = makeContainer();
+
+ // Seed a diver + make it current, mirroring a real re-entry session.
+ final repo = DiverRepository();
+ final now = DateTime.now();
+ final diver = await repo.createDiver(Diver(
+ id: '',
+ name: 'Existing',
+ isDefault: true,
+ createdAt: now,
+ updatedAt: now,
+ ));
+ await container
+ .read(currentDiverIdProvider.notifier)
+ .setCurrentDiver(diver.id);
+ // Let the SettingsNotifier finish its reload before applying.
+ await container.read(settingsProvider.notifier).stream.first;
+
+ final service = container.read(setupApplyServiceProvider);
+ const base = SetupWizardDraft(mode: SetupWizardMode.settings);
+ final draft = base.applyingUnitPreset(UnitPreset.imperial);
+ await service.applySettingsMode(draft);
+
+ final stored =
+ await DiverSettingsRepository().getSettingsForDiver(diver.id);
+ expect(stored!.depthUnit, DepthUnit.feet);
+ expect(stored.volumeUnit, VolumeUnit.cubicFeet);
+ });
+}
+```
+
+Add `import 'package:submersion/features/divers/domain/entities/diver.dart';` to the test imports (needed for the `Diver` constructor in the third test).
+
+Note on the third test: if `settingsProvider.notifier.stream.first` proves flaky for awaiting the reload, replace it with a polling loop (`while (container.read(settingsProvider) == const AppSettings()) { await Future.delayed(const Duration(milliseconds: 10)); }` bounded to ~1s) — the intent is only "reload settled before apply".
+
+- [ ] **Step 2: Run test to verify it fails**
+
+Run: `flutter test test/features/setup_wizard/data/setup_apply_service_test.dart`
+Expected: FAIL — `setup_apply_service.dart` does not exist.
+
+- [ ] **Step 3: Implement the apply service**
+
+`lib/features/setup_wizard/data/setup_apply_service.dart`:
+
+```dart
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+
+import 'package:submersion/features/backup/presentation/providers/backup_providers.dart';
+import 'package:submersion/features/divers/domain/entities/diver.dart';
+import 'package:submersion/features/divers/presentation/providers/diver_providers.dart';
+import 'package:submersion/features/settings/presentation/providers/settings_providers.dart';
+import 'package:submersion/features/setup_wizard/domain/setup_wizard_models.dart';
+
+final setupApplyServiceProvider = Provider(
+ (ref) => SetupApplyService(ref),
+);
+
+/// Persists a completed wizard draft.
+///
+/// First-run ordering is load-bearing: the settings row is seeded through
+/// the repository BEFORE the diver becomes current, because switching the
+/// current diver triggers an unawaited SettingsNotifier reload that would
+/// otherwise race the write (and create a defaults row that clobbers ours).
+class SetupApplyService {
+ final Ref _ref;
+
+ SetupApplyService(this._ref);
+
+ Future applyFirstRun(SetupWizardDraft draft) async {
+ final name = draft.name.trim();
+ if (name.isEmpty) {
+ throw ArgumentError('Diver name must not be empty');
+ }
+
+ final now = DateTime.now();
+ final newDiver = await _ref
+ .read(diverListNotifierProvider.notifier)
+ .addDiver(
+ Diver(
+ id: '',
+ name: name,
+ isDefault: true,
+ createdAt: now,
+ updatedAt: now,
+ ),
+ );
+
+ // Seed the settings row with the draft BEFORE the switch (see class doc).
+ await _ref
+ .read(diverSettingsRepositoryProvider)
+ .createSettingsForDiver(newDiver.id, settings: draft.settings);
+
+ await _ref
+ .read(currentDiverIdProvider.notifier)
+ .setCurrentDiver(newDiver.id);
+
+ await _applyBackupChoices(draft);
+ }
+
+ Future applySettingsMode(SetupWizardDraft draft) async {
+ // Re-entry: the current diver's settings row exists and the
+ // SettingsNotifier is loaded, so the existing setters are race-free.
+ final notifier = _ref.read(settingsProvider.notifier);
+ final s = draft.settings;
+ await notifier.setDepthUnit(s.depthUnit);
+ await notifier.setTemperatureUnit(s.temperatureUnit);
+ await notifier.setPressureUnit(s.pressureUnit);
+ await notifier.setVolumeUnit(s.volumeUnit);
+ await notifier.setWeightUnit(s.weightUnit);
+ await notifier.setAltitudeUnit(s.altitudeUnit);
+ await notifier.setSacUnit(s.sacUnit);
+ await notifier.setTimeFormat(s.timeFormat);
+ await notifier.setDateFormat(s.dateFormat);
+ await notifier.setThemeMode(s.themeMode);
+ await notifier.setThemePresetId(s.themePresetId);
+ await notifier.setMapStyle(s.mapStyle);
+ await notifier.setLocale(s.locale);
+
+ await _applyBackupChoices(draft);
+ }
+
+ Future _applyBackupChoices(SetupWizardDraft draft) async {
+ final backup = _ref.read(backupSettingsProvider.notifier);
+ await backup.setEnabled(draft.backupEnabled);
+ if (draft.backupEnabled) {
+ await backup.setFrequency(draft.backupFrequency);
+ }
+ if (draft.connectedProvider != null && draft.cloudBackupEnabled) {
+ await backup.setCloudBackupEnabled(true);
+ }
+ }
+}
+```
+
+- [ ] **Step 4: Run test to verify it passes**
+
+Run: `flutter test test/features/setup_wizard/data/setup_apply_service_test.dart`
+Expected: PASS (3 tests).
+
+- [ ] **Step 5: Amend the spec's apply paragraph**
+
+In `docs/superpowers/specs/2026-07-10-setup-wizard-design.md`, replace item 3 of the "Apply order on Finish (first-run)" list (the sentence proposing `SettingsNotifier.applySettings(AppSettings)` and its mock-cost note) with:
+
+```markdown
+3. Seed the draft settings into the new diver's `DiverSettings` row via
+ `DiverSettingsRepository.createSettingsForDiver(id, settings: draft)`
+ BEFORE step 2's diver switch. Switching current diver fires an unawaited
+ `SettingsNotifier` reload that would race (and clobber) post-switch
+ writes; seeding first means the reload simply loads the seeded row. No
+ new `SettingsNotifier` API is needed, so no settings mocks change.
+```
+
+Renumber the surrounding list so the switch happens AFTER the seed (create diver → seed settings → set current → backup choices), matching the implementation.
+
+- [ ] **Step 6: Format and commit**
+
+```bash
+dart format .
+git add lib/features/setup_wizard test/features/setup_wizard docs/superpowers/specs/2026-07-10-setup-wizard-design.md
+git commit -m "feat(setup): draft apply service with race-safe settings seeding"
+```
+
+---
+
+### Task 6: Wizard shell + fork + profile steps
+
+**Files:**
+- Create: `lib/features/setup_wizard/presentation/pages/setup_wizard_page.dart`
+- Create: `lib/features/setup_wizard/presentation/widgets/steps/welcome_fork_step.dart`
+- Create: `lib/features/setup_wizard/presentation/widgets/steps/profile_step.dart`
+- Create: `lib/features/setup_wizard/presentation/widgets/steps/placeholder_step.dart` (temporary; deleted as Tasks 7-11 land)
+- Test: `test/features/setup_wizard/presentation/pages/setup_wizard_page_test.dart`
+
+**Interfaces:**
+- Consumes: Tasks 3-4 types/providers; `WizardStepIndicator` + `WizardStepDef` from `package:submersion/shared/widgets/wizard/...`; `OceanBackground` from `package:submersion/core/presentation/widgets/ocean_background.dart`.
+- Produces: `SetupWizardPage({required SetupWizardMode mode})` — the widget Tasks 12's routes build. Internal contract for step widgets (Tasks 7-11): each content step is a plain widget reading/writing `setupWizardProvider(mode)`; choice steps receive explicit callbacks; the shell exposes `SetupShellController` (advance/back) via constructor injection to steps that own their controls.
+
+- [ ] **Step 1: Write the failing widget test**
+
+`test/features/setup_wizard/presentation/pages/setup_wizard_page_test.dart`:
+
+```dart
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:submersion/features/setup_wizard/domain/setup_wizard_models.dart';
+import 'package:submersion/features/setup_wizard/presentation/pages/setup_wizard_page.dart';
+
+import '../../../../helpers/mock_providers.dart';
+import '../../../../helpers/test_app.dart';
+
+void main() {
+ testWidgets('first run shows fork; fresh choice walks to profile and back',
+ (tester) async {
+ final overrides = await getBaseOverrides();
+ await tester.pumpWidget(testApp(
+ overrides: overrides,
+ child: const SetupWizardPage(mode: SetupWizardMode.firstRun),
+ ));
+ await tester.pumpAndSettle();
+
+ expect(find.text('Welcome to Submersion'), findsOneWidget);
+ expect(find.text('Set up a new logbook'), findsOneWidget);
+ expect(find.text('I have existing Submersion data'), findsOneWidget);
+
+ await tester.tap(find.text('Set up a new logbook'));
+ await tester.pumpAndSettle();
+
+ expect(find.text('Create Your Profile'), findsOneWidget);
+
+ // Next disabled with empty name.
+ final nextFinder = find.widgetWithText(FilledButton, 'Next');
+ expect(tester.widget(nextFinder).onPressed, isNull);
+
+ await tester.enterText(find.byType(TextFormField), 'Eric');
+ await tester.pumpAndSettle();
+ expect(tester.widget(nextFinder).onPressed, isNotNull);
+
+ await tester.tap(nextFinder);
+ await tester.pumpAndSettle();
+ // Units placeholder page (real step lands in Task 7).
+ expect(find.text('Units'), findsWidgets);
+
+ await tester.tap(find.widgetWithText(OutlinedButton, 'Back'));
+ await tester.pumpAndSettle();
+ expect(find.text('Create Your Profile'), findsOneWidget);
+ });
+
+ testWidgets('skip setup jumps from profile straight to finish placeholder',
+ (tester) async {
+ final overrides = await getBaseOverrides();
+ await tester.pumpWidget(testApp(
+ overrides: overrides,
+ child: const SetupWizardPage(mode: SetupWizardMode.firstRun),
+ ));
+ await tester.pumpAndSettle();
+
+ await tester.tap(find.text('Skip setup'));
+ await tester.pumpAndSettle();
+ expect(find.text('Create Your Profile'), findsOneWidget);
+
+ await tester.enterText(find.byType(TextFormField), 'Eric');
+ await tester.pumpAndSettle();
+ await tester.tap(find.widgetWithText(FilledButton, 'Next'));
+ await tester.pumpAndSettle();
+
+ expect(find.text("You're all set"), findsOneWidget);
+ });
+
+ testWidgets('settings mode starts at units with no fork', (tester) async {
+ final overrides = await getBaseOverrides();
+ await tester.pumpWidget(testApp(
+ overrides: overrides,
+ child: const SetupWizardPage(mode: SetupWizardMode.settings),
+ ));
+ await tester.pumpAndSettle();
+
+ expect(find.text('Welcome to Submersion'), findsNothing);
+ expect(find.text('Units'), findsWidgets);
+ });
+}
+```
+
+- [ ] **Step 2: Run test to verify it fails**
+
+Run: `flutter test test/features/setup_wizard/presentation/pages/setup_wizard_page_test.dart`
+Expected: FAIL — `setup_wizard_page.dart` does not exist.
+
+- [ ] **Step 3: Implement the shell and first two steps**
+
+`lib/features/setup_wizard/presentation/widgets/steps/welcome_fork_step.dart`:
+
+```dart
+import 'package:flutter/material.dart';
+
+import 'package:submersion/l10n/l10n_extension.dart';
+
+/// First-run fork: start fresh, bring existing data, or skip setup.
+class WelcomeForkStep extends StatelessWidget {
+ final VoidCallback onStartFresh;
+ final VoidCallback onExistingData;
+ final VoidCallback onSkipSetup;
+
+ const WelcomeForkStep({
+ super.key,
+ required this.onStartFresh,
+ required this.onExistingData,
+ required this.onSkipSetup,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+ final l10n = context.l10n;
+
+ return SingleChildScrollView(
+ padding: const EdgeInsets.all(24),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ ExcludeSemantics(
+ child: Center(
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(24),
+ child: Image.asset(
+ 'assets/icon/icon.png',
+ width: 96,
+ height: 96,
+ ),
+ ),
+ ),
+ ),
+ const SizedBox(height: 24),
+ Text(
+ l10n.setup_welcome_title,
+ style: theme.textTheme.headlineSmall?.copyWith(
+ fontWeight: FontWeight.bold,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: 8),
+ Text(
+ l10n.setup_welcome_subtitle,
+ style: theme.textTheme.bodyMedium?.copyWith(
+ color: theme.colorScheme.onSurfaceVariant,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: 32),
+ _ForkCard(
+ icon: Icons.add_circle_outline,
+ title: l10n.setup_welcome_startFresh_title,
+ subtitle: l10n.setup_welcome_startFresh_subtitle,
+ onTap: onStartFresh,
+ ),
+ const SizedBox(height: 12),
+ _ForkCard(
+ icon: Icons.cloud_download_outlined,
+ title: l10n.setup_welcome_existingData_title,
+ subtitle: l10n.setup_welcome_existingData_subtitle,
+ onTap: onExistingData,
+ ),
+ const SizedBox(height: 24),
+ TextButton(
+ onPressed: onSkipSetup,
+ child: Text(l10n.setup_welcome_skipSetup),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class _ForkCard extends StatelessWidget {
+ final IconData icon;
+ final String title;
+ final String subtitle;
+ final VoidCallback onTap;
+
+ const _ForkCard({
+ required this.icon,
+ required this.title,
+ required this.subtitle,
+ required this.onTap,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+ return Card(
+ child: InkWell(
+ borderRadius: BorderRadius.circular(12),
+ onTap: onTap,
+ child: Padding(
+ padding: const EdgeInsets.all(16),
+ child: Row(
+ children: [
+ Icon(icon, size: 32, color: theme.colorScheme.primary),
+ const SizedBox(width: 16),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ title,
+ style: theme.textTheme.titleMedium?.copyWith(
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ const SizedBox(height: 4),
+ Text(
+ subtitle,
+ style: theme.textTheme.bodySmall?.copyWith(
+ color: theme.colorScheme.onSurfaceVariant,
+ ),
+ ),
+ ],
+ ),
+ ),
+ const Icon(Icons.chevron_right),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
+```
+
+`lib/features/setup_wizard/presentation/widgets/steps/profile_step.dart`:
+
+```dart
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+
+import 'package:submersion/features/setup_wizard/domain/setup_wizard_models.dart';
+import 'package:submersion/features/setup_wizard/presentation/providers/setup_wizard_providers.dart';
+import 'package:submersion/l10n/l10n_extension.dart';
+
+/// Collects the diver name (the wizard's only mandatory input).
+class ProfileStep extends ConsumerStatefulWidget {
+ final SetupWizardMode mode;
+
+ const ProfileStep({super.key, required this.mode});
+
+ @override
+ ConsumerState createState() => _ProfileStepState();
+}
+
+class _ProfileStepState extends ConsumerState {
+ late final TextEditingController _nameController;
+
+ @override
+ void initState() {
+ super.initState();
+ _nameController = TextEditingController(
+ text: ref.read(setupWizardProvider(widget.mode)).name,
+ );
+ }
+
+ @override
+ void dispose() {
+ _nameController.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+ final l10n = context.l10n;
+ final notifier = ref.read(setupWizardProvider(widget.mode).notifier);
+
+ return SingleChildScrollView(
+ padding: const EdgeInsets.all(24),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Text(
+ l10n.setup_profile_title,
+ style: theme.textTheme.titleLarge?.copyWith(
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ const SizedBox(height: 8),
+ Text(
+ l10n.setup_profile_subtitle,
+ style: theme.textTheme.bodyMedium?.copyWith(
+ color: theme.colorScheme.onSurfaceVariant,
+ ),
+ ),
+ const SizedBox(height: 24),
+ TextFormField(
+ controller: _nameController,
+ decoration: InputDecoration(
+ labelText: l10n.setup_profile_nameLabel,
+ hintText: l10n.setup_profile_nameHint,
+ prefixIcon: const Icon(Icons.person),
+ ),
+ textCapitalization: TextCapitalization.words,
+ autofocus: true,
+ textInputAction: TextInputAction.done,
+ onChanged: notifier.setName,
+ ),
+ ],
+ ),
+ );
+ }
+}
+```
+
+`lib/features/setup_wizard/presentation/widgets/steps/placeholder_step.dart` (temporary scaffolding so the shell compiles before Tasks 7-11; each later task replaces one usage):
+
+```dart
+import 'package:flutter/material.dart';
+
+/// Temporary stand-in for steps implemented in later tasks.
+class PlaceholderStep extends StatelessWidget {
+ final String title;
+
+ const PlaceholderStep({super.key, required this.title});
+
+ @override
+ Widget build(BuildContext context) {
+ return Center(
+ child: Text(title, style: Theme.of(context).textTheme.titleLarge),
+ );
+ }
+}
+```
+
+`lib/features/setup_wizard/presentation/pages/setup_wizard_page.dart`:
+
+```dart
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+
+import 'package:submersion/core/presentation/widgets/ocean_background.dart';
+import 'package:submersion/features/setup_wizard/domain/setup_wizard_models.dart';
+import 'package:submersion/features/setup_wizard/presentation/providers/setup_wizard_providers.dart';
+import 'package:submersion/features/setup_wizard/presentation/widgets/steps/placeholder_step.dart';
+import 'package:submersion/features/setup_wizard/presentation/widgets/steps/profile_step.dart';
+import 'package:submersion/features/setup_wizard/presentation/widgets/steps/welcome_fork_step.dart';
+import 'package:submersion/l10n/l10n_extension.dart';
+import 'package:submersion/shared/widgets/wizard/wizard_step_def.dart';
+import 'package:submersion/shared/widgets/wizard/wizard_step_indicator.dart';
+
+/// Multi-step setup wizard for new databases (first run) and Settings
+/// re-entry. See docs/superpowers/specs/2026-07-10-setup-wizard-design.md.
+class SetupWizardPage extends ConsumerStatefulWidget {
+ final SetupWizardMode mode;
+
+ const SetupWizardPage({super.key, required this.mode});
+
+ @override
+ ConsumerState createState() => _SetupWizardPageState();
+}
+
+/// Steps that show the progress indicator and the shared Next/Back bar.
+const _formZone = {
+ SetupStepId.profile,
+ SetupStepId.units,
+ SetupStepId.appearance,
+ SetupStepId.backupSync,
+};
+
+class _SetupWizardPageState extends ConsumerState {
+ final _pageController = PageController();
+ int _currentIndex = 0;
+
+ @override
+ void dispose() {
+ _pageController.dispose();
+ super.dispose();
+ }
+
+ Future _animateTo(int index) async {
+ if (_pageController.hasClients) {
+ await _pageController.animateToPage(
+ index,
+ duration: const Duration(milliseconds: 300),
+ curve: Curves.easeInOut,
+ );
+ }
+ if (mounted) setState(() => _currentIndex = index);
+ }
+
+ void _advance() {
+ final steps = computeSteps(ref.read(setupWizardProvider(widget.mode)));
+ if (_currentIndex < steps.length - 1) {
+ _animateTo(_currentIndex + 1);
+ }
+ }
+
+ void _back() {
+ if (_currentIndex > 0) _animateTo(_currentIndex - 1);
+ }
+
+ /// Choice steps mutate the draft (growing the step list), then advance
+ /// after the rebuild that adds the next page.
+ void _chooseAndAdvance(void Function() mutate) {
+ mutate();
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ if (mounted) _advance();
+ });
+ }
+
+ String _stepLabel(SetupStepId id) {
+ final l10n = context.l10n;
+ switch (id) {
+ case SetupStepId.profile:
+ return l10n.setup_step_profile;
+ case SetupStepId.units:
+ return l10n.setup_step_units;
+ case SetupStepId.appearance:
+ return l10n.setup_step_appearance;
+ case SetupStepId.backupSync:
+ return l10n.setup_step_backup;
+ case SetupStepId.finish:
+ return l10n.setup_step_finish;
+ default:
+ return '';
+ }
+ }
+
+ WizardStepDef _defFor(SetupStepId id) {
+ final mode = widget.mode;
+ final notifier = ref.read(setupWizardProvider(mode).notifier);
+ switch (id) {
+ case SetupStepId.welcomeFork:
+ return WizardStepDef(
+ label: '',
+ hideBottomBar: true,
+ canAdvance: setupWizardProvider(mode).select((_) => true),
+ builder: (_) => WelcomeForkStep(
+ onStartFresh: () =>
+ _chooseAndAdvance(() => notifier.choosePath(SetupPath.fresh)),
+ onExistingData: () => _chooseAndAdvance(
+ () => notifier.choosePath(SetupPath.existingData),
+ ),
+ onSkipSetup: () => _chooseAndAdvance(notifier.requestSkip),
+ ),
+ );
+ case SetupStepId.profile:
+ return WizardStepDef(
+ label: _stepLabel(id),
+ canAdvance: setupWizardProvider(
+ mode,
+ ).select((d) => d.name.trim().isNotEmpty),
+ builder: (_) => ProfileStep(mode: mode),
+ );
+ case SetupStepId.units:
+ return WizardStepDef(
+ label: _stepLabel(id),
+ canAdvance: setupWizardProvider(mode).select((_) => true),
+ builder: (_) => const PlaceholderStep(title: 'Units'),
+ );
+ case SetupStepId.appearance:
+ return WizardStepDef(
+ label: _stepLabel(id),
+ canAdvance: setupWizardProvider(mode).select((_) => true),
+ builder: (_) => const PlaceholderStep(title: 'Appearance'),
+ );
+ case SetupStepId.backupSync:
+ return WizardStepDef(
+ label: _stepLabel(id),
+ canAdvance: setupWizardProvider(mode).select((_) => true),
+ builder: (_) => const PlaceholderStep(title: 'Backups & Sync'),
+ );
+ case SetupStepId.finish:
+ return WizardStepDef(
+ label: _stepLabel(id),
+ hideBottomBar: true,
+ canAdvance: setupWizardProvider(mode).select((_) => true),
+ builder: (_) => const PlaceholderStep(title: "You're all set"),
+ );
+ case SetupStepId.existingChoice:
+ return WizardStepDef(
+ label: '',
+ hideBottomBar: true,
+ canAdvance: setupWizardProvider(mode).select((_) => true),
+ builder: (_) => const PlaceholderStep(title: 'Bring your data'),
+ );
+ case SetupStepId.restore:
+ return WizardStepDef(
+ label: '',
+ hideBottomBar: true,
+ canAdvance: setupWizardProvider(mode).select((_) => true),
+ builder: (_) => const PlaceholderStep(title: 'Restore backup'),
+ );
+ case SetupStepId.syncConnect:
+ return WizardStepDef(
+ label: '',
+ hideBottomBar: true,
+ canAdvance: setupWizardProvider(mode).select((_) => true),
+ builder: (_) => const PlaceholderStep(title: 'Connect and pull'),
+ );
+ case SetupStepId.openFolder:
+ return WizardStepDef(
+ label: '',
+ hideBottomBar: true,
+ canAdvance: setupWizardProvider(mode).select((_) => true),
+ builder: (_) => const PlaceholderStep(title: 'Open existing folder'),
+ );
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final draft = ref.watch(setupWizardProvider(widget.mode));
+ final steps = computeSteps(draft);
+ if (_currentIndex >= steps.length) {
+ _currentIndex = steps.length - 1;
+ }
+ final defs = steps.map(_defFor).toList();
+ final currentId = steps[_currentIndex];
+ final inFormZone = _formZone.contains(currentId);
+
+ // Indicator covers the contiguous labelled tail of the flow (profile or
+ // units onward, plus finish); choice steps stay indicator-free.
+ final labelledIds = steps
+ .where((s) => _formZone.contains(s) || s == SetupStepId.finish)
+ .toList();
+ final labelledIndex = labelledIds.indexOf(currentId);
+
+ return Scaffold(
+ body: OceanBackground(
+ child: SafeArea(
+ child: Center(
+ child: ConstrainedBox(
+ constraints: const BoxConstraints(maxWidth: 560),
+ child: Padding(
+ padding: const EdgeInsets.all(16),
+ child: Card(
+ clipBehavior: Clip.antiAlias,
+ child: Column(
+ children: [
+ if (labelledIndex >= 0)
+ WizardStepIndicator(
+ labels: labelledIds.map(_stepLabel).toList(),
+ currentStep: labelledIndex,
+ ),
+ Expanded(
+ child: PageView(
+ controller: _pageController,
+ physics: const NeverScrollableScrollPhysics(),
+ children: [
+ for (final def in defs)
+ Builder(builder: def.builder),
+ ],
+ ),
+ ),
+ if (inFormZone) _buildBottomBar(defs[_currentIndex]),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget _buildBottomBar(WizardStepDef def) {
+ final l10n = context.l10n;
+ return SafeArea(
+ top: false,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
+ child: Row(
+ children: [
+ if (_currentIndex > 0)
+ OutlinedButton(
+ onPressed: _back,
+ child: Text(l10n.setup_common_back),
+ ),
+ const Spacer(),
+ if (_currentIndex < computeStepCount() - 1 &&
+ _skippable(currentStepId()))
+ TextButton(
+ onPressed: _advance,
+ child: Text(l10n.setup_common_skip),
+ ),
+ const SizedBox(width: 8),
+ _NextButton(def: def, onNext: _advance),
+ ],
+ ),
+ ),
+ );
+ }
+
+ SetupStepId currentStepId() {
+ final steps = computeSteps(ref.read(setupWizardProvider(widget.mode)));
+ return steps[_currentIndex];
+ }
+
+ int computeStepCount() {
+ return computeSteps(ref.read(setupWizardProvider(widget.mode))).length;
+ }
+
+ bool _skippable(SetupStepId id) => id != SetupStepId.profile;
+}
+
+class _NextButton extends ConsumerWidget {
+ final WizardStepDef def;
+ final VoidCallback onNext;
+
+ const _NextButton({required this.def, required this.onNext});
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ final canAdvance = ref.watch(def.canAdvance);
+ return FilledButton(
+ style: FilledButton.styleFrom(minimumSize: const Size(100, 44)),
+ onPressed: canAdvance ? onNext : null,
+ child: Text(context.l10n.setup_common_next),
+ );
+ }
+}
+```
+
+Implementation note: `Expanded(child: PageView(...))` inside a `Card` inside `Center` needs bounded height — the `Center > ConstrainedBox > Padding > Card > Column` chain is height-unbounded on desktop. Wrap the `ConstrainedBox` content so the column gets the full safe-area height: replace `Center(child: ConstrainedBox(...))` with `Center(child: ConstrainedBox(constraints: const BoxConstraints(maxWidth: 560), child: SizedBox.expand(child: ...)))` — `SizedBox.expand` inside `Center` clamps to the parent's max size and keeps the card full-height. If the first test run throws unbounded-height layout errors, this is the fix to apply.
+
+- [ ] **Step 4: Run test to verify it passes**
+
+Run: `flutter test test/features/setup_wizard/presentation/pages/setup_wizard_page_test.dart`
+Expected: 3 tests PASS.
+
+- [ ] **Step 5: Format and commit**
+
+```bash
+dart format .
+git add lib/features/setup_wizard test/features/setup_wizard
+git commit -m "feat(setup): wizard shell with fork and profile steps"
+```
+
+---
+
+### Task 7: Units step
+
+**Files:**
+- Create: `lib/features/setup_wizard/presentation/widgets/steps/units_step.dart`
+- Modify: `lib/features/setup_wizard/presentation/pages/setup_wizard_page.dart` (replace the units `PlaceholderStep` with `UnitsStep(mode: mode)`)
+- Test: `test/features/setup_wizard/presentation/widgets/steps/units_step_test.dart`
+
+**Interfaces:**
+- Consumes: `setupWizardProvider`, `presetForLocale`, `UnitPreset`, the unit enums, `TimeFormat`, `DateFormatPreference`.
+- Produces: `UnitsStep({required SetupWizardMode mode})`.
+
+- [ ] **Step 1: Write the failing test**
+
+`test/features/setup_wizard/presentation/widgets/steps/units_step_test.dart`:
+
+```dart
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:submersion/core/constants/units.dart';
+import 'package:submersion/features/settings/presentation/providers/settings_providers.dart';
+import 'package:submersion/features/setup_wizard/domain/setup_wizard_models.dart';
+import 'package:submersion/features/setup_wizard/presentation/providers/setup_wizard_providers.dart';
+import 'package:submersion/features/setup_wizard/presentation/widgets/steps/units_step.dart';
+
+import '../../../../../helpers/test_app.dart';
+
+void main() {
+ testWidgets('preset toggle drives the draft units', (tester) async {
+ late ProviderContainer container;
+ await tester.pumpWidget(testApp(
+ child: Builder(builder: (context) {
+ container = ProviderScope.containerOf(context);
+ return const UnitsStep(mode: SetupWizardMode.firstRun);
+ }),
+ ));
+ await tester.pumpAndSettle();
+
+ await tester.tap(find.text('Imperial'));
+ await tester.pumpAndSettle();
+
+ final draft =
+ container.read(setupWizardProvider(SetupWizardMode.firstRun));
+ expect(draft.settings.unitPreset, UnitPreset.imperial);
+ expect(draft.settings.depthUnit, DepthUnit.feet);
+ });
+
+ testWidgets('advanced expander exposes per-unit overrides', (tester) async {
+ late ProviderContainer container;
+ await tester.pumpWidget(testApp(
+ child: Builder(builder: (context) {
+ container = ProviderScope.containerOf(context);
+ return const UnitsStep(mode: SetupWizardMode.firstRun);
+ }),
+ ));
+ await tester.pumpAndSettle();
+
+ await tester.tap(find.text('Fine-tune units'));
+ await tester.pumpAndSettle();
+
+ // Depth row: switch to feet only.
+ await tester.ensureVisible(find.text('Depth'));
+ await tester.tap(find.byKey(const ValueKey('setup-unit-depth-ft')));
+ await tester.pumpAndSettle();
+
+ final draft =
+ container.read(setupWizardProvider(SetupWizardMode.firstRun));
+ expect(draft.settings.depthUnit, DepthUnit.feet);
+ expect(draft.settings.temperatureUnit, TemperatureUnit.celsius);
+ expect(draft.settings.unitPreset, UnitPreset.custom);
+ });
+}
+```
+
+- [ ] **Step 2: Run test to verify it fails**
+
+Run: `flutter test test/features/setup_wizard/presentation/widgets/steps/units_step_test.dart`
+Expected: FAIL — `units_step.dart` does not exist.
+
+- [ ] **Step 3: Implement the step**
+
+`lib/features/setup_wizard/presentation/widgets/steps/units_step.dart`:
+
+```dart
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+
+import 'package:submersion/core/constants/units.dart';
+import 'package:submersion/features/settings/presentation/providers/settings_providers.dart';
+import 'package:submersion/features/setup_wizard/domain/setup_wizard_models.dart';
+import 'package:submersion/features/setup_wizard/domain/unit_preset_detector.dart';
+import 'package:submersion/features/setup_wizard/presentation/providers/setup_wizard_providers.dart';
+import 'package:submersion/l10n/l10n_extension.dart';
+
+/// Metric/Imperial preset with an expander for per-unit fine-tuning.
+class UnitsStep extends ConsumerStatefulWidget {
+ final SetupWizardMode mode;
+
+ const UnitsStep({super.key, required this.mode});
+
+ @override
+ ConsumerState createState() => _UnitsStepState();
+}
+
+class _UnitsStepState extends ConsumerState {
+ @override
+ void initState() {
+ super.initState();
+ // First-run: preselect the preset matching the device locale, once.
+ if (widget.mode == SetupWizardMode.firstRun) {
+ final draft = ref.read(setupWizardProvider(widget.mode));
+ if (draft.settings.unitPreset == UnitPreset.metric) {
+ final locale =
+ WidgetsBinding.instance.platformDispatcher.locale;
+ final preset = presetForLocale(locale);
+ if (preset != UnitPreset.metric) {
+ Future.microtask(() {
+ if (mounted) {
+ ref
+ .read(setupWizardProvider(widget.mode).notifier)
+ .applyUnitPreset(preset);
+ }
+ });
+ }
+ }
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+ final l10n = context.l10n;
+ final draft = ref.watch(setupWizardProvider(widget.mode));
+ final notifier = ref.read(setupWizardProvider(widget.mode).notifier);
+ final s = draft.settings;
+
+ return SingleChildScrollView(
+ padding: const EdgeInsets.all(24),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Text(
+ l10n.setup_units_title,
+ style: theme.textTheme.titleLarge?.copyWith(
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ const SizedBox(height: 8),
+ Text(
+ l10n.setup_units_subtitle,
+ style: theme.textTheme.bodyMedium?.copyWith(
+ color: theme.colorScheme.onSurfaceVariant,
+ ),
+ ),
+ const SizedBox(height: 24),
+ SegmentedButton(
+ segments: [
+ ButtonSegment(
+ value: UnitPreset.metric,
+ label: Text(l10n.setup_units_metric),
+ ),
+ ButtonSegment(
+ value: UnitPreset.imperial,
+ label: Text(l10n.setup_units_imperial),
+ ),
+ ],
+ emptySelectionAllowed: true,
+ selected: s.unitPreset == UnitPreset.custom
+ ? const {}
+ : {s.unitPreset},
+ onSelectionChanged: (selection) {
+ if (selection.isNotEmpty) {
+ notifier.applyUnitPreset(selection.first);
+ }
+ },
+ ),
+ const SizedBox(height: 16),
+ ExpansionTile(
+ title: Text(l10n.setup_units_advanced),
+ tilePadding: EdgeInsets.zero,
+ childrenPadding: const EdgeInsets.only(bottom: 8),
+ children: [
+ _unitRow(
+ label: l10n.setup_units_depth,
+ keyPrefix: 'setup-unit-depth',
+ values: DepthUnit.values,
+ selected: s.depthUnit,
+ symbol: (u) => u.symbol,
+ onChanged: (u) =>
+ notifier.updateSettings(s.copyWith(depthUnit: u)),
+ ),
+ _unitRow(
+ label: l10n.setup_units_temperature,
+ keyPrefix: 'setup-unit-temperature',
+ values: TemperatureUnit.values,
+ selected: s.temperatureUnit,
+ symbol: (u) => u.symbol,
+ onChanged: (u) =>
+ notifier.updateSettings(s.copyWith(temperatureUnit: u)),
+ ),
+ _unitRow(
+ label: l10n.setup_units_pressure,
+ keyPrefix: 'setup-unit-pressure',
+ values: PressureUnit.values,
+ selected: s.pressureUnit,
+ symbol: (u) => u.symbol,
+ onChanged: (u) =>
+ notifier.updateSettings(s.copyWith(pressureUnit: u)),
+ ),
+ _unitRow(
+ label: l10n.setup_units_volume,
+ keyPrefix: 'setup-unit-volume',
+ values: VolumeUnit.values,
+ selected: s.volumeUnit,
+ symbol: (u) => u.symbol,
+ onChanged: (u) =>
+ notifier.updateSettings(s.copyWith(volumeUnit: u)),
+ ),
+ _unitRow(
+ label: l10n.setup_units_weight,
+ keyPrefix: 'setup-unit-weight',
+ values: WeightUnit.values,
+ selected: s.weightUnit,
+ symbol: (u) => u.symbol,
+ onChanged: (u) =>
+ notifier.updateSettings(s.copyWith(weightUnit: u)),
+ ),
+ _unitRow(
+ label: l10n.setup_units_altitude,
+ keyPrefix: 'setup-unit-altitude',
+ values: AltitudeUnit.values,
+ selected: s.altitudeUnit,
+ symbol: (u) => u.symbol,
+ onChanged: (u) =>
+ notifier.updateSettings(s.copyWith(altitudeUnit: u)),
+ ),
+ _unitRow(
+ label: l10n.setup_units_sac,
+ keyPrefix: 'setup-unit-sac',
+ values: SacUnit.values,
+ selected: s.sacUnit,
+ symbol: (u) => u.symbol,
+ onChanged: (u) =>
+ notifier.updateSettings(s.copyWith(sacUnit: u)),
+ ),
+ _unitRow(
+ label: l10n.setup_units_timeFormat,
+ keyPrefix: 'setup-unit-timeformat',
+ values: TimeFormat.values,
+ selected: s.timeFormat,
+ symbol: (u) => u.displayName,
+ onChanged: (u) =>
+ notifier.updateSettings(s.copyWith(timeFormat: u)),
+ ),
+ _unitRow(
+ label: l10n.setup_units_dateFormat,
+ keyPrefix: 'setup-unit-dateformat',
+ values: DateFormatPreference.values,
+ selected: s.dateFormat,
+ symbol: (u) => u.displayName,
+ onChanged: (u) =>
+ notifier.updateSettings(s.copyWith(dateFormat: u)),
+ ),
+ ],
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _unitRow({
+ required String label,
+ required String keyPrefix,
+ required List values,
+ required T selected,
+ required String Function(T) symbol,
+ required void Function(T) onChanged,
+ }) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(vertical: 6),
+ child: Row(
+ children: [
+ Expanded(child: Text(label)),
+ SegmentedButton(
+ segments: [
+ for (final v in values)
+ ButtonSegment(
+ value: v,
+ label: Text(
+ symbol(v),
+ key: ValueKey('$keyPrefix-${_segmentKey(symbol(v))}'),
+ ),
+ ),
+ ],
+ selected: {selected},
+ showSelectedIcon: false,
+ onSelectionChanged: (sel) => onChanged(sel.first),
+ ),
+ ],
+ ),
+ );
+ }
+
+ String _segmentKey(String symbol) =>
+ symbol.toLowerCase().replaceAll(RegExp(r'[^a-z0-9]'), '');
+}
+```
+
+(The depth "ft" segment therefore carries `ValueKey('setup-unit-depth-ft')`, matching the test.)
+
+- [ ] **Step 4: Replace the placeholder in the shell**
+
+In `setup_wizard_page.dart`, add `import 'package:submersion/features/setup_wizard/presentation/widgets/steps/units_step.dart';` and change the `SetupStepId.units` case builder from `const PlaceholderStep(title: 'Units')` to `UnitsStep(mode: mode)`.
+
+- [ ] **Step 5: Run tests to verify they pass**
+
+Run: `flutter test test/features/setup_wizard/`
+Expected: all PASS (the shell test's `find.text('Units')` assertion still matches the real step's title).
+
+- [ ] **Step 6: Format and commit**
+
+```bash
+dart format .
+git add lib/features/setup_wizard test/features/setup_wizard
+git commit -m "feat(setup): units step with locale-aware preset and fine-tuning"
+```
+
+---
+
+### Task 8: Appearance step + locale preview hook
+
+**Files:**
+- Create: `lib/features/setup_wizard/presentation/widgets/steps/appearance_step.dart`
+- Modify: `lib/features/setup_wizard/presentation/pages/setup_wizard_page.dart` (replace appearance placeholder)
+- Modify: `lib/app.dart` (preview-locale hook, 2 lines)
+- Test: `test/features/setup_wizard/presentation/widgets/steps/appearance_step_test.dart`
+
+**Interfaces:**
+- Consumes: `previewLocaleProvider` (Task 4), `AppThemeRegistry` (`lib/core/theme/app_theme_registry.dart`, preset ids: submersion, console, tropical, minimalist, deep — display name via each preset's `nameKey` l10n key), `MapStyle`.
+- Produces: `AppearanceStep({required SetupWizardMode mode})`; app-wide locale preview while the wizard is open.
+
+- [ ] **Step 1: Write the failing test**
+
+`test/features/setup_wizard/presentation/widgets/steps/appearance_step_test.dart`:
+
+```dart
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:submersion/core/constants/map_style.dart';
+import 'package:submersion/features/setup_wizard/domain/setup_wizard_models.dart';
+import 'package:submersion/features/setup_wizard/presentation/providers/setup_wizard_providers.dart';
+import 'package:submersion/features/setup_wizard/presentation/widgets/steps/appearance_step.dart';
+
+import '../../../../../helpers/test_app.dart';
+
+void main() {
+ testWidgets('theme, map style, and language update draft and preview',
+ (tester) async {
+ late ProviderContainer container;
+ await tester.pumpWidget(testApp(
+ child: Builder(builder: (context) {
+ container = ProviderScope.containerOf(context);
+ return const AppearanceStep(mode: SetupWizardMode.firstRun);
+ }),
+ ));
+ await tester.pumpAndSettle();
+
+ await tester.tap(find.text('Dark'));
+ await tester.pumpAndSettle();
+
+ await tester.ensureVisible(find.text('Map style'));
+ await tester.tap(find.text('Satellite'));
+ await tester.pumpAndSettle();
+
+ await tester.ensureVisible(find.byType(DropdownButtonFormField));
+ await tester.tap(find.byType(DropdownButtonFormField));
+ await tester.pumpAndSettle();
+ await tester.tap(find.text('Deutsch').last);
+ await tester.pumpAndSettle();
+
+ final draft =
+ container.read(setupWizardProvider(SetupWizardMode.firstRun));
+ expect(draft.settings.themeMode, ThemeMode.dark);
+ expect(draft.settings.mapStyle, MapStyle.esriSatellite);
+ expect(draft.settings.locale, 'de');
+ expect(container.read(previewLocaleProvider), 'de');
+ });
+}
+```
+
+- [ ] **Step 2: Run test to verify it fails**
+
+Run: `flutter test test/features/setup_wizard/presentation/widgets/steps/appearance_step_test.dart`
+Expected: FAIL — `appearance_step.dart` does not exist.
+
+- [ ] **Step 3: Implement the step**
+
+`lib/features/setup_wizard/presentation/widgets/steps/appearance_step.dart`:
+
+```dart
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+
+import 'package:submersion/core/constants/map_style.dart';
+import 'package:submersion/core/theme/app_theme_registry.dart';
+import 'package:submersion/features/setup_wizard/domain/setup_wizard_models.dart';
+import 'package:submersion/features/setup_wizard/presentation/providers/setup_wizard_providers.dart';
+import 'package:submersion/l10n/l10n_extension.dart';
+
+/// Language options offered by the wizard. Codes must stay in sync with
+/// AppLocalizations.supportedLocales; names render in their own language.
+const _localeOptions = <(String, String)>[
+ ('system', ''),
+ ('en', 'English'),
+ ('es', 'Espanol'),
+ ('fr', 'Francais'),
+ ('de', 'Deutsch'),
+ ('it', 'Italiano'),
+ ('nl', 'Nederlands'),
+ ('pt', 'Portugues'),
+ ('hu', 'Magyar'),
+ ('ar', 'العربية'),
+ ('he', 'עברית'),
+ ('zh', '简体中文'),
+];
+
+/// Theme mode, color theme, map style, and language.
+class AppearanceStep extends ConsumerWidget {
+ final SetupWizardMode mode;
+
+ const AppearanceStep({super.key, required this.mode});
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ final theme = Theme.of(context);
+ final l10n = context.l10n;
+ final draft = ref.watch(setupWizardProvider(mode));
+ final notifier = ref.read(setupWizardProvider(mode).notifier);
+ final s = draft.settings;
+
+ Widget sectionLabel(String text) => Padding(
+ padding: const EdgeInsets.only(top: 20, bottom: 8),
+ child: Text(text, style: theme.textTheme.titleSmall),
+ );
+
+ return SingleChildScrollView(
+ padding: const EdgeInsets.all(24),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Text(
+ l10n.setup_appearance_title,
+ style: theme.textTheme.titleLarge?.copyWith(
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ const SizedBox(height: 8),
+ Text(
+ l10n.setup_appearance_subtitle,
+ style: theme.textTheme.bodyMedium?.copyWith(
+ color: theme.colorScheme.onSurfaceVariant,
+ ),
+ ),
+ sectionLabel(l10n.setup_appearance_theme),
+ SegmentedButton(
+ segments: [
+ ButtonSegment(
+ value: ThemeMode.system,
+ label: Text(l10n.setup_appearance_themeSystem),
+ ),
+ ButtonSegment(
+ value: ThemeMode.light,
+ label: Text(l10n.setup_appearance_themeLight),
+ ),
+ ButtonSegment(
+ value: ThemeMode.dark,
+ label: Text(l10n.setup_appearance_themeDark),
+ ),
+ ],
+ selected: {s.themeMode},
+ showSelectedIcon: false,
+ onSelectionChanged: (sel) => notifier.updateSettings(
+ s.copyWith(themeMode: sel.first),
+ ),
+ ),
+ sectionLabel(l10n.setup_appearance_themePreset),
+ Wrap(
+ spacing: 8,
+ runSpacing: 8,
+ children: [
+ for (final preset in AppThemeRegistry.presets)
+ ChoiceChip(
+ label: Text(preset.id),
+ selected: s.themePresetId == preset.id,
+ onSelected: (_) => notifier.updateSettings(
+ s.copyWith(themePresetId: preset.id),
+ ),
+ ),
+ ],
+ ),
+ sectionLabel(l10n.setup_appearance_mapStyle),
+ SegmentedButton(
+ segments: [
+ ButtonSegment(
+ value: MapStyle.openStreetMap,
+ label: Text(l10n.setup_appearance_mapStyle_openStreetMap),
+ ),
+ ButtonSegment(
+ value: MapStyle.openTopoMap,
+ label: Text(l10n.setup_appearance_mapStyle_openTopoMap),
+ ),
+ ButtonSegment(
+ value: MapStyle.esriSatellite,
+ label: Text(l10n.setup_appearance_mapStyle_esriSatellite),
+ ),
+ ],
+ selected: {s.mapStyle},
+ showSelectedIcon: false,
+ onSelectionChanged: (sel) => notifier.updateSettings(
+ s.copyWith(mapStyle: sel.first),
+ ),
+ ),
+ sectionLabel(l10n.setup_appearance_language),
+ DropdownButtonFormField(
+ initialValue: s.locale,
+ items: [
+ for (final (code, nativeName) in _localeOptions)
+ DropdownMenuItem(
+ value: code,
+ child: Text(
+ code == 'system'
+ ? l10n.setup_appearance_themeSystem
+ : nativeName,
+ ),
+ ),
+ ],
+ onChanged: (code) {
+ if (code == null) return;
+ notifier.updateSettings(s.copyWith(locale: code));
+ ref.read(previewLocaleProvider.notifier).state =
+ code == 'system' ? null : code;
+ },
+ ),
+ ],
+ ),
+ );
+ }
+}
+```
+
+Note: theme preset chips label with the raw preset `id` — the registry's `nameKey` values (`theme_submersion` etc.) are existing l10n keys; if `context.l10n` exposes them via a lookup helper elsewhere in the codebase, prefer that. Check `rg -n "nameKey" lib --glob '*.dart'` for the existing resolution pattern (the appearance settings page uses one) and mirror it; fall back to capitalized ids only if no pattern exists.
+Note: if the analyzer version in this repo rejects the `(String, String)` record-list syntax or `DropdownButtonFormField.initialValue` (older Flutter), use a small private class with `code`/`nativeName` fields and the `value:` parameter respectively.
+
+- [ ] **Step 4: Wire the preview locale into app.dart**
+
+In `lib/app.dart`:
+1. Add import: `import 'package:submersion/features/setup_wizard/presentation/providers/setup_wizard_providers.dart';`
+2. In `build` (near line 269 where `localeProvider` is read): add `final previewLocale = ref.watch(previewLocaleProvider);`
+3. Change the MaterialApp.router line `locale: _resolveLocale(localeSetting),` to `locale: _resolveLocale(previewLocale ?? localeSetting),`
+
+- [ ] **Step 5: Replace the placeholder in the shell**
+
+In `setup_wizard_page.dart`, import `appearance_step.dart` and change the `SetupStepId.appearance` case builder to `AppearanceStep(mode: mode)`.
+
+- [ ] **Step 6: Run tests to verify they pass**
+
+Run: `flutter test test/features/setup_wizard/ && flutter analyze lib/app.dart lib/features/setup_wizard`
+Expected: tests PASS; analyze clean.
+
+- [ ] **Step 7: Format and commit**
+
+```bash
+dart format .
+git add lib/features/setup_wizard lib/app.dart test/features/setup_wizard
+git commit -m "feat(setup): appearance step with live language preview"
+```
+
+---
+
+### Task 9: Backup & Sync step
+
+**Files:**
+- Create: `lib/features/setup_wizard/presentation/widgets/steps/backup_sync_step.dart`
+- Modify: `lib/features/setup_wizard/presentation/pages/setup_wizard_page.dart` (replace backupSync placeholder)
+- Test: `test/features/setup_wizard/presentation/widgets/steps/backup_sync_step_test.dart`
+
+**Interfaces:**
+- Consumes: `backupSettingsProvider` state only for re-entry status display; draft fields for the toggles; sync gates `isApplePlatformProvider`, `iCloudAvailabilityProvider`, `dropboxConfiguredProvider`, `selectedCloudProviderTypeProvider`, `dropboxStorageProviderInstanceProvider`, `syncInitializerProvider`, `syncStateProvider`, `cloudProviderInstanceFor` (all from `lib/features/settings/presentation/providers/sync_providers.dart`); `DropboxConnectDialog`; `CloudProviderType`; `ICloudAvailability` from `lib/core/services/cloud_storage/icloud_native_service.dart`.
+- Produces: `BackupSyncStep({required SetupWizardMode mode, this.onLibraryFound})` where `onLibraryFound: void Function()?` is invoked when a freshly connected provider already holds a library (fresh-path pivot; wired in Task 11).
+
+- [ ] **Step 1: Write the failing test**
+
+`test/features/setup_wizard/presentation/widgets/steps/backup_sync_step_test.dart`:
+
+```dart
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:submersion/features/backup/domain/entities/backup_settings.dart';
+import 'package:submersion/features/settings/presentation/providers/sync_providers.dart';
+import 'package:submersion/features/setup_wizard/domain/setup_wizard_models.dart';
+import 'package:submersion/features/setup_wizard/presentation/providers/setup_wizard_providers.dart';
+import 'package:submersion/features/setup_wizard/presentation/widgets/steps/backup_sync_step.dart';
+
+import '../../../../../helpers/mock_providers.dart';
+import '../../../../../helpers/test_app.dart';
+
+void main() {
+ testWidgets('backup toggle reveals frequency and updates draft',
+ (tester) async {
+ late ProviderContainer container;
+ final overrides = await getBaseOverrides();
+ await tester.pumpWidget(testApp(
+ overrides: [
+ ...overrides,
+ // Deterministic gates: not Apple, no Dropbox key.
+ isApplePlatformProvider.overrideWithValue(false),
+ dropboxConfiguredProvider.overrideWithValue(false),
+ ],
+ child: Builder(builder: (context) {
+ container = ProviderScope.containerOf(context);
+ return const BackupSyncStep(mode: SetupWizardMode.firstRun);
+ }),
+ ));
+ await tester.pumpAndSettle();
+
+ expect(find.text('Frequency'), findsNothing);
+ await tester.tap(find.text('Automatic backups'));
+ await tester.pumpAndSettle();
+
+ expect(find.text('Frequency'), findsOneWidget);
+ await tester.tap(find.text('Daily'));
+ await tester.pumpAndSettle();
+
+ final draft =
+ container.read(setupWizardProvider(SetupWizardMode.firstRun));
+ expect(draft.backupEnabled, isTrue);
+ expect(draft.backupFrequency, BackupFrequency.daily);
+ });
+
+ testWidgets('non-Apple platform without Dropbox shows S3 card only',
+ (tester) async {
+ final overrides = await getBaseOverrides();
+ await tester.pumpWidget(testApp(
+ overrides: [
+ ...overrides,
+ isApplePlatformProvider.overrideWithValue(false),
+ dropboxConfiguredProvider.overrideWithValue(false),
+ ],
+ child: const BackupSyncStep(mode: SetupWizardMode.firstRun),
+ ));
+ await tester.pumpAndSettle();
+
+ expect(find.text('iCloud'), findsNothing);
+ expect(find.text('Dropbox'), findsNothing);
+ expect(find.text('S3'), findsOneWidget);
+ expect(find.text('Not connected'), findsOneWidget);
+ });
+}
+```
+
+Override note: `isApplePlatformProvider` and `dropboxConfiguredProvider` are plain `Provider`s, so `overrideWithValue` is correct. If either is declared differently in `sync_providers.dart`, switch to `overrideWith((ref) => false)`.
+
+- [ ] **Step 2: Run test to verify it fails**
+
+Run: `flutter test test/features/setup_wizard/presentation/widgets/steps/backup_sync_step_test.dart`
+Expected: FAIL — `backup_sync_step.dart` does not exist.
+
+- [ ] **Step 3: Implement the step**
+
+`lib/features/setup_wizard/presentation/widgets/steps/backup_sync_step.dart`:
+
+```dart
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:go_router/go_router.dart';
+
+import 'package:submersion/core/data/repositories/sync_repository.dart';
+import 'package:submersion/core/services/cloud_storage/icloud_native_service.dart';
+import 'package:submersion/features/backup/domain/entities/backup_settings.dart';
+import 'package:submersion/features/settings/presentation/providers/sync_providers.dart';
+import 'package:submersion/features/settings/presentation/widgets/dropbox_connect_dialog.dart';
+import 'package:submersion/features/setup_wizard/domain/setup_wizard_models.dart';
+import 'package:submersion/features/setup_wizard/presentation/providers/setup_wizard_providers.dart';
+import 'package:submersion/l10n/l10n_extension.dart';
+
+/// Backup schedule plus optional cloud sync provider connection.
+class BackupSyncStep extends ConsumerStatefulWidget {
+ final SetupWizardMode mode;
+
+ /// Fresh-path pivot: called when a just-connected provider already holds
+ /// a Submersion library (Task 11 wires the adopt offer).
+ final void Function()? onLibraryFound;
+
+ const BackupSyncStep({super.key, required this.mode, this.onLibraryFound});
+
+ @override
+ ConsumerState createState() => _BackupSyncStepState();
+}
+
+class _BackupSyncStepState extends ConsumerState {
+ bool _connecting = false;
+
+ Future _connect(CloudProviderType type) async {
+ final notifier = ref.read(setupWizardProvider(widget.mode).notifier);
+ setState(() => _connecting = true);
+ try {
+ if (type == CloudProviderType.dropbox) {
+ final connected = await showDialog(
+ context: context,
+ builder: (_) => DropboxConnectDialog(
+ provider: ref.read(dropboxStorageProviderInstanceProvider),
+ ),
+ );
+ ref.invalidate(dropboxAuthDataProvider);
+ if (connected != true) return;
+ }
+ // Activation contract mirrored from CloudSyncPage._selectProvider.
+ ref.read(selectedCloudProviderTypeProvider.notifier).state = type;
+ final instance = cloudProviderInstanceFor(type);
+ await instance.authenticate();
+ await ref.read(syncInitializerProvider).saveProvider(type);
+ ref.read(syncStateProvider.notifier).refreshState();
+ notifier.setConnectedProvider(type);
+
+ // Fresh-path pivot check: does this account already hold a library?
+ if (widget.onLibraryFound != null) {
+ final peers = await ref
+ .read(syncInitializerProvider)
+ .peerSyncFiles(instance);
+ if (peers.isNotEmpty && mounted) widget.onLibraryFound!();
+ }
+ } catch (e) {
+ ref.read(selectedCloudProviderTypeProvider.notifier).state = null;
+ notifier.setConnectedProvider(null);
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text(context.l10n.setup_sync_error(e))),
+ );
+ }
+ } finally {
+ if (mounted) setState(() => _connecting = false);
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+ final l10n = context.l10n;
+ final draft = ref.watch(setupWizardProvider(widget.mode));
+ final notifier = ref.read(setupWizardProvider(widget.mode).notifier);
+
+ final isApple = ref.watch(isApplePlatformProvider);
+ final iCloudAvailability =
+ ref.watch(iCloudAvailabilityProvider).valueOrNull;
+ final iCloudAvailable =
+ isApple && iCloudAvailability != ICloudAvailability.unsupported;
+ final dropboxConfigured = ref.watch(dropboxConfiguredProvider);
+ final connected = draft.connectedProvider;
+
+ Widget sectionLabel(String text) => Padding(
+ padding: const EdgeInsets.only(top: 20, bottom: 4),
+ child: Text(text, style: theme.textTheme.titleSmall),
+ );
+
+ return SingleChildScrollView(
+ padding: const EdgeInsets.all(24),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Text(
+ l10n.setup_backup_title,
+ style: theme.textTheme.titleLarge?.copyWith(
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ const SizedBox(height: 8),
+ Text(
+ l10n.setup_backup_subtitle,
+ style: theme.textTheme.bodyMedium?.copyWith(
+ color: theme.colorScheme.onSurfaceVariant,
+ ),
+ ),
+ const SizedBox(height: 16),
+ SwitchListTile(
+ contentPadding: EdgeInsets.zero,
+ title: Text(l10n.setup_backup_scheduleToggle),
+ subtitle: Text(l10n.setup_backup_scheduleSubtitle),
+ value: draft.backupEnabled,
+ onChanged: notifier.setBackupEnabled,
+ ),
+ if (draft.backupEnabled) ...[
+ sectionLabel(l10n.setup_backup_frequency),
+ SegmentedButton(
+ segments: [
+ ButtonSegment(
+ value: BackupFrequency.daily,
+ label: Text(l10n.setup_backup_frequency_daily),
+ ),
+ ButtonSegment(
+ value: BackupFrequency.weekly,
+ label: Text(l10n.setup_backup_frequency_weekly),
+ ),
+ ButtonSegment(
+ value: BackupFrequency.monthly,
+ label: Text(l10n.setup_backup_frequency_monthly),
+ ),
+ ],
+ selected: {draft.backupFrequency},
+ showSelectedIcon: false,
+ onSelectionChanged: (sel) =>
+ notifier.setBackupFrequency(sel.first),
+ ),
+ ],
+ sectionLabel(l10n.setup_sync_header),
+ Text(
+ l10n.setup_sync_subtitle,
+ style: theme.textTheme.bodySmall?.copyWith(
+ color: theme.colorScheme.onSurfaceVariant,
+ ),
+ ),
+ const SizedBox(height: 8),
+ if (connected != null)
+ ListTile(
+ contentPadding: EdgeInsets.zero,
+ leading: const Icon(Icons.cloud_done),
+ title: Text(
+ l10n.setup_sync_connectedTo(_providerName(connected)),
+ ),
+ )
+ else ...[
+ Text(
+ l10n.setup_sync_notConnected,
+ style: theme.textTheme.bodySmall,
+ ),
+ const SizedBox(height: 8),
+ if (iCloudAvailable)
+ _providerCard(
+ icon: Icons.cloud,
+ name: 'iCloud',
+ onTap: _connecting
+ ? null
+ : () => _connect(CloudProviderType.icloud),
+ ),
+ if (dropboxConfigured)
+ _providerCard(
+ icon: Icons.cloud_queue,
+ name: 'Dropbox',
+ onTap: _connecting
+ ? null
+ : () => _connect(CloudProviderType.dropbox),
+ ),
+ _providerCard(
+ icon: Icons.storage,
+ name: 'S3',
+ // S3ConfigPage performs its own activation on save.
+ onTap: _connecting
+ ? null
+ : () => context.push('/settings/cloud-sync/s3-config'),
+ ),
+ ],
+ if (connected != null)
+ SwitchListTile(
+ contentPadding: EdgeInsets.zero,
+ title: Text(l10n.setup_backup_cloudCopy),
+ value: draft.cloudBackupEnabled,
+ onChanged: notifier.setCloudBackupEnabled,
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _providerCard({
+ required IconData icon,
+ required String name,
+ required VoidCallback? onTap,
+ }) {
+ return Card(
+ margin: const EdgeInsets.symmetric(vertical: 4),
+ child: ListTile(
+ leading: Icon(icon),
+ title: Text(name),
+ trailing: const Icon(Icons.chevron_right),
+ onTap: onTap,
+ ),
+ );
+ }
+
+ String _providerName(CloudProviderType type) {
+ switch (type) {
+ case CloudProviderType.icloud:
+ return 'iCloud';
+ case CloudProviderType.dropbox:
+ return 'Dropbox';
+ case CloudProviderType.s3:
+ return 'S3';
+ case CloudProviderType.googledrive:
+ return 'Google Drive';
+ }
+ }
+}
+```
+
+Re-entry nuance: in settings mode with a provider already active (`ref.watch(selectedCloudProviderTypeProvider) != null` and no draft connection), render the connected tile using that provider plus a `TextButton` labelled `l10n.setup_sync_manageInSettings` navigating `context.push('/settings/cloud-sync')` instead of the connect cards. Implement as: `final effectiveConnected = connected ?? (widget.mode == SetupWizardMode.settings ? ref.watch(selectedCloudProviderTypeProvider) : null);` and branch on `effectiveConnected`, showing the manage button only in settings mode.
+
+S3 return detection: after `context.push('/settings/cloud-sync/s3-config')` resolves, S3ConfigPage has already activated S3 on save; add `.then((_) { final sel = ref.read(selectedCloudProviderTypeProvider); if (sel == CloudProviderType.s3 && mounted) { ref.read(setupWizardProvider(widget.mode).notifier).setConnectedProvider(sel); } })` to the push call so the wizard reflects it.
+
+- [ ] **Step 4: Replace the placeholder in the shell**
+
+In `setup_wizard_page.dart`, import `backup_sync_step.dart` and change the `SetupStepId.backupSync` builder to `BackupSyncStep(mode: mode)` (the `onLibraryFound` pivot is wired in Task 11).
+
+- [ ] **Step 5: Run tests to verify they pass**
+
+Run: `flutter test test/features/setup_wizard/`
+Expected: all PASS.
+
+- [ ] **Step 6: Format and commit**
+
+```bash
+dart format .
+git add lib/features/setup_wizard test/features/setup_wizard
+git commit -m "feat(setup): backup schedule and cloud sync connection step"
+```
+
+---
+
+### Task 10: Finish step — feature highlights + apply
+
+**Files:**
+- Create: `lib/features/setup_wizard/presentation/widgets/steps/finish_step.dart`
+- Modify: `lib/features/setup_wizard/presentation/pages/setup_wizard_page.dart` (replace finish placeholder)
+- Test: `test/features/setup_wizard/presentation/widgets/steps/finish_step_test.dart`
+
+**Interfaces:**
+- Consumes: `setupApplyServiceProvider` (Task 5), `previewLocaleProvider`, draft.
+- Produces: `FinishStep({required SetupWizardMode mode})`. Navigation contract: first-run completion goes `context.go('/dashboard')` (or a tapped feature route); settings mode pops.
+
+- [ ] **Step 1: Write the failing test**
+
+`test/features/setup_wizard/presentation/widgets/steps/finish_step_test.dart`:
+
+```dart
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:go_router/go_router.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+import 'package:submersion/core/providers/provider.dart';
+import 'package:submersion/features/divers/data/repositories/diver_repository.dart';
+import 'package:submersion/features/setup_wizard/domain/setup_wizard_models.dart';
+import 'package:submersion/features/setup_wizard/presentation/providers/setup_wizard_providers.dart';
+import 'package:submersion/features/setup_wizard/presentation/widgets/steps/finish_step.dart';
+
+import '../../../../../helpers/test_app.dart';
+import '../../../../../helpers/test_database.dart';
+
+void main() {
+ late SharedPreferences prefs;
+
+ setUp(() async {
+ SharedPreferences.setMockInitialValues({});
+ prefs = await SharedPreferences.getInstance();
+ await setUpTestDatabase();
+ });
+
+ tearDown(() async {
+ await tearDownTestDatabase();
+ });
+
+ testWidgets('start logging applies draft and navigates to dashboard',
+ (tester) async {
+ var dashboardShown = false;
+ final router = GoRouter(
+ initialLocation: '/wizard',
+ routes: [
+ GoRoute(
+ path: '/wizard',
+ builder: (context, state) => Consumer(
+ builder: (context, ref, _) {
+ // Seed a completed fresh-path draft before rendering.
+ final notifier = ref.read(
+ setupWizardProvider(SetupWizardMode.firstRun).notifier,
+ );
+ notifier.choosePath(SetupPath.fresh);
+ notifier.setName('Eric');
+ return const Scaffold(
+ body: FinishStep(mode: SetupWizardMode.firstRun),
+ );
+ },
+ ),
+ ),
+ GoRoute(
+ path: '/dashboard',
+ builder: (context, state) {
+ dashboardShown = true;
+ return const Scaffold(body: Text('dashboard'));
+ },
+ ),
+ ],
+ );
+
+ await tester.pumpWidget(testAppRouter(
+ router: router,
+ overrides: [sharedPreferencesProvider.overrideWithValue(prefs)],
+ ));
+ await tester.pumpAndSettle();
+
+ expect(find.text("You're all set"), findsOneWidget);
+ expect(find.text('Download dives from your dive computer'),
+ findsOneWidget);
+
+ await tester.tap(find.text('Start logging'));
+ await tester.runAsync(() async {
+ // Apply performs real DB writes; let them settle outside fake time.
+ await Future.delayed(const Duration(milliseconds: 300));
+ });
+ await tester.pumpAndSettle();
+
+ expect(dashboardShown, isTrue);
+ final divers = await tester.runAsync(() => DiverRepository().getAllDivers());
+ expect(divers, hasLength(1));
+ expect(divers!.single.name, 'Eric');
+ });
+}
+```
+
+- [ ] **Step 2: Run test to verify it fails**
+
+Run: `flutter test test/features/setup_wizard/presentation/widgets/steps/finish_step_test.dart`
+Expected: FAIL — `finish_step.dart` does not exist.
+
+- [ ] **Step 3: Implement the step**
+
+`lib/features/setup_wizard/presentation/widgets/steps/finish_step.dart`:
+
+```dart
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:go_router/go_router.dart';
+
+import 'package:submersion/features/setup_wizard/data/setup_apply_service.dart';
+import 'package:submersion/features/setup_wizard/domain/setup_wizard_models.dart';
+import 'package:submersion/features/setup_wizard/presentation/providers/setup_wizard_providers.dart';
+import 'package:submersion/l10n/l10n_extension.dart';
+
+/// Final step: feature discovery plus the apply-and-go action.
+class FinishStep extends ConsumerStatefulWidget {
+ final SetupWizardMode mode;
+
+ const FinishStep({super.key, required this.mode});
+
+ @override
+ ConsumerState createState() => _FinishStepState();
+}
+
+class _FinishStepState extends ConsumerState {
+ bool _applying = false;
+
+ Future _complete({String route = '/dashboard'}) async {
+ if (_applying) return;
+ setState(() => _applying = true);
+ try {
+ final draft = ref.read(setupWizardProvider(widget.mode));
+ final service = ref.read(setupApplyServiceProvider);
+ if (widget.mode == SetupWizardMode.firstRun) {
+ await service.applyFirstRun(draft);
+ } else {
+ await service.applySettingsMode(draft);
+ }
+ // Locale now persists via settings; drop the preview override.
+ ref.read(previewLocaleProvider.notifier).state = null;
+ if (!mounted) return;
+ if (widget.mode == SetupWizardMode.firstRun) {
+ context.go(route);
+ } else if (route != '/dashboard') {
+ context.go(route);
+ } else {
+ context.pop();
+ }
+ } catch (e) {
+ if (mounted) {
+ setState(() => _applying = false);
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text(context.l10n.setup_finish_error(e))),
+ );
+ }
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+ final l10n = context.l10n;
+
+ final features = <(IconData, String, String)>[
+ (
+ Icons.watch,
+ l10n.setup_finish_feature_diveComputer,
+ '/dive-computers/discover',
+ ),
+ (Icons.file_upload, l10n.setup_finish_feature_import, '/transfer'),
+ (Icons.query_stats, l10n.setup_finish_feature_statistics, '/stats'),
+ (Icons.map, l10n.setup_finish_feature_sites, '/sites'),
+ (Icons.build, l10n.setup_finish_feature_gear, '/gear'),
+ ];
+
+ return SingleChildScrollView(
+ padding: const EdgeInsets.all(24),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Icon(Icons.check_circle,
+ size: 56, color: theme.colorScheme.primary),
+ const SizedBox(height: 16),
+ Text(
+ l10n.setup_finish_title,
+ style: theme.textTheme.headlineSmall?.copyWith(
+ fontWeight: FontWeight.bold,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: 8),
+ Text(
+ l10n.setup_finish_subtitle,
+ style: theme.textTheme.bodyMedium?.copyWith(
+ color: theme.colorScheme.onSurfaceVariant,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: 16),
+ for (final (icon, label, route) in features)
+ ListTile(
+ contentPadding: EdgeInsets.zero,
+ leading: Icon(icon, color: theme.colorScheme.primary),
+ title: Text(label),
+ trailing: const Icon(Icons.chevron_right, size: 18),
+ onTap: _applying ? null : () => _complete(route: route),
+ ),
+ const SizedBox(height: 24),
+ FilledButton.icon(
+ onPressed: _applying ? null : _complete,
+ icon: _applying
+ ? const SizedBox(
+ width: 18,
+ height: 18,
+ child: CircularProgressIndicator(strokeWidth: 2),
+ )
+ : const Icon(Icons.arrow_forward),
+ label: Text(
+ _applying ? l10n.setup_finish_applying : l10n.setup_finish_start,
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
+```
+
+- [ ] **Step 4: Replace the placeholder in the shell**
+
+In `setup_wizard_page.dart`, import `finish_step.dart` and change the `SetupStepId.finish` builder to `FinishStep(mode: mode)`.
+
+- [ ] **Step 5: Run tests to verify they pass**
+
+Run: `flutter test test/features/setup_wizard/`
+Expected: all PASS.
+
+- [ ] **Step 6: Format and commit**
+
+```bash
+dart format .
+git add lib/features/setup_wizard test/features/setup_wizard
+git commit -m "feat(setup): finish step applies draft and offers feature links"
+```
+
+---
+
+### Task 11: Existing-data path — choice, restore, sync pull, open folder
+
+**Files:**
+- Create: `lib/features/setup_wizard/presentation/widgets/steps/existing_choice_step.dart`
+- Create: `lib/features/setup_wizard/presentation/widgets/steps/restore_step.dart`
+- Create: `lib/features/setup_wizard/presentation/widgets/steps/sync_connect_step.dart`
+- Create: `lib/features/setup_wizard/presentation/widgets/steps/open_folder_step.dart`
+- Modify: `lib/features/setup_wizard/presentation/pages/setup_wizard_page.dart` (replace remaining placeholders, delete `placeholder_step.dart`, wire fresh-path `onLibraryFound` pivot)
+- Test: `test/features/setup_wizard/presentation/widgets/steps/existing_data_steps_test.dart`
+
+**Interfaces:**
+- Consumes: `backupOperationProvider` + `RestoreConfirmationDialog` + `RestoreCompletePage` (backup feature); `storageConfigNotifierProvider` (storage_providers.dart); `syncStateProvider`, sync gates (Task 9's set); `realignActiveDiverAfterDataReplace`, `sharedPreferencesProvider`, `hasAnyDiversProvider`; `restartApp` from `package:submersion/main.dart`; `FilePicker` from `package:file_picker/file_picker.dart`; `AppDatabase.currentSchemaVersion`.
+- Produces: the four step widgets; shell fully placeholder-free.
+
+- [ ] **Step 1: Write the failing test**
+
+`test/features/setup_wizard/presentation/widgets/steps/existing_data_steps_test.dart`:
+
+```dart
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:submersion/features/setup_wizard/domain/setup_wizard_models.dart';
+import 'package:submersion/features/setup_wizard/presentation/providers/setup_wizard_providers.dart';
+import 'package:submersion/features/setup_wizard/presentation/widgets/steps/existing_choice_step.dart';
+
+import '../../../../../helpers/mock_providers.dart';
+import '../../../../../helpers/test_app.dart';
+
+void main() {
+ testWidgets('existing choice cards set the draft source', (tester) async {
+ late ProviderContainer container;
+ final overrides = await getBaseOverrides();
+ var advanced = 0;
+ await tester.pumpWidget(testApp(
+ overrides: overrides,
+ child: Builder(builder: (context) {
+ container = ProviderScope.containerOf(context);
+ return ExistingChoiceStep(
+ mode: SetupWizardMode.firstRun,
+ onChosen: () => advanced++,
+ );
+ }),
+ ));
+ await tester.pumpAndSettle();
+
+ expect(find.text('Bring your data'), findsOneWidget);
+ await tester.tap(find.text('Restore a backup file'));
+ await tester.pumpAndSettle();
+
+ final draft =
+ container.read(setupWizardProvider(SetupWizardMode.firstRun));
+ expect(draft.source, ExistingDataSource.restoreBackup);
+ expect(advanced, 1);
+ });
+}
+```
+
+(Restore/sync/folder steps orchestrate platform pickers, dialogs, and app restarts — they get integration coverage via the full-wizard smoke run in Task 14; their pure logic lives in already-tested engines.)
+
+- [ ] **Step 2: Run test to verify it fails**
+
+Run: `flutter test test/features/setup_wizard/presentation/widgets/steps/existing_data_steps_test.dart`
+Expected: FAIL — `existing_choice_step.dart` does not exist.
+
+- [ ] **Step 3: Implement the four steps**
+
+`lib/features/setup_wizard/presentation/widgets/steps/existing_choice_step.dart`:
+
+```dart
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+
+import 'package:submersion/features/setup_wizard/domain/setup_wizard_models.dart';
+import 'package:submersion/features/setup_wizard/presentation/providers/setup_wizard_providers.dart';
+import 'package:submersion/l10n/l10n_extension.dart';
+
+/// Chooses which existing-data source to load from.
+class ExistingChoiceStep extends ConsumerWidget {
+ final SetupWizardMode mode;
+ final VoidCallback onChosen;
+
+ const ExistingChoiceStep({
+ super.key,
+ required this.mode,
+ required this.onChosen,
+ });
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ final theme = Theme.of(context);
+ final l10n = context.l10n;
+ final notifier = ref.read(setupWizardProvider(mode).notifier);
+
+ void choose(ExistingDataSource source) {
+ notifier.chooseSource(source);
+ onChosen();
+ }
+
+ Widget card(IconData icon, String title, String subtitle,
+ ExistingDataSource source) {
+ return Card(
+ child: ListTile(
+ leading: Icon(icon, color: theme.colorScheme.primary),
+ title: Text(title),
+ subtitle: Text(subtitle),
+ trailing: const Icon(Icons.chevron_right),
+ onTap: () => choose(source),
+ ),
+ );
+ }
+
+ return SingleChildScrollView(
+ padding: const EdgeInsets.all(24),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Text(
+ l10n.setup_existing_title,
+ style: theme.textTheme.titleLarge?.copyWith(
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ const SizedBox(height: 8),
+ Text(
+ l10n.setup_existing_subtitle,
+ style: theme.textTheme.bodyMedium?.copyWith(
+ color: theme.colorScheme.onSurfaceVariant,
+ ),
+ ),
+ const SizedBox(height: 16),
+ card(
+ Icons.restore,
+ l10n.setup_existing_restore_title,
+ l10n.setup_existing_restore_subtitle,
+ ExistingDataSource.restoreBackup,
+ ),
+ card(
+ Icons.cloud_sync,
+ l10n.setup_existing_sync_title,
+ l10n.setup_existing_sync_subtitle,
+ ExistingDataSource.cloudSync,
+ ),
+ card(
+ Icons.folder_open,
+ l10n.setup_existing_folder_title,
+ l10n.setup_existing_folder_subtitle,
+ ExistingDataSource.openFolder,
+ ),
+ ],
+ ),
+ );
+ }
+}
+```
+
+`lib/features/setup_wizard/presentation/widgets/steps/restore_step.dart`:
+
+```dart
+import 'dart:io';
+
+import 'package:file_picker/file_picker.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+
+import 'package:submersion/core/database/database.dart';
+import 'package:submersion/features/backup/domain/entities/backup_record.dart';
+import 'package:submersion/features/backup/presentation/pages/restore_complete_page.dart';
+import 'package:submersion/features/backup/presentation/providers/backup_providers.dart';
+import 'package:submersion/features/backup/presentation/widgets/restore_confirmation_dialog.dart';
+import 'package:submersion/l10n/l10n_extension.dart';
+
+/// Restores a backup file; exits the wizard via the existing
+/// RestoreCompletePage -> restartApp() flow.
+class RestoreStep extends ConsumerWidget {
+ const RestoreStep({super.key});
+
+ Future _pickAndRestore(BuildContext context, WidgetRef ref) async {
+ final FilePickerResult? result;
+ try {
+ result = await FilePicker.pickFiles(type: FileType.any);
+ } on Exception catch (e) {
+ if (context.mounted) {
+ ScaffoldMessenger.of(context)
+ .showSnackBar(SnackBar(content: Text('$e')));
+ }
+ return;
+ }
+ if (result == null || result.files.isEmpty) return;
+ final filePath = result.files.single.path;
+ if (filePath == null || !context.mounted) return;
+
+ final file = File(filePath);
+ final record = BackupRecord(
+ id: 'setup-wizard',
+ filename: result.files.single.name,
+ timestamp: await file.lastModified(),
+ sizeBytes: await file.length(),
+ location: BackupLocation.local,
+ diveCount: 0,
+ siteCount: 0,
+ );
+ if (!context.mounted) return;
+
+ final mode = await RestoreConfirmationDialog.show(
+ context,
+ record,
+ currentSchemaVersion: AppDatabase.currentSchemaVersion,
+ );
+ if (mode != null) {
+ await ref
+ .read(backupOperationProvider.notifier)
+ .restoreFromFilePath(filePath, mode: mode);
+ }
+ }
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ final theme = Theme.of(context);
+ final l10n = context.l10n;
+ final operation = ref.watch(backupOperationProvider);
+
+ ref.listen(backupOperationProvider, (prev, next) {
+ if (next.status == BackupOperationStatus.restoreComplete &&
+ context.mounted) {
+ RestoreCompletePage.show(context);
+ }
+ });
+
+ final inProgress = operation.status == BackupOperationStatus.inProgress;
+
+ return Center(
+ child: Padding(
+ padding: const EdgeInsets.all(24),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Text(
+ l10n.setup_restore_title,
+ style: theme.textTheme.titleLarge?.copyWith(
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ const SizedBox(height: 24),
+ if (inProgress) ...[
+ const CircularProgressIndicator(),
+ const SizedBox(height: 12),
+ Text(operation.message ?? l10n.setup_restore_inProgress),
+ ] else ...[
+ if (operation.status == BackupOperationStatus.error &&
+ operation.message != null) ...[
+ Text(
+ operation.message!,
+ style: TextStyle(color: theme.colorScheme.error),
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: 12),
+ ],
+ FilledButton.icon(
+ onPressed: () => _pickAndRestore(context, ref),
+ icon: const Icon(Icons.folder_open),
+ label: Text(l10n.setup_restore_pick),
+ ),
+ ],
+ ],
+ ),
+ ),
+ );
+ }
+}
+```
+
+Check the actual `BackupRecord` import path/constructor before compiling (`rg -n "class BackupRecord" lib/features/backup`) — mirror `backup_settings_page.dart:208-254`, which is the canonical caller (including the `BackupLocation.local` enum import if it lives in a separate file, and `offerReplace:` which the wizard omits because a fresh install has no cloud provider yet).
+
+`lib/features/setup_wizard/presentation/widgets/steps/sync_connect_step.dart`:
+
+```dart
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:go_router/go_router.dart';
+
+import 'package:submersion/core/providers/provider.dart';
+import 'package:submersion/features/divers/presentation/providers/diver_providers.dart';
+import 'package:submersion/features/settings/presentation/providers/sync_providers.dart';
+import 'package:submersion/features/setup_wizard/domain/setup_wizard_models.dart';
+import 'package:submersion/features/setup_wizard/presentation/providers/setup_wizard_providers.dart';
+import 'package:submersion/features/setup_wizard/presentation/widgets/steps/backup_sync_step.dart';
+import 'package:submersion/l10n/l10n_extension.dart';
+
+/// Existing-data path: connect a provider, pull the library, land on the
+/// dashboard once data (and its divers) have arrived.
+class SyncConnectStep extends ConsumerStatefulWidget {
+ final SetupWizardMode mode;
+
+ /// Called when the connected account has no library (pivot to fresh).
+ final VoidCallback onNoLibrary;
+
+ const SyncConnectStep({
+ super.key,
+ required this.mode,
+ required this.onNoLibrary,
+ });
+
+ @override
+ ConsumerState createState() => _SyncConnectStepState();
+}
+
+enum _PullPhase { connect, pulling, done, empty }
+
+class _SyncConnectStepState extends ConsumerState {
+ _PullPhase _phase = _PullPhase.connect;
+
+ Future _startPull() async {
+ setState(() => _phase = _PullPhase.pulling);
+ final connected = ref
+ .read(setupWizardProvider(widget.mode))
+ .connectedProvider;
+ if (connected == null) {
+ setState(() => _phase = _PullPhase.connect);
+ return;
+ }
+ final instance = cloudProviderInstanceFor(connected);
+ final peers = await ref
+ .read(syncInitializerProvider)
+ .peerSyncFiles(instance);
+ if (peers.isEmpty) {
+ if (mounted) setState(() => _phase = _PullPhase.empty);
+ return;
+ }
+ await ref.read(syncStateProvider.notifier).performSync();
+ await realignActiveDiverAfterDataReplace(
+ ref.read(sharedPreferencesProvider),
+ );
+ final hasDivers = await ref.read(hasAnyDiversProvider.future);
+ if (mounted) {
+ setState(() => _phase = hasDivers ? _PullPhase.done : _PullPhase.empty);
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+ final l10n = context.l10n;
+ final syncState = ref.watch(syncStateProvider);
+
+ switch (_phase) {
+ case _PullPhase.connect:
+ // Reuse the provider cards; a successful connect starts the pull.
+ return Column(
+ children: [
+ Expanded(
+ child: BackupSyncStep(mode: widget.mode),
+ ),
+ SafeArea(
+ top: false,
+ child: Padding(
+ padding: const EdgeInsets.all(16),
+ child: _ConnectedGate(
+ mode: widget.mode,
+ onContinue: _startPull,
+ ),
+ ),
+ ),
+ ],
+ );
+ case _PullPhase.pulling:
+ return Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ CircularProgressIndicator(value: syncState.progress),
+ const SizedBox(height: 12),
+ Text(syncState.message ?? l10n.setup_syncPull_syncing),
+ ],
+ ),
+ );
+ case _PullPhase.done:
+ return Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(Icons.check_circle,
+ size: 56, color: theme.colorScheme.primary),
+ const SizedBox(height: 12),
+ Text(l10n.setup_syncPull_success,
+ style: theme.textTheme.titleLarge),
+ const SizedBox(height: 16),
+ FilledButton(
+ onPressed: () => context.go('/dashboard'),
+ child: Text(l10n.setup_syncPull_continue),
+ ),
+ ],
+ ),
+ );
+ case _PullPhase.empty:
+ return Center(
+ child: Padding(
+ padding: const EdgeInsets.all(24),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Text(l10n.setup_syncPull_noLibrary_title,
+ style: theme.textTheme.titleLarge),
+ const SizedBox(height: 8),
+ Text(
+ l10n.setup_syncPull_noLibrary_message,
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: 16),
+ FilledButton(
+ onPressed: widget.onNoLibrary,
+ child: Text(l10n.setup_sync_libraryFound_keepFresh),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+ }
+}
+
+/// Continue button enabled once a provider is connected in the draft.
+class _ConnectedGate extends ConsumerWidget {
+ final SetupWizardMode mode;
+ final VoidCallback onContinue;
+
+ const _ConnectedGate({required this.mode, required this.onContinue});
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ final connected = ref.watch(
+ setupWizardProvider(mode).select((d) => d.connectedProvider != null),
+ );
+ return FilledButton(
+ onPressed: connected ? onContinue : null,
+ child: Text(context.l10n.setup_syncPull_continue),
+ );
+ }
+}
+```
+
+Add the l10n key `setup_syncPull_continue` ("Continue") to Task 2's key set if not already present — it IS present in the list as `setup_syncPull_continue`? If missing, add it to all 11 arb files in this task and regenerate. (Check: Task 2 defines `setup_syncPull_*` keys; `setup_syncPull_continue` must exist — add "setup_syncPull_continue": "Continue" if the Task 2 block lacks it.)
+
+`lib/features/setup_wizard/presentation/widgets/steps/open_folder_step.dart`:
+
+```dart
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+
+import 'package:submersion/features/settings/presentation/providers/storage_providers.dart';
+import 'package:submersion/l10n/l10n_extension.dart';
+import 'package:submersion/main.dart' show restartApp;
+
+/// Points the app at a folder that already contains a Submersion database.
+class OpenFolderStep extends ConsumerStatefulWidget {
+ const OpenFolderStep({super.key});
+
+ @override
+ ConsumerState createState() => _OpenFolderStepState();
+}
+
+class _OpenFolderStepState extends ConsumerState {
+ bool _busy = false;
+ String? _error;
+
+ Future _pickFolder() async {
+ setState(() {
+ _busy = true;
+ _error = null;
+ });
+ final l10n = context.l10n;
+ try {
+ final notifier = ref.read(storageConfigNotifierProvider.notifier);
+ final picked = await notifier.pickCustomFolder();
+ if (picked == null) return;
+
+ final existing = await notifier.checkForExistingDatabase(picked.path);
+ if (existing == null) {
+ setState(() => _error = l10n.setup_folder_notFound_message);
+ return;
+ }
+
+ final result = await notifier.switchToExistingDatabase(picked.path);
+ if (result.success) {
+ // Full provider rebuild: the swapped database has divers, so the
+ // relaunch lands on the dashboard.
+ restartApp();
+ } else {
+ setState(() => _error = result.error ?? '');
+ }
+ } finally {
+ if (mounted) setState(() => _busy = false);
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+ final l10n = context.l10n;
+
+ return Center(
+ child: Padding(
+ padding: const EdgeInsets.all(24),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Text(
+ l10n.setup_folder_title,
+ style: theme.textTheme.titleLarge?.copyWith(
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ const SizedBox(height: 24),
+ if (_busy) ...[
+ const CircularProgressIndicator(),
+ const SizedBox(height: 12),
+ Text(l10n.setup_folder_switching),
+ ] else ...[
+ if (_error != null) ...[
+ Text(
+ _error!,
+ style: TextStyle(color: theme.colorScheme.error),
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: 12),
+ ],
+ FilledButton.icon(
+ onPressed: _pickFolder,
+ icon: const Icon(Icons.folder_open),
+ label: Text(l10n.setup_folder_pick),
+ ),
+ ],
+ ],
+ ),
+ ),
+ );
+ }
+}
+```
+
+Verify `MigrationResult`'s error field name (`rg -n "class MigrationResult" -A 8 lib/core/services/database_migration_service.dart`) — use its actual field (e.g. `result.error` vs `result.message`). Verify `StorageConfigNotifier.pickCustomFolder`'s required params — `storage_settings_page.dart` passes `chooser: _chooseStorageVolume`; if `chooser` is required, port that page's `_chooseStorageVolume` dialog into this step as a private method (Android external-volume selection).
+
+- [ ] **Step 4: Wire the shell**
+
+In `setup_wizard_page.dart`:
+1. Import the four new step files; delete `placeholder_step.dart` and its import.
+2. `SetupStepId.existingChoice` builder → `ExistingChoiceStep(mode: mode, onChosen: () => WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) _advance(); }))` (reuse `_chooseAndAdvance` by passing `() {}` as the mutation — the step already mutates; simplest is `onChosen: () => _chooseAndAdvance(() {})`).
+3. `SetupStepId.restore` builder → `const RestoreStep()`.
+4. `SetupStepId.syncConnect` builder → `SyncConnectStep(mode: mode, onNoLibrary: () { ref.read(setupWizardProvider(mode).notifier).choosePath(SetupPath.fresh); WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) _animateTo(1); }); })` — pivoting to the fresh path re-enters at the profile step (index 1 of the fresh list) with the provider still connected in the draft.
+5. `SetupStepId.openFolder` builder → `const OpenFolderStep()`.
+6. `SetupStepId.backupSync` builder gains the fresh-path pivot: `BackupSyncStep(mode: mode, onLibraryFound: () => _offerAdoptPivot())` where `_offerAdoptPivot` shows an AlertDialog (`setup_sync_libraryFound_title` / `_message`, actions `setup_sync_libraryFound_keepFresh` → pop, `setup_sync_libraryFound_adopt` → pop then `notifier.choosePath(SetupPath.existingData); notifier.chooseSource(ExistingDataSource.cloudSync);` and post-frame `_animateTo(computeSteps(...).length - 1)` to land on the sync-pull step).
+
+- [ ] **Step 5: Run tests to verify they pass**
+
+Run: `flutter test test/features/setup_wizard/`
+Expected: all PASS.
+
+- [ ] **Step 6: Format and commit**
+
+```bash
+dart format .
+git add lib/features/setup_wizard test/features/setup_wizard
+git commit -m "feat(setup): existing-data path with restore, sync pull, and folder adopt"
+```
+
+---
+
+### Task 12: Router swap, Settings entry, delete WelcomePage
+
+**Files:**
+- Modify: `lib/core/router/app_router.dart` (welcome builder swap at ~line 172; new settings child route; imports at lines 18/77-100)
+- Modify: `lib/features/settings/presentation/pages/settings_page.dart` (Setup assistant tile in the Manage section, after the tank-presets tile at ~line 1640)
+- Delete: `lib/features/onboarding/presentation/pages/welcome_page.dart` (and the now-empty `lib/features/onboarding/` tree)
+- Modify: all 11 `lib/l10n/arb/app_*.arb` (remove the 10 `onboarding_welcome_*` keys + the one `@onboarding_welcome_errorCreatingProfile` metadata block from each) + regenerate
+- Test: `test/features/setup_wizard/presentation/pages/first_run_redirect_test.dart`
+
+**Interfaces:**
+- Consumes: `SetupWizardPage` (Task 6), `hasAnyDiversProvider`.
+- Produces: `/welcome` renders the wizard; `/settings/setup-assistant` (name `setupAssistant`) renders re-entry mode; Settings Manage card gains the launcher tile.
+
+- [ ] **Step 1: Write the failing redirect test**
+
+`test/features/setup_wizard/presentation/pages/first_run_redirect_test.dart`:
+
+```dart
+import 'package:flutter_test/flutter_test.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:submersion/core/router/app_router.dart';
+import 'package:submersion/features/divers/presentation/providers/diver_providers.dart';
+import 'package:submersion/features/setup_wizard/presentation/pages/setup_wizard_page.dart';
+
+import '../../../../helpers/mock_providers.dart';
+import '../../../../helpers/test_app.dart';
+
+void main() {
+ testWidgets('zero divers redirects initial route to the setup wizard',
+ (tester) async {
+ final overrides = await getBaseOverrides();
+ final container = ProviderContainer(overrides: [
+ ...overrides,
+ hasAnyDiversProvider.overrideWith((ref) async => false),
+ ]);
+ addTearDown(container.dispose);
+ final router = container.read(appRouterProvider);
+
+ await tester.pumpWidget(UncontrolledProviderScope(
+ container: container,
+ child: testAppRouter(router: router),
+ ));
+ await tester.pumpAndSettle();
+
+ expect(find.byType(SetupWizardPage), findsOneWidget);
+ expect(find.text('Welcome to Submersion'), findsOneWidget);
+ });
+}
+```
+
+Adjust `testAppRouter` usage if it wraps its own ProviderScope (it does — see helper): in that case skip `UncontrolledProviderScope` and instead pass the overrides list directly to `testAppRouter(router: ..., overrides: [...])`, reading the router from a container built with the SAME overrides is not possible then — so mirror `test/core/router/app_router_test.dart`'s established pattern for constructing the router with overrides, which that file already solves; copy its approach exactly.
+
+- [ ] **Step 2: Run test to verify it fails**
+
+Run: `flutter test test/features/setup_wizard/presentation/pages/first_run_redirect_test.dart`
+Expected: FAIL — `/welcome` still builds the old `WelcomePage`, so `find.byType(SetupWizardPage)` finds nothing.
+
+- [ ] **Step 3: Swap the router**
+
+In `lib/core/router/app_router.dart`:
+
+1. Replace the import at line 18:
+```dart
+// OLD
+import 'package:submersion/features/onboarding/presentation/pages/welcome_page.dart';
+// NEW
+import 'package:submersion/features/setup_wizard/domain/setup_wizard_models.dart';
+import 'package:submersion/features/setup_wizard/presentation/pages/setup_wizard_page.dart';
+```
+2. Replace the `/welcome` builder:
+```dart
+ GoRoute(
+ path: '/welcome',
+ name: 'welcome',
+ builder: (context, state) =>
+ const SetupWizardPage(mode: SetupWizardMode.firstRun),
+ ),
+```
+3. Inside the `/settings` route's `routes: [...]` list (after the `backup` child at ~line 892), add:
+```dart
+ GoRoute(
+ path: 'setup-assistant',
+ name: 'setupAssistant',
+ builder: (context, state) =>
+ const SetupWizardPage(mode: SetupWizardMode.settings),
+ ),
+```
+
+- [ ] **Step 4: Add the Settings tile and delete the old page**
+
+In `settings_page.dart`, inside the Manage section's `Card > Column` (after the tank-presets `ListTile` + `Divider`), add:
+
+```dart
+ const Divider(height: 1),
+ ListTile(
+ leading: const Icon(Icons.auto_fix_high),
+ title: Text(context.l10n.settings_manage_setupAssistant),
+ subtitle: Text(
+ context.l10n.settings_manage_setupAssistant_subtitle,
+ ),
+ trailing: const Icon(Icons.chevron_right),
+ onTap: () => context.push('/settings/setup-assistant'),
+ ),
+```
+
+Then:
+```bash
+git rm lib/features/onboarding/presentation/pages/welcome_page.dart
+```
+Confirm nothing references it: `rg -n "WelcomePage|onboarding/presentation" lib test` → expected: no matches.
+
+- [ ] **Step 5: Remove the orphaned l10n keys**
+
+In each of the 11 `lib/l10n/arb/app_*.arb` files, delete the 10 `onboarding_welcome_*` entries and the `@onboarding_welcome_errorCreatingProfile` metadata block. Run `flutter gen-l10n`. Then `rg -n "onboarding_welcome" lib` → expected: no matches.
+
+- [ ] **Step 6: Run tests to verify they pass**
+
+Run: `flutter test test/features/setup_wizard/ test/core/router/`
+Expected: all PASS (including the pre-existing router config tests, which override `hasAnyDiversProvider` to true and never touched WelcomePage).
+
+- [ ] **Step 7: Format and commit**
+
+```bash
+dart format .
+git add lib/core/router lib/features/settings lib/l10n test/features/setup_wizard
+git rm -q lib/features/onboarding/presentation/pages/welcome_page.dart 2>/dev/null || true
+git commit -m "feat(setup): route first run and settings entry to the setup wizard"
+```
+
+---
+
+### Task 13: Documentation updates
+
+**Files:**
+- Modify: `docs/developer/navigation.md` (First-Run Guard section ~L395-407 + Onboarding route entry ~L329-334: `/welcome` now renders SetupWizardPage; add `/settings/setup-assistant`)
+- Modify: `docs/ARCHITECTURE.md` (~L240) and `docs/developer/architecture.md` (~L160): replace `onboarding/ — First-run experience` with `setup_wizard/ — First-run setup wizard and Settings re-entry`
+- Modify: `docs/guide/README.md` (~L33) and `docs/guide/first-dive.md` (~L7-10): describe the new first-run flow (fork; fresh path name/units/appearance/backup-sync/finish; existing-data restore/sync/folder options; everything skippable)
+- Modify: `FEATURE_ROADMAP.md`: add a completed entry under the appropriate release section: `- [x] Setup wizard for new databases (units, appearance, backup/sync, restore/adopt paths) — discussion #523`
+
+**Interfaces:** none (prose only).
+
+- [ ] **Step 1: Apply the edits above**
+
+Keep each doc's existing voice and formatting; update code samples in navigation.md that show the redirect (the redirect logic itself is unchanged — only the page it lands on). Do not document placeholder internals; describe user-visible behavior.
+
+- [ ] **Step 2: Verify no stale references**
+
+Run: `rg -n "WelcomePage|name-only|onboarding/" docs FEATURE_ROADMAP.md | rg -v "setup_wizard|setup wizard"`
+Expected: no hits describing the old flow as current (historical spec/plan docs under docs/superpowers/ are exempt — leave them).
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add docs FEATURE_ROADMAP.md
+git commit -m "docs: describe the setup wizard first-run flow"
+```
+
+---
+
+### Task 14: Full verification sweep
+
+**Files:** none new (fixes only, if the sweep finds issues).
+
+- [ ] **Step 1: Format check**
+
+Run: `dart format .`
+Expected: `0 changed` (any change means a prior task missed formatting — commit the fix).
+
+- [ ] **Step 2: Whole-project analyze**
+
+Run: `flutter analyze`
+Expected: `No issues found!` — run WITHOUT piping through head/tail (piping masks failures). Fix anything reported.
+
+- [ ] **Step 3: Targeted test sweep (never the whole suite)**
+
+Run, in order:
+```bash
+flutter test test/features/setup_wizard/
+flutter test test/features/import_wizard/
+flutter test test/core/router/
+flutter test test/features/settings/presentation/pages/settings_page_test.dart
+flutter test test/features/settings/presentation/providers/settings_notifier_real_test.dart
+flutter test test/features/divers/
+flutter test test/features/backup/
+```
+Expected: all PASS. The settings/backup/divers suites guard the engines the wizard now calls.
+
+- [ ] **Step 4: Desktop smoke run (manual, macOS)**
+
+Run: `flutter run -d macos`
+Walk: fresh sandbox DB (or use the wizard from Settings → Manage → Setup assistant): fork renders → fresh path → name → units (imperial toggle) → appearance (dark + language preview) → backup toggle → finish → dashboard; confirm Settings → Units shows the chosen units. This validates the layout-bounds note in Task 6 on a real window. Report results; do not mark the task complete on test output alone.
+
+- [ ] **Step 5: Final commit (if fixes were made)**
+
+```bash
+git add -u
+git commit -m "chore: verification fixes for setup wizard"
+```
+
+
+
+
+
diff --git a/docs/superpowers/specs/2026-07-10-setup-wizard-design.md b/docs/superpowers/specs/2026-07-10-setup-wizard-design.md
new file mode 100644
index 000000000..5be1c6d44
--- /dev/null
+++ b/docs/superpowers/specs/2026-07-10-setup-wizard-design.md
@@ -0,0 +1,322 @@
+# Setup Wizard for New Databases — Design
+
+**Date:** 2026-07-10
+**Source:** [Discussion #523](https://github.com/submersion-app/submersion/discussions/523)
+**Status:** Implemented (PR #544)
+
+## Revisions (post-implementation)
+
+- **Appearance step removed** from the fresh path per user feedback during
+ device testing. Theme, map style, and language stay in Settings. The fresh
+ path is now Profile → Units → Backup & Sync → Finish. The `previewLocale`
+ locale-preview mechanism (below) was removed with it — the sections
+ describing Appearance and locale preview are retained for history but no
+ longer reflect the shipped flow.
+- **S3 config opens on the root navigator**, not `context.push`. During first
+ run the database has zero divers, so the router's onboarding redirect
+ bounces any go_router navigation back to `/welcome`; S3 config is a modal
+ action pushed above the wizard instead.
+- **Every step past the fork is reversible.** The existing-data steps (choice,
+ restore, sync, open-folder) render a top back bar; the form-zone steps keep
+ their bottom-bar Back button.
+
+## Problem
+
+Submersion's first-run experience is a single name-only welcome page
+(`lib/features/onboarding/presentation/pages/welcome_page.dart`). Every other
+piece of configuration — units, sync, backups, appearance — is deferred to
+scattered Settings pages that casual users may never discover. Two concrete
+failures today:
+
+1. **US divers silently get metric.** Unit settings are per-diver
+ (`DiverSettings` table) and default to meters/celsius/bar. The welcome page
+ never asks.
+2. **Returning users on a new device have no path.** A user with an existing
+ backup or cloud-synced library must create a throwaway diver profile just
+ to reach Settings → Data → restore/sync. Restore-before-profile is not
+ possible.
+
+"New database" is broader than fresh install: it also occurs on storage-folder
+switch to an empty folder and after resets. All of these funnel through the
+same zero-divers router gate.
+
+## Goals
+
+- Replace the name-only welcome page with a multi-step setup wizard that runs
+ whenever the database has zero divers.
+- Offer an **existing-data path** (restore backup, connect cloud sync and
+ adopt, open an existing storage folder) *before* any profile is created.
+- Configure the essentials on the fresh path: profile name, units,
+ appearance (theme, map style, language), backups, and optional cloud sync.
+- End with a feature-discovery screen ("Submersion can also…").
+- Make the wizard re-runnable from Settings so existing users get the same
+ discoverability (with data-sensitive steps hidden).
+- Every step after the profile name is skippable; a "skip setup" escape
+ reproduces today's minimal flow exactly.
+
+## Non-Goals
+
+- Interactive coach-marks/overlay tour of the real UI.
+- Exposing Google Drive (stays hidden, matching `CloudSyncPage`).
+- Deco/GF or diving-defaults configuration in the wizard (novice-hostile;
+ the 50/85 defaults stand).
+- A trimmed wizard for creating *additional* divers.
+- Prior-experience fields on the profile step (considered, rejected — stays
+ in Settings → Diver Profile).
+- Persisting draft wizard state across app restarts.
+
+## User-Facing Flow
+
+One page, two modes.
+
+### First-run mode
+
+Trigger: existing zero-divers redirect in `app_router.dart` sends the user to
+`/welcome`, which now renders `SetupWizardPage(mode: firstRun)`.
+
+```
+Welcome (fork)
+├─ "Set up a new logbook" → FRESH PATH
+│ 1. Profile name (required; sole mandatory input)
+│ 2. Units Metric | Imperial preset + advanced per-unit fine-tune
+│ 3. Backup&Sync scheduled-backup toggle (+ frequency); optional cloud
+│ provider connect; cloud-copy toggle once connected
+│ 4. Finish feature highlights → "Start logging"
+│ └─ applies draft: create diver → set current → write settings
+│ → navigate to /dashboard
+│
+└─ "I have existing Submersion data" → EXISTING-DATA PATH (choose one)
+ a. Restore a backup file → pick file → confirm → restore → soft restart
+ b. Connect cloud sync → pick provider → connect → adopt library →
+ pull progress → "Adopted N dives" → done
+ c. Open existing folder → pick storage folder → use existing database
+ → swap + restart
+```
+
+Flow rules:
+
+- **Fork first, profile later.** Each existing-data option ends with divers
+ present, so the wizard exits without creating a profile. Restore and
+ folder-adopt exit via the existing soft restart (`restartApp()` /
+ `SubmersionRestart`); sync-adopt exits when the user acknowledges the
+ completion screen.
+- **Draft-and-apply.** Nothing is written until Finish (see Architecture).
+ Abandoning the wizard mid-flow leaves the database untouched; next launch
+ starts the wizard fresh.
+- **Skippable steps.** Units, Appearance, and Backup & Sync each have a Skip
+ affordance; skipping accepts the draft defaults. A "Skip setup" escape on
+ the fork step asks only for the name, then applies all defaults —
+ byte-for-byte today's outcome.
+- **Locale-aware unit preset.** The Units step preselects Imperial when the
+ device locale is US, Liberia, or Myanmar; Metric otherwise. The user
+ confirms rather than configures. The advanced expander exposes the same
+ seven unit preferences plus time/date format that Settings → Units has,
+ driving the existing `UnitPreset {metric, imperial, custom}` semantics.
+- **Cross-path pivots.** If a fresh-path user connects a provider that already
+ holds a library, offer "Adopt the existing library instead?" (jumps to path
+ b semantics, discarding the draft). If an existing-data user's provider has
+ no library, offer "Start fresh instead?" and continue the fresh path with
+ the provider already connected. No dead ends.
+- **Finish screen.** Feature highlights with tap-to-go links: dive computer
+ download, file import, statistics, dive sites map, gear service tracking.
+ Links navigate after apply completes, so the app is fully initialized.
+
+### Re-entry mode
+
+Trigger: a "Setup assistant" tile in Settings pushes
+`/settings/setup-assistant` → `SetupWizardPage(mode: settings)`.
+
+- Hidden: fork, existing-data steps, Profile step (name editing lives in
+ Diver Profile).
+- Shown: Units, Appearance, Backup & Sync, Finish.
+- The draft seeds from the **current diver's live settings** instead of
+ defaults; apply updates that diver's `DiverSettings` row (no diver
+ creation).
+- If a sync provider is already connected, the Backup & Sync step shows
+ connection status instead of connect prompts.
+
+## Architecture
+
+### Feature module
+
+```
+lib/features/setup_wizard/
+├── presentation/
+│ ├── pages/setup_wizard_page.dart # shell: PageController + step list
+│ ├── providers/setup_wizard_providers.dart # SetupWizardNotifier (draft state)
+│ └── widgets/steps/
+│ welcome_fork_step.dart, profile_step.dart, units_step.dart,
+│ appearance_step.dart, backup_sync_step.dart, finish_step.dart,
+│ restore_step.dart, sync_connect_step.dart, open_folder_step.dart
+```
+
+`lib/features/onboarding/` (the old `WelcomePage`) is deleted; docs that
+reference it are updated.
+
+### Shared wizard primitives (the one refactor)
+
+`WizardStepDef` (`import_wizard/domain/models/wizard_step_def.dart`) and
+`WizardStepIndicator` (`import_wizard/presentation/widgets/`) move to
+`lib/shared/widgets/wizard/`. The import wizard updates imports only; its
+shell (`UnifiedImportWizard`) is untouched. The setup wizard builds its own
+small shell because it needs what the import shell does not model: branching
+at the fork, mode-dependent step lists, and no hardcoded
+Review → Import → Summary tail.
+
+### State: draft-and-apply
+
+`SetupWizardNotifier` holds a pure-Dart draft:
+
+- `mode` (firstRun | settings), `path` (fresh | existingData | undecided)
+- `name`
+- draft `AppSettings` — seeded from `const AppSettings()` in first-run mode,
+ from the live `settingsProvider` value in re-entry mode
+- backup schedule choice (enabled + `BackupFrequency`)
+
+Apply order on Finish (first-run):
+
+1. `diverListNotifier.addDiver(...)` — create the diver (`isDefault: true`).
+ `DiverRepository.createDiver` also seeds a defaults `DiverSettings` row.
+2. Overwrite that row with the draft via
+ `DiverSettingsRepository.updateSettingsForDiver(id, draft)` — BEFORE the
+ diver becomes current. Switching the current diver fires an unawaited
+ `SettingsNotifier` reload that would race (and clobber) any post-switch
+ settings writes; writing first means the reload simply loads the drafted
+ row. No new `SettingsNotifier` API is needed, so no settings mocks change.
+3. `setCurrentDiver(id)` — existing dual-write to SharedPreferences + the
+ `Settings` table; the notifier reload now picks up the drafted settings.
+4. Apply backup choices via `backupSettingsProvider`: schedule (`setEnabled`,
+ `setFrequency`) and, when a cloud provider was connected in the wizard and
+ the user opted in, `cloudBackupEnabled`. These are device-level
+ SharedPreferences-backed values (`BackupPreferences`), not per-diver rows.
+
+**Live actions vs. draft settings.** External handshakes cannot be drafted:
+sync provider connect (OAuth, iCloud availability, S3 validation), restore,
+adopt, and folder swap act immediately when the user confirms them, exactly
+as their Settings counterparts do. Everything else (name, units, appearance,
+backup schedule) stays in the draft until Finish.
+
+### Existing-data path: purpose-built UI over existing engines
+
+| Wizard step | Reused engine | Exit |
+|---|---|---|
+| Restore backup | backup/restore services behind `BackupSettingsPage` (`backupOperationProvider`, restore confirmation semantics) | soft restart → divers exist → gate passes |
+| Connect sync | provider connect logic from `CloudSyncPage` (iCloud availability, Dropbox OAuth, S3 config validation); adopt + pull via existing sync machinery | pull completes → user acknowledges → wizard navigates |
+| Open existing folder | `DatabaseLocationService` + the `ExistingDatabaseChoice.useExisting` path from `ExistingDatabaseDialog` | database swap + restart |
+
+The wizard re-skins presentation (wizard-native cards/forms) but calls the
+same services, so config logic keeps one source of truth.
+
+### Routing
+
+- `/welcome` keeps its route name and its zero-divers redirect, now building
+ `SetupWizardPage(mode: firstRun)`.
+- New route `/settings/setup-assistant` builds
+ `SetupWizardPage(mode: settings)`; a Settings tile launches it.
+- **No redirect race exists.** The router has no `refreshListenable`; the
+ redirect only evaluates on navigation events and reads
+ `hasAnyDiversProvider.future` fresh each time (`app_router.dart`).
+ Divers appearing mid-wizard (sync adopt) therefore cannot yank the user
+ off the wizard; the wizard exits by navigating itself, at which point the
+ gate re-evaluates and passes. `allDiversProvider` self-invalidates from
+ `watchDiversChanges()`, so the value is fresh by then.
+
+### Locale preview
+
+Draft-and-apply would leave the wizard rendering in the old language after
+the user picks a new one. A small `previewLocaleProvider` (in the setup
+wizard's providers) is consulted by the app's existing `_resolveLocale`
+(`app.dart`): when set, it overrides the settings locale for rendering.
+Cleared/persisted at Finish. First-run mode has no diver row, so this is the
+only way a language choice can take effect mid-wizard.
+
+### Layout
+
+Full-screen steps on mobile; centered constrained-width card over
+`OceanBackground` on desktop (macOS/Windows/Linux) — the same visual family
+as the splash and the old welcome page. `WizardStepIndicator` heads the fresh
+path; the fork and existing-data screens are indicator-free (they are choices,
+not progress). All layouts must be direction-agnostic for RTL locales
+(ar, he).
+
+## Edge Cases and Error Handling
+
+Principle: no step dead-ends, no partial state.
+
+- **Restore fails** → inline error with retry / back-to-fork. The database is
+ untouched on failure (existing restore-service guarantees). Same for
+ folder-adopt.
+- **Provider connect fails** → inline error on the provider card, retry or
+ pick another provider; never blocks Back.
+- **Sync connects, no library found** (existing-data path) → offer "Start
+ fresh instead?" → continues into the fresh path with the provider already
+ connected.
+- **Sync connects, library found** (fresh path) → offer "Adopt the existing
+ library instead?" → switches to the adopt flow; draft is discarded.
+- **App killed mid-wizard** → draft was in-memory; next launch lands back at
+ the wizard start. Acceptable for a two-minute flow.
+- **Platform gating** → provider cards reuse existing gates: iCloud only on
+ Apple platforms and hidden in Developer ID builds; Google Drive hidden
+ everywhere; S3/Dropbox on all platforms.
+- **Migration interplay** → none: `StartupWrapper` completes migrations before
+ the router renders anything, and the redirect skips while
+ `DatabaseService.instance.isMigrating`.
+- **Re-entry with sync configured** → Backup & Sync step becomes a status
+ view (connected provider, last backup) with a link to Settings → Data.
+
+## Testing
+
+Per the repo's 80% coverage bar:
+
+- **Unit — `SetupWizardNotifier`:** step-list computation per mode and path
+ (first-run shows fork; settings mode hides fork/profile; existing-data path
+ replaces fresh steps), draft mutations, apply ordering
+ (create → select → settings → backup), locale→preset detection
+ (`en_US` → Imperial, `de_DE` → Metric).
+- **Widget — steps in isolation:** profile name validation (empty name blocks
+ advance), units preset toggle drives the advanced fields, appearance
+ choices update the draft, skip affordances advance without mutating the
+ draft, fork branching renders the right next step.
+- **Widget — shell:** next/back navigation across the branch, "Skip setup"
+ fast-path, re-entry mode step visibility.
+- **Integration-style (in-memory Drift):** Finish produces exactly one diver
+ (`isDefault: true`, current) and a `DiverSettings` row matching the draft —
+ e.g. Imperial preset writes all seven unit columns; backup schedule lands in
+ backup settings.
+- **Regression:** import wizard tests pass untouched after the
+ `WizardStepDef`/`WizardStepIndicator` move; router tests confirm the
+ zero-divers redirect still lands on the wizard and that completing the
+ wizard reaches `/dashboard`.
+- Known widget-test traps apply (theme animation duration zero; wrap
+ post-pump Drift awaits in `tester.runAsync`; `FormSection` uppercases
+ labels).
+
+## Localization
+
+- All new strings in `lib/l10n/arb/app_en.arb` plus the 10 other locales,
+ regenerated. Estimate 60–80 keys under a `setup_` prefix.
+- The ~10 `onboarding_welcome_*` keys are removed with the old welcome page.
+- RTL: no special-casing beyond Material defaults, but step indicator and
+ fork cards must not hardcode left/right.
+
+## Documentation
+
+- `docs/developer/navigation.md` — first-run guard section and route table.
+- `docs/guide/first-dive.md` + user-guide wiki source — new first-run flow.
+- `docs/ARCHITECTURE.md` / `docs/developer/architecture.md` — feature list
+ (onboarding → setup_wizard).
+- `FEATURE_ROADMAP.md` — entry for the setup wizard.
+
+## Resolved Questions (verified in code)
+
+- **Map style is a real setting:** `MapStyle {openStreetMap, openTopoMap,
+ esriSatellite}` on `AppSettings.mapStyle` with an existing notifier setter —
+ included in the Appearance step.
+- **Auto-backup exists:** `backupSettingsProvider` with `setEnabled` /
+ `setFrequency(BackupFrequency)` and a `cloudBackupEnabled` flag — the
+ Backup & Sync step drives it. `cloudBackupEnabled` (SharedPreferences,
+ default false) only takes effect when a cloud provider is configured
+ (`cloudStorageProviderProvider`), so the wizard surfaces the toggle only
+ after a provider connects — mirroring `BackupSettingsPage`.
+- **No router refresh race:** redirect is navigation-driven only; wizard
+ controls its own exit (see Routing).
diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart
index d1c688cf6..220edc4ec 100644
--- a/lib/core/router/app_router.dart
+++ b/lib/core/router/app_router.dart
@@ -15,7 +15,8 @@ import 'package:submersion/features/dive_log/presentation/providers/dive_provide
import 'package:submersion/features/import_wizard/data/adapters/healthkit_adapter.dart';
import 'package:submersion/features/settings/presentation/providers/settings_providers.dart';
import 'package:submersion/features/import_wizard/presentation/pages/unified_import_wizard.dart';
-import 'package:submersion/features/onboarding/presentation/pages/welcome_page.dart';
+import 'package:submersion/features/setup_wizard/domain/setup_wizard_models.dart';
+import 'package:submersion/features/setup_wizard/presentation/pages/setup_wizard_page.dart';
import 'package:submersion/features/buddies/presentation/pages/buddy_detail_page.dart';
import 'package:submersion/features/buddies/presentation/pages/buddy_edit_page.dart';
import 'package:submersion/features/buddies/presentation/pages/buddy_merge_page.dart';
@@ -173,7 +174,8 @@ final appRouterProvider = Provider((ref) {
GoRoute(
path: '/welcome',
name: 'welcome',
- builder: (context, state) => const WelcomePage(),
+ builder: (context, state) =>
+ const SetupWizardPage(mode: SetupWizardMode.firstRun),
),
ShellRoute(
builder: (context, state, child) => CallbackShortcuts(
@@ -894,6 +896,12 @@ final appRouterProvider = Provider((ref) {
name: 'backupSettings',
builder: (context, state) => const BackupSettingsPage(),
),
+ GoRoute(
+ path: 'setup-assistant',
+ name: 'setupAssistant',
+ builder: (context, state) =>
+ const SetupWizardPage(mode: SetupWizardMode.settings),
+ ),
GoRoute(
path: 'cloud-sync',
name: 'cloudSync',
diff --git a/lib/features/import_wizard/data/adapters/dive_computer_adapter.dart b/lib/features/import_wizard/data/adapters/dive_computer_adapter.dart
index 333dee14d..d545d2f26 100644
--- a/lib/features/import_wizard/data/adapters/dive_computer_adapter.dart
+++ b/lib/features/import_wizard/data/adapters/dive_computer_adapter.dart
@@ -25,7 +25,7 @@ import 'package:submersion/features/import_wizard/domain/models/import_cancellat
import 'package:submersion/features/import_wizard/domain/models/import_phase.dart';
import 'package:submersion/features/import_wizard/domain/models/import_bundle.dart';
import 'package:submersion/features/import_wizard/domain/models/unified_import_result.dart';
-import 'package:submersion/features/import_wizard/domain/models/wizard_step_def.dart';
+import 'package:submersion/shared/widgets/wizard/wizard_step_def.dart';
import 'package:submersion/features/import_wizard/presentation/widgets/dc_adapter_steps.dart';
// ---------------------------------------------------------------------------
diff --git a/lib/features/import_wizard/data/adapters/healthkit_adapter.dart b/lib/features/import_wizard/data/adapters/healthkit_adapter.dart
index e99722e98..1bcba48cf 100644
--- a/lib/features/import_wizard/data/adapters/healthkit_adapter.dart
+++ b/lib/features/import_wizard/data/adapters/healthkit_adapter.dart
@@ -16,7 +16,7 @@ import 'package:submersion/features/import_wizard/domain/models/import_cancellat
import 'package:submersion/features/import_wizard/domain/models/import_phase.dart';
import 'package:submersion/features/import_wizard/domain/models/import_bundle.dart';
import 'package:submersion/features/import_wizard/domain/models/unified_import_result.dart';
-import 'package:submersion/features/import_wizard/domain/models/wizard_step_def.dart';
+import 'package:submersion/shared/widgets/wizard/wizard_step_def.dart';
import 'package:submersion/features/import_wizard/presentation/widgets/healthkit_adapter_steps.dart';
import 'package:submersion/features/settings/presentation/providers/settings_providers.dart';
diff --git a/lib/features/import_wizard/data/adapters/universal_adapter.dart b/lib/features/import_wizard/data/adapters/universal_adapter.dart
index c631e8234..4bb7dd9fd 100644
--- a/lib/features/import_wizard/data/adapters/universal_adapter.dart
+++ b/lib/features/import_wizard/data/adapters/universal_adapter.dart
@@ -37,8 +37,8 @@ import 'package:submersion/features/import_wizard/domain/models/import_bundle.da
as wizard
show ImportEntityType;
import 'package:submersion/features/import_wizard/domain/models/unified_import_result.dart';
-import 'package:submersion/features/import_wizard/domain/models/wizard_step_def.dart';
import 'package:submersion/features/media/presentation/providers/photo_picker_providers.dart';
+import 'package:submersion/shared/widgets/wizard/wizard_step_def.dart';
import 'package:submersion/features/settings/presentation/providers/settings_providers.dart';
import 'package:submersion/features/tags/presentation/providers/tag_providers.dart';
import 'package:submersion/features/tank_presets/domain/services/default_tank_preset_resolver.dart';
diff --git a/lib/features/import_wizard/domain/adapters/import_source_adapter.dart b/lib/features/import_wizard/domain/adapters/import_source_adapter.dart
index 63f8a37e9..3caa48573 100644
--- a/lib/features/import_wizard/domain/adapters/import_source_adapter.dart
+++ b/lib/features/import_wizard/domain/adapters/import_source_adapter.dart
@@ -3,7 +3,7 @@ import 'package:submersion/features/import_wizard/domain/models/import_bundle.da
import 'package:submersion/features/import_wizard/domain/models/import_cancellation_token.dart';
import 'package:submersion/features/import_wizard/domain/models/import_phase.dart';
import 'package:submersion/features/import_wizard/domain/models/unified_import_result.dart';
-import 'package:submersion/features/import_wizard/domain/models/wizard_step_def.dart';
+import 'package:submersion/shared/widgets/wizard/wizard_step_def.dart';
/// Interface for source-specific import adapters.
///
diff --git a/lib/features/import_wizard/domain/services/step_skip_calculator.dart b/lib/features/import_wizard/domain/services/step_skip_calculator.dart
index 1153e2676..59f57c716 100644
--- a/lib/features/import_wizard/domain/services/step_skip_calculator.dart
+++ b/lib/features/import_wizard/domain/services/step_skip_calculator.dart
@@ -1,4 +1,4 @@
-import 'package:submersion/features/import_wizard/domain/models/wizard_step_def.dart';
+import 'package:submersion/shared/widgets/wizard/wizard_step_def.dart';
/// Calculates the next page index by skipping steps whose auto-advance
/// condition is already met.
diff --git a/lib/features/import_wizard/presentation/pages/unified_import_wizard.dart b/lib/features/import_wizard/presentation/pages/unified_import_wizard.dart
index c0dd5720f..588c119f1 100644
--- a/lib/features/import_wizard/presentation/pages/unified_import_wizard.dart
+++ b/lib/features/import_wizard/presentation/pages/unified_import_wizard.dart
@@ -17,7 +17,7 @@ import 'package:submersion/features/import_wizard/data/adapters/dive_computer_ad
import 'package:submersion/features/import_wizard/data/adapters/universal_adapter.dart';
import 'package:submersion/features/import_wizard/domain/adapters/import_source_adapter.dart';
import 'package:submersion/features/import_wizard/domain/models/import_bundle.dart';
-import 'package:submersion/features/import_wizard/domain/models/wizard_step_def.dart';
+import 'package:submersion/shared/widgets/wizard/wizard_step_def.dart';
import 'package:submersion/features/import_wizard/domain/services/step_skip_calculator.dart';
import 'package:submersion/features/import_wizard/presentation/providers/import_wizard_providers.dart';
import 'package:submersion/features/tags/presentation/providers/tag_providers.dart';
@@ -25,7 +25,7 @@ import 'package:submersion/features/trips/presentation/providers/trip_providers.
import 'package:submersion/features/import_wizard/presentation/widgets/import_progress_step.dart';
import 'package:submersion/features/import_wizard/presentation/widgets/import_summary_step.dart';
import 'package:submersion/features/import_wizard/presentation/widgets/review_step.dart';
-import 'package:submersion/features/import_wizard/presentation/widgets/wizard_step_indicator.dart';
+import 'package:submersion/shared/widgets/wizard/wizard_step_indicator.dart';
/// The unified import wizard shell.
///
diff --git a/lib/features/onboarding/presentation/pages/welcome_page.dart b/lib/features/onboarding/presentation/pages/welcome_page.dart
deleted file mode 100644
index 3797d5b5f..000000000
--- a/lib/features/onboarding/presentation/pages/welcome_page.dart
+++ /dev/null
@@ -1,206 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:submersion/core/providers/provider.dart';
-import 'package:go_router/go_router.dart';
-
-import 'package:submersion/core/presentation/widgets/ocean_background.dart';
-import 'package:submersion/features/divers/domain/entities/diver.dart';
-import 'package:submersion/features/divers/presentation/providers/diver_providers.dart';
-import 'package:submersion/l10n/l10n_extension.dart';
-
-class WelcomePage extends ConsumerStatefulWidget {
- const WelcomePage({super.key});
-
- @override
- ConsumerState createState() => _WelcomePageState();
-}
-
-class _WelcomePageState extends ConsumerState {
- final _formKey = GlobalKey();
- final _nameController = TextEditingController();
- bool _isSaving = false;
-
- @override
- void dispose() {
- _nameController.dispose();
- super.dispose();
- }
-
- Future _createProfile() async {
- if (!_formKey.currentState!.validate()) return;
-
- setState(() => _isSaving = true);
-
- try {
- final now = DateTime.now();
- final diver = Diver(
- id: '',
- name: _nameController.text.trim(),
- emergencyContact: const EmergencyContact(),
- insurance: const DiverInsurance(),
- medicalNotes: '',
- notes: '',
- isDefault: true,
- createdAt: now,
- updatedAt: now,
- );
-
- final newDiver = await ref
- .read(diverListNotifierProvider.notifier)
- .addDiver(diver);
-
- // Set as current diver
- await ref
- .read(currentDiverIdProvider.notifier)
- .setCurrentDiver(newDiver.id);
-
- if (mounted) {
- // Navigate to main app
- context.go('/dives');
- }
- } catch (e) {
- if (mounted) {
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(
- content: Text(
- context.l10n.onboarding_welcome_errorCreatingProfile(
- e.toString(),
- ),
- ),
- ),
- );
- setState(() => _isSaving = false);
- }
- }
- }
-
- @override
- Widget build(BuildContext context) {
- final theme = Theme.of(context);
-
- return Scaffold(
- body: OceanBackground(
- child: SafeArea(
- child: Center(
- child: SingleChildScrollView(
- padding: const EdgeInsets.all(32),
- child: ConstrainedBox(
- constraints: const BoxConstraints(maxWidth: 400),
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- // App icon/logo
- ExcludeSemantics(
- child: ClipRRect(
- borderRadius: BorderRadius.circular(24),
- child: Image.asset(
- 'assets/icon/icon.png',
- width: 120,
- height: 120,
- ),
- ),
- ),
- const SizedBox(height: 32),
-
- // Welcome text
- Text(
- context.l10n.onboarding_welcome_title,
- style: theme.textTheme.headlineMedium?.copyWith(
- fontWeight: FontWeight.bold,
- color: Colors.white,
- ),
- textAlign: TextAlign.center,
- ),
- const SizedBox(height: 12),
-
- Text(
- context.l10n.onboarding_welcome_subtitle,
- style: theme.textTheme.bodyLarge?.copyWith(
- color: Colors.white.withValues(alpha: 0.8),
- ),
- textAlign: TextAlign.center,
- ),
- const SizedBox(height: 48),
-
- // Profile creation card
- Card(
- child: Padding(
- padding: const EdgeInsets.all(24),
- child: Form(
- key: _formKey,
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: [
- Text(
- context.l10n.onboarding_welcome_createProfile,
- style: theme.textTheme.titleLarge?.copyWith(
- fontWeight: FontWeight.w600,
- ),
- ),
- const SizedBox(height: 8),
- Text(
- context
- .l10n
- .onboarding_welcome_createProfileSubtitle,
- style: theme.textTheme.bodyMedium?.copyWith(
- color: theme.colorScheme.onSurfaceVariant,
- ),
- ),
- const SizedBox(height: 24),
- TextFormField(
- controller: _nameController,
- decoration: InputDecoration(
- labelText:
- context.l10n.onboarding_welcome_nameLabel,
- prefixIcon: const Icon(Icons.person),
- hintText:
- context.l10n.onboarding_welcome_nameHint,
- ),
- textCapitalization: TextCapitalization.words,
- autofocus: true,
- textInputAction: TextInputAction.done,
- onFieldSubmitted: (_) => _createProfile(),
- validator: (value) {
- if (value == null || value.trim().isEmpty) {
- return context
- .l10n
- .onboarding_welcome_nameValidation;
- }
- return null;
- },
- ),
- const SizedBox(height: 24),
- FilledButton.icon(
- onPressed: _isSaving ? null : _createProfile,
- icon: _isSaving
- ? const SizedBox(
- width: 20,
- height: 20,
- child: CircularProgressIndicator(
- strokeWidth: 2,
- color: Colors.white,
- ),
- )
- : const Icon(Icons.arrow_forward),
- label: Text(
- _isSaving
- ? context.l10n.onboarding_welcome_creating
- : context
- .l10n
- .onboarding_welcome_getStarted,
- ),
- ),
- ],
- ),
- ),
- ),
- ),
- ],
- ),
- ),
- ),
- ),
- ),
- ),
- );
- }
-}
diff --git a/lib/features/settings/presentation/pages/settings_page.dart b/lib/features/settings/presentation/pages/settings_page.dart
index 52b13b1d5..2eb38279a 100644
--- a/lib/features/settings/presentation/pages/settings_page.dart
+++ b/lib/features/settings/presentation/pages/settings_page.dart
@@ -1653,6 +1653,16 @@ class _ManageSectionContent extends StatelessWidget {
onTap: () => context.push('/tank-presets'),
),
const Divider(height: 1),
+ ListTile(
+ leading: const Icon(Icons.auto_fix_high),
+ title: Text(context.l10n.settings_manage_setupAssistant),
+ subtitle: Text(
+ context.l10n.settings_manage_setupAssistant_subtitle,
+ ),
+ trailing: const Icon(Icons.chevron_right),
+ onTap: () => context.push('/settings/setup-assistant'),
+ ),
+ const Divider(height: 1),
ListTile(
leading: const Icon(Icons.checklist),
title: Text(context.l10n.settings_manage_checklistTemplates),
diff --git a/lib/features/setup_wizard/data/setup_apply_service.dart b/lib/features/setup_wizard/data/setup_apply_service.dart
new file mode 100644
index 000000000..5b04b7de9
--- /dev/null
+++ b/lib/features/setup_wizard/data/setup_apply_service.dart
@@ -0,0 +1,90 @@
+import 'package:submersion/core/providers/provider.dart';
+
+import 'package:submersion/features/backup/presentation/providers/backup_providers.dart';
+import 'package:submersion/features/divers/domain/entities/diver.dart';
+import 'package:submersion/features/divers/presentation/providers/diver_providers.dart';
+import 'package:submersion/features/settings/presentation/providers/settings_providers.dart';
+import 'package:submersion/features/setup_wizard/domain/setup_wizard_models.dart';
+
+final setupApplyServiceProvider = Provider(
+ (ref) => SetupApplyService(ref),
+);
+
+/// Persists a completed wizard draft.
+///
+/// First-run ordering is load-bearing: createDiver seeds a defaults
+/// settings row, which is overwritten with the draft BEFORE the diver
+/// becomes current, because switching the current diver triggers an
+/// unawaited SettingsNotifier reload that would otherwise race (and
+/// clobber) any post-switch settings writes.
+class SetupApplyService {
+ final Ref _ref;
+
+ SetupApplyService(this._ref);
+
+ Future applyFirstRun(SetupWizardDraft draft) async {
+ final name = draft.name.trim();
+ if (name.isEmpty) {
+ throw ArgumentError('Diver name must not be empty');
+ }
+
+ final now = DateTime.now();
+ final newDiver = await _ref
+ .read(diverListNotifierProvider.notifier)
+ .addDiver(
+ Diver(
+ id: '',
+ name: name,
+ isDefault: true,
+ createdAt: now,
+ updatedAt: now,
+ ),
+ );
+
+ // Overwrite the defaults row createDiver just made with the draft,
+ // BEFORE the switch (see class doc).
+ await _ref
+ .read(diverSettingsRepositoryProvider)
+ .updateSettingsForDiver(newDiver.id, draft.settings);
+
+ await _ref
+ .read(currentDiverIdProvider.notifier)
+ .setCurrentDiver(newDiver.id);
+
+ await _applyBackupChoices(draft);
+ }
+
+ Future applySettingsMode(SetupWizardDraft draft) async {
+ // Re-entry: the current diver's settings row exists and the
+ // SettingsNotifier is loaded, so the existing setters are race-free.
+ final notifier = _ref.read(settingsProvider.notifier);
+ final s = draft.settings;
+ await notifier.setDepthUnit(s.depthUnit);
+ await notifier.setTemperatureUnit(s.temperatureUnit);
+ await notifier.setPressureUnit(s.pressureUnit);
+ await notifier.setVolumeUnit(s.volumeUnit);
+ await notifier.setWeightUnit(s.weightUnit);
+ await notifier.setAltitudeUnit(s.altitudeUnit);
+ await notifier.setSacUnit(s.sacUnit);
+ await notifier.setTimeFormat(s.timeFormat);
+ await notifier.setDateFormat(s.dateFormat);
+ await notifier.setThemeMode(s.themeMode);
+ await notifier.setThemePresetId(s.themePresetId);
+ await notifier.setMapStyle(s.mapStyle);
+ await notifier.setLocale(s.locale);
+
+ await _applyBackupChoices(draft);
+ }
+
+ Future _applyBackupChoices(SetupWizardDraft draft) async {
+ final backup = _ref.read(backupSettingsProvider.notifier);
+ await backup.setEnabled(draft.backupEnabled);
+ if (draft.backupEnabled) {
+ await backup.setFrequency(draft.backupFrequency);
+ }
+ // Apply the cloud-copy choice in both directions so re-entry can turn it
+ // off. The flag is inert without a configured provider, so writing it is
+ // safe even when none is connected.
+ await backup.setCloudBackupEnabled(draft.cloudBackupEnabled);
+ }
+}
diff --git a/lib/features/setup_wizard/domain/setup_wizard_models.dart b/lib/features/setup_wizard/domain/setup_wizard_models.dart
new file mode 100644
index 000000000..087d295aa
--- /dev/null
+++ b/lib/features/setup_wizard/domain/setup_wizard_models.dart
@@ -0,0 +1,155 @@
+import 'package:submersion/core/constants/units.dart';
+import 'package:submersion/core/data/repositories/sync_repository.dart';
+import 'package:submersion/features/backup/domain/entities/backup_settings.dart';
+import 'package:submersion/features/settings/presentation/providers/settings_providers.dart';
+
+/// Whether the wizard runs at first launch or from Settings.
+enum SetupWizardMode { firstRun, settings }
+
+/// Which top-level branch the user chose on the fork step.
+enum SetupPath { undecided, fresh, existingData }
+
+/// Which existing-data source the user chose.
+enum ExistingDataSource { none, restoreBackup, cloudSync, openFolder }
+
+/// Identity of every wizard step; the shell maps these to widgets.
+enum SetupStepId {
+ welcomeFork,
+ profile,
+ units,
+ backupSync,
+ finish,
+ existingChoice,
+ restore,
+ syncConnect,
+ openFolder,
+}
+
+/// Immutable wizard draft. Nothing is persisted until Finish applies it.
+class SetupWizardDraft {
+ final SetupWizardMode mode;
+ final SetupPath path;
+ final ExistingDataSource source;
+ final bool skipRequested;
+ final String name;
+ final AppSettings settings;
+ final bool backupEnabled;
+ final BackupFrequency backupFrequency;
+ final bool cloudBackupEnabled;
+ final CloudProviderType? connectedProvider;
+
+ const SetupWizardDraft({
+ required this.mode,
+ this.path = SetupPath.undecided,
+ this.source = ExistingDataSource.none,
+ this.skipRequested = false,
+ this.name = '',
+ this.settings = const AppSettings(),
+ this.backupEnabled = false,
+ this.backupFrequency = BackupFrequency.weekly,
+ this.cloudBackupEnabled = false,
+ this.connectedProvider,
+ });
+
+ SetupWizardDraft copyWith({
+ SetupPath? path,
+ ExistingDataSource? source,
+ bool? skipRequested,
+ String? name,
+ AppSettings? settings,
+ bool? backupEnabled,
+ BackupFrequency? backupFrequency,
+ bool? cloudBackupEnabled,
+ CloudProviderType? connectedProvider,
+ bool clearConnectedProvider = false,
+ }) {
+ return SetupWizardDraft(
+ mode: mode,
+ path: path ?? this.path,
+ source: source ?? this.source,
+ skipRequested: skipRequested ?? this.skipRequested,
+ name: name ?? this.name,
+ settings: settings ?? this.settings,
+ backupEnabled: backupEnabled ?? this.backupEnabled,
+ backupFrequency: backupFrequency ?? this.backupFrequency,
+ cloudBackupEnabled: cloudBackupEnabled ?? this.cloudBackupEnabled,
+ connectedProvider: clearConnectedProvider
+ ? null
+ : (connectedProvider ?? this.connectedProvider),
+ );
+ }
+
+ /// Returns a copy with the six core units set to [preset].
+ /// [UnitPreset.custom] returns this draft unchanged.
+ SetupWizardDraft applyingUnitPreset(UnitPreset preset) {
+ switch (preset) {
+ case UnitPreset.metric:
+ return copyWith(
+ settings: settings.copyWith(
+ depthUnit: DepthUnit.meters,
+ temperatureUnit: TemperatureUnit.celsius,
+ pressureUnit: PressureUnit.bar,
+ volumeUnit: VolumeUnit.liters,
+ weightUnit: WeightUnit.kilograms,
+ altitudeUnit: AltitudeUnit.meters,
+ ),
+ );
+ case UnitPreset.imperial:
+ return copyWith(
+ settings: settings.copyWith(
+ depthUnit: DepthUnit.feet,
+ temperatureUnit: TemperatureUnit.fahrenheit,
+ pressureUnit: PressureUnit.psi,
+ volumeUnit: VolumeUnit.cubicFeet,
+ weightUnit: WeightUnit.pounds,
+ altitudeUnit: AltitudeUnit.feet,
+ ),
+ );
+ case UnitPreset.custom:
+ return this;
+ }
+ }
+}
+
+/// Computes the ordered step list for the current draft state.
+List computeSteps(SetupWizardDraft draft) {
+ if (draft.mode == SetupWizardMode.settings) {
+ return const [
+ SetupStepId.units,
+ SetupStepId.backupSync,
+ SetupStepId.finish,
+ ];
+ }
+ switch (draft.path) {
+ case SetupPath.undecided:
+ return const [SetupStepId.welcomeFork];
+ case SetupPath.fresh:
+ if (draft.skipRequested) {
+ return const [
+ SetupStepId.welcomeFork,
+ SetupStepId.profile,
+ SetupStepId.finish,
+ ];
+ }
+ return const [
+ SetupStepId.welcomeFork,
+ SetupStepId.profile,
+ SetupStepId.units,
+ SetupStepId.backupSync,
+ SetupStepId.finish,
+ ];
+ case SetupPath.existingData:
+ final steps = [SetupStepId.welcomeFork, SetupStepId.existingChoice];
+ switch (draft.source) {
+ case ExistingDataSource.none:
+ break;
+ case ExistingDataSource.restoreBackup:
+ steps.add(SetupStepId.restore);
+ case ExistingDataSource.cloudSync:
+ steps.add(SetupStepId.syncConnect);
+ case ExistingDataSource.openFolder:
+ steps.add(SetupStepId.openFolder);
+ }
+ return steps;
+ }
+}
diff --git a/lib/features/setup_wizard/domain/unit_preset_detector.dart b/lib/features/setup_wizard/domain/unit_preset_detector.dart
new file mode 100644
index 000000000..67356c310
--- /dev/null
+++ b/lib/features/setup_wizard/domain/unit_preset_detector.dart
@@ -0,0 +1,15 @@
+import 'dart:ui';
+
+import 'package:submersion/features/settings/presentation/providers/settings_providers.dart';
+
+/// Countries using imperial-style units for everyday measurement.
+const _imperialCountries = {'US', 'LR', 'MM'};
+
+/// Maps a device locale to the unit preset the wizard preselects.
+UnitPreset presetForLocale(Locale locale) {
+ final country = locale.countryCode?.toUpperCase();
+ if (country != null && _imperialCountries.contains(country)) {
+ return UnitPreset.imperial;
+ }
+ return UnitPreset.metric;
+}
diff --git a/lib/features/setup_wizard/presentation/pages/setup_wizard_page.dart b/lib/features/setup_wizard/presentation/pages/setup_wizard_page.dart
new file mode 100644
index 000000000..6b4130216
--- /dev/null
+++ b/lib/features/setup_wizard/presentation/pages/setup_wizard_page.dart
@@ -0,0 +1,359 @@
+import 'package:flutter/material.dart';
+
+import 'package:submersion/core/presentation/widgets/ocean_background.dart';
+import 'package:submersion/core/providers/provider.dart';
+import 'package:submersion/features/setup_wizard/domain/setup_wizard_models.dart';
+import 'package:submersion/features/setup_wizard/presentation/providers/setup_wizard_providers.dart';
+import 'package:submersion/features/setup_wizard/presentation/widgets/steps/backup_sync_step.dart';
+import 'package:submersion/features/setup_wizard/presentation/widgets/steps/existing_choice_step.dart';
+import 'package:submersion/features/setup_wizard/presentation/widgets/steps/finish_step.dart';
+import 'package:submersion/features/setup_wizard/presentation/widgets/steps/open_folder_step.dart';
+import 'package:submersion/features/setup_wizard/presentation/widgets/steps/restore_step.dart';
+import 'package:submersion/features/setup_wizard/presentation/widgets/steps/sync_connect_step.dart';
+import 'package:submersion/features/setup_wizard/presentation/widgets/steps/profile_step.dart';
+import 'package:submersion/features/setup_wizard/presentation/widgets/steps/units_step.dart';
+import 'package:submersion/features/setup_wizard/presentation/widgets/steps/welcome_fork_step.dart';
+import 'package:submersion/l10n/l10n_extension.dart';
+import 'package:submersion/shared/widgets/wizard/wizard_step_def.dart';
+import 'package:submersion/shared/widgets/wizard/wizard_step_indicator.dart';
+
+/// Multi-step setup wizard for new databases (first run) and Settings
+/// re-entry. See docs/superpowers/specs/2026-07-10-setup-wizard-design.md.
+class SetupWizardPage extends ConsumerStatefulWidget {
+ final SetupWizardMode mode;
+
+ const SetupWizardPage({super.key, required this.mode});
+
+ @override
+ ConsumerState createState() => _SetupWizardPageState();
+}
+
+/// Steps that show the progress indicator and the shared Next/Back bar.
+const _formZone = {
+ SetupStepId.profile,
+ SetupStepId.units,
+ SetupStepId.backupSync,
+};
+
+class _SetupWizardPageState extends ConsumerState {
+ final _pageController = PageController();
+ int _currentIndex = 0;
+
+ @override
+ void dispose() {
+ _pageController.dispose();
+ super.dispose();
+ }
+
+ Future _animateTo(int index) async {
+ if (_pageController.hasClients) {
+ await _pageController.animateToPage(
+ index,
+ duration: const Duration(milliseconds: 300),
+ curve: Curves.easeInOut,
+ );
+ }
+ if (mounted) setState(() => _currentIndex = index);
+ }
+
+ void _advance() {
+ final steps = computeSteps(ref.read(setupWizardProvider(widget.mode)));
+ if (_currentIndex < steps.length - 1) {
+ _animateTo(_currentIndex + 1);
+ }
+ }
+
+ void _back() {
+ if (_currentIndex > 0) _animateTo(_currentIndex - 1);
+ }
+
+ /// Choice steps mutate the draft (growing the step list), then advance
+ /// after the rebuild that adds the next page.
+ void _chooseAndAdvance(void Function() mutate) {
+ mutate();
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ if (mounted) _advance();
+ });
+ }
+
+ String _stepLabel(SetupStepId id) {
+ final l10n = context.l10n;
+ switch (id) {
+ case SetupStepId.profile:
+ return l10n.setup_step_profile;
+ case SetupStepId.units:
+ return l10n.setup_step_units;
+ case SetupStepId.backupSync:
+ return l10n.setup_step_backup;
+ case SetupStepId.finish:
+ return l10n.setup_step_finish;
+ default:
+ return '';
+ }
+ }
+
+ WizardStepDef _defFor(SetupStepId id) {
+ final mode = widget.mode;
+ final notifier = ref.read(setupWizardProvider(mode).notifier);
+ switch (id) {
+ case SetupStepId.welcomeFork:
+ return WizardStepDef(
+ label: '',
+ hideBottomBar: true,
+ canAdvance: setupWizardProvider(mode).select((_) => true),
+ builder: (_) => WelcomeForkStep(
+ onStartFresh: () =>
+ _chooseAndAdvance(() => notifier.choosePath(SetupPath.fresh)),
+ onExistingData: () => _chooseAndAdvance(
+ () => notifier.choosePath(SetupPath.existingData),
+ ),
+ onSkipSetup: () => _chooseAndAdvance(notifier.requestSkip),
+ ),
+ );
+ case SetupStepId.profile:
+ return WizardStepDef(
+ label: _stepLabel(id),
+ canAdvance: setupWizardProvider(
+ mode,
+ ).select((d) => d.name.trim().isNotEmpty),
+ builder: (_) => ProfileStep(mode: mode),
+ );
+ case SetupStepId.units:
+ return WizardStepDef(
+ label: _stepLabel(id),
+ canAdvance: setupWizardProvider(mode).select((_) => true),
+ builder: (_) => UnitsStep(mode: mode),
+ );
+ case SetupStepId.backupSync:
+ return WizardStepDef(
+ label: _stepLabel(id),
+ canAdvance: setupWizardProvider(mode).select((_) => true),
+ builder: (_) =>
+ BackupSyncStep(mode: mode, onLibraryFound: _offerAdoptPivot),
+ );
+ case SetupStepId.finish:
+ return WizardStepDef(
+ label: _stepLabel(id),
+ hideBottomBar: true,
+ canAdvance: setupWizardProvider(mode).select((_) => true),
+ builder: (_) => FinishStep(mode: mode),
+ );
+ case SetupStepId.existingChoice:
+ return WizardStepDef(
+ label: '',
+ hideBottomBar: true,
+ canAdvance: setupWizardProvider(mode).select((_) => true),
+ builder: (_) => ExistingChoiceStep(
+ mode: mode,
+ onChosen: () => _chooseAndAdvance(() {}),
+ ),
+ );
+ case SetupStepId.restore:
+ return WizardStepDef(
+ label: '',
+ hideBottomBar: true,
+ canAdvance: setupWizardProvider(mode).select((_) => true),
+ builder: (_) => const RestoreStep(),
+ );
+ case SetupStepId.syncConnect:
+ return WizardStepDef(
+ label: '',
+ hideBottomBar: true,
+ canAdvance: setupWizardProvider(mode).select((_) => true),
+ builder: (_) => SyncConnectStep(
+ mode: mode,
+ onNoLibrary: () {
+ notifier.choosePath(SetupPath.fresh);
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ if (mounted) _animateTo(1);
+ });
+ },
+ ),
+ );
+ case SetupStepId.openFolder:
+ return WizardStepDef(
+ label: '',
+ hideBottomBar: true,
+ canAdvance: setupWizardProvider(mode).select((_) => true),
+ builder: (_) => const OpenFolderStep(),
+ );
+ }
+ }
+
+ /// Fresh-path pivot: a just-connected provider already holds a library.
+ Future _offerAdoptPivot() async {
+ final l10n = context.l10n;
+ final adopt = await showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: Text(l10n.setup_sync_libraryFound_title),
+ content: Text(l10n.setup_sync_libraryFound_message),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context, false),
+ child: Text(l10n.setup_sync_libraryFound_keepFresh),
+ ),
+ FilledButton(
+ onPressed: () => Navigator.pop(context, true),
+ child: Text(l10n.setup_sync_libraryFound_adopt),
+ ),
+ ],
+ ),
+ );
+ if (adopt != true || !mounted) return;
+ final notifier = ref.read(setupWizardProvider(widget.mode).notifier);
+ notifier.choosePath(SetupPath.existingData);
+ notifier.chooseSource(ExistingDataSource.cloudSync);
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ if (!mounted) return;
+ final steps = computeSteps(ref.read(setupWizardProvider(widget.mode)));
+ _animateTo(steps.length - 1);
+ });
+ }
+
+ SetupStepId _currentStepId() {
+ final steps = computeSteps(ref.read(setupWizardProvider(widget.mode)));
+ return steps[_currentIndex.clamp(0, steps.length - 1)];
+ }
+
+ int _stepCount() {
+ return computeSteps(ref.read(setupWizardProvider(widget.mode))).length;
+ }
+
+ bool _skippable(SetupStepId id) => id != SetupStepId.profile;
+
+ @override
+ Widget build(BuildContext context) {
+ final draft = ref.watch(setupWizardProvider(widget.mode));
+ final steps = computeSteps(draft);
+ if (_currentIndex >= steps.length) {
+ _currentIndex = steps.length - 1;
+ // The step list shrank under us (e.g. the adopt pivot from the fresh
+ // path to the shorter existing-data path). Resync the controller
+ // post-frame so the PageView can't sit on an out-of-range page.
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ if (mounted && _pageController.hasClients) {
+ _pageController.jumpToPage(_currentIndex);
+ }
+ });
+ }
+ final defs = steps.map(_defFor).toList();
+ final currentId = steps[_currentIndex];
+ final inFormZone = _formZone.contains(currentId);
+
+ // Indicator covers the contiguous labelled tail of the flow (profile or
+ // units onward, plus finish); choice steps stay indicator-free.
+ final labelledIds = steps
+ .where((s) => _formZone.contains(s) || s == SetupStepId.finish)
+ .toList();
+ final labelledIndex = labelledIds.indexOf(currentId);
+
+ return Scaffold(
+ body: OceanBackground(
+ child: SafeArea(
+ child: Center(
+ child: ConstrainedBox(
+ constraints: const BoxConstraints(maxWidth: 560),
+ child: SizedBox.expand(
+ child: Padding(
+ padding: const EdgeInsets.all(16),
+ child: Card(
+ clipBehavior: Clip.antiAlias,
+ child: Column(
+ children: [
+ if (labelledIndex >= 0)
+ WizardStepIndicator(
+ labels: labelledIds.map(_stepLabel).toList(),
+ currentStep: labelledIndex,
+ )
+ // Choice/existing-data steps have no bottom bar, so a
+ // slim top back bar keeps every step past the fork
+ // reversible.
+ else if (_currentIndex > 0)
+ _buildBackBar(),
+ // Keyed so the PageView survives the indicator and
+ // bottom bar toggling in and out of the Column (a
+ // slot shift would otherwise remount it at page 0).
+ Expanded(
+ key: const ValueKey('setup_wizard_pages'),
+ child: PageView(
+ controller: _pageController,
+ physics: const NeverScrollableScrollPhysics(),
+ children: [
+ for (final def in defs)
+ Builder(builder: def.builder),
+ ],
+ ),
+ ),
+ if (inFormZone) _buildBottomBar(defs[_currentIndex]),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget _buildBackBar() {
+ return Align(
+ alignment: AlignmentDirectional.centerStart,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
+ child: IconButton(
+ icon: const Icon(Icons.arrow_back),
+ tooltip: context.l10n.setup_common_back,
+ onPressed: _back,
+ ),
+ ),
+ );
+ }
+
+ Widget _buildBottomBar(WizardStepDef def) {
+ final l10n = context.l10n;
+ return SafeArea(
+ top: false,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
+ child: Row(
+ children: [
+ if (_currentIndex > 0)
+ OutlinedButton(
+ onPressed: _back,
+ child: Text(l10n.setup_common_back),
+ ),
+ const Spacer(),
+ if (_currentIndex < _stepCount() - 1 &&
+ _skippable(_currentStepId()))
+ TextButton(
+ onPressed: _advance,
+ child: Text(l10n.setup_common_skip),
+ ),
+ const SizedBox(width: 8),
+ _NextButton(def: def, onNext: _advance),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+class _NextButton extends ConsumerWidget {
+ final WizardStepDef def;
+ final VoidCallback onNext;
+
+ const _NextButton({required this.def, required this.onNext});
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ final canAdvance = ref.watch(def.canAdvance);
+ return FilledButton(
+ style: FilledButton.styleFrom(minimumSize: const Size(100, 44)),
+ onPressed: canAdvance ? onNext : null,
+ child: Text(context.l10n.setup_common_next),
+ );
+ }
+}
diff --git a/lib/features/setup_wizard/presentation/providers/setup_wizard_providers.dart b/lib/features/setup_wizard/presentation/providers/setup_wizard_providers.dart
new file mode 100644
index 000000000..6273300d5
--- /dev/null
+++ b/lib/features/setup_wizard/presentation/providers/setup_wizard_providers.dart
@@ -0,0 +1,65 @@
+import 'package:submersion/core/providers/provider.dart';
+
+import 'package:submersion/core/data/repositories/sync_repository.dart';
+import 'package:submersion/features/backup/domain/entities/backup_settings.dart';
+import 'package:submersion/features/backup/presentation/providers/backup_providers.dart';
+import 'package:submersion/features/settings/presentation/providers/settings_providers.dart';
+import 'package:submersion/features/settings/presentation/providers/sync_providers.dart';
+import 'package:submersion/features/setup_wizard/domain/setup_wizard_models.dart';
+
+/// Draft state for one wizard session, keyed by mode.
+final setupWizardProvider = StateNotifierProvider.autoDispose
+ .family(
+ (ref, mode) => SetupWizardNotifier(ref, mode),
+ );
+
+class SetupWizardNotifier extends StateNotifier {
+ final Ref _ref;
+
+ SetupWizardNotifier(this._ref, SetupWizardMode mode)
+ : super(SetupWizardDraft(mode: mode)) {
+ if (mode == SetupWizardMode.settings) {
+ // Seed the draft from live state so re-entry edits start from what the
+ // diver already has -- and so pressing Finish without touching a step
+ // re-applies the current values instead of resetting them to defaults
+ // (e.g. disabling an existing backup schedule).
+ final backup = _ref.read(backupSettingsProvider);
+ state = state.copyWith(
+ settings: _ref.read(settingsProvider),
+ backupEnabled: backup.enabled,
+ backupFrequency: backup.frequency,
+ cloudBackupEnabled: backup.cloudBackupEnabled,
+ connectedProvider: _ref.read(selectedCloudProviderTypeProvider),
+ );
+ }
+ }
+
+ void choosePath(SetupPath path) => state = state.copyWith(path: path);
+
+ void chooseSource(ExistingDataSource source) =>
+ state = state.copyWith(source: source);
+
+ void requestSkip() =>
+ state = state.copyWith(path: SetupPath.fresh, skipRequested: true);
+
+ void setName(String name) => state = state.copyWith(name: name.trim());
+
+ void applyUnitPreset(UnitPreset preset) =>
+ state = state.applyingUnitPreset(preset);
+
+ void updateSettings(AppSettings settings) =>
+ state = state.copyWith(settings: settings);
+
+ void setBackupEnabled(bool value) =>
+ state = state.copyWith(backupEnabled: value);
+
+ void setBackupFrequency(BackupFrequency value) =>
+ state = state.copyWith(backupFrequency: value);
+
+ void setCloudBackupEnabled(bool value) =>
+ state = state.copyWith(cloudBackupEnabled: value);
+
+ void setConnectedProvider(CloudProviderType? type) => state = type == null
+ ? state.copyWith(clearConnectedProvider: true)
+ : state.copyWith(connectedProvider: type);
+}
diff --git a/lib/features/setup_wizard/presentation/widgets/steps/backup_sync_step.dart b/lib/features/setup_wizard/presentation/widgets/steps/backup_sync_step.dart
new file mode 100644
index 000000000..7e269f4a1
--- /dev/null
+++ b/lib/features/setup_wizard/presentation/widgets/steps/backup_sync_step.dart
@@ -0,0 +1,294 @@
+import 'package:flutter/material.dart';
+import 'package:go_router/go_router.dart';
+
+import 'package:submersion/core/data/repositories/sync_repository.dart';
+import 'package:submersion/core/providers/provider.dart';
+import 'package:submersion/core/services/cloud_storage/icloud_native_service.dart';
+import 'package:submersion/features/backup/domain/entities/backup_settings.dart';
+import 'package:submersion/features/settings/presentation/pages/s3_config_page.dart';
+import 'package:submersion/features/settings/presentation/providers/sync_providers.dart';
+import 'package:submersion/features/settings/presentation/widgets/dropbox_connect_dialog.dart';
+import 'package:submersion/features/setup_wizard/domain/setup_wizard_models.dart';
+import 'package:submersion/features/setup_wizard/presentation/providers/setup_wizard_providers.dart';
+import 'package:submersion/l10n/l10n_extension.dart';
+
+/// Backup schedule plus optional cloud sync provider connection.
+class BackupSyncStep extends ConsumerStatefulWidget {
+ final SetupWizardMode mode;
+
+ /// Fresh-path pivot: called when a just-connected provider already holds
+ /// a Submersion library (the shell offers adopting it instead).
+ final void Function()? onLibraryFound;
+
+ const BackupSyncStep({super.key, required this.mode, this.onLibraryFound});
+
+ @override
+ ConsumerState createState() => _BackupSyncStepState();
+}
+
+class _BackupSyncStepState extends ConsumerState {
+ bool _connecting = false;
+
+ Future _connect(CloudProviderType type) async {
+ final notifier = ref.read(setupWizardProvider(widget.mode).notifier);
+ setState(() => _connecting = true);
+ try {
+ if (type == CloudProviderType.dropbox) {
+ final connected = await showDialog(
+ context: context,
+ builder: (_) => DropboxConnectDialog(
+ provider: ref.read(dropboxStorageProviderInstanceProvider),
+ ),
+ );
+ ref.invalidate(dropboxAuthDataProvider);
+ if (connected != true) return;
+ }
+ // Activation contract mirrored from CloudSyncPage._selectProvider.
+ ref.read(selectedCloudProviderTypeProvider.notifier).state = type;
+ final instance = cloudProviderInstanceFor(type);
+ await instance.authenticate();
+ await ref.read(syncInitializerProvider).saveProvider(type);
+ ref.read(syncStateProvider.notifier).refreshState();
+ notifier.setConnectedProvider(type);
+
+ // Fresh-path pivot check: does this account already hold a library?
+ if (widget.onLibraryFound != null) {
+ final peers = await ref
+ .read(syncInitializerProvider)
+ .peerSyncFiles(instance);
+ if (peers.isNotEmpty && mounted) widget.onLibraryFound!();
+ }
+ } catch (e) {
+ ref.read(selectedCloudProviderTypeProvider.notifier).state = null;
+ notifier.setConnectedProvider(null);
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text(context.l10n.setup_sync_error(e))),
+ );
+ }
+ } finally {
+ if (mounted) setState(() => _connecting = false);
+ }
+ }
+
+ void _disconnect() {
+ // Drop the in-wizard selection so the provider cards return; don't
+ // persist a backend the user is abandoning.
+ ref.read(selectedCloudProviderTypeProvider.notifier).state = null;
+ ref.read(syncInitializerProvider).saveProvider(null);
+ ref.read(syncStateProvider.notifier).refreshState();
+ ref
+ .read(setupWizardProvider(widget.mode).notifier)
+ .setConnectedProvider(null);
+ }
+
+ Future _openS3Config() async {
+ // Push on the ROOT navigator, not go_router: during first run the wizard
+ // has zero divers, so the router's onboarding redirect bounces any
+ // context.push to a real route straight back to /welcome (the fork).
+ // S3 config is a modal action within the wizard, not a route destination.
+ await Navigator.of(
+ context,
+ rootNavigator: true,
+ ).push(MaterialPageRoute(builder: (_) => const S3ConfigPage()));
+ if (!mounted) return;
+ // S3ConfigPage activates S3 itself on save; reflect it in the draft.
+ final sel = ref.read(selectedCloudProviderTypeProvider);
+ if (sel == CloudProviderType.s3) {
+ ref
+ .read(setupWizardProvider(widget.mode).notifier)
+ .setConnectedProvider(sel);
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+ final l10n = context.l10n;
+ final draft = ref.watch(setupWizardProvider(widget.mode));
+ final notifier = ref.read(setupWizardProvider(widget.mode).notifier);
+
+ final isApple = ref.watch(isApplePlatformProvider);
+ final iCloudAvailability = ref
+ .watch(iCloudAvailabilityProvider)
+ .valueOrNull;
+ final iCloudAvailable =
+ isApple && iCloudAvailability != ICloudAvailability.unsupported;
+ final dropboxConfigured = ref.watch(dropboxConfiguredProvider);
+
+ // The draft holds the connected provider in both modes: seeded from the
+ // active provider on settings re-entry, set by the connect flow at first
+ // run. A non-null value renders connected status instead of the cards.
+ final connected = draft.connectedProvider;
+
+ Widget sectionLabel(String text) => Padding(
+ padding: const EdgeInsets.only(top: 20, bottom: 4),
+ child: Text(text, style: theme.textTheme.titleSmall),
+ );
+
+ return SingleChildScrollView(
+ padding: const EdgeInsets.all(24),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Text(
+ l10n.setup_backup_title,
+ style: theme.textTheme.titleLarge?.copyWith(
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ const SizedBox(height: 8),
+ Text(
+ l10n.setup_backup_subtitle,
+ style: theme.textTheme.bodyMedium?.copyWith(
+ color: theme.colorScheme.onSurfaceVariant,
+ ),
+ ),
+ const SizedBox(height: 16),
+ SwitchListTile(
+ contentPadding: EdgeInsets.zero,
+ title: Text(l10n.setup_backup_scheduleToggle),
+ subtitle: Text(l10n.setup_backup_scheduleSubtitle),
+ value: draft.backupEnabled,
+ onChanged: notifier.setBackupEnabled,
+ ),
+ if (draft.backupEnabled) ...[
+ sectionLabel(l10n.setup_backup_frequency),
+ SegmentedButton(
+ segments: [
+ ButtonSegment(
+ value: BackupFrequency.daily,
+ label: Text(l10n.setup_backup_frequency_daily),
+ ),
+ ButtonSegment(
+ value: BackupFrequency.weekly,
+ label: Text(l10n.setup_backup_frequency_weekly),
+ ),
+ ButtonSegment(
+ value: BackupFrequency.monthly,
+ label: Text(l10n.setup_backup_frequency_monthly),
+ ),
+ ],
+ selected: {draft.backupFrequency},
+ showSelectedIcon: false,
+ onSelectionChanged: (sel) =>
+ notifier.setBackupFrequency(sel.first),
+ ),
+ ],
+ sectionLabel(l10n.setup_sync_header),
+ Text(
+ l10n.setup_sync_subtitle,
+ style: theme.textTheme.bodySmall?.copyWith(
+ color: theme.colorScheme.onSurfaceVariant,
+ ),
+ ),
+ const SizedBox(height: 8),
+ if (connected != null) ...[
+ ListTile(
+ contentPadding: EdgeInsets.zero,
+ leading: const Icon(Icons.cloud_done),
+ title: Text(
+ l10n.setup_sync_connectedTo(_providerName(connected)),
+ ),
+ ),
+ if (widget.mode == SetupWizardMode.settings)
+ Align(
+ alignment: AlignmentDirectional.centerStart,
+ child: TextButton(
+ onPressed: () => context.push('/settings/cloud-sync'),
+ child: Text(l10n.setup_sync_manageInSettings),
+ ),
+ )
+ else
+ // First run: let the user disconnect and pick a different
+ // backend (e.g. after a failed pull), instead of being locked
+ // into the connected provider with no way to change it.
+ Align(
+ alignment: AlignmentDirectional.centerStart,
+ child: TextButton(
+ onPressed: _connecting ? null : _disconnect,
+ child: Text(l10n.setup_sync_changeProvider),
+ ),
+ ),
+ ] else ...[
+ Text(
+ l10n.setup_sync_notConnected,
+ style: theme.textTheme.bodySmall,
+ ),
+ const SizedBox(height: 8),
+ if (iCloudAvailable)
+ _providerCard(
+ icon: Icons.cloud,
+ name: 'iCloud',
+ onTap: _connecting
+ ? null
+ : () => _connect(CloudProviderType.icloud),
+ )
+ // Apple platform where iCloud can't be used (signed out or a
+ // build without the entitlement): explain rather than hide.
+ else if (isApple &&
+ iCloudAvailability == ICloudAvailability.unsupported)
+ Card(
+ margin: const EdgeInsets.symmetric(vertical: 4),
+ child: ListTile(
+ enabled: false,
+ leading: const Icon(Icons.cloud_off),
+ title: const Text('iCloud'),
+ subtitle: Text(l10n.setup_sync_icloudUnavailable),
+ ),
+ ),
+ if (dropboxConfigured)
+ _providerCard(
+ icon: Icons.cloud_queue,
+ name: 'Dropbox',
+ onTap: _connecting
+ ? null
+ : () => _connect(CloudProviderType.dropbox),
+ ),
+ _providerCard(
+ icon: Icons.storage,
+ name: 'S3',
+ onTap: _connecting ? null : _openS3Config,
+ ),
+ ],
+ if (connected != null)
+ SwitchListTile(
+ contentPadding: EdgeInsets.zero,
+ title: Text(l10n.setup_backup_cloudCopy),
+ value: draft.cloudBackupEnabled,
+ onChanged: notifier.setCloudBackupEnabled,
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _providerCard({
+ required IconData icon,
+ required String name,
+ required VoidCallback? onTap,
+ }) {
+ return Card(
+ margin: const EdgeInsets.symmetric(vertical: 4),
+ child: ListTile(
+ leading: Icon(icon),
+ title: Text(name),
+ trailing: const Icon(Icons.chevron_right),
+ onTap: onTap,
+ ),
+ );
+ }
+
+ String _providerName(CloudProviderType type) {
+ switch (type) {
+ case CloudProviderType.icloud:
+ return 'iCloud';
+ case CloudProviderType.dropbox:
+ return 'Dropbox';
+ case CloudProviderType.s3:
+ return 'S3';
+ case CloudProviderType.googledrive:
+ return 'Google Drive';
+ }
+ }
+}
diff --git a/lib/features/setup_wizard/presentation/widgets/steps/existing_choice_step.dart b/lib/features/setup_wizard/presentation/widgets/steps/existing_choice_step.dart
new file mode 100644
index 000000000..23b0e6ded
--- /dev/null
+++ b/lib/features/setup_wizard/presentation/widgets/steps/existing_choice_step.dart
@@ -0,0 +1,90 @@
+import 'package:flutter/material.dart';
+
+import 'package:submersion/core/providers/provider.dart';
+import 'package:submersion/features/setup_wizard/domain/setup_wizard_models.dart';
+import 'package:submersion/features/setup_wizard/presentation/providers/setup_wizard_providers.dart';
+import 'package:submersion/l10n/l10n_extension.dart';
+
+/// Chooses which existing-data source to load from.
+class ExistingChoiceStep extends ConsumerWidget {
+ final SetupWizardMode mode;
+ final VoidCallback onChosen;
+
+ const ExistingChoiceStep({
+ super.key,
+ required this.mode,
+ required this.onChosen,
+ });
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ final theme = Theme.of(context);
+ final l10n = context.l10n;
+ // Keeps the autoDispose family instance alive while this step shows.
+ ref.watch(setupWizardProvider(mode));
+
+ void choose(ExistingDataSource source) {
+ // Read at tap time: a build-time capture could outlive the notifier.
+ ref.read(setupWizardProvider(mode).notifier).chooseSource(source);
+ onChosen();
+ }
+
+ Widget card(
+ IconData icon,
+ String title,
+ String subtitle,
+ ExistingDataSource source,
+ ) {
+ return Card(
+ child: ListTile(
+ leading: Icon(icon, color: theme.colorScheme.primary),
+ title: Text(title),
+ subtitle: Text(subtitle),
+ trailing: const Icon(Icons.chevron_right),
+ onTap: () => choose(source),
+ ),
+ );
+ }
+
+ return SingleChildScrollView(
+ padding: const EdgeInsets.all(24),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Text(
+ l10n.setup_existing_title,
+ style: theme.textTheme.titleLarge?.copyWith(
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ const SizedBox(height: 8),
+ Text(
+ l10n.setup_existing_subtitle,
+ style: theme.textTheme.bodyMedium?.copyWith(
+ color: theme.colorScheme.onSurfaceVariant,
+ ),
+ ),
+ const SizedBox(height: 16),
+ card(
+ Icons.restore,
+ l10n.setup_existing_restore_title,
+ l10n.setup_existing_restore_subtitle,
+ ExistingDataSource.restoreBackup,
+ ),
+ card(
+ Icons.cloud_sync,
+ l10n.setup_existing_sync_title,
+ l10n.setup_existing_sync_subtitle,
+ ExistingDataSource.cloudSync,
+ ),
+ card(
+ Icons.folder_open,
+ l10n.setup_existing_folder_title,
+ l10n.setup_existing_folder_subtitle,
+ ExistingDataSource.openFolder,
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/features/setup_wizard/presentation/widgets/steps/finish_step.dart b/lib/features/setup_wizard/presentation/widgets/steps/finish_step.dart
new file mode 100644
index 000000000..6d941eeb7
--- /dev/null
+++ b/lib/features/setup_wizard/presentation/widgets/steps/finish_step.dart
@@ -0,0 +1,143 @@
+import 'package:flutter/material.dart';
+import 'package:go_router/go_router.dart';
+
+import 'package:submersion/core/providers/provider.dart';
+import 'package:submersion/features/setup_wizard/data/setup_apply_service.dart';
+import 'package:submersion/features/setup_wizard/domain/setup_wizard_models.dart';
+import 'package:submersion/features/setup_wizard/presentation/providers/setup_wizard_providers.dart';
+import 'package:submersion/l10n/l10n_extension.dart';
+
+/// Feature-discovery destinations offered on the finish screen, in display
+/// order. Public so a router test can assert each one resolves to a real
+/// route (a bad link 404s to the "Page Not Found" screen).
+const kSetupFinishFeatureRoutes = [
+ '/dive-computers/discover', // dive computer download
+ '/transfer', // file import
+ '/statistics', // statistics
+ '/sites', // dive sites map
+ '/equipment', // gear / service tracking
+];
+
+/// Final step: feature discovery plus the apply-and-go action.
+class FinishStep extends ConsumerStatefulWidget {
+ final SetupWizardMode mode;
+
+ const FinishStep({super.key, required this.mode});
+
+ @override
+ ConsumerState createState() => _FinishStepState();
+}
+
+class _FinishStepState extends ConsumerState {
+ bool _applying = false;
+
+ Future _complete({String route = '/dashboard'}) async {
+ if (_applying) return;
+ setState(() => _applying = true);
+ try {
+ final draft = ref.read(setupWizardProvider(widget.mode));
+ final service = ref.read(setupApplyServiceProvider);
+ if (widget.mode == SetupWizardMode.firstRun) {
+ await service.applyFirstRun(draft);
+ } else {
+ await service.applySettingsMode(draft);
+ }
+ if (!mounted) return;
+ if (widget.mode == SetupWizardMode.firstRun || route != '/dashboard') {
+ context.go(route);
+ } else {
+ context.pop();
+ }
+ } catch (e) {
+ if (mounted) {
+ setState(() => _applying = false);
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text(context.l10n.setup_finish_error(e))),
+ );
+ }
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+ final l10n = context.l10n;
+
+ final features = <(IconData, String, String)>[
+ (
+ Icons.watch,
+ l10n.setup_finish_feature_diveComputer,
+ kSetupFinishFeatureRoutes[0],
+ ),
+ (
+ Icons.file_upload,
+ l10n.setup_finish_feature_import,
+ kSetupFinishFeatureRoutes[1],
+ ),
+ (
+ Icons.query_stats,
+ l10n.setup_finish_feature_statistics,
+ kSetupFinishFeatureRoutes[2],
+ ),
+ (
+ Icons.map,
+ l10n.setup_finish_feature_sites,
+ kSetupFinishFeatureRoutes[3],
+ ),
+ (
+ Icons.build,
+ l10n.setup_finish_feature_gear,
+ kSetupFinishFeatureRoutes[4],
+ ),
+ ];
+
+ return SingleChildScrollView(
+ padding: const EdgeInsets.all(24),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Icon(Icons.check_circle, size: 56, color: theme.colorScheme.primary),
+ const SizedBox(height: 16),
+ Text(
+ l10n.setup_finish_title,
+ style: theme.textTheme.headlineSmall?.copyWith(
+ fontWeight: FontWeight.bold,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: 8),
+ Text(
+ l10n.setup_finish_subtitle,
+ style: theme.textTheme.bodyMedium?.copyWith(
+ color: theme.colorScheme.onSurfaceVariant,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: 16),
+ for (final (icon, label, route) in features)
+ ListTile(
+ contentPadding: EdgeInsets.zero,
+ leading: Icon(icon, color: theme.colorScheme.primary),
+ title: Text(label),
+ trailing: const Icon(Icons.chevron_right, size: 18),
+ onTap: _applying ? null : () => _complete(route: route),
+ ),
+ const SizedBox(height: 24),
+ FilledButton.icon(
+ onPressed: _applying ? null : _complete,
+ icon: _applying
+ ? const SizedBox(
+ width: 18,
+ height: 18,
+ child: CircularProgressIndicator(strokeWidth: 2),
+ )
+ : const Icon(Icons.arrow_forward),
+ label: Text(
+ _applying ? l10n.setup_finish_applying : l10n.setup_finish_start,
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/features/setup_wizard/presentation/widgets/steps/open_folder_step.dart b/lib/features/setup_wizard/presentation/widgets/steps/open_folder_step.dart
new file mode 100644
index 000000000..078e96464
--- /dev/null
+++ b/lib/features/setup_wizard/presentation/widgets/steps/open_folder_step.dart
@@ -0,0 +1,92 @@
+import 'package:flutter/material.dart';
+
+import 'package:submersion/core/providers/provider.dart';
+import 'package:submersion/features/settings/presentation/providers/storage_providers.dart';
+import 'package:submersion/l10n/l10n_extension.dart';
+import 'package:submersion/main.dart' show restartApp;
+
+/// Points the app at a folder that already contains a Submersion database.
+class OpenFolderStep extends ConsumerStatefulWidget {
+ const OpenFolderStep({super.key});
+
+ @override
+ ConsumerState createState() => _OpenFolderStepState();
+}
+
+class _OpenFolderStepState extends ConsumerState {
+ bool _busy = false;
+ String? _error;
+
+ Future _pickFolder() async {
+ setState(() {
+ _busy = true;
+ _error = null;
+ });
+ final l10n = context.l10n;
+ try {
+ final notifier = ref.read(storageConfigNotifierProvider.notifier);
+ final picked = await notifier.pickCustomFolder();
+ if (picked == null) return;
+
+ final existing = await notifier.checkForExistingDatabase(picked.path);
+ if (existing == null) {
+ setState(() => _error = l10n.setup_folder_notFound_message);
+ return;
+ }
+
+ final result = await notifier.switchToExistingDatabase(picked.path);
+ if (result.success) {
+ // Full provider rebuild: the swapped database has divers, so the
+ // relaunch lands on the dashboard.
+ restartApp();
+ } else {
+ setState(() => _error = result.errorMessage ?? '');
+ }
+ } finally {
+ if (mounted) setState(() => _busy = false);
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+ final l10n = context.l10n;
+
+ return Center(
+ child: Padding(
+ padding: const EdgeInsets.all(24),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Text(
+ l10n.setup_folder_title,
+ style: theme.textTheme.titleLarge?.copyWith(
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ const SizedBox(height: 24),
+ if (_busy) ...[
+ const CircularProgressIndicator(),
+ const SizedBox(height: 12),
+ Text(l10n.setup_folder_switching),
+ ] else ...[
+ if (_error != null) ...[
+ Text(
+ _error!,
+ style: TextStyle(color: theme.colorScheme.error),
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: 12),
+ ],
+ FilledButton.icon(
+ onPressed: _pickFolder,
+ icon: const Icon(Icons.folder_open),
+ label: Text(l10n.setup_folder_pick),
+ ),
+ ],
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/features/setup_wizard/presentation/widgets/steps/profile_step.dart b/lib/features/setup_wizard/presentation/widgets/steps/profile_step.dart
new file mode 100644
index 000000000..026c4b832
--- /dev/null
+++ b/lib/features/setup_wizard/presentation/widgets/steps/profile_step.dart
@@ -0,0 +1,76 @@
+import 'package:flutter/material.dart';
+
+import 'package:submersion/core/providers/provider.dart';
+import 'package:submersion/features/setup_wizard/domain/setup_wizard_models.dart';
+import 'package:submersion/features/setup_wizard/presentation/providers/setup_wizard_providers.dart';
+import 'package:submersion/l10n/l10n_extension.dart';
+
+/// Collects the diver name (the wizard's only mandatory input).
+class ProfileStep extends ConsumerStatefulWidget {
+ final SetupWizardMode mode;
+
+ const ProfileStep({super.key, required this.mode});
+
+ @override
+ ConsumerState createState() => _ProfileStepState();
+}
+
+class _ProfileStepState extends ConsumerState {
+ late final TextEditingController _nameController;
+
+ @override
+ void initState() {
+ super.initState();
+ _nameController = TextEditingController(
+ text: ref.read(setupWizardProvider(widget.mode)).name,
+ );
+ }
+
+ @override
+ void dispose() {
+ _nameController.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+ final l10n = context.l10n;
+ final notifier = ref.read(setupWizardProvider(widget.mode).notifier);
+
+ return SingleChildScrollView(
+ padding: const EdgeInsets.all(24),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Text(
+ l10n.setup_profile_title,
+ style: theme.textTheme.titleLarge?.copyWith(
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ const SizedBox(height: 8),
+ Text(
+ l10n.setup_profile_subtitle,
+ style: theme.textTheme.bodyMedium?.copyWith(
+ color: theme.colorScheme.onSurfaceVariant,
+ ),
+ ),
+ const SizedBox(height: 24),
+ TextFormField(
+ controller: _nameController,
+ decoration: InputDecoration(
+ labelText: l10n.setup_profile_nameLabel,
+ hintText: l10n.setup_profile_nameHint,
+ prefixIcon: const Icon(Icons.person),
+ ),
+ textCapitalization: TextCapitalization.words,
+ autofocus: true,
+ textInputAction: TextInputAction.done,
+ onChanged: notifier.setName,
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/features/setup_wizard/presentation/widgets/steps/restore_step.dart b/lib/features/setup_wizard/presentation/widgets/steps/restore_step.dart
new file mode 100644
index 000000000..cbbc72daf
--- /dev/null
+++ b/lib/features/setup_wizard/presentation/widgets/steps/restore_step.dart
@@ -0,0 +1,125 @@
+import 'dart:io';
+
+import 'package:file_picker/file_picker.dart';
+import 'package:flutter/material.dart';
+
+import 'package:submersion/core/database/database.dart';
+import 'package:submersion/core/providers/provider.dart';
+import 'package:submersion/features/backup/domain/entities/backup_record.dart';
+import 'package:submersion/features/backup/presentation/pages/restore_complete_page.dart';
+import 'package:submersion/features/backup/presentation/providers/backup_providers.dart';
+import 'package:submersion/features/backup/presentation/widgets/restore_confirmation_dialog.dart';
+import 'package:submersion/l10n/l10n_extension.dart';
+
+/// A chosen backup file: its absolute path and display name.
+typedef PickedBackupFile = ({String path, String name});
+
+/// Restores a backup file; exits the wizard via the existing
+/// RestoreCompletePage -> restartApp() flow.
+class RestoreStep extends ConsumerWidget {
+ /// [pickBackupFile] is injectable so the restore flow can be exercised
+ /// without a real platform file picker; it defaults to file_picker.
+ const RestoreStep({super.key, this.pickBackupFile = _pickViaFilePicker});
+
+ final Future Function() pickBackupFile;
+
+ static Future _pickViaFilePicker() async {
+ final result = await FilePicker.pickFiles(type: FileType.any);
+ if (result == null || result.files.isEmpty) return null;
+ final file = result.files.single;
+ final path = file.path;
+ return path == null ? null : (path: path, name: file.name);
+ }
+
+ Future _pickAndRestore(BuildContext context, WidgetRef ref) async {
+ final PickedBackupFile? picked;
+ try {
+ picked = await pickBackupFile();
+ } on Exception catch (e) {
+ if (context.mounted) {
+ ScaffoldMessenger.of(
+ context,
+ ).showSnackBar(SnackBar(content: Text('$e')));
+ }
+ return;
+ }
+ if (picked == null || !context.mounted) return;
+
+ final file = File(picked.path);
+ final record = BackupRecord(
+ id: 'setup-wizard',
+ filename: picked.name,
+ timestamp: await file.lastModified(),
+ sizeBytes: await file.length(),
+ location: BackupLocation.local,
+ diveCount: 0,
+ siteCount: 0,
+ );
+ if (!context.mounted) return;
+
+ final mode = await RestoreConfirmationDialog.show(
+ context,
+ record,
+ currentSchemaVersion: AppDatabase.currentSchemaVersion,
+ );
+ if (mode != null) {
+ await ref
+ .read(backupOperationProvider.notifier)
+ .restoreFromFilePath(picked.path, mode: mode);
+ }
+ }
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ final theme = Theme.of(context);
+ final l10n = context.l10n;
+ final operation = ref.watch(backupOperationProvider);
+
+ ref.listen(backupOperationProvider, (prev, next) {
+ if (next.status == BackupOperationStatus.restoreComplete &&
+ context.mounted) {
+ RestoreCompletePage.show(context);
+ }
+ });
+
+ final inProgress = operation.status == BackupOperationStatus.inProgress;
+
+ return Center(
+ child: Padding(
+ padding: const EdgeInsets.all(24),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Text(
+ l10n.setup_restore_title,
+ style: theme.textTheme.titleLarge?.copyWith(
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ const SizedBox(height: 24),
+ if (inProgress) ...[
+ const CircularProgressIndicator(),
+ const SizedBox(height: 12),
+ Text(operation.message ?? l10n.setup_restore_inProgress),
+ ] else ...[
+ if (operation.status == BackupOperationStatus.error &&
+ operation.message != null) ...[
+ Text(
+ operation.message!,
+ style: TextStyle(color: theme.colorScheme.error),
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: 12),
+ ],
+ FilledButton.icon(
+ onPressed: () => _pickAndRestore(context, ref),
+ icon: const Icon(Icons.folder_open),
+ label: Text(l10n.setup_restore_pick),
+ ),
+ ],
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/features/setup_wizard/presentation/widgets/steps/sync_connect_step.dart b/lib/features/setup_wizard/presentation/widgets/steps/sync_connect_step.dart
new file mode 100644
index 000000000..5d9c0a8bb
--- /dev/null
+++ b/lib/features/setup_wizard/presentation/widgets/steps/sync_connect_step.dart
@@ -0,0 +1,194 @@
+import 'package:flutter/material.dart';
+import 'package:go_router/go_router.dart';
+
+import 'package:submersion/core/providers/provider.dart';
+import 'package:submersion/features/divers/presentation/providers/diver_providers.dart';
+import 'package:submersion/features/settings/presentation/providers/settings_providers.dart';
+import 'package:submersion/features/settings/presentation/providers/sync_providers.dart';
+import 'package:submersion/features/setup_wizard/domain/setup_wizard_models.dart';
+import 'package:submersion/features/setup_wizard/presentation/providers/setup_wizard_providers.dart';
+import 'package:submersion/features/setup_wizard/presentation/widgets/steps/backup_sync_step.dart';
+import 'package:submersion/l10n/l10n_extension.dart';
+
+/// Existing-data path: connect a provider, pull the library, land on the
+/// dashboard once data (and its divers) have arrived.
+class SyncConnectStep extends ConsumerStatefulWidget {
+ final SetupWizardMode mode;
+
+ /// Called when the connected account has no library (pivot to fresh).
+ final VoidCallback onNoLibrary;
+
+ const SyncConnectStep({
+ super.key,
+ required this.mode,
+ required this.onNoLibrary,
+ });
+
+ @override
+ ConsumerState createState() => _SyncConnectStepState();
+}
+
+enum _PullPhase { connect, pulling, done, empty }
+
+class _SyncConnectStepState extends ConsumerState {
+ _PullPhase _phase = _PullPhase.connect;
+
+ Future _startPull() async {
+ setState(() => _phase = _PullPhase.pulling);
+ final connected = ref
+ .read(setupWizardProvider(widget.mode))
+ .connectedProvider;
+ if (connected == null) {
+ setState(() => _phase = _PullPhase.connect);
+ return;
+ }
+ try {
+ final instance = cloudProviderInstanceFor(connected);
+ final peers = await ref
+ .read(syncInitializerProvider)
+ .peerSyncFiles(instance);
+ if (peers.isEmpty) {
+ if (mounted) setState(() => _phase = _PullPhase.empty);
+ return;
+ }
+ await ref.read(syncStateProvider.notifier).performSync();
+ final syncState = ref.read(syncStateProvider);
+ if (syncState.status == SyncStatus.error) {
+ // A failed sync must not fall through to the "No library found" UI.
+ if (mounted) {
+ setState(() => _phase = _PullPhase.connect);
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text(
+ context.l10n.setup_sync_error(syncState.message ?? ''),
+ ),
+ ),
+ );
+ }
+ return;
+ }
+ await realignActiveDiverAfterDataReplace(
+ ref.read(sharedPreferencesProvider),
+ );
+ final hasDivers = await ref.read(hasAnyDiversProvider.future);
+ if (mounted) {
+ setState(() => _phase = hasDivers ? _PullPhase.done : _PullPhase.empty);
+ }
+ } catch (e) {
+ // Listing peers or syncing can fail (network/auth). Return to the
+ // connect UI with an error rather than crashing or hanging on the
+ // spinner.
+ if (mounted) {
+ setState(() => _phase = _PullPhase.connect);
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text(context.l10n.setup_sync_error(e))),
+ );
+ }
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+ final l10n = context.l10n;
+ final syncState = ref.watch(syncStateProvider);
+
+ switch (_phase) {
+ case _PullPhase.connect:
+ // Reuse the provider cards; connecting enables the continue gate.
+ return Column(
+ children: [
+ Expanded(child: BackupSyncStep(mode: widget.mode)),
+ SafeArea(
+ top: false,
+ child: Padding(
+ padding: const EdgeInsets.all(16),
+ child: _ConnectedGate(
+ mode: widget.mode,
+ onContinue: _startPull,
+ ),
+ ),
+ ),
+ ],
+ );
+ case _PullPhase.pulling:
+ return Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ CircularProgressIndicator(value: syncState.progress),
+ const SizedBox(height: 12),
+ Text(syncState.message ?? l10n.setup_syncPull_syncing),
+ ],
+ ),
+ );
+ case _PullPhase.done:
+ return Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(
+ Icons.check_circle,
+ size: 56,
+ color: theme.colorScheme.primary,
+ ),
+ const SizedBox(height: 12),
+ Text(
+ l10n.setup_syncPull_success,
+ style: theme.textTheme.titleLarge,
+ ),
+ const SizedBox(height: 16),
+ FilledButton(
+ onPressed: () => context.go('/dashboard'),
+ child: Text(l10n.setup_syncPull_continue),
+ ),
+ ],
+ ),
+ );
+ case _PullPhase.empty:
+ return Center(
+ child: Padding(
+ padding: const EdgeInsets.all(24),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Text(
+ l10n.setup_syncPull_noLibrary_title,
+ style: theme.textTheme.titleLarge,
+ ),
+ const SizedBox(height: 8),
+ Text(
+ l10n.setup_syncPull_noLibrary_message,
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: 16),
+ FilledButton(
+ onPressed: widget.onNoLibrary,
+ child: Text(l10n.setup_sync_libraryFound_keepFresh),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+ }
+}
+
+/// Continue button enabled once a provider is connected in the draft.
+class _ConnectedGate extends ConsumerWidget {
+ final SetupWizardMode mode;
+ final VoidCallback onContinue;
+
+ const _ConnectedGate({required this.mode, required this.onContinue});
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ final connected = ref.watch(
+ setupWizardProvider(mode).select((d) => d.connectedProvider != null),
+ );
+ return FilledButton(
+ onPressed: connected ? onContinue : null,
+ child: Text(context.l10n.setup_syncPull_continue),
+ );
+ }
+}
diff --git a/lib/features/setup_wizard/presentation/widgets/steps/units_step.dart b/lib/features/setup_wizard/presentation/widgets/steps/units_step.dart
new file mode 100644
index 000000000..8c0e70331
--- /dev/null
+++ b/lib/features/setup_wizard/presentation/widgets/steps/units_step.dart
@@ -0,0 +1,248 @@
+import 'package:flutter/material.dart';
+
+import 'package:submersion/core/constants/units.dart';
+import 'package:submersion/core/providers/provider.dart';
+import 'package:submersion/features/settings/presentation/providers/settings_providers.dart';
+import 'package:submersion/features/setup_wizard/domain/setup_wizard_models.dart';
+import 'package:submersion/features/setup_wizard/domain/unit_preset_detector.dart';
+import 'package:submersion/features/setup_wizard/presentation/providers/setup_wizard_providers.dart';
+import 'package:submersion/l10n/l10n_extension.dart';
+
+/// Metric/Imperial preset with an expander for per-unit fine-tuning.
+class UnitsStep extends ConsumerStatefulWidget {
+ final SetupWizardMode mode;
+
+ const UnitsStep({super.key, required this.mode});
+
+ @override
+ ConsumerState createState() => _UnitsStepState();
+}
+
+class _UnitsStepState extends ConsumerState {
+ @override
+ void initState() {
+ super.initState();
+ // First-run: preselect the preset matching the device locale, once.
+ if (widget.mode == SetupWizardMode.firstRun) {
+ final draft = ref.read(setupWizardProvider(widget.mode));
+ if (draft.settings.unitPreset == UnitPreset.metric) {
+ final locale = WidgetsBinding.instance.platformDispatcher.locale;
+ final preset = presetForLocale(locale);
+ if (preset != UnitPreset.metric) {
+ Future.microtask(() {
+ if (mounted) {
+ ref
+ .read(setupWizardProvider(widget.mode).notifier)
+ .applyUnitPreset(preset);
+ }
+ });
+ }
+ }
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+ final l10n = context.l10n;
+ final draft = ref.watch(setupWizardProvider(widget.mode));
+ final notifier = ref.read(setupWizardProvider(widget.mode).notifier);
+ final s = draft.settings;
+
+ return SingleChildScrollView(
+ padding: const EdgeInsets.all(24),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Text(
+ l10n.setup_units_title,
+ style: theme.textTheme.titleLarge?.copyWith(
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ const SizedBox(height: 8),
+ Text(
+ l10n.setup_units_subtitle,
+ style: theme.textTheme.bodyMedium?.copyWith(
+ color: theme.colorScheme.onSurfaceVariant,
+ ),
+ ),
+ const SizedBox(height: 24),
+ SegmentedButton(
+ segments: [
+ ButtonSegment(
+ value: UnitPreset.metric,
+ label: Text(l10n.setup_units_metric),
+ ),
+ ButtonSegment(
+ value: UnitPreset.imperial,
+ label: Text(l10n.setup_units_imperial),
+ ),
+ ],
+ emptySelectionAllowed: true,
+ selected: s.unitPreset == UnitPreset.custom
+ ? const {}
+ : {s.unitPreset},
+ onSelectionChanged: (selection) {
+ if (selection.isNotEmpty) {
+ notifier.applyUnitPreset(selection.first);
+ }
+ },
+ ),
+ const SizedBox(height: 16),
+ ExpansionTile(
+ title: Text(l10n.setup_units_advanced),
+ tilePadding: EdgeInsets.zero,
+ childrenPadding: const EdgeInsets.only(bottom: 8),
+ children: [
+ _unitRow(
+ label: l10n.setup_units_depth,
+ keyPrefix: 'setup-unit-depth',
+ values: DepthUnit.values,
+ selected: s.depthUnit,
+ symbol: (u) => u.symbol,
+ onChanged: (u) =>
+ notifier.updateSettings(s.copyWith(depthUnit: u)),
+ ),
+ _unitRow(
+ label: l10n.setup_units_temperature,
+ keyPrefix: 'setup-unit-temperature',
+ values: TemperatureUnit.values,
+ selected: s.temperatureUnit,
+ symbol: (u) => u.symbol,
+ onChanged: (u) =>
+ notifier.updateSettings(s.copyWith(temperatureUnit: u)),
+ ),
+ _unitRow(
+ label: l10n.setup_units_pressure,
+ keyPrefix: 'setup-unit-pressure',
+ values: PressureUnit.values,
+ selected: s.pressureUnit,
+ symbol: (u) => u.symbol,
+ onChanged: (u) =>
+ notifier.updateSettings(s.copyWith(pressureUnit: u)),
+ ),
+ _unitRow(
+ label: l10n.setup_units_volume,
+ keyPrefix: 'setup-unit-volume',
+ values: VolumeUnit.values,
+ selected: s.volumeUnit,
+ symbol: (u) => u.symbol,
+ onChanged: (u) =>
+ notifier.updateSettings(s.copyWith(volumeUnit: u)),
+ ),
+ _unitRow(
+ label: l10n.setup_units_weight,
+ keyPrefix: 'setup-unit-weight',
+ values: WeightUnit.values,
+ selected: s.weightUnit,
+ symbol: (u) => u.symbol,
+ onChanged: (u) =>
+ notifier.updateSettings(s.copyWith(weightUnit: u)),
+ ),
+ _unitRow(
+ label: l10n.setup_units_altitude,
+ keyPrefix: 'setup-unit-altitude',
+ values: AltitudeUnit.values,
+ selected: s.altitudeUnit,
+ symbol: (u) => u.symbol,
+ onChanged: (u) =>
+ notifier.updateSettings(s.copyWith(altitudeUnit: u)),
+ ),
+ _unitRow(
+ label: l10n.setup_units_sac,
+ keyPrefix: 'setup-unit-sac',
+ values: SacUnit.values,
+ selected: s.sacUnit,
+ symbol: (u) => u.symbol,
+ onChanged: (u) =>
+ notifier.updateSettings(s.copyWith(sacUnit: u)),
+ ),
+ _unitRow(
+ label: l10n.setup_units_timeFormat,
+ keyPrefix: 'setup-unit-timeformat',
+ values: TimeFormat.values,
+ selected: s.timeFormat,
+ symbol: (u) => u.displayName,
+ onChanged: (u) =>
+ notifier.updateSettings(s.copyWith(timeFormat: u)),
+ ),
+ // Five wide options; a segmented control overflows the row.
+ _dropdownRow(
+ label: l10n.setup_units_dateFormat,
+ values: DateFormatPreference.values,
+ selected: s.dateFormat,
+ display: (u) => u.displayName,
+ onChanged: (u) =>
+ notifier.updateSettings(s.copyWith(dateFormat: u)),
+ ),
+ ],
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _unitRow({
+ required String label,
+ required String keyPrefix,
+ required List values,
+ required T selected,
+ required String Function(T) symbol,
+ required void Function(T) onChanged,
+ }) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(vertical: 6),
+ child: Row(
+ children: [
+ Expanded(child: Text(label)),
+ SegmentedButton(
+ segments: [
+ for (final v in values)
+ ButtonSegment(
+ value: v,
+ label: Text(
+ symbol(v),
+ key: ValueKey('$keyPrefix-${_segmentKey(symbol(v))}'),
+ ),
+ ),
+ ],
+ selected: {selected},
+ showSelectedIcon: false,
+ onSelectionChanged: (sel) => onChanged(sel.first),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _dropdownRow({
+ required String label,
+ required List values,
+ required T selected,
+ required String Function(T) display,
+ required void Function(T) onChanged,
+ }) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(vertical: 6),
+ child: Row(
+ children: [
+ Expanded(child: Text(label)),
+ DropdownButton(
+ value: selected,
+ items: [
+ for (final v in values)
+ DropdownMenuItem(value: v, child: Text(display(v))),
+ ],
+ onChanged: (v) {
+ if (v != null) onChanged(v);
+ },
+ ),
+ ],
+ ),
+ );
+ }
+
+ String _segmentKey(String symbol) =>
+ symbol.toLowerCase().replaceAll(RegExp(r'[^a-z0-9]'), '');
+}
diff --git a/lib/features/setup_wizard/presentation/widgets/steps/welcome_fork_step.dart b/lib/features/setup_wizard/presentation/widgets/steps/welcome_fork_step.dart
new file mode 100644
index 000000000..3cf6568e7
--- /dev/null
+++ b/lib/features/setup_wizard/presentation/widgets/steps/welcome_fork_step.dart
@@ -0,0 +1,134 @@
+import 'package:flutter/material.dart';
+
+import 'package:submersion/l10n/l10n_extension.dart';
+
+/// First-run fork: start fresh, bring existing data, or skip setup.
+class WelcomeForkStep extends StatelessWidget {
+ final VoidCallback onStartFresh;
+ final VoidCallback onExistingData;
+ final VoidCallback onSkipSetup;
+
+ const WelcomeForkStep({
+ super.key,
+ required this.onStartFresh,
+ required this.onExistingData,
+ required this.onSkipSetup,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+ final l10n = context.l10n;
+
+ return SingleChildScrollView(
+ padding: const EdgeInsets.all(24),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ ExcludeSemantics(
+ child: Center(
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(24),
+ child: Image.asset(
+ 'assets/icon/icon.png',
+ width: 96,
+ height: 96,
+ ),
+ ),
+ ),
+ ),
+ const SizedBox(height: 24),
+ Text(
+ l10n.setup_welcome_title,
+ style: theme.textTheme.headlineSmall?.copyWith(
+ fontWeight: FontWeight.bold,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: 8),
+ Text(
+ l10n.setup_welcome_subtitle,
+ style: theme.textTheme.bodyMedium?.copyWith(
+ color: theme.colorScheme.onSurfaceVariant,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: 32),
+ _ForkCard(
+ icon: Icons.add_circle_outline,
+ title: l10n.setup_welcome_startFresh_title,
+ subtitle: l10n.setup_welcome_startFresh_subtitle,
+ onTap: onStartFresh,
+ ),
+ const SizedBox(height: 12),
+ _ForkCard(
+ icon: Icons.cloud_download_outlined,
+ title: l10n.setup_welcome_existingData_title,
+ subtitle: l10n.setup_welcome_existingData_subtitle,
+ onTap: onExistingData,
+ ),
+ const SizedBox(height: 24),
+ TextButton(
+ onPressed: onSkipSetup,
+ child: Text(l10n.setup_welcome_skipSetup),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class _ForkCard extends StatelessWidget {
+ final IconData icon;
+ final String title;
+ final String subtitle;
+ final VoidCallback onTap;
+
+ const _ForkCard({
+ required this.icon,
+ required this.title,
+ required this.subtitle,
+ required this.onTap,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+ return Card(
+ child: InkWell(
+ borderRadius: BorderRadius.circular(12),
+ onTap: onTap,
+ child: Padding(
+ padding: const EdgeInsets.all(16),
+ child: Row(
+ children: [
+ Icon(icon, size: 32, color: theme.colorScheme.primary),
+ const SizedBox(width: 16),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ title,
+ style: theme.textTheme.titleMedium?.copyWith(
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ const SizedBox(height: 4),
+ Text(
+ subtitle,
+ style: theme.textTheme.bodySmall?.copyWith(
+ color: theme.colorScheme.onSurfaceVariant,
+ ),
+ ),
+ ],
+ ),
+ ),
+ const Icon(Icons.chevron_right),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb
index 70b410e5c..3db76ae60 100644
--- a/lib/l10n/arb/app_ar.arb
+++ b/lib/l10n/arb/app_ar.arb
@@ -67,6 +67,94 @@
"settings_cloudSync_postRestore_syncing": "تتم مزامنة مكتبتك المستعادة مع السحابة…",
"settings_cloudSync_postRestore_synced": "تمت مزامنة المكتبة المستعادة.",
"settings_cloudSync_replace_reviewAction": "مراجعة",
+ "settings_manage_setupAssistant": "مساعد الإعداد",
+ "settings_manage_setupAssistant_subtitle": "مراجعة الوحدات والمظهر وخيارات النسخ الاحتياطي",
+ "setup_backup_cloudCopy": "الاحتفاظ بنسخة احتياطية في السحابة",
+ "setup_backup_frequency": "التكرار",
+ "setup_backup_frequency_daily": "يوميًا",
+ "setup_backup_frequency_monthly": "شهريًا",
+ "setup_backup_frequency_weekly": "أسبوعيًا",
+ "setup_backup_scheduleSubtitle": "انسخ سجل غوصك احتياطيًا وفق جدول زمني",
+ "setup_backup_scheduleToggle": "نسخ احتياطي تلقائي",
+ "setup_backup_subtitle": "احمِ سجل غوصك من اليوم الأول.",
+ "setup_backup_title": "النسخ الاحتياطي والمزامنة",
+ "setup_common_back": "رجوع",
+ "setup_common_next": "التالي",
+ "setup_common_skip": "تخطي",
+ "setup_existing_folder_subtitle": "وجّه Submersion إلى مجلد يحتوي بالفعل على مكتبة",
+ "setup_existing_folder_title": "فتح مجلد موجود",
+ "setup_existing_restore_subtitle": "اختر ملف نسخ احتياطي تم تصديره من Submersion",
+ "setup_existing_restore_title": "استعادة ملف نسخ احتياطي",
+ "setup_existing_subtitle": "اختر طريقة تحميل مكتبة Submersion الحالية",
+ "setup_existing_sync_subtitle": "اسحب مكتبتك من iCloud أو Dropbox أو S3",
+ "setup_existing_sync_title": "توصيل مزامنة السحابة",
+ "setup_existing_title": "أحضر بياناتك",
+ "setup_finish_applying": "جارٍ الإعداد...",
+ "setup_finish_error": "تعذّر إكمال الإعداد: {error}",
+ "setup_finish_feature_diveComputer": "نزّل الغوصات من كمبيوتر الغوص",
+ "setup_finish_feature_gear": "تتبّع المعدات ومواعيد الصيانة",
+ "setup_finish_feature_import": "استورد السجلات من الملفات والتطبيقات الأخرى",
+ "setup_finish_feature_sites": "اعرض مواقع الغوص على الخريطة",
+ "setup_finish_feature_statistics": "استكشف إحصاءات الغوص",
+ "setup_finish_start": "ابدأ التسجيل",
+ "setup_finish_subtitle": "يمكن لـ Submersion أيضًا...",
+ "setup_finish_title": "كل شيء جاهز",
+ "setup_folder_notFound_message": "المجلد المحدد لا يحتوي على قاعدة بيانات Submersion.",
+ "setup_folder_notFound_title": "لا توجد مكتبة في هذا المجلد",
+ "setup_folder_pick": "اختيار مجلد",
+ "setup_folder_switching": "جارٍ فتح المكتبة...",
+ "setup_folder_title": "فتح مجلد موجود",
+ "setup_profile_nameHint": "أدخل اسمك",
+ "setup_profile_nameLabel": "اسمك",
+ "setup_profile_nameValidation": "الرجاء إدخال اسمك",
+ "setup_profile_subtitle": "أدخل اسمك للبدء. يمكنك إضافة المزيد من التفاصيل لاحقاً.",
+ "setup_profile_title": "إنشاء ملفك الشخصي",
+ "setup_restore_inProgress": "جارٍ الاستعادة...",
+ "setup_restore_pick": "اختيار ملف النسخ الاحتياطي",
+ "setup_restore_title": "استعادة النسخة الاحتياطية",
+ "setup_step_backup": "نسخ احتياطي",
+ "setup_step_finish": "تم",
+ "setup_step_profile": "الملف الشخصي",
+ "setup_step_units": "الوحدات",
+ "setup_syncPull_continue": "متابعة",
+ "setup_syncPull_noLibrary_message": "لم يتم العثور على مكتبة Submersion في هذا الحساب. هل تبدأ من جديد؟ سيتم الاحتفاظ باتصالك.",
+ "setup_syncPull_noLibrary_title": "لم يتم العثور على مكتبة",
+ "setup_syncPull_success": "تم اعتماد المكتبة",
+ "setup_syncPull_syncing": "جارٍ سحب مكتبتك...",
+ "setup_syncPull_title": "الاتصال والسحب",
+ "setup_sync_changeProvider": "تغيير المزوّد",
+ "setup_sync_connectedTo": "متصل بـ {provider}",
+ "setup_sync_error": "تعذّر الاتصال: {error}",
+ "setup_sync_header": "مزامنة السحابة",
+ "setup_sync_icloudUnavailable": "iCloud غير متاح على هذا الجهاز",
+ "setup_sync_libraryFound_adopt": "اعتماد المكتبة الحالية",
+ "setup_sync_libraryFound_keepFresh": "البدء من جديد",
+ "setup_sync_libraryFound_message": "يحتوي هذا الحساب بالفعل على مكتبة Submersion. هل تريد اعتمادها بدلًا من البدء من جديد؟",
+ "setup_sync_libraryFound_title": "تم العثور على مكتبة موجودة",
+ "setup_sync_manageInSettings": "الإدارة في الإعدادات",
+ "setup_sync_notConnected": "غير متصل",
+ "setup_sync_subtitle": "زامن سجلك عبر الأجهزة",
+ "setup_units_advanced": "ضبط الوحدات",
+ "setup_units_altitude": "الارتفاع",
+ "setup_units_dateFormat": "تنسيق التاريخ",
+ "setup_units_depth": "العمق",
+ "setup_units_imperial": "إمبراطوري",
+ "setup_units_metric": "متري",
+ "setup_units_pressure": "الضغط",
+ "setup_units_sac": "معدل SAC",
+ "setup_units_subtitle": "اختر كيفية عرض القياسات. يمكنك ضبط كل وحدة.",
+ "setup_units_temperature": "درجة الحرارة",
+ "setup_units_timeFormat": "تنسيق الوقت",
+ "setup_units_title": "الوحدات",
+ "setup_units_volume": "الحجم",
+ "setup_units_weight": "الوزن",
+ "setup_welcome_existingData_subtitle": "استعد نسخة احتياطية، أو صِل مزامنة السحابة، أو افتح مجلدًا موجودًا",
+ "setup_welcome_existingData_title": "لديّ بيانات Submersion موجودة",
+ "setup_welcome_skipSetup": "تخطي الإعداد",
+ "setup_welcome_startFresh_subtitle": "أنشئ ملف الغواص الخاص بك واضبط التطبيق",
+ "setup_welcome_startFresh_title": "إعداد سجل جديد",
+ "setup_welcome_subtitle": "تسجيل وتحليل متقدم للغوص",
+ "setup_welcome_title": "مرحباً بك في Submersion",
"@trips_itinerary_dayLabel": {
"placeholders": {
"dayNumber": {
@@ -3359,16 +3447,6 @@
"nav_tooltip_expandMenu": "توسيع القائمة",
"nav_transfer": "نقل البيانات",
"nav_trips": "الرحلات",
- "onboarding_welcome_createProfile": "إنشاء ملفك الشخصي",
- "onboarding_welcome_createProfileSubtitle": "أدخل اسمك للبدء. يمكنك إضافة المزيد من التفاصيل لاحقاً.",
- "onboarding_welcome_creating": "جارٍ الإنشاء...",
- "onboarding_welcome_errorCreatingProfile": "خطأ في إنشاء الملف الشخصي: {error}",
- "onboarding_welcome_getStarted": "البدء",
- "onboarding_welcome_nameHint": "أدخل اسمك",
- "onboarding_welcome_nameLabel": "اسمك",
- "onboarding_welcome_nameValidation": "الرجاء إدخال اسمك",
- "onboarding_welcome_subtitle": "تسجيل وتحليل متقدم للغوص",
- "onboarding_welcome_title": "مرحباً بك في Submersion",
"plannerCanvas_bailout_available": "المتاح {liters}",
"plannerCanvas_bailout_insufficient": "غاز الإنقاذ غير كافٍ لأسوأ حالة",
"plannerCanvas_bailout_required": "المطلوب {liters}",
diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb
index 84ba3da89..a22af7fc7 100644
--- a/lib/l10n/arb/app_de.arb
+++ b/lib/l10n/arb/app_de.arb
@@ -67,6 +67,94 @@
"settings_cloudSync_postRestore_syncing": "Wiederhergestellte Bibliothek wird mit der Cloud synchronisiert…",
"settings_cloudSync_postRestore_synced": "Wiederhergestellte Bibliothek synchronisiert.",
"settings_cloudSync_replace_reviewAction": "Überprüfen",
+ "settings_manage_setupAssistant": "Einrichtungsassistent",
+ "settings_manage_setupAssistant_subtitle": "Einheiten, Darstellung und Backup-Einstellungen erneut durchgehen",
+ "setup_backup_cloudCopy": "Eine Sicherungskopie in der Cloud aufbewahren",
+ "setup_backup_frequency": "Häufigkeit",
+ "setup_backup_frequency_daily": "Täglich",
+ "setup_backup_frequency_monthly": "Monatlich",
+ "setup_backup_frequency_weekly": "Wöchentlich",
+ "setup_backup_scheduleSubtitle": "Ihr Logbuch regelmäßig sichern",
+ "setup_backup_scheduleToggle": "Automatische Sicherungen",
+ "setup_backup_subtitle": "Schützen Sie Ihr Tauchlogbuch vom ersten Tag an.",
+ "setup_backup_title": "Sicherung & Synchronisierung",
+ "setup_common_back": "Zurück",
+ "setup_common_next": "Weiter",
+ "setup_common_skip": "Überspringen",
+ "setup_existing_folder_subtitle": "Submersion auf einen Ordner verweisen, der bereits eine Bibliothek enthält",
+ "setup_existing_folder_title": "Vorhandenen Ordner öffnen",
+ "setup_existing_restore_subtitle": "Eine aus Submersion exportierte Sicherungsdatei auswählen",
+ "setup_existing_restore_title": "Sicherungsdatei wiederherstellen",
+ "setup_existing_subtitle": "Wählen Sie, wie Ihre vorhandene Submersion-Bibliothek geladen werden soll",
+ "setup_existing_sync_subtitle": "Ihre Bibliothek aus iCloud, Dropbox oder S3 laden",
+ "setup_existing_sync_title": "Cloud-Synchronisierung verbinden",
+ "setup_existing_title": "Daten übernehmen",
+ "setup_finish_applying": "Wird eingerichtet...",
+ "setup_finish_error": "Einrichtung konnte nicht abgeschlossen werden: {error}",
+ "setup_finish_feature_diveComputer": "Tauchgänge von Ihrem Tauchcomputer herunterladen",
+ "setup_finish_feature_gear": "Ausrüstung und Wartungsintervalle verwalten",
+ "setup_finish_feature_import": "Logbücher aus Dateien und anderen Apps importieren",
+ "setup_finish_feature_sites": "Ihre Tauchplätze auf der Karte anzeigen",
+ "setup_finish_feature_statistics": "Statistiken zu Ihren Tauchgängen entdecken",
+ "setup_finish_start": "Loslegen",
+ "setup_finish_subtitle": "Submersion kann außerdem...",
+ "setup_finish_title": "Alles bereit",
+ "setup_folder_notFound_message": "Der ausgewählte Ordner enthält keine Submersion-Datenbank.",
+ "setup_folder_notFound_title": "Keine Bibliothek in diesem Ordner",
+ "setup_folder_pick": "Ordner auswählen",
+ "setup_folder_switching": "Bibliothek wird geöffnet...",
+ "setup_folder_title": "Vorhandenen Ordner öffnen",
+ "setup_profile_nameHint": "Geben Sie Ihren Namen ein",
+ "setup_profile_nameLabel": "Ihr Name",
+ "setup_profile_nameValidation": "Bitte geben Sie Ihren Namen ein",
+ "setup_profile_subtitle": "Geben Sie Ihren Namen ein, um zu beginnen. Sie können später weitere Details hinzufügen.",
+ "setup_profile_title": "Erstellen Sie Ihr Profil",
+ "setup_restore_inProgress": "Wird wiederhergestellt...",
+ "setup_restore_pick": "Sicherungsdatei auswählen",
+ "setup_restore_title": "Sicherung wiederherstellen",
+ "setup_step_backup": "Sicherung",
+ "setup_step_finish": "Fertig",
+ "setup_step_profile": "Profil",
+ "setup_step_units": "Einheiten",
+ "setup_syncPull_continue": "Weiter",
+ "setup_syncPull_noLibrary_message": "Auf diesem Konto wurde keine Submersion-Bibliothek gefunden. Stattdessen neu beginnen? Die Verbindung bleibt bestehen.",
+ "setup_syncPull_noLibrary_title": "Keine Bibliothek gefunden",
+ "setup_syncPull_success": "Bibliothek übernommen",
+ "setup_syncPull_syncing": "Bibliothek wird geladen...",
+ "setup_syncPull_title": "Verbinden und laden",
+ "setup_sync_changeProvider": "Anbieter wechseln",
+ "setup_sync_connectedTo": "Verbunden mit {provider}",
+ "setup_sync_error": "Verbindung fehlgeschlagen: {error}",
+ "setup_sync_header": "Cloud-Synchronisierung",
+ "setup_sync_icloudUnavailable": "iCloud ist auf diesem Gerät nicht verfügbar",
+ "setup_sync_libraryFound_adopt": "Vorhandene Bibliothek übernehmen",
+ "setup_sync_libraryFound_keepFresh": "Neu beginnen",
+ "setup_sync_libraryFound_message": "Dieses Konto enthält bereits eine Submersion-Bibliothek. Diese übernehmen, statt neu zu beginnen?",
+ "setup_sync_libraryFound_title": "Vorhandene Bibliothek gefunden",
+ "setup_sync_manageInSettings": "In den Einstellungen verwalten",
+ "setup_sync_notConnected": "Nicht verbunden",
+ "setup_sync_subtitle": "Ihr Logbuch auf allen Geräten synchronisieren",
+ "setup_units_advanced": "Einheiten anpassen",
+ "setup_units_altitude": "Höhe",
+ "setup_units_dateFormat": "Datumsformat",
+ "setup_units_depth": "Tiefe",
+ "setup_units_imperial": "Imperial",
+ "setup_units_metric": "Metrisch",
+ "setup_units_pressure": "Druck",
+ "setup_units_sac": "SAC-Rate",
+ "setup_units_subtitle": "Wählen Sie, wie Messwerte angezeigt werden. Jede Einheit lässt sich einzeln anpassen.",
+ "setup_units_temperature": "Temperatur",
+ "setup_units_timeFormat": "Zeitformat",
+ "setup_units_title": "Einheiten",
+ "setup_units_volume": "Volumen",
+ "setup_units_weight": "Gewicht",
+ "setup_welcome_existingData_subtitle": "Sicherung wiederherstellen, Cloud-Sync verbinden oder vorhandenen Ordner öffnen",
+ "setup_welcome_existingData_title": "Ich habe bereits Submersion-Daten",
+ "setup_welcome_skipSetup": "Einrichtung überspringen",
+ "setup_welcome_startFresh_subtitle": "Taucherprofil erstellen und die App einrichten",
+ "setup_welcome_startFresh_title": "Neues Logbuch einrichten",
+ "setup_welcome_subtitle": "Erweiterte Tauchprotokollierung und -analyse",
+ "setup_welcome_title": "Willkommen bei Submersion",
"@trips_itinerary_dayLabel": {
"placeholders": {
"dayNumber": {
@@ -3359,16 +3447,6 @@
"nav_tooltip_expandMenu": "Menü ausklappen",
"nav_transfer": "Übertragung",
"nav_trips": "Reisen",
- "onboarding_welcome_createProfile": "Erstellen Sie Ihr Profil",
- "onboarding_welcome_createProfileSubtitle": "Geben Sie Ihren Namen ein, um zu beginnen. Sie können später weitere Details hinzufügen.",
- "onboarding_welcome_creating": "Wird erstellt...",
- "onboarding_welcome_errorCreatingProfile": "Fehler beim Erstellen des Profils: {error}",
- "onboarding_welcome_getStarted": "Loslegen",
- "onboarding_welcome_nameHint": "Geben Sie Ihren Namen ein",
- "onboarding_welcome_nameLabel": "Ihr Name",
- "onboarding_welcome_nameValidation": "Bitte geben Sie Ihren Namen ein",
- "onboarding_welcome_subtitle": "Erweiterte Tauchprotokollierung und -analyse",
- "onboarding_welcome_title": "Willkommen bei Submersion",
"plannerCanvas_bailout_available": "Verfügbar {liters}",
"plannerCanvas_bailout_insufficient": "Bailout-Gas reicht für den schlimmsten Fall nicht aus",
"plannerCanvas_bailout_required": "Benötigt {liters}",
diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb
index 284eb3032..204df7a81 100644
--- a/lib/l10n/arb/app_en.arb
+++ b/lib/l10n/arb/app_en.arb
@@ -2595,6 +2595,115 @@
}
}
},
+ "settings_manage_setupAssistant": "Setup assistant",
+ "settings_manage_setupAssistant_subtitle": "Revisit units, appearance, and backup choices",
+ "setup_backup_cloudCopy": "Keep a backup copy in the cloud",
+ "setup_backup_frequency": "Frequency",
+ "setup_backup_frequency_daily": "Daily",
+ "setup_backup_frequency_monthly": "Monthly",
+ "setup_backup_frequency_weekly": "Weekly",
+ "setup_backup_scheduleSubtitle": "Back up your logbook on a schedule",
+ "setup_backup_scheduleToggle": "Automatic backups",
+ "setup_backup_subtitle": "Protect your dive log from day one.",
+ "setup_backup_title": "Backups & Sync",
+ "setup_common_back": "Back",
+ "setup_common_next": "Next",
+ "setup_common_skip": "Skip",
+ "setup_existing_folder_subtitle": "Point Submersion at a folder that already contains a library",
+ "setup_existing_folder_title": "Open an existing folder",
+ "setup_existing_restore_subtitle": "Pick a backup file exported from Submersion",
+ "setup_existing_restore_title": "Restore a backup file",
+ "setup_existing_subtitle": "Choose how to load your existing Submersion library",
+ "setup_existing_sync_subtitle": "Pull your library from iCloud, Dropbox, or S3",
+ "setup_existing_sync_title": "Connect cloud sync",
+ "setup_existing_title": "Bring your data",
+ "setup_finish_applying": "Setting up...",
+ "setup_finish_error": "Could not complete setup: {error}",
+ "@setup_finish_error": {
+ "placeholders": {
+ "error": {
+ "type": "Object"
+ }
+ }
+ },
+ "setup_finish_feature_diveComputer": "Download dives from your dive computer",
+ "setup_finish_feature_gear": "Track gear and service intervals",
+ "setup_finish_feature_import": "Import logs from files and other apps",
+ "setup_finish_feature_sites": "Map your dive sites",
+ "setup_finish_feature_statistics": "Explore statistics about your diving",
+ "setup_finish_start": "Start logging",
+ "setup_finish_subtitle": "Submersion can also...",
+ "setup_finish_title": "You're all set",
+ "setup_folder_notFound_message": "The selected folder does not contain a Submersion database.",
+ "setup_folder_notFound_title": "No library in that folder",
+ "setup_folder_pick": "Choose folder",
+ "setup_folder_switching": "Opening library...",
+ "setup_folder_title": "Open existing folder",
+ "setup_profile_nameHint": "Enter your name",
+ "setup_profile_nameLabel": "Your Name",
+ "setup_profile_nameValidation": "Please enter your name",
+ "setup_profile_subtitle": "Enter your name to get started. You can add more details later.",
+ "setup_profile_title": "Create Your Profile",
+ "setup_restore_inProgress": "Restoring...",
+ "setup_restore_pick": "Choose backup file",
+ "setup_restore_title": "Restore backup",
+ "setup_step_backup": "Backup",
+ "setup_step_finish": "Done",
+ "setup_step_profile": "Profile",
+ "setup_step_units": "Units",
+ "setup_syncPull_continue": "Continue",
+ "setup_syncPull_noLibrary_message": "No existing Submersion library was found on this account. Start fresh instead? Your connection will be kept.",
+ "setup_syncPull_noLibrary_title": "No library found",
+ "setup_syncPull_success": "Library adopted",
+ "setup_syncPull_syncing": "Pulling your library...",
+ "setup_syncPull_title": "Connect and pull",
+ "setup_sync_changeProvider": "Change provider",
+ "setup_sync_connectedTo": "Connected to {provider}",
+ "@setup_sync_connectedTo": {
+ "placeholders": {
+ "provider": {
+ "type": "String"
+ }
+ }
+ },
+ "setup_sync_error": "Could not connect: {error}",
+ "@setup_sync_error": {
+ "placeholders": {
+ "error": {
+ "type": "Object"
+ }
+ }
+ },
+ "setup_sync_header": "Cloud sync",
+ "setup_sync_icloudUnavailable": "iCloud is not available on this device",
+ "setup_sync_libraryFound_adopt": "Adopt existing library",
+ "setup_sync_libraryFound_keepFresh": "Start fresh",
+ "setup_sync_libraryFound_message": "This account already contains a Submersion library. Adopt it instead of starting fresh?",
+ "setup_sync_libraryFound_title": "Existing library found",
+ "setup_sync_manageInSettings": "Manage in Settings",
+ "setup_sync_notConnected": "Not connected",
+ "setup_sync_subtitle": "Sync your logbook across devices",
+ "setup_units_advanced": "Fine-tune units",
+ "setup_units_altitude": "Altitude",
+ "setup_units_dateFormat": "Date format",
+ "setup_units_depth": "Depth",
+ "setup_units_imperial": "Imperial",
+ "setup_units_metric": "Metric",
+ "setup_units_pressure": "Pressure",
+ "setup_units_sac": "SAC rate",
+ "setup_units_subtitle": "Choose how measurements are displayed. You can fine-tune each unit.",
+ "setup_units_temperature": "Temperature",
+ "setup_units_timeFormat": "Time format",
+ "setup_units_title": "Units",
+ "setup_units_volume": "Volume",
+ "setup_units_weight": "Weight",
+ "setup_welcome_existingData_subtitle": "Restore a backup, connect cloud sync, or open an existing folder",
+ "setup_welcome_existingData_title": "I have existing Submersion data",
+ "setup_welcome_skipSetup": "Skip setup",
+ "setup_welcome_startFresh_subtitle": "Create your diver profile and configure the app",
+ "setup_welcome_startFresh_title": "Set up a new logbook",
+ "setup_welcome_subtitle": "Advanced dive logging and analytics",
+ "setup_welcome_title": "Welcome to Submersion",
"siteMatchReview_title": "Match Sites",
"siteMatchReview_diveNumber": "Dive #{number}",
"@siteMatchReview_diveNumber": {
@@ -5987,23 +6096,6 @@
"@nav_trips": {
"description": "Navigation label for trips section"
},
- "onboarding_welcome_createProfile": "Create Your Profile",
- "onboarding_welcome_createProfileSubtitle": "Enter your name to get started. You can add more details later.",
- "onboarding_welcome_creating": "Creating...",
- "onboarding_welcome_errorCreatingProfile": "Error creating profile: {error}",
- "onboarding_welcome_getStarted": "Get Started",
- "onboarding_welcome_nameHint": "Enter your name",
- "onboarding_welcome_nameLabel": "Your Name",
- "onboarding_welcome_nameValidation": "Please enter your name",
- "onboarding_welcome_subtitle": "Advanced dive logging and analytics",
- "onboarding_welcome_title": "Welcome to Submersion",
- "@onboarding_welcome_errorCreatingProfile": {
- "placeholders": {
- "error": {
- "type": "Object"
- }
- }
- },
"plannerCanvas_bailout_available": "Available {liters}",
"@plannerCanvas_bailout_available": {
"placeholders": {
diff --git a/lib/l10n/arb/app_es.arb b/lib/l10n/arb/app_es.arb
index fae8c1761..893885779 100644
--- a/lib/l10n/arb/app_es.arb
+++ b/lib/l10n/arb/app_es.arb
@@ -67,6 +67,94 @@
"settings_cloudSync_postRestore_syncing": "Sincronizando tu biblioteca restaurada con la nube…",
"settings_cloudSync_postRestore_synced": "Biblioteca restaurada sincronizada.",
"settings_cloudSync_replace_reviewAction": "Revisar",
+ "settings_manage_setupAssistant": "Asistente de configuración",
+ "settings_manage_setupAssistant_subtitle": "Revisa unidades, apariencia y opciones de copia de seguridad",
+ "setup_backup_cloudCopy": "Guardar una copia de seguridad en la nube",
+ "setup_backup_frequency": "Frecuencia",
+ "setup_backup_frequency_daily": "Diaria",
+ "setup_backup_frequency_monthly": "Mensual",
+ "setup_backup_frequency_weekly": "Semanal",
+ "setup_backup_scheduleSubtitle": "Haz copias de seguridad de tu diario según una programación",
+ "setup_backup_scheduleToggle": "Copias de seguridad automáticas",
+ "setup_backup_subtitle": "Protege tu registro de buceo desde el primer día.",
+ "setup_backup_title": "Copias de seguridad y sincronización",
+ "setup_common_back": "Atrás",
+ "setup_common_next": "Siguiente",
+ "setup_common_skip": "Omitir",
+ "setup_existing_folder_subtitle": "Apunta Submersion a una carpeta que ya contiene una biblioteca",
+ "setup_existing_folder_title": "Abrir una carpeta existente",
+ "setup_existing_restore_subtitle": "Elige un archivo de copia de seguridad exportado desde Submersion",
+ "setup_existing_restore_title": "Restaurar un archivo de copia de seguridad",
+ "setup_existing_subtitle": "Elige cómo cargar tu biblioteca existente de Submersion",
+ "setup_existing_sync_subtitle": "Descarga tu biblioteca desde iCloud, Dropbox o S3",
+ "setup_existing_sync_title": "Conectar sincronización en la nube",
+ "setup_existing_title": "Trae tus datos",
+ "setup_finish_applying": "Configurando...",
+ "setup_finish_error": "No se pudo completar la configuración: {error}",
+ "setup_finish_feature_diveComputer": "Descarga inmersiones desde tu ordenador de buceo",
+ "setup_finish_feature_gear": "Controla tu equipo y los intervalos de mantenimiento",
+ "setup_finish_feature_import": "Importa registros desde archivos y otras aplicaciones",
+ "setup_finish_feature_sites": "Visualiza tus puntos de buceo en el mapa",
+ "setup_finish_feature_statistics": "Explora estadísticas sobre tus inmersiones",
+ "setup_finish_start": "Empezar a registrar",
+ "setup_finish_subtitle": "Submersion también puede...",
+ "setup_finish_title": "Todo listo",
+ "setup_folder_notFound_message": "La carpeta seleccionada no contiene una base de datos de Submersion.",
+ "setup_folder_notFound_title": "No hay biblioteca en esa carpeta",
+ "setup_folder_pick": "Elegir carpeta",
+ "setup_folder_switching": "Abriendo biblioteca...",
+ "setup_folder_title": "Abrir carpeta existente",
+ "setup_profile_nameHint": "Ingresa tu nombre",
+ "setup_profile_nameLabel": "Tu Nombre",
+ "setup_profile_nameValidation": "Por favor ingresa tu nombre",
+ "setup_profile_subtitle": "Ingresa tu nombre para comenzar. Puedes agregar más detalles después.",
+ "setup_profile_title": "Crea Tu Perfil",
+ "setup_restore_inProgress": "Restaurando...",
+ "setup_restore_pick": "Elegir archivo de copia de seguridad",
+ "setup_restore_title": "Restaurar copia de seguridad",
+ "setup_step_backup": "Copia",
+ "setup_step_finish": "Listo",
+ "setup_step_profile": "Perfil",
+ "setup_step_units": "Unidades",
+ "setup_syncPull_continue": "Continuar",
+ "setup_syncPull_noLibrary_message": "No se encontró ninguna biblioteca de Submersion en esta cuenta. ¿Empezar de cero? Tu conexión se mantendrá.",
+ "setup_syncPull_noLibrary_title": "No se encontró biblioteca",
+ "setup_syncPull_success": "Biblioteca adoptada",
+ "setup_syncPull_syncing": "Descargando tu biblioteca...",
+ "setup_syncPull_title": "Conectar y descargar",
+ "setup_sync_changeProvider": "Cambiar proveedor",
+ "setup_sync_connectedTo": "Conectado a {provider}",
+ "setup_sync_error": "No se pudo conectar: {error}",
+ "setup_sync_header": "Sincronización en la nube",
+ "setup_sync_icloudUnavailable": "iCloud no está disponible en este dispositivo",
+ "setup_sync_libraryFound_adopt": "Adoptar biblioteca existente",
+ "setup_sync_libraryFound_keepFresh": "Empezar de cero",
+ "setup_sync_libraryFound_message": "Esta cuenta ya contiene una biblioteca de Submersion. ¿Adoptarla en lugar de empezar de cero?",
+ "setup_sync_libraryFound_title": "Se encontró una biblioteca existente",
+ "setup_sync_manageInSettings": "Gestionar en Ajustes",
+ "setup_sync_notConnected": "No conectado",
+ "setup_sync_subtitle": "Sincroniza tu diario entre dispositivos",
+ "setup_units_advanced": "Ajustar unidades",
+ "setup_units_altitude": "Altitud",
+ "setup_units_dateFormat": "Formato de fecha",
+ "setup_units_depth": "Profundidad",
+ "setup_units_imperial": "Imperial",
+ "setup_units_metric": "Métrico",
+ "setup_units_pressure": "Presión",
+ "setup_units_sac": "Tasa SAC",
+ "setup_units_subtitle": "Elige cómo se muestran las medidas. Puedes ajustar cada unidad.",
+ "setup_units_temperature": "Temperatura",
+ "setup_units_timeFormat": "Formato de hora",
+ "setup_units_title": "Unidades",
+ "setup_units_volume": "Volumen",
+ "setup_units_weight": "Peso",
+ "setup_welcome_existingData_subtitle": "Restaura una copia de seguridad, conecta la sincronización o abre una carpeta existente",
+ "setup_welcome_existingData_title": "Ya tengo datos de Submersion",
+ "setup_welcome_skipSetup": "Omitir configuración",
+ "setup_welcome_startFresh_subtitle": "Crea tu perfil de buceador y configura la aplicación",
+ "setup_welcome_startFresh_title": "Configurar un diario nuevo",
+ "setup_welcome_subtitle": "Registro y análisis avanzado de inmersiones",
+ "setup_welcome_title": "Bienvenido a Submersion",
"@trips_itinerary_dayLabel": {
"placeholders": {
"dayNumber": {
@@ -3359,16 +3447,6 @@
"nav_tooltip_expandMenu": "Expandir menú",
"nav_transfer": "Transferencia",
"nav_trips": "Viajes",
- "onboarding_welcome_createProfile": "Crea Tu Perfil",
- "onboarding_welcome_createProfileSubtitle": "Ingresa tu nombre para comenzar. Puedes agregar más detalles después.",
- "onboarding_welcome_creating": "Creando...",
- "onboarding_welcome_errorCreatingProfile": "Error al crear perfil: {error}",
- "onboarding_welcome_getStarted": "Comenzar",
- "onboarding_welcome_nameHint": "Ingresa tu nombre",
- "onboarding_welcome_nameLabel": "Tu Nombre",
- "onboarding_welcome_nameValidation": "Por favor ingresa tu nombre",
- "onboarding_welcome_subtitle": "Registro y análisis avanzado de inmersiones",
- "onboarding_welcome_title": "Bienvenido a Submersion",
"plannerCanvas_bailout_available": "Disponible {liters}",
"plannerCanvas_bailout_insufficient": "Gas de bailout insuficiente para el peor caso",
"plannerCanvas_bailout_required": "Necesario {liters}",
diff --git a/lib/l10n/arb/app_fr.arb b/lib/l10n/arb/app_fr.arb
index 8ae48641b..81d7a24d8 100644
--- a/lib/l10n/arb/app_fr.arb
+++ b/lib/l10n/arb/app_fr.arb
@@ -67,6 +67,94 @@
"settings_cloudSync_postRestore_syncing": "Synchronisation de votre bibliothèque restaurée avec le cloud…",
"settings_cloudSync_postRestore_synced": "Bibliothèque restaurée synchronisée.",
"settings_cloudSync_replace_reviewAction": "Examiner",
+ "settings_manage_setupAssistant": "Assistant de configuration",
+ "settings_manage_setupAssistant_subtitle": "Revoir les unités, l'apparence et les choix de sauvegarde",
+ "setup_backup_cloudCopy": "Conserver une copie de sauvegarde dans le cloud",
+ "setup_backup_frequency": "Fréquence",
+ "setup_backup_frequency_daily": "Quotidienne",
+ "setup_backup_frequency_monthly": "Mensuelle",
+ "setup_backup_frequency_weekly": "Hebdomadaire",
+ "setup_backup_scheduleSubtitle": "Sauvegardez votre carnet selon un calendrier",
+ "setup_backup_scheduleToggle": "Sauvegardes automatiques",
+ "setup_backup_subtitle": "Protégez votre carnet de plongée dès le premier jour.",
+ "setup_backup_title": "Sauvegardes et synchronisation",
+ "setup_common_back": "Retour",
+ "setup_common_next": "Suivant",
+ "setup_common_skip": "Ignorer",
+ "setup_existing_folder_subtitle": "Pointez Submersion vers un dossier contenant déjà une bibliothèque",
+ "setup_existing_folder_title": "Ouvrir un dossier existant",
+ "setup_existing_restore_subtitle": "Choisissez un fichier de sauvegarde exporté depuis Submersion",
+ "setup_existing_restore_title": "Restaurer un fichier de sauvegarde",
+ "setup_existing_subtitle": "Choisissez comment charger votre bibliothèque Submersion existante",
+ "setup_existing_sync_subtitle": "Récupérez votre bibliothèque depuis iCloud, Dropbox ou S3",
+ "setup_existing_sync_title": "Connecter la synchronisation cloud",
+ "setup_existing_title": "Récupérez vos données",
+ "setup_finish_applying": "Configuration...",
+ "setup_finish_error": "Impossible de terminer la configuration : {error}",
+ "setup_finish_feature_diveComputer": "Téléchargez vos plongées depuis votre ordinateur de plongée",
+ "setup_finish_feature_gear": "Suivez votre matériel et ses entretiens",
+ "setup_finish_feature_import": "Importez des carnets depuis des fichiers et d'autres applications",
+ "setup_finish_feature_sites": "Cartographiez vos sites de plongée",
+ "setup_finish_feature_statistics": "Explorez les statistiques de vos plongées",
+ "setup_finish_start": "Commencer",
+ "setup_finish_subtitle": "Submersion peut aussi...",
+ "setup_finish_title": "Tout est prêt",
+ "setup_folder_notFound_message": "Le dossier sélectionné ne contient pas de base de données Submersion.",
+ "setup_folder_notFound_title": "Aucune bibliothèque dans ce dossier",
+ "setup_folder_pick": "Choisir un dossier",
+ "setup_folder_switching": "Ouverture de la bibliothèque...",
+ "setup_folder_title": "Ouvrir un dossier existant",
+ "setup_profile_nameHint": "Entrez votre nom",
+ "setup_profile_nameLabel": "Votre nom",
+ "setup_profile_nameValidation": "Veuillez entrer votre nom",
+ "setup_profile_subtitle": "Entrez votre nom pour commencer. Vous pourrez ajouter plus de détails plus tard.",
+ "setup_profile_title": "Créer votre profil",
+ "setup_restore_inProgress": "Restauration...",
+ "setup_restore_pick": "Choisir un fichier de sauvegarde",
+ "setup_restore_title": "Restaurer la sauvegarde",
+ "setup_step_backup": "Sauvegarde",
+ "setup_step_finish": "Terminé",
+ "setup_step_profile": "Profil",
+ "setup_step_units": "Unités",
+ "setup_syncPull_continue": "Continuer",
+ "setup_syncPull_noLibrary_message": "Aucune bibliothèque Submersion n'a été trouvée sur ce compte. Repartir de zéro ? Votre connexion sera conservée.",
+ "setup_syncPull_noLibrary_title": "Aucune bibliothèque trouvée",
+ "setup_syncPull_success": "Bibliothèque adoptée",
+ "setup_syncPull_syncing": "Récupération de votre bibliothèque...",
+ "setup_syncPull_title": "Connecter et récupérer",
+ "setup_sync_changeProvider": "Changer de fournisseur",
+ "setup_sync_connectedTo": "Connecté à {provider}",
+ "setup_sync_error": "Connexion impossible : {error}",
+ "setup_sync_header": "Synchronisation cloud",
+ "setup_sync_icloudUnavailable": "iCloud n'est pas disponible sur cet appareil",
+ "setup_sync_libraryFound_adopt": "Adopter la bibliothèque existante",
+ "setup_sync_libraryFound_keepFresh": "Repartir de zéro",
+ "setup_sync_libraryFound_message": "Ce compte contient déjà une bibliothèque Submersion. L'adopter plutôt que de repartir de zéro ?",
+ "setup_sync_libraryFound_title": "Bibliothèque existante trouvée",
+ "setup_sync_manageInSettings": "Gérer dans les réglages",
+ "setup_sync_notConnected": "Non connecté",
+ "setup_sync_subtitle": "Synchronisez votre carnet entre vos appareils",
+ "setup_units_advanced": "Ajuster les unités",
+ "setup_units_altitude": "Altitude",
+ "setup_units_dateFormat": "Format de date",
+ "setup_units_depth": "Profondeur",
+ "setup_units_imperial": "Impérial",
+ "setup_units_metric": "Métrique",
+ "setup_units_pressure": "Pression",
+ "setup_units_sac": "Taux SAC",
+ "setup_units_subtitle": "Choisissez l'affichage des mesures. Chaque unité peut être ajustée.",
+ "setup_units_temperature": "Température",
+ "setup_units_timeFormat": "Format de l'heure",
+ "setup_units_title": "Unités",
+ "setup_units_volume": "Volume",
+ "setup_units_weight": "Poids",
+ "setup_welcome_existingData_subtitle": "Restaurez une sauvegarde, connectez la synchronisation ou ouvrez un dossier existant",
+ "setup_welcome_existingData_title": "J'ai déjà des données Submersion",
+ "setup_welcome_skipSetup": "Ignorer la configuration",
+ "setup_welcome_startFresh_subtitle": "Créez votre profil de plongeur et configurez l'application",
+ "setup_welcome_startFresh_title": "Créer un nouveau carnet",
+ "setup_welcome_subtitle": "Enregistrement et analyse avancés de plongée",
+ "setup_welcome_title": "Bienvenue dans Submersion",
"@trips_itinerary_dayLabel": {
"placeholders": {
"dayNumber": {
@@ -3325,16 +3413,6 @@
"nav_tooltip_expandMenu": "Developper le menu",
"nav_transfer": "Transfert",
"nav_trips": "Voyages",
- "onboarding_welcome_createProfile": "Créer votre profil",
- "onboarding_welcome_createProfileSubtitle": "Entrez votre nom pour commencer. Vous pourrez ajouter plus de détails plus tard.",
- "onboarding_welcome_creating": "Création...",
- "onboarding_welcome_errorCreatingProfile": "Erreur lors de la création du profil : {error}",
- "onboarding_welcome_getStarted": "Commencer",
- "onboarding_welcome_nameHint": "Entrez votre nom",
- "onboarding_welcome_nameLabel": "Votre nom",
- "onboarding_welcome_nameValidation": "Veuillez entrer votre nom",
- "onboarding_welcome_subtitle": "Enregistrement et analyse avancés de plongée",
- "onboarding_welcome_title": "Bienvenue dans Submersion",
"plannerCanvas_bailout_available": "Disponible {liters}",
"plannerCanvas_bailout_insufficient": "Gaz de bailout insuffisant pour le pire cas",
"plannerCanvas_bailout_required": "Nécessaire {liters}",
diff --git a/lib/l10n/arb/app_he.arb b/lib/l10n/arb/app_he.arb
index ef8a84478..4817cd0cc 100644
--- a/lib/l10n/arb/app_he.arb
+++ b/lib/l10n/arb/app_he.arb
@@ -67,6 +67,94 @@
"settings_cloudSync_postRestore_syncing": "מסנכרן את הספרייה המשוחזרת שלך עם הענן…",
"settings_cloudSync_postRestore_synced": "הספרייה המשוחזרת סונכרנה.",
"settings_cloudSync_replace_reviewAction": "סקירה",
+ "settings_manage_setupAssistant": "אשף ההגדרה",
+ "settings_manage_setupAssistant_subtitle": "עיון מחדש ביחידות, במראה ובאפשרויות הגיבוי",
+ "setup_backup_cloudCopy": "שמירת עותק גיבוי בענן",
+ "setup_backup_frequency": "תדירות",
+ "setup_backup_frequency_daily": "יומי",
+ "setup_backup_frequency_monthly": "חודשי",
+ "setup_backup_frequency_weekly": "שבועי",
+ "setup_backup_scheduleSubtitle": "גיבוי יומן הצלילה לפי לוח זמנים",
+ "setup_backup_scheduleToggle": "גיבויים אוטומטיים",
+ "setup_backup_subtitle": "הגנו על יומן הצלילה שלכם מהיום הראשון.",
+ "setup_backup_title": "גיבויים וסנכרון",
+ "setup_common_back": "חזרה",
+ "setup_common_next": "הבא",
+ "setup_common_skip": "דילוג",
+ "setup_existing_folder_subtitle": "הפנו את Submersion לתיקייה שכבר מכילה ספרייה",
+ "setup_existing_folder_title": "פתיחת תיקייה קיימת",
+ "setup_existing_restore_subtitle": "בחרו קובץ גיבוי שיוצא מ-Submersion",
+ "setup_existing_restore_title": "שחזור קובץ גיבוי",
+ "setup_existing_subtitle": "בחרו כיצד לטעון את ספריית Submersion הקיימת שלכם",
+ "setup_existing_sync_subtitle": "משכו את הספרייה שלכם מ-iCloud, מ-Dropbox או מ-S3",
+ "setup_existing_sync_title": "חיבור סנכרון ענן",
+ "setup_existing_title": "הביאו את הנתונים שלכם",
+ "setup_finish_applying": "מגדיר...",
+ "setup_finish_error": "לא ניתן להשלים את ההגדרה: {error}",
+ "setup_finish_feature_diveComputer": "הורדת צלילות ממחשב הצלילה",
+ "setup_finish_feature_gear": "מעקב אחר ציוד ומועדי טיפולים",
+ "setup_finish_feature_import": "ייבוא יומנים מקבצים ומאפליקציות אחרות",
+ "setup_finish_feature_sites": "מיפוי אתרי הצלילה שלכם",
+ "setup_finish_feature_statistics": "חקר סטטיסטיקות על הצלילות שלכם",
+ "setup_finish_start": "התחלת רישום",
+ "setup_finish_subtitle": "Submersion יכול גם...",
+ "setup_finish_title": "הכול מוכן",
+ "setup_folder_notFound_message": "התיקייה שנבחרה אינה מכילה מסד נתונים של Submersion.",
+ "setup_folder_notFound_title": "אין ספרייה בתיקייה זו",
+ "setup_folder_pick": "בחירת תיקייה",
+ "setup_folder_switching": "פותח ספרייה...",
+ "setup_folder_title": "פתיחת תיקייה קיימת",
+ "setup_profile_nameHint": "הזן את שמך",
+ "setup_profile_nameLabel": "השם שלך",
+ "setup_profile_nameValidation": "נא להזין את שמך",
+ "setup_profile_subtitle": "הזן את שמך כדי להתחיל. תוכל להוסיף פרטים נוספים מאוחר יותר.",
+ "setup_profile_title": "צור את הפרופיל שלך",
+ "setup_restore_inProgress": "משחזר...",
+ "setup_restore_pick": "בחירת קובץ גיבוי",
+ "setup_restore_title": "שחזור גיבוי",
+ "setup_step_backup": "גיבוי",
+ "setup_step_finish": "סיום",
+ "setup_step_profile": "פרופיל",
+ "setup_step_units": "יחידות",
+ "setup_syncPull_continue": "המשך",
+ "setup_syncPull_noLibrary_message": "לא נמצאה ספריית Submersion בחשבון זה. להתחיל מחדש? החיבור יישמר.",
+ "setup_syncPull_noLibrary_title": "לא נמצאה ספרייה",
+ "setup_syncPull_success": "הספרייה אומצה",
+ "setup_syncPull_syncing": "מושך את הספרייה...",
+ "setup_syncPull_title": "חיבור ומשיכה",
+ "setup_sync_changeProvider": "החלפת ספק",
+ "setup_sync_connectedTo": "מחובר אל {provider}",
+ "setup_sync_error": "החיבור נכשל: {error}",
+ "setup_sync_header": "סנכרון ענן",
+ "setup_sync_icloudUnavailable": "iCloud אינו זמין במכשיר זה",
+ "setup_sync_libraryFound_adopt": "אימוץ הספרייה הקיימת",
+ "setup_sync_libraryFound_keepFresh": "התחלה חדשה",
+ "setup_sync_libraryFound_message": "חשבון זה כבר מכיל ספריית Submersion. לאמץ אותה במקום להתחיל מחדש?",
+ "setup_sync_libraryFound_title": "נמצאה ספרייה קיימת",
+ "setup_sync_manageInSettings": "ניהול בהגדרות",
+ "setup_sync_notConnected": "לא מחובר",
+ "setup_sync_subtitle": "סנכרון היומן בין מכשירים",
+ "setup_units_advanced": "כוונון יחידות",
+ "setup_units_altitude": "גובה",
+ "setup_units_dateFormat": "תבנית תאריך",
+ "setup_units_depth": "עומק",
+ "setup_units_imperial": "אימפריאלי",
+ "setup_units_metric": "מטרי",
+ "setup_units_pressure": "לחץ",
+ "setup_units_sac": "קצב SAC",
+ "setup_units_subtitle": "בחרו כיצד יוצגו המדידות. ניתן לכוונן כל יחידה.",
+ "setup_units_temperature": "טמפרטורה",
+ "setup_units_timeFormat": "תבנית שעה",
+ "setup_units_title": "יחידות",
+ "setup_units_volume": "נפח",
+ "setup_units_weight": "משקל",
+ "setup_welcome_existingData_subtitle": "שחזרו גיבוי, חברו סנכרון ענן או פתחו תיקייה קיימת",
+ "setup_welcome_existingData_title": "יש לי כבר נתוני Submersion",
+ "setup_welcome_skipSetup": "דילוג על ההגדרה",
+ "setup_welcome_startFresh_subtitle": "צרו פרופיל צולל והגדירו את האפליקציה",
+ "setup_welcome_startFresh_title": "הגדרת יומן חדש",
+ "setup_welcome_subtitle": "רישום וניתוח צלילה מתקדם",
+ "setup_welcome_title": "ברוכים הבאים ל-Submersion",
"@trips_itinerary_dayLabel": {
"placeholders": {
"dayNumber": {
@@ -3325,16 +3413,6 @@
"nav_tooltip_expandMenu": "הרחבת תפריט",
"nav_transfer": "העברה",
"nav_trips": "טיולים",
- "onboarding_welcome_createProfile": "צור את הפרופיל שלך",
- "onboarding_welcome_createProfileSubtitle": "הזן את שמך כדי להתחיל. תוכל להוסיף פרטים נוספים מאוחר יותר.",
- "onboarding_welcome_creating": "יוצר...",
- "onboarding_welcome_errorCreatingProfile": "שגיאה ביצירת פרופיל: {error}",
- "onboarding_welcome_getStarted": "התחל",
- "onboarding_welcome_nameHint": "הזן את שמך",
- "onboarding_welcome_nameLabel": "השם שלך",
- "onboarding_welcome_nameValidation": "נא להזין את שמך",
- "onboarding_welcome_subtitle": "רישום וניתוח צלילה מתקדם",
- "onboarding_welcome_title": "ברוכים הבאים ל-Submersion",
"plannerCanvas_bailout_available": "זמין {liters}",
"plannerCanvas_bailout_insufficient": "גז חילוץ אינו מספיק למקרה הגרוע ביותר",
"plannerCanvas_bailout_required": "נדרש {liters}",
diff --git a/lib/l10n/arb/app_hu.arb b/lib/l10n/arb/app_hu.arb
index a3bfb4716..575c6f728 100644
--- a/lib/l10n/arb/app_hu.arb
+++ b/lib/l10n/arb/app_hu.arb
@@ -67,6 +67,94 @@
"settings_cloudSync_postRestore_syncing": "A visszaállított könyvtár szinkronizálása a felhővel…",
"settings_cloudSync_postRestore_synced": "A visszaállított könyvtár szinkronizálva.",
"settings_cloudSync_replace_reviewAction": "Áttekintés",
+ "settings_manage_setupAssistant": "Beállítási varázsló",
+ "settings_manage_setupAssistant_subtitle": "Mértékegységek, megjelenés és biztonsági mentés áttekintése",
+ "setup_backup_cloudCopy": "Biztonsági másolat megőrzése a felhőben",
+ "setup_backup_frequency": "Gyakoriság",
+ "setup_backup_frequency_daily": "Naponta",
+ "setup_backup_frequency_monthly": "Havonta",
+ "setup_backup_frequency_weekly": "Hetente",
+ "setup_backup_scheduleSubtitle": "Naplója rendszeres biztonsági mentése",
+ "setup_backup_scheduleToggle": "Automatikus biztonsági mentés",
+ "setup_backup_subtitle": "Védje merülési naplóját az első naptól kezdve.",
+ "setup_backup_title": "Biztonsági mentés és szinkronizálás",
+ "setup_common_back": "Vissza",
+ "setup_common_next": "Tovább",
+ "setup_common_skip": "Kihagyás",
+ "setup_existing_folder_subtitle": "Irányítsa a Submersiont egy olyan mappára, amely már tartalmaz könyvtárat",
+ "setup_existing_folder_title": "Meglévő mappa megnyitása",
+ "setup_existing_restore_subtitle": "Válasszon egy Submersionból exportált biztonságimentés-fájlt",
+ "setup_existing_restore_title": "Biztonsági mentés visszaállítása",
+ "setup_existing_subtitle": "Válassza ki, hogyan töltse be meglévő Submersion-könyvtárát",
+ "setup_existing_sync_subtitle": "Könyvtár letöltése iCloudból, Dropboxból vagy S3-ból",
+ "setup_existing_sync_title": "Felhőszinkronizálás csatlakoztatása",
+ "setup_existing_title": "Adatok áthozása",
+ "setup_finish_applying": "Beállítás...",
+ "setup_finish_error": "A beállítás nem fejeződött be: {error}",
+ "setup_finish_feature_diveComputer": "Merülések letöltése a búvárkomputerről",
+ "setup_finish_feature_gear": "Felszerelés és szervizintervallumok nyomon követése",
+ "setup_finish_feature_import": "Naplók importálása fájlokból és más alkalmazásokból",
+ "setup_finish_feature_sites": "Merülőhelyek megjelenítése térképen",
+ "setup_finish_feature_statistics": "Merülési statisztikák felfedezése",
+ "setup_finish_start": "Naplózás indítása",
+ "setup_finish_subtitle": "A Submersion ezenkívül...",
+ "setup_finish_title": "Minden készen áll",
+ "setup_folder_notFound_message": "A kiválasztott mappa nem tartalmaz Submersion-adatbázist.",
+ "setup_folder_notFound_title": "Nincs könyvtár ebben a mappában",
+ "setup_folder_pick": "Mappa kiválasztása",
+ "setup_folder_switching": "Könyvtár megnyitása...",
+ "setup_folder_title": "Meglévő mappa megnyitása",
+ "setup_profile_nameHint": "Add meg a neved",
+ "setup_profile_nameLabel": "Neved",
+ "setup_profile_nameValidation": "Add meg a neved",
+ "setup_profile_subtitle": "Add meg a neved a kezdéshez. Később további részleteket adhatsz hozzá.",
+ "setup_profile_title": "Profil létrehozása",
+ "setup_restore_inProgress": "Visszaállítás...",
+ "setup_restore_pick": "Biztonságimentés-fájl kiválasztása",
+ "setup_restore_title": "Biztonsági mentés visszaállítása",
+ "setup_step_backup": "Mentés",
+ "setup_step_finish": "Kész",
+ "setup_step_profile": "Profil",
+ "setup_step_units": "Egységek",
+ "setup_syncPull_continue": "Folytatás",
+ "setup_syncPull_noLibrary_message": "Ezen a fiókon nem található Submersion-könyvtár. Új kezdés? A kapcsolat megmarad.",
+ "setup_syncPull_noLibrary_title": "Nem található könyvtár",
+ "setup_syncPull_success": "Könyvtár átvéve",
+ "setup_syncPull_syncing": "Könyvtár letöltése...",
+ "setup_syncPull_title": "Csatlakozás és letöltés",
+ "setup_sync_changeProvider": "Szolgáltató módosítása",
+ "setup_sync_connectedTo": "Csatlakoztatva: {provider}",
+ "setup_sync_error": "Nem sikerült csatlakozni: {error}",
+ "setup_sync_header": "Felhőszinkronizálás",
+ "setup_sync_icloudUnavailable": "Az iCloud nem érhető el ezen az eszközön",
+ "setup_sync_libraryFound_adopt": "Meglévő könyvtár átvétele",
+ "setup_sync_libraryFound_keepFresh": "Új kezdés",
+ "setup_sync_libraryFound_message": "Ez a fiók már tartalmaz Submersion-könyvtárat. Átveszi ahelyett, hogy újat kezdene?",
+ "setup_sync_libraryFound_title": "Meglévő könyvtár található",
+ "setup_sync_manageInSettings": "Kezelés a beállításokban",
+ "setup_sync_notConnected": "Nincs csatlakoztatva",
+ "setup_sync_subtitle": "Napló szinkronizálása az eszközök között",
+ "setup_units_advanced": "Egységek finomhangolása",
+ "setup_units_altitude": "Magasság",
+ "setup_units_dateFormat": "Dátumformátum",
+ "setup_units_depth": "Mélység",
+ "setup_units_imperial": "Angolszász",
+ "setup_units_metric": "Metrikus",
+ "setup_units_pressure": "Nyomás",
+ "setup_units_sac": "SAC-érték",
+ "setup_units_subtitle": "Válassza ki a mértékegységek megjelenítését. Minden egység külön beállítható.",
+ "setup_units_temperature": "Hőmérséklet",
+ "setup_units_timeFormat": "Időformátum",
+ "setup_units_title": "Egységek",
+ "setup_units_volume": "Térfogat",
+ "setup_units_weight": "Súly",
+ "setup_welcome_existingData_subtitle": "Biztonsági mentés visszaállítása, felhőszinkronizálás vagy meglévő mappa megnyitása",
+ "setup_welcome_existingData_title": "Már vannak Submersion-adataim",
+ "setup_welcome_skipSetup": "Beállítás kihagyása",
+ "setup_welcome_startFresh_subtitle": "Búvárprofil létrehozása és az alkalmazás beállítása",
+ "setup_welcome_startFresh_title": "Új napló beállítása",
+ "setup_welcome_subtitle": "Fejlett merülés napló és elemzés",
+ "setup_welcome_title": "Üdvözöl a Submersion",
"@trips_itinerary_dayLabel": {
"placeholders": {
"dayNumber": {
@@ -3325,16 +3413,6 @@
"nav_tooltip_expandMenu": "Menu kinyitasa",
"nav_transfer": "Atvitel",
"nav_trips": "Utak",
- "onboarding_welcome_createProfile": "Profil létrehozása",
- "onboarding_welcome_createProfileSubtitle": "Add meg a neved a kezdéshez. Később további részleteket adhatsz hozzá.",
- "onboarding_welcome_creating": "Létrehozás...",
- "onboarding_welcome_errorCreatingProfile": "Hiba a profil létrehozásakor: {error}",
- "onboarding_welcome_getStarted": "Kezdés",
- "onboarding_welcome_nameHint": "Add meg a neved",
- "onboarding_welcome_nameLabel": "Neved",
- "onboarding_welcome_nameValidation": "Add meg a neved",
- "onboarding_welcome_subtitle": "Fejlett merülés napló és elemzés",
- "onboarding_welcome_title": "Üdvözöl a Submersion",
"plannerCanvas_bailout_available": "Elérhető {liters}",
"plannerCanvas_bailout_insufficient": "A bailout gáz nem elegendő a legrosszabb esethez",
"plannerCanvas_bailout_required": "Szükséges {liters}",
diff --git a/lib/l10n/arb/app_it.arb b/lib/l10n/arb/app_it.arb
index 0798d31f3..c7aeae0e2 100644
--- a/lib/l10n/arb/app_it.arb
+++ b/lib/l10n/arb/app_it.arb
@@ -67,6 +67,94 @@
"settings_cloudSync_postRestore_syncing": "Sincronizzazione della libreria ripristinata con il cloud…",
"settings_cloudSync_postRestore_synced": "Libreria ripristinata sincronizzata.",
"settings_cloudSync_replace_reviewAction": "Rivedi",
+ "settings_manage_setupAssistant": "Assistente di configurazione",
+ "settings_manage_setupAssistant_subtitle": "Rivedi unità, aspetto e opzioni di backup",
+ "setup_backup_cloudCopy": "Conserva una copia di backup nel cloud",
+ "setup_backup_frequency": "Frequenza",
+ "setup_backup_frequency_daily": "Giornaliera",
+ "setup_backup_frequency_monthly": "Mensile",
+ "setup_backup_frequency_weekly": "Settimanale",
+ "setup_backup_scheduleSubtitle": "Esegui il backup del tuo logbook a intervalli regolari",
+ "setup_backup_scheduleToggle": "Backup automatici",
+ "setup_backup_subtitle": "Proteggi il tuo registro immersioni fin dal primo giorno.",
+ "setup_backup_title": "Backup e sincronizzazione",
+ "setup_common_back": "Indietro",
+ "setup_common_next": "Avanti",
+ "setup_common_skip": "Salta",
+ "setup_existing_folder_subtitle": "Indica a Submersion una cartella che contiene già una libreria",
+ "setup_existing_folder_title": "Apri una cartella esistente",
+ "setup_existing_restore_subtitle": "Scegli un file di backup esportato da Submersion",
+ "setup_existing_restore_title": "Ripristina un file di backup",
+ "setup_existing_subtitle": "Scegli come caricare la tua libreria Submersion esistente",
+ "setup_existing_sync_subtitle": "Scarica la tua libreria da iCloud, Dropbox o S3",
+ "setup_existing_sync_title": "Collega la sincronizzazione cloud",
+ "setup_existing_title": "Porta i tuoi dati",
+ "setup_finish_applying": "Configurazione...",
+ "setup_finish_error": "Impossibile completare la configurazione: {error}",
+ "setup_finish_feature_diveComputer": "Scarica le immersioni dal tuo computer subacqueo",
+ "setup_finish_feature_gear": "Tieni traccia dell'attrezzatura e delle scadenze di manutenzione",
+ "setup_finish_feature_import": "Importa registri da file e altre app",
+ "setup_finish_feature_sites": "Visualizza i tuoi punti di immersione sulla mappa",
+ "setup_finish_feature_statistics": "Esplora le statistiche delle tue immersioni",
+ "setup_finish_start": "Inizia a registrare",
+ "setup_finish_subtitle": "Submersion può anche...",
+ "setup_finish_title": "Tutto pronto",
+ "setup_folder_notFound_message": "La cartella selezionata non contiene un database Submersion.",
+ "setup_folder_notFound_title": "Nessuna libreria in quella cartella",
+ "setup_folder_pick": "Scegli cartella",
+ "setup_folder_switching": "Apertura della libreria...",
+ "setup_folder_title": "Apri cartella esistente",
+ "setup_profile_nameHint": "Inserisci il tuo nome",
+ "setup_profile_nameLabel": "Il Tuo Nome",
+ "setup_profile_nameValidation": "Inserisci il tuo nome",
+ "setup_profile_subtitle": "Inserisci il tuo nome per iniziare. Potrai aggiungere altri dettagli in seguito.",
+ "setup_profile_title": "Crea il Tuo Profilo",
+ "setup_restore_inProgress": "Ripristino...",
+ "setup_restore_pick": "Scegli file di backup",
+ "setup_restore_title": "Ripristina backup",
+ "setup_step_backup": "Backup",
+ "setup_step_finish": "Fine",
+ "setup_step_profile": "Profilo",
+ "setup_step_units": "Unità",
+ "setup_syncPull_continue": "Continua",
+ "setup_syncPull_noLibrary_message": "Nessuna libreria Submersion trovata su questo account. Iniziare da zero? La connessione verrà mantenuta.",
+ "setup_syncPull_noLibrary_title": "Nessuna libreria trovata",
+ "setup_syncPull_success": "Libreria adottata",
+ "setup_syncPull_syncing": "Download della libreria...",
+ "setup_syncPull_title": "Connetti e scarica",
+ "setup_sync_changeProvider": "Cambia provider",
+ "setup_sync_connectedTo": "Connesso a {provider}",
+ "setup_sync_error": "Impossibile connettersi: {error}",
+ "setup_sync_header": "Sincronizzazione cloud",
+ "setup_sync_icloudUnavailable": "iCloud non è disponibile su questo dispositivo",
+ "setup_sync_libraryFound_adopt": "Adotta la libreria esistente",
+ "setup_sync_libraryFound_keepFresh": "Inizia da zero",
+ "setup_sync_libraryFound_message": "Questo account contiene già una libreria Submersion. Adottarla invece di iniziare da zero?",
+ "setup_sync_libraryFound_title": "Trovata libreria esistente",
+ "setup_sync_manageInSettings": "Gestisci nelle impostazioni",
+ "setup_sync_notConnected": "Non connesso",
+ "setup_sync_subtitle": "Sincronizza il tuo logbook su tutti i dispositivi",
+ "setup_units_advanced": "Regola le unità",
+ "setup_units_altitude": "Altitudine",
+ "setup_units_dateFormat": "Formato data",
+ "setup_units_depth": "Profondità",
+ "setup_units_imperial": "Imperiale",
+ "setup_units_metric": "Metrico",
+ "setup_units_pressure": "Pressione",
+ "setup_units_sac": "Tasso SAC",
+ "setup_units_subtitle": "Scegli come visualizzare le misure. Puoi regolare ogni unità.",
+ "setup_units_temperature": "Temperatura",
+ "setup_units_timeFormat": "Formato ora",
+ "setup_units_title": "Unità",
+ "setup_units_volume": "Volume",
+ "setup_units_weight": "Peso",
+ "setup_welcome_existingData_subtitle": "Ripristina un backup, collega la sincronizzazione o apri una cartella esistente",
+ "setup_welcome_existingData_title": "Ho già dati di Submersion",
+ "setup_welcome_skipSetup": "Salta configurazione",
+ "setup_welcome_startFresh_subtitle": "Crea il tuo profilo subacqueo e configura l'app",
+ "setup_welcome_startFresh_title": "Configura un nuovo logbook",
+ "setup_welcome_subtitle": "Registrazione e analisi avanzata delle immersioni",
+ "setup_welcome_title": "Benvenuto in Submersion",
"@trips_itinerary_dayLabel": {
"placeholders": {
"dayNumber": {
@@ -3325,16 +3413,6 @@
"nav_tooltip_expandMenu": "Espandi menu",
"nav_transfer": "Trasferimento",
"nav_trips": "Viaggi",
- "onboarding_welcome_createProfile": "Crea il Tuo Profilo",
- "onboarding_welcome_createProfileSubtitle": "Inserisci il tuo nome per iniziare. Potrai aggiungere altri dettagli in seguito.",
- "onboarding_welcome_creating": "Creazione...",
- "onboarding_welcome_errorCreatingProfile": "Errore durante la creazione del profilo: {error}",
- "onboarding_welcome_getStarted": "Inizia",
- "onboarding_welcome_nameHint": "Inserisci il tuo nome",
- "onboarding_welcome_nameLabel": "Il Tuo Nome",
- "onboarding_welcome_nameValidation": "Inserisci il tuo nome",
- "onboarding_welcome_subtitle": "Registrazione e analisi avanzata delle immersioni",
- "onboarding_welcome_title": "Benvenuto in Submersion",
"plannerCanvas_bailout_available": "Disponibile {liters}",
"plannerCanvas_bailout_insufficient": "Gas di bailout insufficiente per il caso peggiore",
"plannerCanvas_bailout_required": "Necessario {liters}",
diff --git a/lib/l10n/arb/app_localizations.dart b/lib/l10n/arb/app_localizations.dart
index e8d6fb7a3..5eb4b76ba 100644
--- a/lib/l10n/arb/app_localizations.dart
+++ b/lib/l10n/arb/app_localizations.dart
@@ -8450,6 +8450,534 @@ abstract class AppLocalizations {
/// **'{count, plural, one{1 issue} other{{count} issues}}'**
String forms_section_issues(int count);
+ /// No description provided for @settings_manage_setupAssistant.
+ ///
+ /// In en, this message translates to:
+ /// **'Setup assistant'**
+ String get settings_manage_setupAssistant;
+
+ /// No description provided for @settings_manage_setupAssistant_subtitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Revisit units, appearance, and backup choices'**
+ String get settings_manage_setupAssistant_subtitle;
+
+ /// No description provided for @setup_backup_cloudCopy.
+ ///
+ /// In en, this message translates to:
+ /// **'Keep a backup copy in the cloud'**
+ String get setup_backup_cloudCopy;
+
+ /// No description provided for @setup_backup_frequency.
+ ///
+ /// In en, this message translates to:
+ /// **'Frequency'**
+ String get setup_backup_frequency;
+
+ /// No description provided for @setup_backup_frequency_daily.
+ ///
+ /// In en, this message translates to:
+ /// **'Daily'**
+ String get setup_backup_frequency_daily;
+
+ /// No description provided for @setup_backup_frequency_monthly.
+ ///
+ /// In en, this message translates to:
+ /// **'Monthly'**
+ String get setup_backup_frequency_monthly;
+
+ /// No description provided for @setup_backup_frequency_weekly.
+ ///
+ /// In en, this message translates to:
+ /// **'Weekly'**
+ String get setup_backup_frequency_weekly;
+
+ /// No description provided for @setup_backup_scheduleSubtitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Back up your logbook on a schedule'**
+ String get setup_backup_scheduleSubtitle;
+
+ /// No description provided for @setup_backup_scheduleToggle.
+ ///
+ /// In en, this message translates to:
+ /// **'Automatic backups'**
+ String get setup_backup_scheduleToggle;
+
+ /// No description provided for @setup_backup_subtitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Protect your dive log from day one.'**
+ String get setup_backup_subtitle;
+
+ /// No description provided for @setup_backup_title.
+ ///
+ /// In en, this message translates to:
+ /// **'Backups & Sync'**
+ String get setup_backup_title;
+
+ /// No description provided for @setup_common_back.
+ ///
+ /// In en, this message translates to:
+ /// **'Back'**
+ String get setup_common_back;
+
+ /// No description provided for @setup_common_next.
+ ///
+ /// In en, this message translates to:
+ /// **'Next'**
+ String get setup_common_next;
+
+ /// No description provided for @setup_common_skip.
+ ///
+ /// In en, this message translates to:
+ /// **'Skip'**
+ String get setup_common_skip;
+
+ /// No description provided for @setup_existing_folder_subtitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Point Submersion at a folder that already contains a library'**
+ String get setup_existing_folder_subtitle;
+
+ /// No description provided for @setup_existing_folder_title.
+ ///
+ /// In en, this message translates to:
+ /// **'Open an existing folder'**
+ String get setup_existing_folder_title;
+
+ /// No description provided for @setup_existing_restore_subtitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Pick a backup file exported from Submersion'**
+ String get setup_existing_restore_subtitle;
+
+ /// No description provided for @setup_existing_restore_title.
+ ///
+ /// In en, this message translates to:
+ /// **'Restore a backup file'**
+ String get setup_existing_restore_title;
+
+ /// No description provided for @setup_existing_subtitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Choose how to load your existing Submersion library'**
+ String get setup_existing_subtitle;
+
+ /// No description provided for @setup_existing_sync_subtitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Pull your library from iCloud, Dropbox, or S3'**
+ String get setup_existing_sync_subtitle;
+
+ /// No description provided for @setup_existing_sync_title.
+ ///
+ /// In en, this message translates to:
+ /// **'Connect cloud sync'**
+ String get setup_existing_sync_title;
+
+ /// No description provided for @setup_existing_title.
+ ///
+ /// In en, this message translates to:
+ /// **'Bring your data'**
+ String get setup_existing_title;
+
+ /// No description provided for @setup_finish_applying.
+ ///
+ /// In en, this message translates to:
+ /// **'Setting up...'**
+ String get setup_finish_applying;
+
+ /// No description provided for @setup_finish_error.
+ ///
+ /// In en, this message translates to:
+ /// **'Could not complete setup: {error}'**
+ String setup_finish_error(Object error);
+
+ /// No description provided for @setup_finish_feature_diveComputer.
+ ///
+ /// In en, this message translates to:
+ /// **'Download dives from your dive computer'**
+ String get setup_finish_feature_diveComputer;
+
+ /// No description provided for @setup_finish_feature_gear.
+ ///
+ /// In en, this message translates to:
+ /// **'Track gear and service intervals'**
+ String get setup_finish_feature_gear;
+
+ /// No description provided for @setup_finish_feature_import.
+ ///
+ /// In en, this message translates to:
+ /// **'Import logs from files and other apps'**
+ String get setup_finish_feature_import;
+
+ /// No description provided for @setup_finish_feature_sites.
+ ///
+ /// In en, this message translates to:
+ /// **'Map your dive sites'**
+ String get setup_finish_feature_sites;
+
+ /// No description provided for @setup_finish_feature_statistics.
+ ///
+ /// In en, this message translates to:
+ /// **'Explore statistics about your diving'**
+ String get setup_finish_feature_statistics;
+
+ /// No description provided for @setup_finish_start.
+ ///
+ /// In en, this message translates to:
+ /// **'Start logging'**
+ String get setup_finish_start;
+
+ /// No description provided for @setup_finish_subtitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Submersion can also...'**
+ String get setup_finish_subtitle;
+
+ /// No description provided for @setup_finish_title.
+ ///
+ /// In en, this message translates to:
+ /// **'You\'re all set'**
+ String get setup_finish_title;
+
+ /// No description provided for @setup_folder_notFound_message.
+ ///
+ /// In en, this message translates to:
+ /// **'The selected folder does not contain a Submersion database.'**
+ String get setup_folder_notFound_message;
+
+ /// No description provided for @setup_folder_notFound_title.
+ ///
+ /// In en, this message translates to:
+ /// **'No library in that folder'**
+ String get setup_folder_notFound_title;
+
+ /// No description provided for @setup_folder_pick.
+ ///
+ /// In en, this message translates to:
+ /// **'Choose folder'**
+ String get setup_folder_pick;
+
+ /// No description provided for @setup_folder_switching.
+ ///
+ /// In en, this message translates to:
+ /// **'Opening library...'**
+ String get setup_folder_switching;
+
+ /// No description provided for @setup_folder_title.
+ ///
+ /// In en, this message translates to:
+ /// **'Open existing folder'**
+ String get setup_folder_title;
+
+ /// No description provided for @setup_profile_nameHint.
+ ///
+ /// In en, this message translates to:
+ /// **'Enter your name'**
+ String get setup_profile_nameHint;
+
+ /// No description provided for @setup_profile_nameLabel.
+ ///
+ /// In en, this message translates to:
+ /// **'Your Name'**
+ String get setup_profile_nameLabel;
+
+ /// No description provided for @setup_profile_nameValidation.
+ ///
+ /// In en, this message translates to:
+ /// **'Please enter your name'**
+ String get setup_profile_nameValidation;
+
+ /// No description provided for @setup_profile_subtitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Enter your name to get started. You can add more details later.'**
+ String get setup_profile_subtitle;
+
+ /// No description provided for @setup_profile_title.
+ ///
+ /// In en, this message translates to:
+ /// **'Create Your Profile'**
+ String get setup_profile_title;
+
+ /// No description provided for @setup_restore_inProgress.
+ ///
+ /// In en, this message translates to:
+ /// **'Restoring...'**
+ String get setup_restore_inProgress;
+
+ /// No description provided for @setup_restore_pick.
+ ///
+ /// In en, this message translates to:
+ /// **'Choose backup file'**
+ String get setup_restore_pick;
+
+ /// No description provided for @setup_restore_title.
+ ///
+ /// In en, this message translates to:
+ /// **'Restore backup'**
+ String get setup_restore_title;
+
+ /// No description provided for @setup_step_backup.
+ ///
+ /// In en, this message translates to:
+ /// **'Backup'**
+ String get setup_step_backup;
+
+ /// No description provided for @setup_step_finish.
+ ///
+ /// In en, this message translates to:
+ /// **'Done'**
+ String get setup_step_finish;
+
+ /// No description provided for @setup_step_profile.
+ ///
+ /// In en, this message translates to:
+ /// **'Profile'**
+ String get setup_step_profile;
+
+ /// No description provided for @setup_step_units.
+ ///
+ /// In en, this message translates to:
+ /// **'Units'**
+ String get setup_step_units;
+
+ /// No description provided for @setup_syncPull_continue.
+ ///
+ /// In en, this message translates to:
+ /// **'Continue'**
+ String get setup_syncPull_continue;
+
+ /// No description provided for @setup_syncPull_noLibrary_message.
+ ///
+ /// In en, this message translates to:
+ /// **'No existing Submersion library was found on this account. Start fresh instead? Your connection will be kept.'**
+ String get setup_syncPull_noLibrary_message;
+
+ /// No description provided for @setup_syncPull_noLibrary_title.
+ ///
+ /// In en, this message translates to:
+ /// **'No library found'**
+ String get setup_syncPull_noLibrary_title;
+
+ /// No description provided for @setup_syncPull_success.
+ ///
+ /// In en, this message translates to:
+ /// **'Library adopted'**
+ String get setup_syncPull_success;
+
+ /// No description provided for @setup_syncPull_syncing.
+ ///
+ /// In en, this message translates to:
+ /// **'Pulling your library...'**
+ String get setup_syncPull_syncing;
+
+ /// No description provided for @setup_syncPull_title.
+ ///
+ /// In en, this message translates to:
+ /// **'Connect and pull'**
+ String get setup_syncPull_title;
+
+ /// No description provided for @setup_sync_changeProvider.
+ ///
+ /// In en, this message translates to:
+ /// **'Change provider'**
+ String get setup_sync_changeProvider;
+
+ /// No description provided for @setup_sync_connectedTo.
+ ///
+ /// In en, this message translates to:
+ /// **'Connected to {provider}'**
+ String setup_sync_connectedTo(String provider);
+
+ /// No description provided for @setup_sync_error.
+ ///
+ /// In en, this message translates to:
+ /// **'Could not connect: {error}'**
+ String setup_sync_error(Object error);
+
+ /// No description provided for @setup_sync_header.
+ ///
+ /// In en, this message translates to:
+ /// **'Cloud sync'**
+ String get setup_sync_header;
+
+ /// No description provided for @setup_sync_icloudUnavailable.
+ ///
+ /// In en, this message translates to:
+ /// **'iCloud is not available on this device'**
+ String get setup_sync_icloudUnavailable;
+
+ /// No description provided for @setup_sync_libraryFound_adopt.
+ ///
+ /// In en, this message translates to:
+ /// **'Adopt existing library'**
+ String get setup_sync_libraryFound_adopt;
+
+ /// No description provided for @setup_sync_libraryFound_keepFresh.
+ ///
+ /// In en, this message translates to:
+ /// **'Start fresh'**
+ String get setup_sync_libraryFound_keepFresh;
+
+ /// No description provided for @setup_sync_libraryFound_message.
+ ///
+ /// In en, this message translates to:
+ /// **'This account already contains a Submersion library. Adopt it instead of starting fresh?'**
+ String get setup_sync_libraryFound_message;
+
+ /// No description provided for @setup_sync_libraryFound_title.
+ ///
+ /// In en, this message translates to:
+ /// **'Existing library found'**
+ String get setup_sync_libraryFound_title;
+
+ /// No description provided for @setup_sync_manageInSettings.
+ ///
+ /// In en, this message translates to:
+ /// **'Manage in Settings'**
+ String get setup_sync_manageInSettings;
+
+ /// No description provided for @setup_sync_notConnected.
+ ///
+ /// In en, this message translates to:
+ /// **'Not connected'**
+ String get setup_sync_notConnected;
+
+ /// No description provided for @setup_sync_subtitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Sync your logbook across devices'**
+ String get setup_sync_subtitle;
+
+ /// No description provided for @setup_units_advanced.
+ ///
+ /// In en, this message translates to:
+ /// **'Fine-tune units'**
+ String get setup_units_advanced;
+
+ /// No description provided for @setup_units_altitude.
+ ///
+ /// In en, this message translates to:
+ /// **'Altitude'**
+ String get setup_units_altitude;
+
+ /// No description provided for @setup_units_dateFormat.
+ ///
+ /// In en, this message translates to:
+ /// **'Date format'**
+ String get setup_units_dateFormat;
+
+ /// No description provided for @setup_units_depth.
+ ///
+ /// In en, this message translates to:
+ /// **'Depth'**
+ String get setup_units_depth;
+
+ /// No description provided for @setup_units_imperial.
+ ///
+ /// In en, this message translates to:
+ /// **'Imperial'**
+ String get setup_units_imperial;
+
+ /// No description provided for @setup_units_metric.
+ ///
+ /// In en, this message translates to:
+ /// **'Metric'**
+ String get setup_units_metric;
+
+ /// No description provided for @setup_units_pressure.
+ ///
+ /// In en, this message translates to:
+ /// **'Pressure'**
+ String get setup_units_pressure;
+
+ /// No description provided for @setup_units_sac.
+ ///
+ /// In en, this message translates to:
+ /// **'SAC rate'**
+ String get setup_units_sac;
+
+ /// No description provided for @setup_units_subtitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Choose how measurements are displayed. You can fine-tune each unit.'**
+ String get setup_units_subtitle;
+
+ /// No description provided for @setup_units_temperature.
+ ///
+ /// In en, this message translates to:
+ /// **'Temperature'**
+ String get setup_units_temperature;
+
+ /// No description provided for @setup_units_timeFormat.
+ ///
+ /// In en, this message translates to:
+ /// **'Time format'**
+ String get setup_units_timeFormat;
+
+ /// No description provided for @setup_units_title.
+ ///
+ /// In en, this message translates to:
+ /// **'Units'**
+ String get setup_units_title;
+
+ /// No description provided for @setup_units_volume.
+ ///
+ /// In en, this message translates to:
+ /// **'Volume'**
+ String get setup_units_volume;
+
+ /// No description provided for @setup_units_weight.
+ ///
+ /// In en, this message translates to:
+ /// **'Weight'**
+ String get setup_units_weight;
+
+ /// No description provided for @setup_welcome_existingData_subtitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Restore a backup, connect cloud sync, or open an existing folder'**
+ String get setup_welcome_existingData_subtitle;
+
+ /// No description provided for @setup_welcome_existingData_title.
+ ///
+ /// In en, this message translates to:
+ /// **'I have existing Submersion data'**
+ String get setup_welcome_existingData_title;
+
+ /// No description provided for @setup_welcome_skipSetup.
+ ///
+ /// In en, this message translates to:
+ /// **'Skip setup'**
+ String get setup_welcome_skipSetup;
+
+ /// No description provided for @setup_welcome_startFresh_subtitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Create your diver profile and configure the app'**
+ String get setup_welcome_startFresh_subtitle;
+
+ /// No description provided for @setup_welcome_startFresh_title.
+ ///
+ /// In en, this message translates to:
+ /// **'Set up a new logbook'**
+ String get setup_welcome_startFresh_title;
+
+ /// No description provided for @setup_welcome_subtitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Advanced dive logging and analytics'**
+ String get setup_welcome_subtitle;
+
+ /// No description provided for @setup_welcome_title.
+ ///
+ /// In en, this message translates to:
+ /// **'Welcome to Submersion'**
+ String get setup_welcome_title;
+
/// No description provided for @siteMatchReview_title.
///
/// In en, this message translates to:
@@ -18068,66 +18596,6 @@ abstract class AppLocalizations {
/// **'Trips'**
String get nav_trips;
- /// No description provided for @onboarding_welcome_createProfile.
- ///
- /// In en, this message translates to:
- /// **'Create Your Profile'**
- String get onboarding_welcome_createProfile;
-
- /// No description provided for @onboarding_welcome_createProfileSubtitle.
- ///
- /// In en, this message translates to:
- /// **'Enter your name to get started. You can add more details later.'**
- String get onboarding_welcome_createProfileSubtitle;
-
- /// No description provided for @onboarding_welcome_creating.
- ///
- /// In en, this message translates to:
- /// **'Creating...'**
- String get onboarding_welcome_creating;
-
- /// No description provided for @onboarding_welcome_errorCreatingProfile.
- ///
- /// In en, this message translates to:
- /// **'Error creating profile: {error}'**
- String onboarding_welcome_errorCreatingProfile(Object error);
-
- /// No description provided for @onboarding_welcome_getStarted.
- ///
- /// In en, this message translates to:
- /// **'Get Started'**
- String get onboarding_welcome_getStarted;
-
- /// No description provided for @onboarding_welcome_nameHint.
- ///
- /// In en, this message translates to:
- /// **'Enter your name'**
- String get onboarding_welcome_nameHint;
-
- /// No description provided for @onboarding_welcome_nameLabel.
- ///
- /// In en, this message translates to:
- /// **'Your Name'**
- String get onboarding_welcome_nameLabel;
-
- /// No description provided for @onboarding_welcome_nameValidation.
- ///
- /// In en, this message translates to:
- /// **'Please enter your name'**
- String get onboarding_welcome_nameValidation;
-
- /// No description provided for @onboarding_welcome_subtitle.
- ///
- /// In en, this message translates to:
- /// **'Advanced dive logging and analytics'**
- String get onboarding_welcome_subtitle;
-
- /// No description provided for @onboarding_welcome_title.
- ///
- /// In en, this message translates to:
- /// **'Welcome to Submersion'**
- String get onboarding_welcome_title;
-
/// No description provided for @plannerCanvas_bailout_available.
///
/// 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 c7a0e4fc4..654360611 100644
--- a/lib/l10n/arb/app_localizations_ar.dart
+++ b/lib/l10n/arb/app_localizations_ar.dart
@@ -4902,6 +4902,292 @@ class AppLocalizationsAr extends AppLocalizations {
return '$_temp0';
}
+ @override
+ String get settings_manage_setupAssistant => 'مساعد الإعداد';
+
+ @override
+ String get settings_manage_setupAssistant_subtitle =>
+ 'مراجعة الوحدات والمظهر وخيارات النسخ الاحتياطي';
+
+ @override
+ String get setup_backup_cloudCopy => 'الاحتفاظ بنسخة احتياطية في السحابة';
+
+ @override
+ String get setup_backup_frequency => 'التكرار';
+
+ @override
+ String get setup_backup_frequency_daily => 'يوميًا';
+
+ @override
+ String get setup_backup_frequency_monthly => 'شهريًا';
+
+ @override
+ String get setup_backup_frequency_weekly => 'أسبوعيًا';
+
+ @override
+ String get setup_backup_scheduleSubtitle =>
+ 'انسخ سجل غوصك احتياطيًا وفق جدول زمني';
+
+ @override
+ String get setup_backup_scheduleToggle => 'نسخ احتياطي تلقائي';
+
+ @override
+ String get setup_backup_subtitle => 'احمِ سجل غوصك من اليوم الأول.';
+
+ @override
+ String get setup_backup_title => 'النسخ الاحتياطي والمزامنة';
+
+ @override
+ String get setup_common_back => 'رجوع';
+
+ @override
+ String get setup_common_next => 'التالي';
+
+ @override
+ String get setup_common_skip => 'تخطي';
+
+ @override
+ String get setup_existing_folder_subtitle =>
+ 'وجّه Submersion إلى مجلد يحتوي بالفعل على مكتبة';
+
+ @override
+ String get setup_existing_folder_title => 'فتح مجلد موجود';
+
+ @override
+ String get setup_existing_restore_subtitle =>
+ 'اختر ملف نسخ احتياطي تم تصديره من Submersion';
+
+ @override
+ String get setup_existing_restore_title => 'استعادة ملف نسخ احتياطي';
+
+ @override
+ String get setup_existing_subtitle =>
+ 'اختر طريقة تحميل مكتبة Submersion الحالية';
+
+ @override
+ String get setup_existing_sync_subtitle =>
+ 'اسحب مكتبتك من iCloud أو Dropbox أو S3';
+
+ @override
+ String get setup_existing_sync_title => 'توصيل مزامنة السحابة';
+
+ @override
+ String get setup_existing_title => 'أحضر بياناتك';
+
+ @override
+ String get setup_finish_applying => 'جارٍ الإعداد...';
+
+ @override
+ String setup_finish_error(Object error) {
+ return 'تعذّر إكمال الإعداد: $error';
+ }
+
+ @override
+ String get setup_finish_feature_diveComputer =>
+ 'نزّل الغوصات من كمبيوتر الغوص';
+
+ @override
+ String get setup_finish_feature_gear => 'تتبّع المعدات ومواعيد الصيانة';
+
+ @override
+ String get setup_finish_feature_import =>
+ 'استورد السجلات من الملفات والتطبيقات الأخرى';
+
+ @override
+ String get setup_finish_feature_sites => 'اعرض مواقع الغوص على الخريطة';
+
+ @override
+ String get setup_finish_feature_statistics => 'استكشف إحصاءات الغوص';
+
+ @override
+ String get setup_finish_start => 'ابدأ التسجيل';
+
+ @override
+ String get setup_finish_subtitle => 'يمكن لـ Submersion أيضًا...';
+
+ @override
+ String get setup_finish_title => 'كل شيء جاهز';
+
+ @override
+ String get setup_folder_notFound_message =>
+ 'المجلد المحدد لا يحتوي على قاعدة بيانات Submersion.';
+
+ @override
+ String get setup_folder_notFound_title => 'لا توجد مكتبة في هذا المجلد';
+
+ @override
+ String get setup_folder_pick => 'اختيار مجلد';
+
+ @override
+ String get setup_folder_switching => 'جارٍ فتح المكتبة...';
+
+ @override
+ String get setup_folder_title => 'فتح مجلد موجود';
+
+ @override
+ String get setup_profile_nameHint => 'أدخل اسمك';
+
+ @override
+ String get setup_profile_nameLabel => 'اسمك';
+
+ @override
+ String get setup_profile_nameValidation => 'الرجاء إدخال اسمك';
+
+ @override
+ String get setup_profile_subtitle =>
+ 'أدخل اسمك للبدء. يمكنك إضافة المزيد من التفاصيل لاحقاً.';
+
+ @override
+ String get setup_profile_title => 'إنشاء ملفك الشخصي';
+
+ @override
+ String get setup_restore_inProgress => 'جارٍ الاستعادة...';
+
+ @override
+ String get setup_restore_pick => 'اختيار ملف النسخ الاحتياطي';
+
+ @override
+ String get setup_restore_title => 'استعادة النسخة الاحتياطية';
+
+ @override
+ String get setup_step_backup => 'نسخ احتياطي';
+
+ @override
+ String get setup_step_finish => 'تم';
+
+ @override
+ String get setup_step_profile => 'الملف الشخصي';
+
+ @override
+ String get setup_step_units => 'الوحدات';
+
+ @override
+ String get setup_syncPull_continue => 'متابعة';
+
+ @override
+ String get setup_syncPull_noLibrary_message =>
+ 'لم يتم العثور على مكتبة Submersion في هذا الحساب. هل تبدأ من جديد؟ سيتم الاحتفاظ باتصالك.';
+
+ @override
+ String get setup_syncPull_noLibrary_title => 'لم يتم العثور على مكتبة';
+
+ @override
+ String get setup_syncPull_success => 'تم اعتماد المكتبة';
+
+ @override
+ String get setup_syncPull_syncing => 'جارٍ سحب مكتبتك...';
+
+ @override
+ String get setup_syncPull_title => 'الاتصال والسحب';
+
+ @override
+ String get setup_sync_changeProvider => 'تغيير المزوّد';
+
+ @override
+ String setup_sync_connectedTo(String provider) {
+ return 'متصل بـ $provider';
+ }
+
+ @override
+ String setup_sync_error(Object error) {
+ return 'تعذّر الاتصال: $error';
+ }
+
+ @override
+ String get setup_sync_header => 'مزامنة السحابة';
+
+ @override
+ String get setup_sync_icloudUnavailable => 'iCloud غير متاح على هذا الجهاز';
+
+ @override
+ String get setup_sync_libraryFound_adopt => 'اعتماد المكتبة الحالية';
+
+ @override
+ String get setup_sync_libraryFound_keepFresh => 'البدء من جديد';
+
+ @override
+ String get setup_sync_libraryFound_message =>
+ 'يحتوي هذا الحساب بالفعل على مكتبة Submersion. هل تريد اعتمادها بدلًا من البدء من جديد؟';
+
+ @override
+ String get setup_sync_libraryFound_title => 'تم العثور على مكتبة موجودة';
+
+ @override
+ String get setup_sync_manageInSettings => 'الإدارة في الإعدادات';
+
+ @override
+ String get setup_sync_notConnected => 'غير متصل';
+
+ @override
+ String get setup_sync_subtitle => 'زامن سجلك عبر الأجهزة';
+
+ @override
+ String get setup_units_advanced => 'ضبط الوحدات';
+
+ @override
+ String get setup_units_altitude => 'الارتفاع';
+
+ @override
+ String get setup_units_dateFormat => 'تنسيق التاريخ';
+
+ @override
+ String get setup_units_depth => 'العمق';
+
+ @override
+ String get setup_units_imperial => 'إمبراطوري';
+
+ @override
+ String get setup_units_metric => 'متري';
+
+ @override
+ String get setup_units_pressure => 'الضغط';
+
+ @override
+ String get setup_units_sac => 'معدل SAC';
+
+ @override
+ String get setup_units_subtitle =>
+ 'اختر كيفية عرض القياسات. يمكنك ضبط كل وحدة.';
+
+ @override
+ String get setup_units_temperature => 'درجة الحرارة';
+
+ @override
+ String get setup_units_timeFormat => 'تنسيق الوقت';
+
+ @override
+ String get setup_units_title => 'الوحدات';
+
+ @override
+ String get setup_units_volume => 'الحجم';
+
+ @override
+ String get setup_units_weight => 'الوزن';
+
+ @override
+ String get setup_welcome_existingData_subtitle =>
+ 'استعد نسخة احتياطية، أو صِل مزامنة السحابة، أو افتح مجلدًا موجودًا';
+
+ @override
+ String get setup_welcome_existingData_title =>
+ 'لديّ بيانات Submersion موجودة';
+
+ @override
+ String get setup_welcome_skipSetup => 'تخطي الإعداد';
+
+ @override
+ String get setup_welcome_startFresh_subtitle =>
+ 'أنشئ ملف الغواص الخاص بك واضبط التطبيق';
+
+ @override
+ String get setup_welcome_startFresh_title => 'إعداد سجل جديد';
+
+ @override
+ String get setup_welcome_subtitle => 'تسجيل وتحليل متقدم للغوص';
+
+ @override
+ String get setup_welcome_title => 'مرحباً بك في Submersion';
+
@override
String get siteMatchReview_title => 'مطابقة المواقع';
@@ -10402,39 +10688,6 @@ class AppLocalizationsAr extends AppLocalizations {
@override
String get nav_trips => 'الرحلات';
- @override
- String get onboarding_welcome_createProfile => 'إنشاء ملفك الشخصي';
-
- @override
- String get onboarding_welcome_createProfileSubtitle =>
- 'أدخل اسمك للبدء. يمكنك إضافة المزيد من التفاصيل لاحقاً.';
-
- @override
- String get onboarding_welcome_creating => 'جارٍ الإنشاء...';
-
- @override
- String onboarding_welcome_errorCreatingProfile(Object error) {
- return 'خطأ في إنشاء الملف الشخصي: $error';
- }
-
- @override
- String get onboarding_welcome_getStarted => 'البدء';
-
- @override
- String get onboarding_welcome_nameHint => 'أدخل اسمك';
-
- @override
- String get onboarding_welcome_nameLabel => 'اسمك';
-
- @override
- String get onboarding_welcome_nameValidation => 'الرجاء إدخال اسمك';
-
- @override
- String get onboarding_welcome_subtitle => 'تسجيل وتحليل متقدم للغوص';
-
- @override
- String get onboarding_welcome_title => 'مرحباً بك في Submersion';
-
@override
String plannerCanvas_bailout_available(String liters) {
return 'المتاح $liters';
diff --git a/lib/l10n/arb/app_localizations_de.dart b/lib/l10n/arb/app_localizations_de.dart
index 4a407713e..74da145de 100644
--- a/lib/l10n/arb/app_localizations_de.dart
+++ b/lib/l10n/arb/app_localizations_de.dart
@@ -5016,6 +5016,300 @@ class AppLocalizationsDe extends AppLocalizations {
return '$_temp0';
}
+ @override
+ String get settings_manage_setupAssistant => 'Einrichtungsassistent';
+
+ @override
+ String get settings_manage_setupAssistant_subtitle =>
+ 'Einheiten, Darstellung und Backup-Einstellungen erneut durchgehen';
+
+ @override
+ String get setup_backup_cloudCopy =>
+ 'Eine Sicherungskopie in der Cloud aufbewahren';
+
+ @override
+ String get setup_backup_frequency => 'Häufigkeit';
+
+ @override
+ String get setup_backup_frequency_daily => 'Täglich';
+
+ @override
+ String get setup_backup_frequency_monthly => 'Monatlich';
+
+ @override
+ String get setup_backup_frequency_weekly => 'Wöchentlich';
+
+ @override
+ String get setup_backup_scheduleSubtitle => 'Ihr Logbuch regelmäßig sichern';
+
+ @override
+ String get setup_backup_scheduleToggle => 'Automatische Sicherungen';
+
+ @override
+ String get setup_backup_subtitle =>
+ 'Schützen Sie Ihr Tauchlogbuch vom ersten Tag an.';
+
+ @override
+ String get setup_backup_title => 'Sicherung & Synchronisierung';
+
+ @override
+ String get setup_common_back => 'Zurück';
+
+ @override
+ String get setup_common_next => 'Weiter';
+
+ @override
+ String get setup_common_skip => 'Überspringen';
+
+ @override
+ String get setup_existing_folder_subtitle =>
+ 'Submersion auf einen Ordner verweisen, der bereits eine Bibliothek enthält';
+
+ @override
+ String get setup_existing_folder_title => 'Vorhandenen Ordner öffnen';
+
+ @override
+ String get setup_existing_restore_subtitle =>
+ 'Eine aus Submersion exportierte Sicherungsdatei auswählen';
+
+ @override
+ String get setup_existing_restore_title => 'Sicherungsdatei wiederherstellen';
+
+ @override
+ String get setup_existing_subtitle =>
+ 'Wählen Sie, wie Ihre vorhandene Submersion-Bibliothek geladen werden soll';
+
+ @override
+ String get setup_existing_sync_subtitle =>
+ 'Ihre Bibliothek aus iCloud, Dropbox oder S3 laden';
+
+ @override
+ String get setup_existing_sync_title => 'Cloud-Synchronisierung verbinden';
+
+ @override
+ String get setup_existing_title => 'Daten übernehmen';
+
+ @override
+ String get setup_finish_applying => 'Wird eingerichtet...';
+
+ @override
+ String setup_finish_error(Object error) {
+ return 'Einrichtung konnte nicht abgeschlossen werden: $error';
+ }
+
+ @override
+ String get setup_finish_feature_diveComputer =>
+ 'Tauchgänge von Ihrem Tauchcomputer herunterladen';
+
+ @override
+ String get setup_finish_feature_gear =>
+ 'Ausrüstung und Wartungsintervalle verwalten';
+
+ @override
+ String get setup_finish_feature_import =>
+ 'Logbücher aus Dateien und anderen Apps importieren';
+
+ @override
+ String get setup_finish_feature_sites =>
+ 'Ihre Tauchplätze auf der Karte anzeigen';
+
+ @override
+ String get setup_finish_feature_statistics =>
+ 'Statistiken zu Ihren Tauchgängen entdecken';
+
+ @override
+ String get setup_finish_start => 'Loslegen';
+
+ @override
+ String get setup_finish_subtitle => 'Submersion kann außerdem...';
+
+ @override
+ String get setup_finish_title => 'Alles bereit';
+
+ @override
+ String get setup_folder_notFound_message =>
+ 'Der ausgewählte Ordner enthält keine Submersion-Datenbank.';
+
+ @override
+ String get setup_folder_notFound_title => 'Keine Bibliothek in diesem Ordner';
+
+ @override
+ String get setup_folder_pick => 'Ordner auswählen';
+
+ @override
+ String get setup_folder_switching => 'Bibliothek wird geöffnet...';
+
+ @override
+ String get setup_folder_title => 'Vorhandenen Ordner öffnen';
+
+ @override
+ String get setup_profile_nameHint => 'Geben Sie Ihren Namen ein';
+
+ @override
+ String get setup_profile_nameLabel => 'Ihr Name';
+
+ @override
+ String get setup_profile_nameValidation => 'Bitte geben Sie Ihren Namen ein';
+
+ @override
+ String get setup_profile_subtitle =>
+ 'Geben Sie Ihren Namen ein, um zu beginnen. Sie können später weitere Details hinzufügen.';
+
+ @override
+ String get setup_profile_title => 'Erstellen Sie Ihr Profil';
+
+ @override
+ String get setup_restore_inProgress => 'Wird wiederhergestellt...';
+
+ @override
+ String get setup_restore_pick => 'Sicherungsdatei auswählen';
+
+ @override
+ String get setup_restore_title => 'Sicherung wiederherstellen';
+
+ @override
+ String get setup_step_backup => 'Sicherung';
+
+ @override
+ String get setup_step_finish => 'Fertig';
+
+ @override
+ String get setup_step_profile => 'Profil';
+
+ @override
+ String get setup_step_units => 'Einheiten';
+
+ @override
+ String get setup_syncPull_continue => 'Weiter';
+
+ @override
+ String get setup_syncPull_noLibrary_message =>
+ 'Auf diesem Konto wurde keine Submersion-Bibliothek gefunden. Stattdessen neu beginnen? Die Verbindung bleibt bestehen.';
+
+ @override
+ String get setup_syncPull_noLibrary_title => 'Keine Bibliothek gefunden';
+
+ @override
+ String get setup_syncPull_success => 'Bibliothek übernommen';
+
+ @override
+ String get setup_syncPull_syncing => 'Bibliothek wird geladen...';
+
+ @override
+ String get setup_syncPull_title => 'Verbinden und laden';
+
+ @override
+ String get setup_sync_changeProvider => 'Anbieter wechseln';
+
+ @override
+ String setup_sync_connectedTo(String provider) {
+ return 'Verbunden mit $provider';
+ }
+
+ @override
+ String setup_sync_error(Object error) {
+ return 'Verbindung fehlgeschlagen: $error';
+ }
+
+ @override
+ String get setup_sync_header => 'Cloud-Synchronisierung';
+
+ @override
+ String get setup_sync_icloudUnavailable =>
+ 'iCloud ist auf diesem Gerät nicht verfügbar';
+
+ @override
+ String get setup_sync_libraryFound_adopt =>
+ 'Vorhandene Bibliothek übernehmen';
+
+ @override
+ String get setup_sync_libraryFound_keepFresh => 'Neu beginnen';
+
+ @override
+ String get setup_sync_libraryFound_message =>
+ 'Dieses Konto enthält bereits eine Submersion-Bibliothek. Diese übernehmen, statt neu zu beginnen?';
+
+ @override
+ String get setup_sync_libraryFound_title => 'Vorhandene Bibliothek gefunden';
+
+ @override
+ String get setup_sync_manageInSettings => 'In den Einstellungen verwalten';
+
+ @override
+ String get setup_sync_notConnected => 'Nicht verbunden';
+
+ @override
+ String get setup_sync_subtitle =>
+ 'Ihr Logbuch auf allen Geräten synchronisieren';
+
+ @override
+ String get setup_units_advanced => 'Einheiten anpassen';
+
+ @override
+ String get setup_units_altitude => 'Höhe';
+
+ @override
+ String get setup_units_dateFormat => 'Datumsformat';
+
+ @override
+ String get setup_units_depth => 'Tiefe';
+
+ @override
+ String get setup_units_imperial => 'Imperial';
+
+ @override
+ String get setup_units_metric => 'Metrisch';
+
+ @override
+ String get setup_units_pressure => 'Druck';
+
+ @override
+ String get setup_units_sac => 'SAC-Rate';
+
+ @override
+ String get setup_units_subtitle =>
+ 'Wählen Sie, wie Messwerte angezeigt werden. Jede Einheit lässt sich einzeln anpassen.';
+
+ @override
+ String get setup_units_temperature => 'Temperatur';
+
+ @override
+ String get setup_units_timeFormat => 'Zeitformat';
+
+ @override
+ String get setup_units_title => 'Einheiten';
+
+ @override
+ String get setup_units_volume => 'Volumen';
+
+ @override
+ String get setup_units_weight => 'Gewicht';
+
+ @override
+ String get setup_welcome_existingData_subtitle =>
+ 'Sicherung wiederherstellen, Cloud-Sync verbinden oder vorhandenen Ordner öffnen';
+
+ @override
+ String get setup_welcome_existingData_title =>
+ 'Ich habe bereits Submersion-Daten';
+
+ @override
+ String get setup_welcome_skipSetup => 'Einrichtung überspringen';
+
+ @override
+ String get setup_welcome_startFresh_subtitle =>
+ 'Taucherprofil erstellen und die App einrichten';
+
+ @override
+ String get setup_welcome_startFresh_title => 'Neues Logbuch einrichten';
+
+ @override
+ String get setup_welcome_subtitle =>
+ 'Erweiterte Tauchprotokollierung und -analyse';
+
+ @override
+ String get setup_welcome_title => 'Willkommen bei Submersion';
+
@override
String get siteMatchReview_title => 'Tauchplätze zuordnen';
@@ -10602,41 +10896,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get nav_trips => 'Reisen';
- @override
- String get onboarding_welcome_createProfile => 'Erstellen Sie Ihr Profil';
-
- @override
- String get onboarding_welcome_createProfileSubtitle =>
- 'Geben Sie Ihren Namen ein, um zu beginnen. Sie können später weitere Details hinzufügen.';
-
- @override
- String get onboarding_welcome_creating => 'Wird erstellt...';
-
- @override
- String onboarding_welcome_errorCreatingProfile(Object error) {
- return 'Fehler beim Erstellen des Profils: $error';
- }
-
- @override
- String get onboarding_welcome_getStarted => 'Loslegen';
-
- @override
- String get onboarding_welcome_nameHint => 'Geben Sie Ihren Namen ein';
-
- @override
- String get onboarding_welcome_nameLabel => 'Ihr Name';
-
- @override
- String get onboarding_welcome_nameValidation =>
- 'Bitte geben Sie Ihren Namen ein';
-
- @override
- String get onboarding_welcome_subtitle =>
- 'Erweiterte Tauchprotokollierung und -analyse';
-
- @override
- String get onboarding_welcome_title => 'Willkommen bei Submersion';
-
@override
String plannerCanvas_bailout_available(String liters) {
return 'Verfügbar $liters';
diff --git a/lib/l10n/arb/app_localizations_en.dart b/lib/l10n/arb/app_localizations_en.dart
index 1cd6f7dd2..a8685d329 100644
--- a/lib/l10n/arb/app_localizations_en.dart
+++ b/lib/l10n/arb/app_localizations_en.dart
@@ -4923,6 +4923,294 @@ class AppLocalizationsEn extends AppLocalizations {
return '$_temp0';
}
+ @override
+ String get settings_manage_setupAssistant => 'Setup assistant';
+
+ @override
+ String get settings_manage_setupAssistant_subtitle =>
+ 'Revisit units, appearance, and backup choices';
+
+ @override
+ String get setup_backup_cloudCopy => 'Keep a backup copy in the cloud';
+
+ @override
+ String get setup_backup_frequency => 'Frequency';
+
+ @override
+ String get setup_backup_frequency_daily => 'Daily';
+
+ @override
+ String get setup_backup_frequency_monthly => 'Monthly';
+
+ @override
+ String get setup_backup_frequency_weekly => 'Weekly';
+
+ @override
+ String get setup_backup_scheduleSubtitle =>
+ 'Back up your logbook on a schedule';
+
+ @override
+ String get setup_backup_scheduleToggle => 'Automatic backups';
+
+ @override
+ String get setup_backup_subtitle => 'Protect your dive log from day one.';
+
+ @override
+ String get setup_backup_title => 'Backups & Sync';
+
+ @override
+ String get setup_common_back => 'Back';
+
+ @override
+ String get setup_common_next => 'Next';
+
+ @override
+ String get setup_common_skip => 'Skip';
+
+ @override
+ String get setup_existing_folder_subtitle =>
+ 'Point Submersion at a folder that already contains a library';
+
+ @override
+ String get setup_existing_folder_title => 'Open an existing folder';
+
+ @override
+ String get setup_existing_restore_subtitle =>
+ 'Pick a backup file exported from Submersion';
+
+ @override
+ String get setup_existing_restore_title => 'Restore a backup file';
+
+ @override
+ String get setup_existing_subtitle =>
+ 'Choose how to load your existing Submersion library';
+
+ @override
+ String get setup_existing_sync_subtitle =>
+ 'Pull your library from iCloud, Dropbox, or S3';
+
+ @override
+ String get setup_existing_sync_title => 'Connect cloud sync';
+
+ @override
+ String get setup_existing_title => 'Bring your data';
+
+ @override
+ String get setup_finish_applying => 'Setting up...';
+
+ @override
+ String setup_finish_error(Object error) {
+ return 'Could not complete setup: $error';
+ }
+
+ @override
+ String get setup_finish_feature_diveComputer =>
+ 'Download dives from your dive computer';
+
+ @override
+ String get setup_finish_feature_gear => 'Track gear and service intervals';
+
+ @override
+ String get setup_finish_feature_import =>
+ 'Import logs from files and other apps';
+
+ @override
+ String get setup_finish_feature_sites => 'Map your dive sites';
+
+ @override
+ String get setup_finish_feature_statistics =>
+ 'Explore statistics about your diving';
+
+ @override
+ String get setup_finish_start => 'Start logging';
+
+ @override
+ String get setup_finish_subtitle => 'Submersion can also...';
+
+ @override
+ String get setup_finish_title => 'You\'re all set';
+
+ @override
+ String get setup_folder_notFound_message =>
+ 'The selected folder does not contain a Submersion database.';
+
+ @override
+ String get setup_folder_notFound_title => 'No library in that folder';
+
+ @override
+ String get setup_folder_pick => 'Choose folder';
+
+ @override
+ String get setup_folder_switching => 'Opening library...';
+
+ @override
+ String get setup_folder_title => 'Open existing folder';
+
+ @override
+ String get setup_profile_nameHint => 'Enter your name';
+
+ @override
+ String get setup_profile_nameLabel => 'Your Name';
+
+ @override
+ String get setup_profile_nameValidation => 'Please enter your name';
+
+ @override
+ String get setup_profile_subtitle =>
+ 'Enter your name to get started. You can add more details later.';
+
+ @override
+ String get setup_profile_title => 'Create Your Profile';
+
+ @override
+ String get setup_restore_inProgress => 'Restoring...';
+
+ @override
+ String get setup_restore_pick => 'Choose backup file';
+
+ @override
+ String get setup_restore_title => 'Restore backup';
+
+ @override
+ String get setup_step_backup => 'Backup';
+
+ @override
+ String get setup_step_finish => 'Done';
+
+ @override
+ String get setup_step_profile => 'Profile';
+
+ @override
+ String get setup_step_units => 'Units';
+
+ @override
+ String get setup_syncPull_continue => 'Continue';
+
+ @override
+ String get setup_syncPull_noLibrary_message =>
+ 'No existing Submersion library was found on this account. Start fresh instead? Your connection will be kept.';
+
+ @override
+ String get setup_syncPull_noLibrary_title => 'No library found';
+
+ @override
+ String get setup_syncPull_success => 'Library adopted';
+
+ @override
+ String get setup_syncPull_syncing => 'Pulling your library...';
+
+ @override
+ String get setup_syncPull_title => 'Connect and pull';
+
+ @override
+ String get setup_sync_changeProvider => 'Change provider';
+
+ @override
+ String setup_sync_connectedTo(String provider) {
+ return 'Connected to $provider';
+ }
+
+ @override
+ String setup_sync_error(Object error) {
+ return 'Could not connect: $error';
+ }
+
+ @override
+ String get setup_sync_header => 'Cloud sync';
+
+ @override
+ String get setup_sync_icloudUnavailable =>
+ 'iCloud is not available on this device';
+
+ @override
+ String get setup_sync_libraryFound_adopt => 'Adopt existing library';
+
+ @override
+ String get setup_sync_libraryFound_keepFresh => 'Start fresh';
+
+ @override
+ String get setup_sync_libraryFound_message =>
+ 'This account already contains a Submersion library. Adopt it instead of starting fresh?';
+
+ @override
+ String get setup_sync_libraryFound_title => 'Existing library found';
+
+ @override
+ String get setup_sync_manageInSettings => 'Manage in Settings';
+
+ @override
+ String get setup_sync_notConnected => 'Not connected';
+
+ @override
+ String get setup_sync_subtitle => 'Sync your logbook across devices';
+
+ @override
+ String get setup_units_advanced => 'Fine-tune units';
+
+ @override
+ String get setup_units_altitude => 'Altitude';
+
+ @override
+ String get setup_units_dateFormat => 'Date format';
+
+ @override
+ String get setup_units_depth => 'Depth';
+
+ @override
+ String get setup_units_imperial => 'Imperial';
+
+ @override
+ String get setup_units_metric => 'Metric';
+
+ @override
+ String get setup_units_pressure => 'Pressure';
+
+ @override
+ String get setup_units_sac => 'SAC rate';
+
+ @override
+ String get setup_units_subtitle =>
+ 'Choose how measurements are displayed. You can fine-tune each unit.';
+
+ @override
+ String get setup_units_temperature => 'Temperature';
+
+ @override
+ String get setup_units_timeFormat => 'Time format';
+
+ @override
+ String get setup_units_title => 'Units';
+
+ @override
+ String get setup_units_volume => 'Volume';
+
+ @override
+ String get setup_units_weight => 'Weight';
+
+ @override
+ String get setup_welcome_existingData_subtitle =>
+ 'Restore a backup, connect cloud sync, or open an existing folder';
+
+ @override
+ String get setup_welcome_existingData_title =>
+ 'I have existing Submersion data';
+
+ @override
+ String get setup_welcome_skipSetup => 'Skip setup';
+
+ @override
+ String get setup_welcome_startFresh_subtitle =>
+ 'Create your diver profile and configure the app';
+
+ @override
+ String get setup_welcome_startFresh_title => 'Set up a new logbook';
+
+ @override
+ String get setup_welcome_subtitle => 'Advanced dive logging and analytics';
+
+ @override
+ String get setup_welcome_title => 'Welcome to Submersion';
+
@override
String get siteMatchReview_title => 'Match Sites';
@@ -10432,40 +10720,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get nav_trips => 'Trips';
- @override
- String get onboarding_welcome_createProfile => 'Create Your Profile';
-
- @override
- String get onboarding_welcome_createProfileSubtitle =>
- 'Enter your name to get started. You can add more details later.';
-
- @override
- String get onboarding_welcome_creating => 'Creating...';
-
- @override
- String onboarding_welcome_errorCreatingProfile(Object error) {
- return 'Error creating profile: $error';
- }
-
- @override
- String get onboarding_welcome_getStarted => 'Get Started';
-
- @override
- String get onboarding_welcome_nameHint => 'Enter your name';
-
- @override
- String get onboarding_welcome_nameLabel => 'Your Name';
-
- @override
- String get onboarding_welcome_nameValidation => 'Please enter your name';
-
- @override
- String get onboarding_welcome_subtitle =>
- 'Advanced dive logging and analytics';
-
- @override
- String get onboarding_welcome_title => 'Welcome to Submersion';
-
@override
String plannerCanvas_bailout_available(String liters) {
return 'Available $liters';
diff --git a/lib/l10n/arb/app_localizations_es.dart b/lib/l10n/arb/app_localizations_es.dart
index 12b9d217e..fe4673985 100644
--- a/lib/l10n/arb/app_localizations_es.dart
+++ b/lib/l10n/arb/app_localizations_es.dart
@@ -5014,6 +5014,300 @@ class AppLocalizationsEs extends AppLocalizations {
return '$_temp0';
}
+ @override
+ String get settings_manage_setupAssistant => 'Asistente de configuración';
+
+ @override
+ String get settings_manage_setupAssistant_subtitle =>
+ 'Revisa unidades, apariencia y opciones de copia de seguridad';
+
+ @override
+ String get setup_backup_cloudCopy =>
+ 'Guardar una copia de seguridad en la nube';
+
+ @override
+ String get setup_backup_frequency => 'Frecuencia';
+
+ @override
+ String get setup_backup_frequency_daily => 'Diaria';
+
+ @override
+ String get setup_backup_frequency_monthly => 'Mensual';
+
+ @override
+ String get setup_backup_frequency_weekly => 'Semanal';
+
+ @override
+ String get setup_backup_scheduleSubtitle =>
+ 'Haz copias de seguridad de tu diario según una programación';
+
+ @override
+ String get setup_backup_scheduleToggle => 'Copias de seguridad automáticas';
+
+ @override
+ String get setup_backup_subtitle =>
+ 'Protege tu registro de buceo desde el primer día.';
+
+ @override
+ String get setup_backup_title => 'Copias de seguridad y sincronización';
+
+ @override
+ String get setup_common_back => 'Atrás';
+
+ @override
+ String get setup_common_next => 'Siguiente';
+
+ @override
+ String get setup_common_skip => 'Omitir';
+
+ @override
+ String get setup_existing_folder_subtitle =>
+ 'Apunta Submersion a una carpeta que ya contiene una biblioteca';
+
+ @override
+ String get setup_existing_folder_title => 'Abrir una carpeta existente';
+
+ @override
+ String get setup_existing_restore_subtitle =>
+ 'Elige un archivo de copia de seguridad exportado desde Submersion';
+
+ @override
+ String get setup_existing_restore_title =>
+ 'Restaurar un archivo de copia de seguridad';
+
+ @override
+ String get setup_existing_subtitle =>
+ 'Elige cómo cargar tu biblioteca existente de Submersion';
+
+ @override
+ String get setup_existing_sync_subtitle =>
+ 'Descarga tu biblioteca desde iCloud, Dropbox o S3';
+
+ @override
+ String get setup_existing_sync_title => 'Conectar sincronización en la nube';
+
+ @override
+ String get setup_existing_title => 'Trae tus datos';
+
+ @override
+ String get setup_finish_applying => 'Configurando...';
+
+ @override
+ String setup_finish_error(Object error) {
+ return 'No se pudo completar la configuración: $error';
+ }
+
+ @override
+ String get setup_finish_feature_diveComputer =>
+ 'Descarga inmersiones desde tu ordenador de buceo';
+
+ @override
+ String get setup_finish_feature_gear =>
+ 'Controla tu equipo y los intervalos de mantenimiento';
+
+ @override
+ String get setup_finish_feature_import =>
+ 'Importa registros desde archivos y otras aplicaciones';
+
+ @override
+ String get setup_finish_feature_sites =>
+ 'Visualiza tus puntos de buceo en el mapa';
+
+ @override
+ String get setup_finish_feature_statistics =>
+ 'Explora estadísticas sobre tus inmersiones';
+
+ @override
+ String get setup_finish_start => 'Empezar a registrar';
+
+ @override
+ String get setup_finish_subtitle => 'Submersion también puede...';
+
+ @override
+ String get setup_finish_title => 'Todo listo';
+
+ @override
+ String get setup_folder_notFound_message =>
+ 'La carpeta seleccionada no contiene una base de datos de Submersion.';
+
+ @override
+ String get setup_folder_notFound_title => 'No hay biblioteca en esa carpeta';
+
+ @override
+ String get setup_folder_pick => 'Elegir carpeta';
+
+ @override
+ String get setup_folder_switching => 'Abriendo biblioteca...';
+
+ @override
+ String get setup_folder_title => 'Abrir carpeta existente';
+
+ @override
+ String get setup_profile_nameHint => 'Ingresa tu nombre';
+
+ @override
+ String get setup_profile_nameLabel => 'Tu Nombre';
+
+ @override
+ String get setup_profile_nameValidation => 'Por favor ingresa tu nombre';
+
+ @override
+ String get setup_profile_subtitle =>
+ 'Ingresa tu nombre para comenzar. Puedes agregar más detalles después.';
+
+ @override
+ String get setup_profile_title => 'Crea Tu Perfil';
+
+ @override
+ String get setup_restore_inProgress => 'Restaurando...';
+
+ @override
+ String get setup_restore_pick => 'Elegir archivo de copia de seguridad';
+
+ @override
+ String get setup_restore_title => 'Restaurar copia de seguridad';
+
+ @override
+ String get setup_step_backup => 'Copia';
+
+ @override
+ String get setup_step_finish => 'Listo';
+
+ @override
+ String get setup_step_profile => 'Perfil';
+
+ @override
+ String get setup_step_units => 'Unidades';
+
+ @override
+ String get setup_syncPull_continue => 'Continuar';
+
+ @override
+ String get setup_syncPull_noLibrary_message =>
+ 'No se encontró ninguna biblioteca de Submersion en esta cuenta. ¿Empezar de cero? Tu conexión se mantendrá.';
+
+ @override
+ String get setup_syncPull_noLibrary_title => 'No se encontró biblioteca';
+
+ @override
+ String get setup_syncPull_success => 'Biblioteca adoptada';
+
+ @override
+ String get setup_syncPull_syncing => 'Descargando tu biblioteca...';
+
+ @override
+ String get setup_syncPull_title => 'Conectar y descargar';
+
+ @override
+ String get setup_sync_changeProvider => 'Cambiar proveedor';
+
+ @override
+ String setup_sync_connectedTo(String provider) {
+ return 'Conectado a $provider';
+ }
+
+ @override
+ String setup_sync_error(Object error) {
+ return 'No se pudo conectar: $error';
+ }
+
+ @override
+ String get setup_sync_header => 'Sincronización en la nube';
+
+ @override
+ String get setup_sync_icloudUnavailable =>
+ 'iCloud no está disponible en este dispositivo';
+
+ @override
+ String get setup_sync_libraryFound_adopt => 'Adoptar biblioteca existente';
+
+ @override
+ String get setup_sync_libraryFound_keepFresh => 'Empezar de cero';
+
+ @override
+ String get setup_sync_libraryFound_message =>
+ 'Esta cuenta ya contiene una biblioteca de Submersion. ¿Adoptarla en lugar de empezar de cero?';
+
+ @override
+ String get setup_sync_libraryFound_title =>
+ 'Se encontró una biblioteca existente';
+
+ @override
+ String get setup_sync_manageInSettings => 'Gestionar en Ajustes';
+
+ @override
+ String get setup_sync_notConnected => 'No conectado';
+
+ @override
+ String get setup_sync_subtitle => 'Sincroniza tu diario entre dispositivos';
+
+ @override
+ String get setup_units_advanced => 'Ajustar unidades';
+
+ @override
+ String get setup_units_altitude => 'Altitud';
+
+ @override
+ String get setup_units_dateFormat => 'Formato de fecha';
+
+ @override
+ String get setup_units_depth => 'Profundidad';
+
+ @override
+ String get setup_units_imperial => 'Imperial';
+
+ @override
+ String get setup_units_metric => 'Métrico';
+
+ @override
+ String get setup_units_pressure => 'Presión';
+
+ @override
+ String get setup_units_sac => 'Tasa SAC';
+
+ @override
+ String get setup_units_subtitle =>
+ 'Elige cómo se muestran las medidas. Puedes ajustar cada unidad.';
+
+ @override
+ String get setup_units_temperature => 'Temperatura';
+
+ @override
+ String get setup_units_timeFormat => 'Formato de hora';
+
+ @override
+ String get setup_units_title => 'Unidades';
+
+ @override
+ String get setup_units_volume => 'Volumen';
+
+ @override
+ String get setup_units_weight => 'Peso';
+
+ @override
+ String get setup_welcome_existingData_subtitle =>
+ 'Restaura una copia de seguridad, conecta la sincronización o abre una carpeta existente';
+
+ @override
+ String get setup_welcome_existingData_title => 'Ya tengo datos de Submersion';
+
+ @override
+ String get setup_welcome_skipSetup => 'Omitir configuración';
+
+ @override
+ String get setup_welcome_startFresh_subtitle =>
+ 'Crea tu perfil de buceador y configura la aplicación';
+
+ @override
+ String get setup_welcome_startFresh_title => 'Configurar un diario nuevo';
+
+ @override
+ String get setup_welcome_subtitle =>
+ 'Registro y análisis avanzado de inmersiones';
+
+ @override
+ String get setup_welcome_title => 'Bienvenido a Submersion';
+
@override
String get siteMatchReview_title => 'Asociar puntos';
@@ -10588,40 +10882,6 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get nav_trips => 'Viajes';
- @override
- String get onboarding_welcome_createProfile => 'Crea Tu Perfil';
-
- @override
- String get onboarding_welcome_createProfileSubtitle =>
- 'Ingresa tu nombre para comenzar. Puedes agregar más detalles después.';
-
- @override
- String get onboarding_welcome_creating => 'Creando...';
-
- @override
- String onboarding_welcome_errorCreatingProfile(Object error) {
- return 'Error al crear perfil: $error';
- }
-
- @override
- String get onboarding_welcome_getStarted => 'Comenzar';
-
- @override
- String get onboarding_welcome_nameHint => 'Ingresa tu nombre';
-
- @override
- String get onboarding_welcome_nameLabel => 'Tu Nombre';
-
- @override
- String get onboarding_welcome_nameValidation => 'Por favor ingresa tu nombre';
-
- @override
- String get onboarding_welcome_subtitle =>
- 'Registro y análisis avanzado de inmersiones';
-
- @override
- String get onboarding_welcome_title => 'Bienvenido a Submersion';
-
@override
String plannerCanvas_bailout_available(String liters) {
return 'Disponible $liters';
diff --git a/lib/l10n/arb/app_localizations_fr.dart b/lib/l10n/arb/app_localizations_fr.dart
index 58559c1f7..44fdfd05f 100644
--- a/lib/l10n/arb/app_localizations_fr.dart
+++ b/lib/l10n/arb/app_localizations_fr.dart
@@ -5038,6 +5038,302 @@ class AppLocalizationsFr extends AppLocalizations {
return '$_temp0';
}
+ @override
+ String get settings_manage_setupAssistant => 'Assistant de configuration';
+
+ @override
+ String get settings_manage_setupAssistant_subtitle =>
+ 'Revoir les unités, l\'apparence et les choix de sauvegarde';
+
+ @override
+ String get setup_backup_cloudCopy =>
+ 'Conserver une copie de sauvegarde dans le cloud';
+
+ @override
+ String get setup_backup_frequency => 'Fréquence';
+
+ @override
+ String get setup_backup_frequency_daily => 'Quotidienne';
+
+ @override
+ String get setup_backup_frequency_monthly => 'Mensuelle';
+
+ @override
+ String get setup_backup_frequency_weekly => 'Hebdomadaire';
+
+ @override
+ String get setup_backup_scheduleSubtitle =>
+ 'Sauvegardez votre carnet selon un calendrier';
+
+ @override
+ String get setup_backup_scheduleToggle => 'Sauvegardes automatiques';
+
+ @override
+ String get setup_backup_subtitle =>
+ 'Protégez votre carnet de plongée dès le premier jour.';
+
+ @override
+ String get setup_backup_title => 'Sauvegardes et synchronisation';
+
+ @override
+ String get setup_common_back => 'Retour';
+
+ @override
+ String get setup_common_next => 'Suivant';
+
+ @override
+ String get setup_common_skip => 'Ignorer';
+
+ @override
+ String get setup_existing_folder_subtitle =>
+ 'Pointez Submersion vers un dossier contenant déjà une bibliothèque';
+
+ @override
+ String get setup_existing_folder_title => 'Ouvrir un dossier existant';
+
+ @override
+ String get setup_existing_restore_subtitle =>
+ 'Choisissez un fichier de sauvegarde exporté depuis Submersion';
+
+ @override
+ String get setup_existing_restore_title =>
+ 'Restaurer un fichier de sauvegarde';
+
+ @override
+ String get setup_existing_subtitle =>
+ 'Choisissez comment charger votre bibliothèque Submersion existante';
+
+ @override
+ String get setup_existing_sync_subtitle =>
+ 'Récupérez votre bibliothèque depuis iCloud, Dropbox ou S3';
+
+ @override
+ String get setup_existing_sync_title => 'Connecter la synchronisation cloud';
+
+ @override
+ String get setup_existing_title => 'Récupérez vos données';
+
+ @override
+ String get setup_finish_applying => 'Configuration...';
+
+ @override
+ String setup_finish_error(Object error) {
+ return 'Impossible de terminer la configuration : $error';
+ }
+
+ @override
+ String get setup_finish_feature_diveComputer =>
+ 'Téléchargez vos plongées depuis votre ordinateur de plongée';
+
+ @override
+ String get setup_finish_feature_gear =>
+ 'Suivez votre matériel et ses entretiens';
+
+ @override
+ String get setup_finish_feature_import =>
+ 'Importez des carnets depuis des fichiers et d\'autres applications';
+
+ @override
+ String get setup_finish_feature_sites => 'Cartographiez vos sites de plongée';
+
+ @override
+ String get setup_finish_feature_statistics =>
+ 'Explorez les statistiques de vos plongées';
+
+ @override
+ String get setup_finish_start => 'Commencer';
+
+ @override
+ String get setup_finish_subtitle => 'Submersion peut aussi...';
+
+ @override
+ String get setup_finish_title => 'Tout est prêt';
+
+ @override
+ String get setup_folder_notFound_message =>
+ 'Le dossier sélectionné ne contient pas de base de données Submersion.';
+
+ @override
+ String get setup_folder_notFound_title =>
+ 'Aucune bibliothèque dans ce dossier';
+
+ @override
+ String get setup_folder_pick => 'Choisir un dossier';
+
+ @override
+ String get setup_folder_switching => 'Ouverture de la bibliothèque...';
+
+ @override
+ String get setup_folder_title => 'Ouvrir un dossier existant';
+
+ @override
+ String get setup_profile_nameHint => 'Entrez votre nom';
+
+ @override
+ String get setup_profile_nameLabel => 'Votre nom';
+
+ @override
+ String get setup_profile_nameValidation => 'Veuillez entrer votre nom';
+
+ @override
+ String get setup_profile_subtitle =>
+ 'Entrez votre nom pour commencer. Vous pourrez ajouter plus de détails plus tard.';
+
+ @override
+ String get setup_profile_title => 'Créer votre profil';
+
+ @override
+ String get setup_restore_inProgress => 'Restauration...';
+
+ @override
+ String get setup_restore_pick => 'Choisir un fichier de sauvegarde';
+
+ @override
+ String get setup_restore_title => 'Restaurer la sauvegarde';
+
+ @override
+ String get setup_step_backup => 'Sauvegarde';
+
+ @override
+ String get setup_step_finish => 'Terminé';
+
+ @override
+ String get setup_step_profile => 'Profil';
+
+ @override
+ String get setup_step_units => 'Unités';
+
+ @override
+ String get setup_syncPull_continue => 'Continuer';
+
+ @override
+ String get setup_syncPull_noLibrary_message =>
+ 'Aucune bibliothèque Submersion n\'a été trouvée sur ce compte. Repartir de zéro ? Votre connexion sera conservée.';
+
+ @override
+ String get setup_syncPull_noLibrary_title => 'Aucune bibliothèque trouvée';
+
+ @override
+ String get setup_syncPull_success => 'Bibliothèque adoptée';
+
+ @override
+ String get setup_syncPull_syncing => 'Récupération de votre bibliothèque...';
+
+ @override
+ String get setup_syncPull_title => 'Connecter et récupérer';
+
+ @override
+ String get setup_sync_changeProvider => 'Changer de fournisseur';
+
+ @override
+ String setup_sync_connectedTo(String provider) {
+ return 'Connecté à $provider';
+ }
+
+ @override
+ String setup_sync_error(Object error) {
+ return 'Connexion impossible : $error';
+ }
+
+ @override
+ String get setup_sync_header => 'Synchronisation cloud';
+
+ @override
+ String get setup_sync_icloudUnavailable =>
+ 'iCloud n\'est pas disponible sur cet appareil';
+
+ @override
+ String get setup_sync_libraryFound_adopt =>
+ 'Adopter la bibliothèque existante';
+
+ @override
+ String get setup_sync_libraryFound_keepFresh => 'Repartir de zéro';
+
+ @override
+ String get setup_sync_libraryFound_message =>
+ 'Ce compte contient déjà une bibliothèque Submersion. L\'adopter plutôt que de repartir de zéro ?';
+
+ @override
+ String get setup_sync_libraryFound_title => 'Bibliothèque existante trouvée';
+
+ @override
+ String get setup_sync_manageInSettings => 'Gérer dans les réglages';
+
+ @override
+ String get setup_sync_notConnected => 'Non connecté';
+
+ @override
+ String get setup_sync_subtitle =>
+ 'Synchronisez votre carnet entre vos appareils';
+
+ @override
+ String get setup_units_advanced => 'Ajuster les unités';
+
+ @override
+ String get setup_units_altitude => 'Altitude';
+
+ @override
+ String get setup_units_dateFormat => 'Format de date';
+
+ @override
+ String get setup_units_depth => 'Profondeur';
+
+ @override
+ String get setup_units_imperial => 'Impérial';
+
+ @override
+ String get setup_units_metric => 'Métrique';
+
+ @override
+ String get setup_units_pressure => 'Pression';
+
+ @override
+ String get setup_units_sac => 'Taux SAC';
+
+ @override
+ String get setup_units_subtitle =>
+ 'Choisissez l\'affichage des mesures. Chaque unité peut être ajustée.';
+
+ @override
+ String get setup_units_temperature => 'Température';
+
+ @override
+ String get setup_units_timeFormat => 'Format de l\'heure';
+
+ @override
+ String get setup_units_title => 'Unités';
+
+ @override
+ String get setup_units_volume => 'Volume';
+
+ @override
+ String get setup_units_weight => 'Poids';
+
+ @override
+ String get setup_welcome_existingData_subtitle =>
+ 'Restaurez une sauvegarde, connectez la synchronisation ou ouvrez un dossier existant';
+
+ @override
+ String get setup_welcome_existingData_title =>
+ 'J\'ai déjà des données Submersion';
+
+ @override
+ String get setup_welcome_skipSetup => 'Ignorer la configuration';
+
+ @override
+ String get setup_welcome_startFresh_subtitle =>
+ 'Créez votre profil de plongeur et configurez l\'application';
+
+ @override
+ String get setup_welcome_startFresh_title => 'Créer un nouveau carnet';
+
+ @override
+ String get setup_welcome_subtitle =>
+ 'Enregistrement et analyse avancés de plongée';
+
+ @override
+ String get setup_welcome_title => 'Bienvenue dans Submersion';
+
@override
String get siteMatchReview_title => 'Associer les sites';
@@ -10639,40 +10935,6 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get nav_trips => 'Voyages';
- @override
- String get onboarding_welcome_createProfile => 'Créer votre profil';
-
- @override
- String get onboarding_welcome_createProfileSubtitle =>
- 'Entrez votre nom pour commencer. Vous pourrez ajouter plus de détails plus tard.';
-
- @override
- String get onboarding_welcome_creating => 'Création...';
-
- @override
- String onboarding_welcome_errorCreatingProfile(Object error) {
- return 'Erreur lors de la création du profil : $error';
- }
-
- @override
- String get onboarding_welcome_getStarted => 'Commencer';
-
- @override
- String get onboarding_welcome_nameHint => 'Entrez votre nom';
-
- @override
- String get onboarding_welcome_nameLabel => 'Votre nom';
-
- @override
- String get onboarding_welcome_nameValidation => 'Veuillez entrer votre nom';
-
- @override
- String get onboarding_welcome_subtitle =>
- 'Enregistrement et analyse avancés de plongée';
-
- @override
- String get onboarding_welcome_title => 'Bienvenue dans Submersion';
-
@override
String plannerCanvas_bailout_available(String liters) {
return 'Disponible $liters';
diff --git a/lib/l10n/arb/app_localizations_he.dart b/lib/l10n/arb/app_localizations_he.dart
index 893bf8ee5..5bb4c6fe0 100644
--- a/lib/l10n/arb/app_localizations_he.dart
+++ b/lib/l10n/arb/app_localizations_he.dart
@@ -4878,6 +4878,290 @@ class AppLocalizationsHe extends AppLocalizations {
return '$_temp0';
}
+ @override
+ String get settings_manage_setupAssistant => 'אשף ההגדרה';
+
+ @override
+ String get settings_manage_setupAssistant_subtitle =>
+ 'עיון מחדש ביחידות, במראה ובאפשרויות הגיבוי';
+
+ @override
+ String get setup_backup_cloudCopy => 'שמירת עותק גיבוי בענן';
+
+ @override
+ String get setup_backup_frequency => 'תדירות';
+
+ @override
+ String get setup_backup_frequency_daily => 'יומי';
+
+ @override
+ String get setup_backup_frequency_monthly => 'חודשי';
+
+ @override
+ String get setup_backup_frequency_weekly => 'שבועי';
+
+ @override
+ String get setup_backup_scheduleSubtitle => 'גיבוי יומן הצלילה לפי לוח זמנים';
+
+ @override
+ String get setup_backup_scheduleToggle => 'גיבויים אוטומטיים';
+
+ @override
+ String get setup_backup_subtitle => 'הגנו על יומן הצלילה שלכם מהיום הראשון.';
+
+ @override
+ String get setup_backup_title => 'גיבויים וסנכרון';
+
+ @override
+ String get setup_common_back => 'חזרה';
+
+ @override
+ String get setup_common_next => 'הבא';
+
+ @override
+ String get setup_common_skip => 'דילוג';
+
+ @override
+ String get setup_existing_folder_subtitle =>
+ 'הפנו את Submersion לתיקייה שכבר מכילה ספרייה';
+
+ @override
+ String get setup_existing_folder_title => 'פתיחת תיקייה קיימת';
+
+ @override
+ String get setup_existing_restore_subtitle =>
+ 'בחרו קובץ גיבוי שיוצא מ-Submersion';
+
+ @override
+ String get setup_existing_restore_title => 'שחזור קובץ גיבוי';
+
+ @override
+ String get setup_existing_subtitle =>
+ 'בחרו כיצד לטעון את ספריית Submersion הקיימת שלכם';
+
+ @override
+ String get setup_existing_sync_subtitle =>
+ 'משכו את הספרייה שלכם מ-iCloud, מ-Dropbox או מ-S3';
+
+ @override
+ String get setup_existing_sync_title => 'חיבור סנכרון ענן';
+
+ @override
+ String get setup_existing_title => 'הביאו את הנתונים שלכם';
+
+ @override
+ String get setup_finish_applying => 'מגדיר...';
+
+ @override
+ String setup_finish_error(Object error) {
+ return 'לא ניתן להשלים את ההגדרה: $error';
+ }
+
+ @override
+ String get setup_finish_feature_diveComputer => 'הורדת צלילות ממחשב הצלילה';
+
+ @override
+ String get setup_finish_feature_gear => 'מעקב אחר ציוד ומועדי טיפולים';
+
+ @override
+ String get setup_finish_feature_import =>
+ 'ייבוא יומנים מקבצים ומאפליקציות אחרות';
+
+ @override
+ String get setup_finish_feature_sites => 'מיפוי אתרי הצלילה שלכם';
+
+ @override
+ String get setup_finish_feature_statistics =>
+ 'חקר סטטיסטיקות על הצלילות שלכם';
+
+ @override
+ String get setup_finish_start => 'התחלת רישום';
+
+ @override
+ String get setup_finish_subtitle => 'Submersion יכול גם...';
+
+ @override
+ String get setup_finish_title => 'הכול מוכן';
+
+ @override
+ String get setup_folder_notFound_message =>
+ 'התיקייה שנבחרה אינה מכילה מסד נתונים של Submersion.';
+
+ @override
+ String get setup_folder_notFound_title => 'אין ספרייה בתיקייה זו';
+
+ @override
+ String get setup_folder_pick => 'בחירת תיקייה';
+
+ @override
+ String get setup_folder_switching => 'פותח ספרייה...';
+
+ @override
+ String get setup_folder_title => 'פתיחת תיקייה קיימת';
+
+ @override
+ String get setup_profile_nameHint => 'הזן את שמך';
+
+ @override
+ String get setup_profile_nameLabel => 'השם שלך';
+
+ @override
+ String get setup_profile_nameValidation => 'נא להזין את שמך';
+
+ @override
+ String get setup_profile_subtitle =>
+ 'הזן את שמך כדי להתחיל. תוכל להוסיף פרטים נוספים מאוחר יותר.';
+
+ @override
+ String get setup_profile_title => 'צור את הפרופיל שלך';
+
+ @override
+ String get setup_restore_inProgress => 'משחזר...';
+
+ @override
+ String get setup_restore_pick => 'בחירת קובץ גיבוי';
+
+ @override
+ String get setup_restore_title => 'שחזור גיבוי';
+
+ @override
+ String get setup_step_backup => 'גיבוי';
+
+ @override
+ String get setup_step_finish => 'סיום';
+
+ @override
+ String get setup_step_profile => 'פרופיל';
+
+ @override
+ String get setup_step_units => 'יחידות';
+
+ @override
+ String get setup_syncPull_continue => 'המשך';
+
+ @override
+ String get setup_syncPull_noLibrary_message =>
+ 'לא נמצאה ספריית Submersion בחשבון זה. להתחיל מחדש? החיבור יישמר.';
+
+ @override
+ String get setup_syncPull_noLibrary_title => 'לא נמצאה ספרייה';
+
+ @override
+ String get setup_syncPull_success => 'הספרייה אומצה';
+
+ @override
+ String get setup_syncPull_syncing => 'מושך את הספרייה...';
+
+ @override
+ String get setup_syncPull_title => 'חיבור ומשיכה';
+
+ @override
+ String get setup_sync_changeProvider => 'החלפת ספק';
+
+ @override
+ String setup_sync_connectedTo(String provider) {
+ return 'מחובר אל $provider';
+ }
+
+ @override
+ String setup_sync_error(Object error) {
+ return 'החיבור נכשל: $error';
+ }
+
+ @override
+ String get setup_sync_header => 'סנכרון ענן';
+
+ @override
+ String get setup_sync_icloudUnavailable => 'iCloud אינו זמין במכשיר זה';
+
+ @override
+ String get setup_sync_libraryFound_adopt => 'אימוץ הספרייה הקיימת';
+
+ @override
+ String get setup_sync_libraryFound_keepFresh => 'התחלה חדשה';
+
+ @override
+ String get setup_sync_libraryFound_message =>
+ 'חשבון זה כבר מכיל ספריית Submersion. לאמץ אותה במקום להתחיל מחדש?';
+
+ @override
+ String get setup_sync_libraryFound_title => 'נמצאה ספרייה קיימת';
+
+ @override
+ String get setup_sync_manageInSettings => 'ניהול בהגדרות';
+
+ @override
+ String get setup_sync_notConnected => 'לא מחובר';
+
+ @override
+ String get setup_sync_subtitle => 'סנכרון היומן בין מכשירים';
+
+ @override
+ String get setup_units_advanced => 'כוונון יחידות';
+
+ @override
+ String get setup_units_altitude => 'גובה';
+
+ @override
+ String get setup_units_dateFormat => 'תבנית תאריך';
+
+ @override
+ String get setup_units_depth => 'עומק';
+
+ @override
+ String get setup_units_imperial => 'אימפריאלי';
+
+ @override
+ String get setup_units_metric => 'מטרי';
+
+ @override
+ String get setup_units_pressure => 'לחץ';
+
+ @override
+ String get setup_units_sac => 'קצב SAC';
+
+ @override
+ String get setup_units_subtitle =>
+ 'בחרו כיצד יוצגו המדידות. ניתן לכוונן כל יחידה.';
+
+ @override
+ String get setup_units_temperature => 'טמפרטורה';
+
+ @override
+ String get setup_units_timeFormat => 'תבנית שעה';
+
+ @override
+ String get setup_units_title => 'יחידות';
+
+ @override
+ String get setup_units_volume => 'נפח';
+
+ @override
+ String get setup_units_weight => 'משקל';
+
+ @override
+ String get setup_welcome_existingData_subtitle =>
+ 'שחזרו גיבוי, חברו סנכרון ענן או פתחו תיקייה קיימת';
+
+ @override
+ String get setup_welcome_existingData_title => 'יש לי כבר נתוני Submersion';
+
+ @override
+ String get setup_welcome_skipSetup => 'דילוג על ההגדרה';
+
+ @override
+ String get setup_welcome_startFresh_subtitle =>
+ 'צרו פרופיל צולל והגדירו את האפליקציה';
+
+ @override
+ String get setup_welcome_startFresh_title => 'הגדרת יומן חדש';
+
+ @override
+ String get setup_welcome_subtitle => 'רישום וניתוח צלילה מתקדם';
+
+ @override
+ String get setup_welcome_title => 'ברוכים הבאים ל-Submersion';
+
@override
String get siteMatchReview_title => 'התאמת אתרים';
@@ -10332,39 +10616,6 @@ class AppLocalizationsHe extends AppLocalizations {
@override
String get nav_trips => 'טיולים';
- @override
- String get onboarding_welcome_createProfile => 'צור את הפרופיל שלך';
-
- @override
- String get onboarding_welcome_createProfileSubtitle =>
- 'הזן את שמך כדי להתחיל. תוכל להוסיף פרטים נוספים מאוחר יותר.';
-
- @override
- String get onboarding_welcome_creating => 'יוצר...';
-
- @override
- String onboarding_welcome_errorCreatingProfile(Object error) {
- return 'שגיאה ביצירת פרופיל: $error';
- }
-
- @override
- String get onboarding_welcome_getStarted => 'התחל';
-
- @override
- String get onboarding_welcome_nameHint => 'הזן את שמך';
-
- @override
- String get onboarding_welcome_nameLabel => 'השם שלך';
-
- @override
- String get onboarding_welcome_nameValidation => 'נא להזין את שמך';
-
- @override
- String get onboarding_welcome_subtitle => 'רישום וניתוח צלילה מתקדם';
-
- @override
- String get onboarding_welcome_title => 'ברוכים הבאים ל-Submersion';
-
@override
String plannerCanvas_bailout_available(String liters) {
return 'זמין $liters';
diff --git a/lib/l10n/arb/app_localizations_hu.dart b/lib/l10n/arb/app_localizations_hu.dart
index f7b737f3d..e273dad82 100644
--- a/lib/l10n/arb/app_localizations_hu.dart
+++ b/lib/l10n/arb/app_localizations_hu.dart
@@ -4997,6 +4997,298 @@ class AppLocalizationsHu extends AppLocalizations {
return '$_temp0';
}
+ @override
+ String get settings_manage_setupAssistant => 'Beállítási varázsló';
+
+ @override
+ String get settings_manage_setupAssistant_subtitle =>
+ 'Mértékegységek, megjelenés és biztonsági mentés áttekintése';
+
+ @override
+ String get setup_backup_cloudCopy =>
+ 'Biztonsági másolat megőrzése a felhőben';
+
+ @override
+ String get setup_backup_frequency => 'Gyakoriság';
+
+ @override
+ String get setup_backup_frequency_daily => 'Naponta';
+
+ @override
+ String get setup_backup_frequency_monthly => 'Havonta';
+
+ @override
+ String get setup_backup_frequency_weekly => 'Hetente';
+
+ @override
+ String get setup_backup_scheduleSubtitle =>
+ 'Naplója rendszeres biztonsági mentése';
+
+ @override
+ String get setup_backup_scheduleToggle => 'Automatikus biztonsági mentés';
+
+ @override
+ String get setup_backup_subtitle =>
+ 'Védje merülési naplóját az első naptól kezdve.';
+
+ @override
+ String get setup_backup_title => 'Biztonsági mentés és szinkronizálás';
+
+ @override
+ String get setup_common_back => 'Vissza';
+
+ @override
+ String get setup_common_next => 'Tovább';
+
+ @override
+ String get setup_common_skip => 'Kihagyás';
+
+ @override
+ String get setup_existing_folder_subtitle =>
+ 'Irányítsa a Submersiont egy olyan mappára, amely már tartalmaz könyvtárat';
+
+ @override
+ String get setup_existing_folder_title => 'Meglévő mappa megnyitása';
+
+ @override
+ String get setup_existing_restore_subtitle =>
+ 'Válasszon egy Submersionból exportált biztonságimentés-fájlt';
+
+ @override
+ String get setup_existing_restore_title => 'Biztonsági mentés visszaállítása';
+
+ @override
+ String get setup_existing_subtitle =>
+ 'Válassza ki, hogyan töltse be meglévő Submersion-könyvtárát';
+
+ @override
+ String get setup_existing_sync_subtitle =>
+ 'Könyvtár letöltése iCloudból, Dropboxból vagy S3-ból';
+
+ @override
+ String get setup_existing_sync_title => 'Felhőszinkronizálás csatlakoztatása';
+
+ @override
+ String get setup_existing_title => 'Adatok áthozása';
+
+ @override
+ String get setup_finish_applying => 'Beállítás...';
+
+ @override
+ String setup_finish_error(Object error) {
+ return 'A beállítás nem fejeződött be: $error';
+ }
+
+ @override
+ String get setup_finish_feature_diveComputer =>
+ 'Merülések letöltése a búvárkomputerről';
+
+ @override
+ String get setup_finish_feature_gear =>
+ 'Felszerelés és szervizintervallumok nyomon követése';
+
+ @override
+ String get setup_finish_feature_import =>
+ 'Naplók importálása fájlokból és más alkalmazásokból';
+
+ @override
+ String get setup_finish_feature_sites =>
+ 'Merülőhelyek megjelenítése térképen';
+
+ @override
+ String get setup_finish_feature_statistics =>
+ 'Merülési statisztikák felfedezése';
+
+ @override
+ String get setup_finish_start => 'Naplózás indítása';
+
+ @override
+ String get setup_finish_subtitle => 'A Submersion ezenkívül...';
+
+ @override
+ String get setup_finish_title => 'Minden készen áll';
+
+ @override
+ String get setup_folder_notFound_message =>
+ 'A kiválasztott mappa nem tartalmaz Submersion-adatbázist.';
+
+ @override
+ String get setup_folder_notFound_title => 'Nincs könyvtár ebben a mappában';
+
+ @override
+ String get setup_folder_pick => 'Mappa kiválasztása';
+
+ @override
+ String get setup_folder_switching => 'Könyvtár megnyitása...';
+
+ @override
+ String get setup_folder_title => 'Meglévő mappa megnyitása';
+
+ @override
+ String get setup_profile_nameHint => 'Add meg a neved';
+
+ @override
+ String get setup_profile_nameLabel => 'Neved';
+
+ @override
+ String get setup_profile_nameValidation => 'Add meg a neved';
+
+ @override
+ String get setup_profile_subtitle =>
+ 'Add meg a neved a kezdéshez. Később további részleteket adhatsz hozzá.';
+
+ @override
+ String get setup_profile_title => 'Profil létrehozása';
+
+ @override
+ String get setup_restore_inProgress => 'Visszaállítás...';
+
+ @override
+ String get setup_restore_pick => 'Biztonságimentés-fájl kiválasztása';
+
+ @override
+ String get setup_restore_title => 'Biztonsági mentés visszaállítása';
+
+ @override
+ String get setup_step_backup => 'Mentés';
+
+ @override
+ String get setup_step_finish => 'Kész';
+
+ @override
+ String get setup_step_profile => 'Profil';
+
+ @override
+ String get setup_step_units => 'Egységek';
+
+ @override
+ String get setup_syncPull_continue => 'Folytatás';
+
+ @override
+ String get setup_syncPull_noLibrary_message =>
+ 'Ezen a fiókon nem található Submersion-könyvtár. Új kezdés? A kapcsolat megmarad.';
+
+ @override
+ String get setup_syncPull_noLibrary_title => 'Nem található könyvtár';
+
+ @override
+ String get setup_syncPull_success => 'Könyvtár átvéve';
+
+ @override
+ String get setup_syncPull_syncing => 'Könyvtár letöltése...';
+
+ @override
+ String get setup_syncPull_title => 'Csatlakozás és letöltés';
+
+ @override
+ String get setup_sync_changeProvider => 'Szolgáltató módosítása';
+
+ @override
+ String setup_sync_connectedTo(String provider) {
+ return 'Csatlakoztatva: $provider';
+ }
+
+ @override
+ String setup_sync_error(Object error) {
+ return 'Nem sikerült csatlakozni: $error';
+ }
+
+ @override
+ String get setup_sync_header => 'Felhőszinkronizálás';
+
+ @override
+ String get setup_sync_icloudUnavailable =>
+ 'Az iCloud nem érhető el ezen az eszközön';
+
+ @override
+ String get setup_sync_libraryFound_adopt => 'Meglévő könyvtár átvétele';
+
+ @override
+ String get setup_sync_libraryFound_keepFresh => 'Új kezdés';
+
+ @override
+ String get setup_sync_libraryFound_message =>
+ 'Ez a fiók már tartalmaz Submersion-könyvtárat. Átveszi ahelyett, hogy újat kezdene?';
+
+ @override
+ String get setup_sync_libraryFound_title => 'Meglévő könyvtár található';
+
+ @override
+ String get setup_sync_manageInSettings => 'Kezelés a beállításokban';
+
+ @override
+ String get setup_sync_notConnected => 'Nincs csatlakoztatva';
+
+ @override
+ String get setup_sync_subtitle => 'Napló szinkronizálása az eszközök között';
+
+ @override
+ String get setup_units_advanced => 'Egységek finomhangolása';
+
+ @override
+ String get setup_units_altitude => 'Magasság';
+
+ @override
+ String get setup_units_dateFormat => 'Dátumformátum';
+
+ @override
+ String get setup_units_depth => 'Mélység';
+
+ @override
+ String get setup_units_imperial => 'Angolszász';
+
+ @override
+ String get setup_units_metric => 'Metrikus';
+
+ @override
+ String get setup_units_pressure => 'Nyomás';
+
+ @override
+ String get setup_units_sac => 'SAC-érték';
+
+ @override
+ String get setup_units_subtitle =>
+ 'Válassza ki a mértékegységek megjelenítését. Minden egység külön beállítható.';
+
+ @override
+ String get setup_units_temperature => 'Hőmérséklet';
+
+ @override
+ String get setup_units_timeFormat => 'Időformátum';
+
+ @override
+ String get setup_units_title => 'Egységek';
+
+ @override
+ String get setup_units_volume => 'Térfogat';
+
+ @override
+ String get setup_units_weight => 'Súly';
+
+ @override
+ String get setup_welcome_existingData_subtitle =>
+ 'Biztonsági mentés visszaállítása, felhőszinkronizálás vagy meglévő mappa megnyitása';
+
+ @override
+ String get setup_welcome_existingData_title =>
+ 'Már vannak Submersion-adataim';
+
+ @override
+ String get setup_welcome_skipSetup => 'Beállítás kihagyása';
+
+ @override
+ String get setup_welcome_startFresh_subtitle =>
+ 'Búvárprofil létrehozása és az alkalmazás beállítása';
+
+ @override
+ String get setup_welcome_startFresh_title => 'Új napló beállítása';
+
+ @override
+ String get setup_welcome_subtitle => 'Fejlett merülés napló és elemzés';
+
+ @override
+ String get setup_welcome_title => 'Üdvözöl a Submersion';
+
@override
String get siteMatchReview_title => 'Helyek hozzárendelése';
@@ -10575,39 +10867,6 @@ class AppLocalizationsHu extends AppLocalizations {
@override
String get nav_trips => 'Utak';
- @override
- String get onboarding_welcome_createProfile => 'Profil létrehozása';
-
- @override
- String get onboarding_welcome_createProfileSubtitle =>
- 'Add meg a neved a kezdéshez. Később további részleteket adhatsz hozzá.';
-
- @override
- String get onboarding_welcome_creating => 'Létrehozás...';
-
- @override
- String onboarding_welcome_errorCreatingProfile(Object error) {
- return 'Hiba a profil létrehozásakor: $error';
- }
-
- @override
- String get onboarding_welcome_getStarted => 'Kezdés';
-
- @override
- String get onboarding_welcome_nameHint => 'Add meg a neved';
-
- @override
- String get onboarding_welcome_nameLabel => 'Neved';
-
- @override
- String get onboarding_welcome_nameValidation => 'Add meg a neved';
-
- @override
- String get onboarding_welcome_subtitle => 'Fejlett merülés napló és elemzés';
-
- @override
- String get onboarding_welcome_title => 'Üdvözöl a Submersion';
-
@override
String plannerCanvas_bailout_available(String liters) {
return 'Elérhető $liters';
diff --git a/lib/l10n/arb/app_localizations_it.dart b/lib/l10n/arb/app_localizations_it.dart
index 5471039b6..1e4c71eac 100644
--- a/lib/l10n/arb/app_localizations_it.dart
+++ b/lib/l10n/arb/app_localizations_it.dart
@@ -5017,6 +5017,299 @@ class AppLocalizationsIt extends AppLocalizations {
return '$_temp0';
}
+ @override
+ String get settings_manage_setupAssistant => 'Assistente di configurazione';
+
+ @override
+ String get settings_manage_setupAssistant_subtitle =>
+ 'Rivedi unità, aspetto e opzioni di backup';
+
+ @override
+ String get setup_backup_cloudCopy => 'Conserva una copia di backup nel cloud';
+
+ @override
+ String get setup_backup_frequency => 'Frequenza';
+
+ @override
+ String get setup_backup_frequency_daily => 'Giornaliera';
+
+ @override
+ String get setup_backup_frequency_monthly => 'Mensile';
+
+ @override
+ String get setup_backup_frequency_weekly => 'Settimanale';
+
+ @override
+ String get setup_backup_scheduleSubtitle =>
+ 'Esegui il backup del tuo logbook a intervalli regolari';
+
+ @override
+ String get setup_backup_scheduleToggle => 'Backup automatici';
+
+ @override
+ String get setup_backup_subtitle =>
+ 'Proteggi il tuo registro immersioni fin dal primo giorno.';
+
+ @override
+ String get setup_backup_title => 'Backup e sincronizzazione';
+
+ @override
+ String get setup_common_back => 'Indietro';
+
+ @override
+ String get setup_common_next => 'Avanti';
+
+ @override
+ String get setup_common_skip => 'Salta';
+
+ @override
+ String get setup_existing_folder_subtitle =>
+ 'Indica a Submersion una cartella che contiene già una libreria';
+
+ @override
+ String get setup_existing_folder_title => 'Apri una cartella esistente';
+
+ @override
+ String get setup_existing_restore_subtitle =>
+ 'Scegli un file di backup esportato da Submersion';
+
+ @override
+ String get setup_existing_restore_title => 'Ripristina un file di backup';
+
+ @override
+ String get setup_existing_subtitle =>
+ 'Scegli come caricare la tua libreria Submersion esistente';
+
+ @override
+ String get setup_existing_sync_subtitle =>
+ 'Scarica la tua libreria da iCloud, Dropbox o S3';
+
+ @override
+ String get setup_existing_sync_title => 'Collega la sincronizzazione cloud';
+
+ @override
+ String get setup_existing_title => 'Porta i tuoi dati';
+
+ @override
+ String get setup_finish_applying => 'Configurazione...';
+
+ @override
+ String setup_finish_error(Object error) {
+ return 'Impossibile completare la configurazione: $error';
+ }
+
+ @override
+ String get setup_finish_feature_diveComputer =>
+ 'Scarica le immersioni dal tuo computer subacqueo';
+
+ @override
+ String get setup_finish_feature_gear =>
+ 'Tieni traccia dell\'attrezzatura e delle scadenze di manutenzione';
+
+ @override
+ String get setup_finish_feature_import =>
+ 'Importa registri da file e altre app';
+
+ @override
+ String get setup_finish_feature_sites =>
+ 'Visualizza i tuoi punti di immersione sulla mappa';
+
+ @override
+ String get setup_finish_feature_statistics =>
+ 'Esplora le statistiche delle tue immersioni';
+
+ @override
+ String get setup_finish_start => 'Inizia a registrare';
+
+ @override
+ String get setup_finish_subtitle => 'Submersion può anche...';
+
+ @override
+ String get setup_finish_title => 'Tutto pronto';
+
+ @override
+ String get setup_folder_notFound_message =>
+ 'La cartella selezionata non contiene un database Submersion.';
+
+ @override
+ String get setup_folder_notFound_title =>
+ 'Nessuna libreria in quella cartella';
+
+ @override
+ String get setup_folder_pick => 'Scegli cartella';
+
+ @override
+ String get setup_folder_switching => 'Apertura della libreria...';
+
+ @override
+ String get setup_folder_title => 'Apri cartella esistente';
+
+ @override
+ String get setup_profile_nameHint => 'Inserisci il tuo nome';
+
+ @override
+ String get setup_profile_nameLabel => 'Il Tuo Nome';
+
+ @override
+ String get setup_profile_nameValidation => 'Inserisci il tuo nome';
+
+ @override
+ String get setup_profile_subtitle =>
+ 'Inserisci il tuo nome per iniziare. Potrai aggiungere altri dettagli in seguito.';
+
+ @override
+ String get setup_profile_title => 'Crea il Tuo Profilo';
+
+ @override
+ String get setup_restore_inProgress => 'Ripristino...';
+
+ @override
+ String get setup_restore_pick => 'Scegli file di backup';
+
+ @override
+ String get setup_restore_title => 'Ripristina backup';
+
+ @override
+ String get setup_step_backup => 'Backup';
+
+ @override
+ String get setup_step_finish => 'Fine';
+
+ @override
+ String get setup_step_profile => 'Profilo';
+
+ @override
+ String get setup_step_units => 'Unità';
+
+ @override
+ String get setup_syncPull_continue => 'Continua';
+
+ @override
+ String get setup_syncPull_noLibrary_message =>
+ 'Nessuna libreria Submersion trovata su questo account. Iniziare da zero? La connessione verrà mantenuta.';
+
+ @override
+ String get setup_syncPull_noLibrary_title => 'Nessuna libreria trovata';
+
+ @override
+ String get setup_syncPull_success => 'Libreria adottata';
+
+ @override
+ String get setup_syncPull_syncing => 'Download della libreria...';
+
+ @override
+ String get setup_syncPull_title => 'Connetti e scarica';
+
+ @override
+ String get setup_sync_changeProvider => 'Cambia provider';
+
+ @override
+ String setup_sync_connectedTo(String provider) {
+ return 'Connesso a $provider';
+ }
+
+ @override
+ String setup_sync_error(Object error) {
+ return 'Impossibile connettersi: $error';
+ }
+
+ @override
+ String get setup_sync_header => 'Sincronizzazione cloud';
+
+ @override
+ String get setup_sync_icloudUnavailable =>
+ 'iCloud non è disponibile su questo dispositivo';
+
+ @override
+ String get setup_sync_libraryFound_adopt => 'Adotta la libreria esistente';
+
+ @override
+ String get setup_sync_libraryFound_keepFresh => 'Inizia da zero';
+
+ @override
+ String get setup_sync_libraryFound_message =>
+ 'Questo account contiene già una libreria Submersion. Adottarla invece di iniziare da zero?';
+
+ @override
+ String get setup_sync_libraryFound_title => 'Trovata libreria esistente';
+
+ @override
+ String get setup_sync_manageInSettings => 'Gestisci nelle impostazioni';
+
+ @override
+ String get setup_sync_notConnected => 'Non connesso';
+
+ @override
+ String get setup_sync_subtitle =>
+ 'Sincronizza il tuo logbook su tutti i dispositivi';
+
+ @override
+ String get setup_units_advanced => 'Regola le unità';
+
+ @override
+ String get setup_units_altitude => 'Altitudine';
+
+ @override
+ String get setup_units_dateFormat => 'Formato data';
+
+ @override
+ String get setup_units_depth => 'Profondità';
+
+ @override
+ String get setup_units_imperial => 'Imperiale';
+
+ @override
+ String get setup_units_metric => 'Metrico';
+
+ @override
+ String get setup_units_pressure => 'Pressione';
+
+ @override
+ String get setup_units_sac => 'Tasso SAC';
+
+ @override
+ String get setup_units_subtitle =>
+ 'Scegli come visualizzare le misure. Puoi regolare ogni unità.';
+
+ @override
+ String get setup_units_temperature => 'Temperatura';
+
+ @override
+ String get setup_units_timeFormat => 'Formato ora';
+
+ @override
+ String get setup_units_title => 'Unità';
+
+ @override
+ String get setup_units_volume => 'Volume';
+
+ @override
+ String get setup_units_weight => 'Peso';
+
+ @override
+ String get setup_welcome_existingData_subtitle =>
+ 'Ripristina un backup, collega la sincronizzazione o apri una cartella esistente';
+
+ @override
+ String get setup_welcome_existingData_title => 'Ho già dati di Submersion';
+
+ @override
+ String get setup_welcome_skipSetup => 'Salta configurazione';
+
+ @override
+ String get setup_welcome_startFresh_subtitle =>
+ 'Crea il tuo profilo subacqueo e configura l\'app';
+
+ @override
+ String get setup_welcome_startFresh_title => 'Configura un nuovo logbook';
+
+ @override
+ String get setup_welcome_subtitle =>
+ 'Registrazione e analisi avanzata delle immersioni';
+
+ @override
+ String get setup_welcome_title => 'Benvenuto in Submersion';
+
@override
String get siteMatchReview_title => 'Associa siti';
@@ -10603,40 +10896,6 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get nav_trips => 'Viaggi';
- @override
- String get onboarding_welcome_createProfile => 'Crea il Tuo Profilo';
-
- @override
- String get onboarding_welcome_createProfileSubtitle =>
- 'Inserisci il tuo nome per iniziare. Potrai aggiungere altri dettagli in seguito.';
-
- @override
- String get onboarding_welcome_creating => 'Creazione...';
-
- @override
- String onboarding_welcome_errorCreatingProfile(Object error) {
- return 'Errore durante la creazione del profilo: $error';
- }
-
- @override
- String get onboarding_welcome_getStarted => 'Inizia';
-
- @override
- String get onboarding_welcome_nameHint => 'Inserisci il tuo nome';
-
- @override
- String get onboarding_welcome_nameLabel => 'Il Tuo Nome';
-
- @override
- String get onboarding_welcome_nameValidation => 'Inserisci il tuo nome';
-
- @override
- String get onboarding_welcome_subtitle =>
- 'Registrazione e analisi avanzata delle immersioni';
-
- @override
- String get onboarding_welcome_title => 'Benvenuto in Submersion';
-
@override
String plannerCanvas_bailout_available(String liters) {
return 'Disponibile $liters';
diff --git a/lib/l10n/arb/app_localizations_nl.dart b/lib/l10n/arb/app_localizations_nl.dart
index 3c584e72d..8209a14b8 100644
--- a/lib/l10n/arb/app_localizations_nl.dart
+++ b/lib/l10n/arb/app_localizations_nl.dart
@@ -4979,6 +4979,295 @@ class AppLocalizationsNl extends AppLocalizations {
return '$_temp0';
}
+ @override
+ String get settings_manage_setupAssistant => 'Configuratie-assistent';
+
+ @override
+ String get settings_manage_setupAssistant_subtitle =>
+ 'Eenheden, weergave en back-upkeuzes opnieuw bekijken';
+
+ @override
+ String get setup_backup_cloudCopy => 'Een back-upkopie in de cloud bewaren';
+
+ @override
+ String get setup_backup_frequency => 'Frequentie';
+
+ @override
+ String get setup_backup_frequency_daily => 'Dagelijks';
+
+ @override
+ String get setup_backup_frequency_monthly => 'Maandelijks';
+
+ @override
+ String get setup_backup_frequency_weekly => 'Wekelijks';
+
+ @override
+ String get setup_backup_scheduleSubtitle =>
+ 'Maak volgens schema een back-up van je logboek';
+
+ @override
+ String get setup_backup_scheduleToggle => 'Automatische back-ups';
+
+ @override
+ String get setup_backup_subtitle => 'Bescherm je duiklogboek vanaf dag één.';
+
+ @override
+ String get setup_backup_title => 'Back-ups en synchronisatie';
+
+ @override
+ String get setup_common_back => 'Terug';
+
+ @override
+ String get setup_common_next => 'Volgende';
+
+ @override
+ String get setup_common_skip => 'Overslaan';
+
+ @override
+ String get setup_existing_folder_subtitle =>
+ 'Wijs Submersion naar een map die al een bibliotheek bevat';
+
+ @override
+ String get setup_existing_folder_title => 'Een bestaande map openen';
+
+ @override
+ String get setup_existing_restore_subtitle =>
+ 'Kies een back-upbestand dat uit Submersion is geëxporteerd';
+
+ @override
+ String get setup_existing_restore_title => 'Een back-upbestand terugzetten';
+
+ @override
+ String get setup_existing_subtitle =>
+ 'Kies hoe je bestaande Submersion-bibliotheek wordt geladen';
+
+ @override
+ String get setup_existing_sync_subtitle =>
+ 'Haal je bibliotheek op uit iCloud, Dropbox of S3';
+
+ @override
+ String get setup_existing_sync_title => 'Cloudsynchronisatie verbinden';
+
+ @override
+ String get setup_existing_title => 'Breng je gegevens mee';
+
+ @override
+ String get setup_finish_applying => 'Instellen...';
+
+ @override
+ String setup_finish_error(Object error) {
+ return 'Kon de installatie niet voltooien: $error';
+ }
+
+ @override
+ String get setup_finish_feature_diveComputer =>
+ 'Duiken downloaden vanaf je duikcomputer';
+
+ @override
+ String get setup_finish_feature_gear =>
+ 'Uitrusting en onderhoudsintervallen bijhouden';
+
+ @override
+ String get setup_finish_feature_import =>
+ 'Logboeken importeren uit bestanden en andere apps';
+
+ @override
+ String get setup_finish_feature_sites => 'Je duikstekken op de kaart zetten';
+
+ @override
+ String get setup_finish_feature_statistics =>
+ 'Statistieken over je duiken verkennen';
+
+ @override
+ String get setup_finish_start => 'Beginnen met loggen';
+
+ @override
+ String get setup_finish_subtitle => 'Submersion kan ook...';
+
+ @override
+ String get setup_finish_title => 'Je bent klaar';
+
+ @override
+ String get setup_folder_notFound_message =>
+ 'De geselecteerde map bevat geen Submersion-database.';
+
+ @override
+ String get setup_folder_notFound_title => 'Geen bibliotheek in die map';
+
+ @override
+ String get setup_folder_pick => 'Map kiezen';
+
+ @override
+ String get setup_folder_switching => 'Bibliotheek openen...';
+
+ @override
+ String get setup_folder_title => 'Bestaande map openen';
+
+ @override
+ String get setup_profile_nameHint => 'Voer je naam in';
+
+ @override
+ String get setup_profile_nameLabel => 'Je naam';
+
+ @override
+ String get setup_profile_nameValidation => 'Voer je naam in';
+
+ @override
+ String get setup_profile_subtitle =>
+ 'Voer je naam in om te beginnen. Je kunt later meer details toevoegen.';
+
+ @override
+ String get setup_profile_title => 'Maak je profiel aan';
+
+ @override
+ String get setup_restore_inProgress => 'Terugzetten...';
+
+ @override
+ String get setup_restore_pick => 'Back-upbestand kiezen';
+
+ @override
+ String get setup_restore_title => 'Back-up terugzetten';
+
+ @override
+ String get setup_step_backup => 'Back-up';
+
+ @override
+ String get setup_step_finish => 'Klaar';
+
+ @override
+ String get setup_step_profile => 'Profiel';
+
+ @override
+ String get setup_step_units => 'Eenheden';
+
+ @override
+ String get setup_syncPull_continue => 'Doorgaan';
+
+ @override
+ String get setup_syncPull_noLibrary_message =>
+ 'Er is geen Submersion-bibliotheek gevonden op dit account. Opnieuw beginnen? Je verbinding blijft behouden.';
+
+ @override
+ String get setup_syncPull_noLibrary_title => 'Geen bibliotheek gevonden';
+
+ @override
+ String get setup_syncPull_success => 'Bibliotheek overgenomen';
+
+ @override
+ String get setup_syncPull_syncing => 'Bibliotheek ophalen...';
+
+ @override
+ String get setup_syncPull_title => 'Verbinden en ophalen';
+
+ @override
+ String get setup_sync_changeProvider => 'Provider wijzigen';
+
+ @override
+ String setup_sync_connectedTo(String provider) {
+ return 'Verbonden met $provider';
+ }
+
+ @override
+ String setup_sync_error(Object error) {
+ return 'Kon geen verbinding maken: $error';
+ }
+
+ @override
+ String get setup_sync_header => 'Cloudsynchronisatie';
+
+ @override
+ String get setup_sync_icloudUnavailable =>
+ 'iCloud is niet beschikbaar op dit apparaat';
+
+ @override
+ String get setup_sync_libraryFound_adopt => 'Bestaande bibliotheek overnemen';
+
+ @override
+ String get setup_sync_libraryFound_keepFresh => 'Opnieuw beginnen';
+
+ @override
+ String get setup_sync_libraryFound_message =>
+ 'Dit account bevat al een Submersion-bibliotheek. Deze overnemen in plaats van opnieuw te beginnen?';
+
+ @override
+ String get setup_sync_libraryFound_title => 'Bestaande bibliotheek gevonden';
+
+ @override
+ String get setup_sync_manageInSettings => 'Beheren in Instellingen';
+
+ @override
+ String get setup_sync_notConnected => 'Niet verbonden';
+
+ @override
+ String get setup_sync_subtitle => 'Synchroniseer je logboek tussen apparaten';
+
+ @override
+ String get setup_units_advanced => 'Eenheden verfijnen';
+
+ @override
+ String get setup_units_altitude => 'Hoogte';
+
+ @override
+ String get setup_units_dateFormat => 'Datumnotatie';
+
+ @override
+ String get setup_units_depth => 'Diepte';
+
+ @override
+ String get setup_units_imperial => 'Imperiaal';
+
+ @override
+ String get setup_units_metric => 'Metrisch';
+
+ @override
+ String get setup_units_pressure => 'Druk';
+
+ @override
+ String get setup_units_sac => 'SAC-waarde';
+
+ @override
+ String get setup_units_subtitle =>
+ 'Kies hoe metingen worden weergegeven. Je kunt elke eenheid apart instellen.';
+
+ @override
+ String get setup_units_temperature => 'Temperatuur';
+
+ @override
+ String get setup_units_timeFormat => 'Tijdnotatie';
+
+ @override
+ String get setup_units_title => 'Eenheden';
+
+ @override
+ String get setup_units_volume => 'Volume';
+
+ @override
+ String get setup_units_weight => 'Gewicht';
+
+ @override
+ String get setup_welcome_existingData_subtitle =>
+ 'Zet een back-up terug, verbind cloudsynchronisatie of open een bestaande map';
+
+ @override
+ String get setup_welcome_existingData_title =>
+ 'Ik heb al Submersion-gegevens';
+
+ @override
+ String get setup_welcome_skipSetup => 'Installatie overslaan';
+
+ @override
+ String get setup_welcome_startFresh_subtitle =>
+ 'Maak je duikersprofiel en stel de app in';
+
+ @override
+ String get setup_welcome_startFresh_title => 'Een nieuw logboek instellen';
+
+ @override
+ String get setup_welcome_subtitle => 'Geavanceerd duiklogboek en analyse';
+
+ @override
+ String get setup_welcome_title => 'Welkom bij Submersion';
+
@override
String get siteMatchReview_title => 'Stekken koppelen';
@@ -10526,40 +10815,6 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get nav_trips => 'Reizen';
- @override
- String get onboarding_welcome_createProfile => 'Maak je profiel aan';
-
- @override
- String get onboarding_welcome_createProfileSubtitle =>
- 'Voer je naam in om te beginnen. Je kunt later meer details toevoegen.';
-
- @override
- String get onboarding_welcome_creating => 'Aanmaken...';
-
- @override
- String onboarding_welcome_errorCreatingProfile(Object error) {
- return 'Fout bij aanmaken profiel: $error';
- }
-
- @override
- String get onboarding_welcome_getStarted => 'Aan de slag';
-
- @override
- String get onboarding_welcome_nameHint => 'Voer je naam in';
-
- @override
- String get onboarding_welcome_nameLabel => 'Je naam';
-
- @override
- String get onboarding_welcome_nameValidation => 'Voer je naam in';
-
- @override
- String get onboarding_welcome_subtitle =>
- 'Geavanceerd duiklogboek en analyse';
-
- @override
- String get onboarding_welcome_title => 'Welkom bij Submersion';
-
@override
String plannerCanvas_bailout_available(String liters) {
return 'Beschikbaar $liters';
diff --git a/lib/l10n/arb/app_localizations_pt.dart b/lib/l10n/arb/app_localizations_pt.dart
index d9fd5cbf4..865a3d813 100644
--- a/lib/l10n/arb/app_localizations_pt.dart
+++ b/lib/l10n/arb/app_localizations_pt.dart
@@ -5018,6 +5018,298 @@ class AppLocalizationsPt extends AppLocalizations {
return '$_temp0';
}
+ @override
+ String get settings_manage_setupAssistant => 'Assistente de configuração';
+
+ @override
+ String get settings_manage_setupAssistant_subtitle =>
+ 'Reveja unidades, aparência e opções de backup';
+
+ @override
+ String get setup_backup_cloudCopy => 'Manter uma cópia de backup na nuvem';
+
+ @override
+ String get setup_backup_frequency => 'Frequência';
+
+ @override
+ String get setup_backup_frequency_daily => 'Diária';
+
+ @override
+ String get setup_backup_frequency_monthly => 'Mensal';
+
+ @override
+ String get setup_backup_frequency_weekly => 'Semanal';
+
+ @override
+ String get setup_backup_scheduleSubtitle =>
+ 'Faça backup do seu registro periodicamente';
+
+ @override
+ String get setup_backup_scheduleToggle => 'Backups automáticos';
+
+ @override
+ String get setup_backup_subtitle =>
+ 'Proteja seu registro de mergulho desde o primeiro dia.';
+
+ @override
+ String get setup_backup_title => 'Backups e sincronização';
+
+ @override
+ String get setup_common_back => 'Voltar';
+
+ @override
+ String get setup_common_next => 'Avançar';
+
+ @override
+ String get setup_common_skip => 'Ignorar';
+
+ @override
+ String get setup_existing_folder_subtitle =>
+ 'Aponte o Submersion para uma pasta que já contém uma biblioteca';
+
+ @override
+ String get setup_existing_folder_title => 'Abrir uma pasta existente';
+
+ @override
+ String get setup_existing_restore_subtitle =>
+ 'Escolha um arquivo de backup exportado do Submersion';
+
+ @override
+ String get setup_existing_restore_title => 'Restaurar um arquivo de backup';
+
+ @override
+ String get setup_existing_subtitle =>
+ 'Escolha como carregar sua biblioteca existente do Submersion';
+
+ @override
+ String get setup_existing_sync_subtitle =>
+ 'Baixe sua biblioteca do iCloud, Dropbox ou S3';
+
+ @override
+ String get setup_existing_sync_title => 'Conectar sincronização na nuvem';
+
+ @override
+ String get setup_existing_title => 'Traga seus dados';
+
+ @override
+ String get setup_finish_applying => 'Configurando...';
+
+ @override
+ String setup_finish_error(Object error) {
+ return 'Não foi possível concluir a configuração: $error';
+ }
+
+ @override
+ String get setup_finish_feature_diveComputer =>
+ 'Baixe mergulhos do seu computador de mergulho';
+
+ @override
+ String get setup_finish_feature_gear =>
+ 'Acompanhe equipamentos e intervalos de manutenção';
+
+ @override
+ String get setup_finish_feature_import =>
+ 'Importe registros de arquivos e outros aplicativos';
+
+ @override
+ String get setup_finish_feature_sites =>
+ 'Veja seus pontos de mergulho no mapa';
+
+ @override
+ String get setup_finish_feature_statistics =>
+ 'Explore estatísticas sobre seus mergulhos';
+
+ @override
+ String get setup_finish_start => 'Começar a registrar';
+
+ @override
+ String get setup_finish_subtitle => 'O Submersion também pode...';
+
+ @override
+ String get setup_finish_title => 'Tudo pronto';
+
+ @override
+ String get setup_folder_notFound_message =>
+ 'A pasta selecionada não contém um banco de dados do Submersion.';
+
+ @override
+ String get setup_folder_notFound_title => 'Nenhuma biblioteca nessa pasta';
+
+ @override
+ String get setup_folder_pick => 'Escolher pasta';
+
+ @override
+ String get setup_folder_switching => 'Abrindo biblioteca...';
+
+ @override
+ String get setup_folder_title => 'Abrir pasta existente';
+
+ @override
+ String get setup_profile_nameHint => 'Digite seu nome';
+
+ @override
+ String get setup_profile_nameLabel => 'Seu Nome';
+
+ @override
+ String get setup_profile_nameValidation => 'Digite seu nome';
+
+ @override
+ String get setup_profile_subtitle =>
+ 'Digite seu nome para começar. Você pode adicionar mais detalhes depois.';
+
+ @override
+ String get setup_profile_title => 'Crie Seu Perfil';
+
+ @override
+ String get setup_restore_inProgress => 'Restaurando...';
+
+ @override
+ String get setup_restore_pick => 'Escolher arquivo de backup';
+
+ @override
+ String get setup_restore_title => 'Restaurar backup';
+
+ @override
+ String get setup_step_backup => 'Backup';
+
+ @override
+ String get setup_step_finish => 'Concluído';
+
+ @override
+ String get setup_step_profile => 'Perfil';
+
+ @override
+ String get setup_step_units => 'Unidades';
+
+ @override
+ String get setup_syncPull_continue => 'Continuar';
+
+ @override
+ String get setup_syncPull_noLibrary_message =>
+ 'Nenhuma biblioteca do Submersion foi encontrada nesta conta. Começar do zero? Sua conexão será mantida.';
+
+ @override
+ String get setup_syncPull_noLibrary_title => 'Nenhuma biblioteca encontrada';
+
+ @override
+ String get setup_syncPull_success => 'Biblioteca adotada';
+
+ @override
+ String get setup_syncPull_syncing => 'Baixando sua biblioteca...';
+
+ @override
+ String get setup_syncPull_title => 'Conectar e baixar';
+
+ @override
+ String get setup_sync_changeProvider => 'Alterar provedor';
+
+ @override
+ String setup_sync_connectedTo(String provider) {
+ return 'Conectado a $provider';
+ }
+
+ @override
+ String setup_sync_error(Object error) {
+ return 'Não foi possível conectar: $error';
+ }
+
+ @override
+ String get setup_sync_header => 'Sincronização na nuvem';
+
+ @override
+ String get setup_sync_icloudUnavailable =>
+ 'O iCloud não está disponível neste dispositivo';
+
+ @override
+ String get setup_sync_libraryFound_adopt => 'Adotar biblioteca existente';
+
+ @override
+ String get setup_sync_libraryFound_keepFresh => 'Começar do zero';
+
+ @override
+ String get setup_sync_libraryFound_message =>
+ 'Esta conta já contém uma biblioteca do Submersion. Adotá-la em vez de começar do zero?';
+
+ @override
+ String get setup_sync_libraryFound_title => 'Biblioteca existente encontrada';
+
+ @override
+ String get setup_sync_manageInSettings => 'Gerenciar nas Configurações';
+
+ @override
+ String get setup_sync_notConnected => 'Não conectado';
+
+ @override
+ String get setup_sync_subtitle =>
+ 'Sincronize seu registro entre dispositivos';
+
+ @override
+ String get setup_units_advanced => 'Ajustar unidades';
+
+ @override
+ String get setup_units_altitude => 'Altitude';
+
+ @override
+ String get setup_units_dateFormat => 'Formato de data';
+
+ @override
+ String get setup_units_depth => 'Profundidade';
+
+ @override
+ String get setup_units_imperial => 'Imperial';
+
+ @override
+ String get setup_units_metric => 'Métrico';
+
+ @override
+ String get setup_units_pressure => 'Pressão';
+
+ @override
+ String get setup_units_sac => 'Taxa SAC';
+
+ @override
+ String get setup_units_subtitle =>
+ 'Escolha como as medidas são exibidas. Você pode ajustar cada unidade.';
+
+ @override
+ String get setup_units_temperature => 'Temperatura';
+
+ @override
+ String get setup_units_timeFormat => 'Formato de hora';
+
+ @override
+ String get setup_units_title => 'Unidades';
+
+ @override
+ String get setup_units_volume => 'Volume';
+
+ @override
+ String get setup_units_weight => 'Peso';
+
+ @override
+ String get setup_welcome_existingData_subtitle =>
+ 'Restaure um backup, conecte a sincronização ou abra uma pasta existente';
+
+ @override
+ String get setup_welcome_existingData_title => 'Já tenho dados do Submersion';
+
+ @override
+ String get setup_welcome_skipSetup => 'Ignorar configuração';
+
+ @override
+ String get setup_welcome_startFresh_subtitle =>
+ 'Crie seu perfil de mergulhador e configure o aplicativo';
+
+ @override
+ String get setup_welcome_startFresh_title => 'Configurar um novo registro';
+
+ @override
+ String get setup_welcome_subtitle =>
+ 'Registro e análise avançada de mergulhos';
+
+ @override
+ String get setup_welcome_title => 'Bem-vindo ao Submersion';
+
@override
String get siteMatchReview_title => 'Associar pontos';
@@ -10609,40 +10901,6 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get nav_trips => 'Viagens';
- @override
- String get onboarding_welcome_createProfile => 'Crie Seu Perfil';
-
- @override
- String get onboarding_welcome_createProfileSubtitle =>
- 'Digite seu nome para começar. Você pode adicionar mais detalhes depois.';
-
- @override
- String get onboarding_welcome_creating => 'Criando...';
-
- @override
- String onboarding_welcome_errorCreatingProfile(Object error) {
- return 'Erro ao criar perfil: $error';
- }
-
- @override
- String get onboarding_welcome_getStarted => 'Começar';
-
- @override
- String get onboarding_welcome_nameHint => 'Digite seu nome';
-
- @override
- String get onboarding_welcome_nameLabel => 'Seu Nome';
-
- @override
- String get onboarding_welcome_nameValidation => 'Digite seu nome';
-
- @override
- String get onboarding_welcome_subtitle =>
- 'Registro e análise avançada de mergulhos';
-
- @override
- String get onboarding_welcome_title => 'Bem-vindo ao Submersion';
-
@override
String plannerCanvas_bailout_available(String liters) {
return 'Disponível $liters';
diff --git a/lib/l10n/arb/app_localizations_zh.dart b/lib/l10n/arb/app_localizations_zh.dart
index afcae4989..994d8b182 100644
--- a/lib/l10n/arb/app_localizations_zh.dart
+++ b/lib/l10n/arb/app_localizations_zh.dart
@@ -4762,6 +4762,278 @@ class AppLocalizationsZh extends AppLocalizations {
return '$_temp0';
}
+ @override
+ String get settings_manage_setupAssistant => '设置向导';
+
+ @override
+ String get settings_manage_setupAssistant_subtitle => '重新设置单位、外观和备份选项';
+
+ @override
+ String get setup_backup_cloudCopy => '在云端保留备份副本';
+
+ @override
+ String get setup_backup_frequency => '频率';
+
+ @override
+ String get setup_backup_frequency_daily => '每天';
+
+ @override
+ String get setup_backup_frequency_monthly => '每月';
+
+ @override
+ String get setup_backup_frequency_weekly => '每周';
+
+ @override
+ String get setup_backup_scheduleSubtitle => '按计划备份你的潜水日志';
+
+ @override
+ String get setup_backup_scheduleToggle => '自动备份';
+
+ @override
+ String get setup_backup_subtitle => '从第一天起保护你的潜水日志。';
+
+ @override
+ String get setup_backup_title => '备份与同步';
+
+ @override
+ String get setup_common_back => '返回';
+
+ @override
+ String get setup_common_next => '下一步';
+
+ @override
+ String get setup_common_skip => '跳过';
+
+ @override
+ String get setup_existing_folder_subtitle => '让 Submersion 使用已包含资料库的文件夹';
+
+ @override
+ String get setup_existing_folder_title => '打开现有文件夹';
+
+ @override
+ String get setup_existing_restore_subtitle => '选择从 Submersion 导出的备份文件';
+
+ @override
+ String get setup_existing_restore_title => '恢复备份文件';
+
+ @override
+ String get setup_existing_subtitle => '选择如何加载现有的 Submersion 资料库';
+
+ @override
+ String get setup_existing_sync_subtitle => '从 iCloud、Dropbox 或 S3 拉取资料库';
+
+ @override
+ String get setup_existing_sync_title => '连接云同步';
+
+ @override
+ String get setup_existing_title => '导入你的数据';
+
+ @override
+ String get setup_finish_applying => '正在设置...';
+
+ @override
+ String setup_finish_error(Object error) {
+ return '无法完成设置:$error';
+ }
+
+ @override
+ String get setup_finish_feature_diveComputer => '从潜水电脑下载潜水记录';
+
+ @override
+ String get setup_finish_feature_gear => '跟踪装备和保养周期';
+
+ @override
+ String get setup_finish_feature_import => '从文件和其他应用导入日志';
+
+ @override
+ String get setup_finish_feature_sites => '在地图上标记潜点';
+
+ @override
+ String get setup_finish_feature_statistics => '探索你的潜水统计数据';
+
+ @override
+ String get setup_finish_start => '开始记录';
+
+ @override
+ String get setup_finish_subtitle => 'Submersion 还可以...';
+
+ @override
+ String get setup_finish_title => '一切就绪';
+
+ @override
+ String get setup_folder_notFound_message => '所选文件夹不包含 Submersion 数据库。';
+
+ @override
+ String get setup_folder_notFound_title => '该文件夹中没有资料库';
+
+ @override
+ String get setup_folder_pick => '选择文件夹';
+
+ @override
+ String get setup_folder_switching => '正在打开资料库...';
+
+ @override
+ String get setup_folder_title => '打开现有文件夹';
+
+ @override
+ String get setup_profile_nameHint => '输入您的名字';
+
+ @override
+ String get setup_profile_nameLabel => '您的名字';
+
+ @override
+ String get setup_profile_nameValidation => '请输入您的名字';
+
+ @override
+ String get setup_profile_subtitle => '输入您的名字以开始使用。稍后可以添加更多详细信息。';
+
+ @override
+ String get setup_profile_title => '创建您的档案';
+
+ @override
+ String get setup_restore_inProgress => '正在恢复...';
+
+ @override
+ String get setup_restore_pick => '选择备份文件';
+
+ @override
+ String get setup_restore_title => '恢复备份';
+
+ @override
+ String get setup_step_backup => '备份';
+
+ @override
+ String get setup_step_finish => '完成';
+
+ @override
+ String get setup_step_profile => '个人资料';
+
+ @override
+ String get setup_step_units => '单位';
+
+ @override
+ String get setup_syncPull_continue => '继续';
+
+ @override
+ String get setup_syncPull_noLibrary_message =>
+ '此账户中未找到 Submersion 资料库。要重新开始吗?你的连接将被保留。';
+
+ @override
+ String get setup_syncPull_noLibrary_title => '未找到资料库';
+
+ @override
+ String get setup_syncPull_success => '已采用资料库';
+
+ @override
+ String get setup_syncPull_syncing => '正在拉取资料库...';
+
+ @override
+ String get setup_syncPull_title => '连接并拉取';
+
+ @override
+ String get setup_sync_changeProvider => '更换服务商';
+
+ @override
+ String setup_sync_connectedTo(String provider) {
+ return '已连接到 $provider';
+ }
+
+ @override
+ String setup_sync_error(Object error) {
+ return '无法连接:$error';
+ }
+
+ @override
+ String get setup_sync_header => '云同步';
+
+ @override
+ String get setup_sync_icloudUnavailable => '此设备不支持 iCloud';
+
+ @override
+ String get setup_sync_libraryFound_adopt => '采用现有资料库';
+
+ @override
+ String get setup_sync_libraryFound_keepFresh => '重新开始';
+
+ @override
+ String get setup_sync_libraryFound_message =>
+ '此账户已包含 Submersion 资料库。要采用它而不是重新开始吗?';
+
+ @override
+ String get setup_sync_libraryFound_title => '发现现有资料库';
+
+ @override
+ String get setup_sync_manageInSettings => '在设置中管理';
+
+ @override
+ String get setup_sync_notConnected => '未连接';
+
+ @override
+ String get setup_sync_subtitle => '在多设备间同步日志';
+
+ @override
+ String get setup_units_advanced => '微调单位';
+
+ @override
+ String get setup_units_altitude => '海拔';
+
+ @override
+ String get setup_units_dateFormat => '日期格式';
+
+ @override
+ String get setup_units_depth => '深度';
+
+ @override
+ String get setup_units_imperial => '英制';
+
+ @override
+ String get setup_units_metric => '公制';
+
+ @override
+ String get setup_units_pressure => '压力';
+
+ @override
+ String get setup_units_sac => 'SAC 耗气率';
+
+ @override
+ String get setup_units_subtitle => '选择测量值的显示方式。每个单位都可以单独微调。';
+
+ @override
+ String get setup_units_temperature => '温度';
+
+ @override
+ String get setup_units_timeFormat => '时间格式';
+
+ @override
+ String get setup_units_title => '单位';
+
+ @override
+ String get setup_units_volume => '容积';
+
+ @override
+ String get setup_units_weight => '重量';
+
+ @override
+ String get setup_welcome_existingData_subtitle => '恢复备份、连接云同步或打开现有文件夹';
+
+ @override
+ String get setup_welcome_existingData_title => '我已有 Submersion 数据';
+
+ @override
+ String get setup_welcome_skipSetup => '跳过设置';
+
+ @override
+ String get setup_welcome_startFresh_subtitle => '创建潜水员档案并配置应用';
+
+ @override
+ String get setup_welcome_startFresh_title => '创建新日志';
+
+ @override
+ String get setup_welcome_subtitle => '高级潜水日志与分析';
+
+ @override
+ String get setup_welcome_title => '欢迎使用 Submersion';
+
@override
String get siteMatchReview_title => '匹配潜水点';
@@ -10131,39 +10403,6 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get nav_trips => '旅行';
- @override
- String get onboarding_welcome_createProfile => '创建您的档案';
-
- @override
- String get onboarding_welcome_createProfileSubtitle =>
- '输入您的名字以开始使用。稍后可以添加更多详细信息。';
-
- @override
- String get onboarding_welcome_creating => '正在创建...';
-
- @override
- String onboarding_welcome_errorCreatingProfile(Object error) {
- return '创建档案时出错:$error';
- }
-
- @override
- String get onboarding_welcome_getStarted => '开始使用';
-
- @override
- String get onboarding_welcome_nameHint => '输入您的名字';
-
- @override
- String get onboarding_welcome_nameLabel => '您的名字';
-
- @override
- String get onboarding_welcome_nameValidation => '请输入您的名字';
-
- @override
- String get onboarding_welcome_subtitle => '高级潜水日志与分析';
-
- @override
- String get onboarding_welcome_title => '欢迎使用 Submersion';
-
@override
String plannerCanvas_bailout_available(String liters) {
return '可用 $liters';
diff --git a/lib/l10n/arb/app_nl.arb b/lib/l10n/arb/app_nl.arb
index b7ab1f914..601235450 100644
--- a/lib/l10n/arb/app_nl.arb
+++ b/lib/l10n/arb/app_nl.arb
@@ -67,6 +67,94 @@
"settings_cloudSync_postRestore_syncing": "Je herstelde bibliotheek wordt met de cloud gesynchroniseerd…",
"settings_cloudSync_postRestore_synced": "Herstelde bibliotheek gesynchroniseerd.",
"settings_cloudSync_replace_reviewAction": "Controleren",
+ "settings_manage_setupAssistant": "Configuratie-assistent",
+ "settings_manage_setupAssistant_subtitle": "Eenheden, weergave en back-upkeuzes opnieuw bekijken",
+ "setup_backup_cloudCopy": "Een back-upkopie in de cloud bewaren",
+ "setup_backup_frequency": "Frequentie",
+ "setup_backup_frequency_daily": "Dagelijks",
+ "setup_backup_frequency_monthly": "Maandelijks",
+ "setup_backup_frequency_weekly": "Wekelijks",
+ "setup_backup_scheduleSubtitle": "Maak volgens schema een back-up van je logboek",
+ "setup_backup_scheduleToggle": "Automatische back-ups",
+ "setup_backup_subtitle": "Bescherm je duiklogboek vanaf dag één.",
+ "setup_backup_title": "Back-ups en synchronisatie",
+ "setup_common_back": "Terug",
+ "setup_common_next": "Volgende",
+ "setup_common_skip": "Overslaan",
+ "setup_existing_folder_subtitle": "Wijs Submersion naar een map die al een bibliotheek bevat",
+ "setup_existing_folder_title": "Een bestaande map openen",
+ "setup_existing_restore_subtitle": "Kies een back-upbestand dat uit Submersion is geëxporteerd",
+ "setup_existing_restore_title": "Een back-upbestand terugzetten",
+ "setup_existing_subtitle": "Kies hoe je bestaande Submersion-bibliotheek wordt geladen",
+ "setup_existing_sync_subtitle": "Haal je bibliotheek op uit iCloud, Dropbox of S3",
+ "setup_existing_sync_title": "Cloudsynchronisatie verbinden",
+ "setup_existing_title": "Breng je gegevens mee",
+ "setup_finish_applying": "Instellen...",
+ "setup_finish_error": "Kon de installatie niet voltooien: {error}",
+ "setup_finish_feature_diveComputer": "Duiken downloaden vanaf je duikcomputer",
+ "setup_finish_feature_gear": "Uitrusting en onderhoudsintervallen bijhouden",
+ "setup_finish_feature_import": "Logboeken importeren uit bestanden en andere apps",
+ "setup_finish_feature_sites": "Je duikstekken op de kaart zetten",
+ "setup_finish_feature_statistics": "Statistieken over je duiken verkennen",
+ "setup_finish_start": "Beginnen met loggen",
+ "setup_finish_subtitle": "Submersion kan ook...",
+ "setup_finish_title": "Je bent klaar",
+ "setup_folder_notFound_message": "De geselecteerde map bevat geen Submersion-database.",
+ "setup_folder_notFound_title": "Geen bibliotheek in die map",
+ "setup_folder_pick": "Map kiezen",
+ "setup_folder_switching": "Bibliotheek openen...",
+ "setup_folder_title": "Bestaande map openen",
+ "setup_profile_nameHint": "Voer je naam in",
+ "setup_profile_nameLabel": "Je naam",
+ "setup_profile_nameValidation": "Voer je naam in",
+ "setup_profile_subtitle": "Voer je naam in om te beginnen. Je kunt later meer details toevoegen.",
+ "setup_profile_title": "Maak je profiel aan",
+ "setup_restore_inProgress": "Terugzetten...",
+ "setup_restore_pick": "Back-upbestand kiezen",
+ "setup_restore_title": "Back-up terugzetten",
+ "setup_step_backup": "Back-up",
+ "setup_step_finish": "Klaar",
+ "setup_step_profile": "Profiel",
+ "setup_step_units": "Eenheden",
+ "setup_syncPull_continue": "Doorgaan",
+ "setup_syncPull_noLibrary_message": "Er is geen Submersion-bibliotheek gevonden op dit account. Opnieuw beginnen? Je verbinding blijft behouden.",
+ "setup_syncPull_noLibrary_title": "Geen bibliotheek gevonden",
+ "setup_syncPull_success": "Bibliotheek overgenomen",
+ "setup_syncPull_syncing": "Bibliotheek ophalen...",
+ "setup_syncPull_title": "Verbinden en ophalen",
+ "setup_sync_changeProvider": "Provider wijzigen",
+ "setup_sync_connectedTo": "Verbonden met {provider}",
+ "setup_sync_error": "Kon geen verbinding maken: {error}",
+ "setup_sync_header": "Cloudsynchronisatie",
+ "setup_sync_icloudUnavailable": "iCloud is niet beschikbaar op dit apparaat",
+ "setup_sync_libraryFound_adopt": "Bestaande bibliotheek overnemen",
+ "setup_sync_libraryFound_keepFresh": "Opnieuw beginnen",
+ "setup_sync_libraryFound_message": "Dit account bevat al een Submersion-bibliotheek. Deze overnemen in plaats van opnieuw te beginnen?",
+ "setup_sync_libraryFound_title": "Bestaande bibliotheek gevonden",
+ "setup_sync_manageInSettings": "Beheren in Instellingen",
+ "setup_sync_notConnected": "Niet verbonden",
+ "setup_sync_subtitle": "Synchroniseer je logboek tussen apparaten",
+ "setup_units_advanced": "Eenheden verfijnen",
+ "setup_units_altitude": "Hoogte",
+ "setup_units_dateFormat": "Datumnotatie",
+ "setup_units_depth": "Diepte",
+ "setup_units_imperial": "Imperiaal",
+ "setup_units_metric": "Metrisch",
+ "setup_units_pressure": "Druk",
+ "setup_units_sac": "SAC-waarde",
+ "setup_units_subtitle": "Kies hoe metingen worden weergegeven. Je kunt elke eenheid apart instellen.",
+ "setup_units_temperature": "Temperatuur",
+ "setup_units_timeFormat": "Tijdnotatie",
+ "setup_units_title": "Eenheden",
+ "setup_units_volume": "Volume",
+ "setup_units_weight": "Gewicht",
+ "setup_welcome_existingData_subtitle": "Zet een back-up terug, verbind cloudsynchronisatie of open een bestaande map",
+ "setup_welcome_existingData_title": "Ik heb al Submersion-gegevens",
+ "setup_welcome_skipSetup": "Installatie overslaan",
+ "setup_welcome_startFresh_subtitle": "Maak je duikersprofiel en stel de app in",
+ "setup_welcome_startFresh_title": "Een nieuw logboek instellen",
+ "setup_welcome_subtitle": "Geavanceerd duiklogboek en analyse",
+ "setup_welcome_title": "Welkom bij Submersion",
"@trips_itinerary_dayLabel": {
"placeholders": {
"dayNumber": {
@@ -3359,16 +3447,6 @@
"nav_tooltip_expandMenu": "Menu uitklappen",
"nav_transfer": "Overdracht",
"nav_trips": "Reizen",
- "onboarding_welcome_createProfile": "Maak je profiel aan",
- "onboarding_welcome_createProfileSubtitle": "Voer je naam in om te beginnen. Je kunt later meer details toevoegen.",
- "onboarding_welcome_creating": "Aanmaken...",
- "onboarding_welcome_errorCreatingProfile": "Fout bij aanmaken profiel: {error}",
- "onboarding_welcome_getStarted": "Aan de slag",
- "onboarding_welcome_nameHint": "Voer je naam in",
- "onboarding_welcome_nameLabel": "Je naam",
- "onboarding_welcome_nameValidation": "Voer je naam in",
- "onboarding_welcome_subtitle": "Geavanceerd duiklogboek en analyse",
- "onboarding_welcome_title": "Welkom bij Submersion",
"plannerCanvas_bailout_available": "Beschikbaar {liters}",
"plannerCanvas_bailout_insufficient": "Bailoutgas onvoldoende voor het slechtste geval",
"plannerCanvas_bailout_required": "Nodig {liters}",
diff --git a/lib/l10n/arb/app_pt.arb b/lib/l10n/arb/app_pt.arb
index e31eb85f8..250a418f5 100644
--- a/lib/l10n/arb/app_pt.arb
+++ b/lib/l10n/arb/app_pt.arb
@@ -67,6 +67,94 @@
"settings_cloudSync_postRestore_syncing": "A sincronizar a sua biblioteca restaurada com a nuvem…",
"settings_cloudSync_postRestore_synced": "Biblioteca restaurada sincronizada.",
"settings_cloudSync_replace_reviewAction": "Rever",
+ "settings_manage_setupAssistant": "Assistente de configuração",
+ "settings_manage_setupAssistant_subtitle": "Reveja unidades, aparência e opções de backup",
+ "setup_backup_cloudCopy": "Manter uma cópia de backup na nuvem",
+ "setup_backup_frequency": "Frequência",
+ "setup_backup_frequency_daily": "Diária",
+ "setup_backup_frequency_monthly": "Mensal",
+ "setup_backup_frequency_weekly": "Semanal",
+ "setup_backup_scheduleSubtitle": "Faça backup do seu registro periodicamente",
+ "setup_backup_scheduleToggle": "Backups automáticos",
+ "setup_backup_subtitle": "Proteja seu registro de mergulho desde o primeiro dia.",
+ "setup_backup_title": "Backups e sincronização",
+ "setup_common_back": "Voltar",
+ "setup_common_next": "Avançar",
+ "setup_common_skip": "Ignorar",
+ "setup_existing_folder_subtitle": "Aponte o Submersion para uma pasta que já contém uma biblioteca",
+ "setup_existing_folder_title": "Abrir uma pasta existente",
+ "setup_existing_restore_subtitle": "Escolha um arquivo de backup exportado do Submersion",
+ "setup_existing_restore_title": "Restaurar um arquivo de backup",
+ "setup_existing_subtitle": "Escolha como carregar sua biblioteca existente do Submersion",
+ "setup_existing_sync_subtitle": "Baixe sua biblioteca do iCloud, Dropbox ou S3",
+ "setup_existing_sync_title": "Conectar sincronização na nuvem",
+ "setup_existing_title": "Traga seus dados",
+ "setup_finish_applying": "Configurando...",
+ "setup_finish_error": "Não foi possível concluir a configuração: {error}",
+ "setup_finish_feature_diveComputer": "Baixe mergulhos do seu computador de mergulho",
+ "setup_finish_feature_gear": "Acompanhe equipamentos e intervalos de manutenção",
+ "setup_finish_feature_import": "Importe registros de arquivos e outros aplicativos",
+ "setup_finish_feature_sites": "Veja seus pontos de mergulho no mapa",
+ "setup_finish_feature_statistics": "Explore estatísticas sobre seus mergulhos",
+ "setup_finish_start": "Começar a registrar",
+ "setup_finish_subtitle": "O Submersion também pode...",
+ "setup_finish_title": "Tudo pronto",
+ "setup_folder_notFound_message": "A pasta selecionada não contém um banco de dados do Submersion.",
+ "setup_folder_notFound_title": "Nenhuma biblioteca nessa pasta",
+ "setup_folder_pick": "Escolher pasta",
+ "setup_folder_switching": "Abrindo biblioteca...",
+ "setup_folder_title": "Abrir pasta existente",
+ "setup_profile_nameHint": "Digite seu nome",
+ "setup_profile_nameLabel": "Seu Nome",
+ "setup_profile_nameValidation": "Digite seu nome",
+ "setup_profile_subtitle": "Digite seu nome para começar. Você pode adicionar mais detalhes depois.",
+ "setup_profile_title": "Crie Seu Perfil",
+ "setup_restore_inProgress": "Restaurando...",
+ "setup_restore_pick": "Escolher arquivo de backup",
+ "setup_restore_title": "Restaurar backup",
+ "setup_step_backup": "Backup",
+ "setup_step_finish": "Concluído",
+ "setup_step_profile": "Perfil",
+ "setup_step_units": "Unidades",
+ "setup_syncPull_continue": "Continuar",
+ "setup_syncPull_noLibrary_message": "Nenhuma biblioteca do Submersion foi encontrada nesta conta. Começar do zero? Sua conexão será mantida.",
+ "setup_syncPull_noLibrary_title": "Nenhuma biblioteca encontrada",
+ "setup_syncPull_success": "Biblioteca adotada",
+ "setup_syncPull_syncing": "Baixando sua biblioteca...",
+ "setup_syncPull_title": "Conectar e baixar",
+ "setup_sync_changeProvider": "Alterar provedor",
+ "setup_sync_connectedTo": "Conectado a {provider}",
+ "setup_sync_error": "Não foi possível conectar: {error}",
+ "setup_sync_header": "Sincronização na nuvem",
+ "setup_sync_icloudUnavailable": "O iCloud não está disponível neste dispositivo",
+ "setup_sync_libraryFound_adopt": "Adotar biblioteca existente",
+ "setup_sync_libraryFound_keepFresh": "Começar do zero",
+ "setup_sync_libraryFound_message": "Esta conta já contém uma biblioteca do Submersion. Adotá-la em vez de começar do zero?",
+ "setup_sync_libraryFound_title": "Biblioteca existente encontrada",
+ "setup_sync_manageInSettings": "Gerenciar nas Configurações",
+ "setup_sync_notConnected": "Não conectado",
+ "setup_sync_subtitle": "Sincronize seu registro entre dispositivos",
+ "setup_units_advanced": "Ajustar unidades",
+ "setup_units_altitude": "Altitude",
+ "setup_units_dateFormat": "Formato de data",
+ "setup_units_depth": "Profundidade",
+ "setup_units_imperial": "Imperial",
+ "setup_units_metric": "Métrico",
+ "setup_units_pressure": "Pressão",
+ "setup_units_sac": "Taxa SAC",
+ "setup_units_subtitle": "Escolha como as medidas são exibidas. Você pode ajustar cada unidade.",
+ "setup_units_temperature": "Temperatura",
+ "setup_units_timeFormat": "Formato de hora",
+ "setup_units_title": "Unidades",
+ "setup_units_volume": "Volume",
+ "setup_units_weight": "Peso",
+ "setup_welcome_existingData_subtitle": "Restaure um backup, conecte a sincronização ou abra uma pasta existente",
+ "setup_welcome_existingData_title": "Já tenho dados do Submersion",
+ "setup_welcome_skipSetup": "Ignorar configuração",
+ "setup_welcome_startFresh_subtitle": "Crie seu perfil de mergulhador e configure o aplicativo",
+ "setup_welcome_startFresh_title": "Configurar um novo registro",
+ "setup_welcome_subtitle": "Registro e análise avançada de mergulhos",
+ "setup_welcome_title": "Bem-vindo ao Submersion",
"@trips_itinerary_dayLabel": {
"placeholders": {
"dayNumber": {
@@ -3359,16 +3447,6 @@
"nav_tooltip_expandMenu": "Expandir menu",
"nav_transfer": "Transferir",
"nav_trips": "Viagens",
- "onboarding_welcome_createProfile": "Crie Seu Perfil",
- "onboarding_welcome_createProfileSubtitle": "Digite seu nome para começar. Você pode adicionar mais detalhes depois.",
- "onboarding_welcome_creating": "Criando...",
- "onboarding_welcome_errorCreatingProfile": "Erro ao criar perfil: {error}",
- "onboarding_welcome_getStarted": "Começar",
- "onboarding_welcome_nameHint": "Digite seu nome",
- "onboarding_welcome_nameLabel": "Seu Nome",
- "onboarding_welcome_nameValidation": "Digite seu nome",
- "onboarding_welcome_subtitle": "Registro e análise avançada de mergulhos",
- "onboarding_welcome_title": "Bem-vindo ao Submersion",
"plannerCanvas_bailout_available": "Disponível {liters}",
"plannerCanvas_bailout_insufficient": "Gás de bailout insuficiente para o pior caso",
"plannerCanvas_bailout_required": "Necessário {liters}",
diff --git a/lib/l10n/arb/app_zh.arb b/lib/l10n/arb/app_zh.arb
index a90a2bba2..2724a3236 100644
--- a/lib/l10n/arb/app_zh.arb
+++ b/lib/l10n/arb/app_zh.arb
@@ -105,6 +105,94 @@
}
}
},
+ "settings_manage_setupAssistant": "设置向导",
+ "settings_manage_setupAssistant_subtitle": "重新设置单位、外观和备份选项",
+ "setup_backup_cloudCopy": "在云端保留备份副本",
+ "setup_backup_frequency": "频率",
+ "setup_backup_frequency_daily": "每天",
+ "setup_backup_frequency_monthly": "每月",
+ "setup_backup_frequency_weekly": "每周",
+ "setup_backup_scheduleSubtitle": "按计划备份你的潜水日志",
+ "setup_backup_scheduleToggle": "自动备份",
+ "setup_backup_subtitle": "从第一天起保护你的潜水日志。",
+ "setup_backup_title": "备份与同步",
+ "setup_common_back": "返回",
+ "setup_common_next": "下一步",
+ "setup_common_skip": "跳过",
+ "setup_existing_folder_subtitle": "让 Submersion 使用已包含资料库的文件夹",
+ "setup_existing_folder_title": "打开现有文件夹",
+ "setup_existing_restore_subtitle": "选择从 Submersion 导出的备份文件",
+ "setup_existing_restore_title": "恢复备份文件",
+ "setup_existing_subtitle": "选择如何加载现有的 Submersion 资料库",
+ "setup_existing_sync_subtitle": "从 iCloud、Dropbox 或 S3 拉取资料库",
+ "setup_existing_sync_title": "连接云同步",
+ "setup_existing_title": "导入你的数据",
+ "setup_finish_applying": "正在设置...",
+ "setup_finish_error": "无法完成设置:{error}",
+ "setup_finish_feature_diveComputer": "从潜水电脑下载潜水记录",
+ "setup_finish_feature_gear": "跟踪装备和保养周期",
+ "setup_finish_feature_import": "从文件和其他应用导入日志",
+ "setup_finish_feature_sites": "在地图上标记潜点",
+ "setup_finish_feature_statistics": "探索你的潜水统计数据",
+ "setup_finish_start": "开始记录",
+ "setup_finish_subtitle": "Submersion 还可以...",
+ "setup_finish_title": "一切就绪",
+ "setup_folder_notFound_message": "所选文件夹不包含 Submersion 数据库。",
+ "setup_folder_notFound_title": "该文件夹中没有资料库",
+ "setup_folder_pick": "选择文件夹",
+ "setup_folder_switching": "正在打开资料库...",
+ "setup_folder_title": "打开现有文件夹",
+ "setup_profile_nameHint": "输入您的名字",
+ "setup_profile_nameLabel": "您的名字",
+ "setup_profile_nameValidation": "请输入您的名字",
+ "setup_profile_subtitle": "输入您的名字以开始使用。稍后可以添加更多详细信息。",
+ "setup_profile_title": "创建您的档案",
+ "setup_restore_inProgress": "正在恢复...",
+ "setup_restore_pick": "选择备份文件",
+ "setup_restore_title": "恢复备份",
+ "setup_step_backup": "备份",
+ "setup_step_finish": "完成",
+ "setup_step_profile": "个人资料",
+ "setup_step_units": "单位",
+ "setup_syncPull_continue": "继续",
+ "setup_syncPull_noLibrary_message": "此账户中未找到 Submersion 资料库。要重新开始吗?你的连接将被保留。",
+ "setup_syncPull_noLibrary_title": "未找到资料库",
+ "setup_syncPull_success": "已采用资料库",
+ "setup_syncPull_syncing": "正在拉取资料库...",
+ "setup_syncPull_title": "连接并拉取",
+ "setup_sync_changeProvider": "更换服务商",
+ "setup_sync_connectedTo": "已连接到 {provider}",
+ "setup_sync_error": "无法连接:{error}",
+ "setup_sync_header": "云同步",
+ "setup_sync_icloudUnavailable": "此设备不支持 iCloud",
+ "setup_sync_libraryFound_adopt": "采用现有资料库",
+ "setup_sync_libraryFound_keepFresh": "重新开始",
+ "setup_sync_libraryFound_message": "此账户已包含 Submersion 资料库。要采用它而不是重新开始吗?",
+ "setup_sync_libraryFound_title": "发现现有资料库",
+ "setup_sync_manageInSettings": "在设置中管理",
+ "setup_sync_notConnected": "未连接",
+ "setup_sync_subtitle": "在多设备间同步日志",
+ "setup_units_advanced": "微调单位",
+ "setup_units_altitude": "海拔",
+ "setup_units_dateFormat": "日期格式",
+ "setup_units_depth": "深度",
+ "setup_units_imperial": "英制",
+ "setup_units_metric": "公制",
+ "setup_units_pressure": "压力",
+ "setup_units_sac": "SAC 耗气率",
+ "setup_units_subtitle": "选择测量值的显示方式。每个单位都可以单独微调。",
+ "setup_units_temperature": "温度",
+ "setup_units_timeFormat": "时间格式",
+ "setup_units_title": "单位",
+ "setup_units_volume": "容积",
+ "setup_units_weight": "重量",
+ "setup_welcome_existingData_subtitle": "恢复备份、连接云同步或打开现有文件夹",
+ "setup_welcome_existingData_title": "我已有 Submersion 数据",
+ "setup_welcome_skipSetup": "跳过设置",
+ "setup_welcome_startFresh_subtitle": "创建潜水员档案并配置应用",
+ "setup_welcome_startFresh_title": "创建新日志",
+ "setup_welcome_subtitle": "高级潜水日志与分析",
+ "setup_welcome_title": "欢迎使用 Submersion",
"@tankPresets_deleteDefaultMessage": {
"placeholders": {
"name": {
@@ -3510,16 +3598,6 @@
"nav_tooltip_expandMenu": "展开菜单",
"nav_transfer": "传输",
"nav_trips": "旅行",
- "onboarding_welcome_createProfile": "创建您的档案",
- "onboarding_welcome_createProfileSubtitle": "输入您的名字以开始使用。稍后可以添加更多详细信息。",
- "onboarding_welcome_creating": "正在创建...",
- "onboarding_welcome_errorCreatingProfile": "创建档案时出错:{error}",
- "onboarding_welcome_getStarted": "开始使用",
- "onboarding_welcome_nameHint": "输入您的名字",
- "onboarding_welcome_nameLabel": "您的名字",
- "onboarding_welcome_nameValidation": "请输入您的名字",
- "onboarding_welcome_subtitle": "高级潜水日志与分析",
- "onboarding_welcome_title": "欢迎使用 Submersion",
"plannerCanvas_bailout_available": "可用 {liters}",
"plannerCanvas_bailout_insufficient": "逃生气体不足以应对最坏情况",
"plannerCanvas_bailout_required": "需要 {liters}",
diff --git a/lib/features/import_wizard/domain/models/wizard_step_def.dart b/lib/shared/widgets/wizard/wizard_step_def.dart
similarity index 100%
rename from lib/features/import_wizard/domain/models/wizard_step_def.dart
rename to lib/shared/widgets/wizard/wizard_step_def.dart
diff --git a/lib/features/import_wizard/presentation/widgets/wizard_step_indicator.dart b/lib/shared/widgets/wizard/wizard_step_indicator.dart
similarity index 100%
rename from lib/features/import_wizard/presentation/widgets/wizard_step_indicator.dart
rename to lib/shared/widgets/wizard/wizard_step_indicator.dart
diff --git a/test/features/import_wizard/domain/adapters/import_source_adapter_test.dart b/test/features/import_wizard/domain/adapters/import_source_adapter_test.dart
index 824b44e97..27ac412be 100644
--- a/test/features/import_wizard/domain/adapters/import_source_adapter_test.dart
+++ b/test/features/import_wizard/domain/adapters/import_source_adapter_test.dart
@@ -5,7 +5,7 @@ import 'package:submersion/features/import_wizard/domain/models/import_bundle.da
import 'package:submersion/features/import_wizard/domain/models/import_cancellation_token.dart';
import 'package:submersion/features/import_wizard/domain/models/import_phase.dart';
import 'package:submersion/features/import_wizard/domain/models/unified_import_result.dart';
-import 'package:submersion/features/import_wizard/domain/models/wizard_step_def.dart';
+import 'package:submersion/shared/widgets/wizard/wizard_step_def.dart';
/// Minimal concrete implementation for testing the abstract class defaults.
class _TestAdapter extends ImportSourceAdapter {
diff --git a/test/features/import_wizard/domain/models/wizard_step_def_test.dart b/test/features/import_wizard/domain/models/wizard_step_def_test.dart
index 309917887..e386c569c 100644
--- a/test/features/import_wizard/domain/models/wizard_step_def_test.dart
+++ b/test/features/import_wizard/domain/models/wizard_step_def_test.dart
@@ -1,7 +1,7 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
-import 'package:submersion/features/import_wizard/domain/models/wizard_step_def.dart';
+import 'package:submersion/shared/widgets/wizard/wizard_step_def.dart';
// A simple provider that always resolves to true.
final _alwaysTrueProvider = Provider((ref) => true);
diff --git a/test/features/import_wizard/domain/services/step_skip_calculator_test.dart b/test/features/import_wizard/domain/services/step_skip_calculator_test.dart
index 94f6de209..807642c0d 100644
--- a/test/features/import_wizard/domain/services/step_skip_calculator_test.dart
+++ b/test/features/import_wizard/domain/services/step_skip_calculator_test.dart
@@ -2,7 +2,7 @@ import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod/misc.dart' show ProviderListenable;
import 'package:flutter_test/flutter_test.dart';
-import 'package:submersion/features/import_wizard/domain/models/wizard_step_def.dart';
+import 'package:submersion/shared/widgets/wizard/wizard_step_def.dart';
import 'package:submersion/features/import_wizard/domain/services/step_skip_calculator.dart';
/// Dummy provider for steps that should always show as "ready".
diff --git a/test/features/import_wizard/presentation/pages/unified_import_wizard_test.dart b/test/features/import_wizard/presentation/pages/unified_import_wizard_test.dart
index cc4555bb2..c8814a5e2 100644
--- a/test/features/import_wizard/presentation/pages/unified_import_wizard_test.dart
+++ b/test/features/import_wizard/presentation/pages/unified_import_wizard_test.dart
@@ -12,7 +12,7 @@ import 'package:submersion/features/import_wizard/domain/models/import_bundle.da
import 'package:submersion/features/import_wizard/domain/models/import_cancellation_token.dart';
import 'package:submersion/features/import_wizard/domain/models/import_phase.dart';
import 'package:submersion/features/import_wizard/domain/models/unified_import_result.dart';
-import 'package:submersion/features/import_wizard/domain/models/wizard_step_def.dart';
+import 'package:submersion/shared/widgets/wizard/wizard_step_def.dart';
import 'package:submersion/features/import_wizard/presentation/pages/unified_import_wizard.dart';
import 'package:submersion/features/import_wizard/presentation/providers/import_wizard_providers.dart';
import 'package:submersion/features/universal_import/presentation/providers/universal_import_providers.dart';
diff --git a/test/features/import_wizard/presentation/widgets/import_progress_step_test.dart b/test/features/import_wizard/presentation/widgets/import_progress_step_test.dart
index d20fcf634..8d69e3226 100644
--- a/test/features/import_wizard/presentation/widgets/import_progress_step_test.dart
+++ b/test/features/import_wizard/presentation/widgets/import_progress_step_test.dart
@@ -8,7 +8,7 @@ import 'package:submersion/features/import_wizard/domain/models/import_bundle.da
import 'package:submersion/features/import_wizard/domain/models/import_cancellation_token.dart';
import 'package:submersion/features/import_wizard/domain/models/import_phase.dart';
import 'package:submersion/features/import_wizard/domain/models/unified_import_result.dart';
-import 'package:submersion/features/import_wizard/domain/models/wizard_step_def.dart';
+import 'package:submersion/shared/widgets/wizard/wizard_step_def.dart';
import 'package:submersion/features/import_wizard/presentation/providers/import_wizard_providers.dart';
import 'package:submersion/features/import_wizard/presentation/widgets/import_progress_step.dart';
import 'package:submersion/l10n/arb/app_localizations.dart';
diff --git a/test/features/import_wizard/presentation/widgets/import_summary_step_test.dart b/test/features/import_wizard/presentation/widgets/import_summary_step_test.dart
index 642575b9b..eab379cfe 100644
--- a/test/features/import_wizard/presentation/widgets/import_summary_step_test.dart
+++ b/test/features/import_wizard/presentation/widgets/import_summary_step_test.dart
@@ -11,7 +11,7 @@ import 'package:submersion/features/import_wizard/domain/models/import_cancellat
import 'package:submersion/features/import_wizard/domain/models/import_file_outcome.dart';
import 'package:submersion/features/import_wizard/domain/models/import_phase.dart';
import 'package:submersion/features/import_wizard/domain/models/unified_import_result.dart';
-import 'package:submersion/features/import_wizard/domain/models/wizard_step_def.dart';
+import 'package:submersion/shared/widgets/wizard/wizard_step_def.dart';
import 'package:submersion/features/import_wizard/presentation/providers/import_wizard_providers.dart';
import 'package:submersion/features/import_wizard/presentation/widgets/import_summary_step.dart';
import 'package:submersion/features/dive_sites/presentation/providers/site_match_review_notifier.dart';
diff --git a/test/features/import_wizard/presentation/widgets/review_step_pending_test.dart b/test/features/import_wizard/presentation/widgets/review_step_pending_test.dart
index 48921572d..7d034a97c 100644
--- a/test/features/import_wizard/presentation/widgets/review_step_pending_test.dart
+++ b/test/features/import_wizard/presentation/widgets/review_step_pending_test.dart
@@ -9,7 +9,7 @@ import 'package:submersion/features/import_wizard/domain/models/import_bundle.da
import 'package:submersion/features/import_wizard/domain/models/import_cancellation_token.dart';
import 'package:submersion/features/import_wizard/domain/models/import_phase.dart';
import 'package:submersion/features/import_wizard/domain/models/unified_import_result.dart';
-import 'package:submersion/features/import_wizard/domain/models/wizard_step_def.dart';
+import 'package:submersion/shared/widgets/wizard/wizard_step_def.dart';
import 'package:submersion/features/import_wizard/presentation/providers/import_wizard_providers.dart';
import 'package:submersion/features/import_wizard/presentation/widgets/review_step.dart';
import 'package:submersion/l10n/arb/app_localizations.dart';
diff --git a/test/features/import_wizard/presentation/widgets/review_step_test.dart b/test/features/import_wizard/presentation/widgets/review_step_test.dart
index 20288ad83..a832cadf0 100644
--- a/test/features/import_wizard/presentation/widgets/review_step_test.dart
+++ b/test/features/import_wizard/presentation/widgets/review_step_test.dart
@@ -11,7 +11,7 @@ import 'package:submersion/features/import_wizard/domain/models/import_bundle.da
import 'package:submersion/features/import_wizard/domain/models/import_cancellation_token.dart';
import 'package:submersion/features/import_wizard/domain/models/import_phase.dart';
import 'package:submersion/features/import_wizard/domain/models/unified_import_result.dart';
-import 'package:submersion/features/import_wizard/domain/models/wizard_step_def.dart';
+import 'package:submersion/shared/widgets/wizard/wizard_step_def.dart';
import 'package:submersion/features/import_wizard/presentation/providers/import_wizard_providers.dart';
import 'package:submersion/features/import_wizard/presentation/widgets/entity_review_list.dart';
import 'package:submersion/features/import_wizard/presentation/widgets/review_step.dart';
diff --git a/test/features/import_wizard/presentation/widgets/wizard_step_indicator_test.dart b/test/features/import_wizard/presentation/widgets/wizard_step_indicator_test.dart
index 572d0db70..9edf7322c 100644
--- a/test/features/import_wizard/presentation/widgets/wizard_step_indicator_test.dart
+++ b/test/features/import_wizard/presentation/widgets/wizard_step_indicator_test.dart
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
-import 'package:submersion/features/import_wizard/presentation/widgets/wizard_step_indicator.dart';
+import 'package:submersion/shared/widgets/wizard/wizard_step_indicator.dart';
void main() {
const labels = ['Select', 'Review', 'Import', 'Done'];
diff --git a/test/features/setup_wizard/data/setup_apply_service_test.dart b/test/features/setup_wizard/data/setup_apply_service_test.dart
new file mode 100644
index 000000000..9edee8858
--- /dev/null
+++ b/test/features/setup_wizard/data/setup_apply_service_test.dart
@@ -0,0 +1,172 @@
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+import 'package:submersion/core/constants/units.dart';
+import 'package:submersion/core/providers/provider.dart';
+import 'package:submersion/features/backup/domain/entities/backup_settings.dart';
+import 'package:submersion/features/backup/presentation/providers/backup_providers.dart';
+import 'package:submersion/features/divers/data/repositories/diver_repository.dart';
+import 'package:submersion/features/divers/domain/entities/diver.dart';
+import 'package:submersion/features/divers/presentation/providers/diver_providers.dart';
+import 'package:submersion/features/settings/data/repositories/diver_settings_repository.dart';
+import 'package:submersion/features/settings/presentation/providers/settings_providers.dart';
+import 'package:submersion/features/setup_wizard/data/setup_apply_service.dart';
+import 'package:submersion/features/setup_wizard/domain/setup_wizard_models.dart';
+import 'package:submersion/features/setup_wizard/presentation/providers/setup_wizard_providers.dart';
+
+import '../../../helpers/test_database.dart';
+
+void main() {
+ late SharedPreferences prefs;
+
+ setUp(() async {
+ SharedPreferences.setMockInitialValues({});
+ prefs = await SharedPreferences.getInstance();
+ await setUpTestDatabase();
+ });
+
+ tearDown(() async {
+ await tearDownTestDatabase();
+ });
+
+ ProviderContainer makeContainer() {
+ final container = ProviderContainer(
+ overrides: [sharedPreferencesProvider.overrideWithValue(prefs)],
+ );
+ addTearDown(container.dispose);
+ return container;
+ }
+
+ test(
+ 'applyFirstRun creates default diver, seeds settings, sets current',
+ () async {
+ final container = makeContainer();
+ final service = container.read(setupApplyServiceProvider);
+
+ const draft = SetupWizardDraft(
+ mode: SetupWizardMode.firstRun,
+ path: SetupPath.fresh,
+ name: 'Eric',
+ backupEnabled: true,
+ backupFrequency: BackupFrequency.daily,
+ );
+ final imperialDraft = draft.applyingUnitPreset(UnitPreset.imperial);
+
+ await service.applyFirstRun(imperialDraft);
+
+ final divers = await DiverRepository().getAllDivers();
+ expect(divers, hasLength(1));
+ expect(divers.single.name, 'Eric');
+ expect(divers.single.isDefault, isTrue);
+
+ // Settings row was seeded with the draft BEFORE the diver switch.
+ final stored = await DiverSettingsRepository().getSettingsForDiver(
+ divers.single.id,
+ );
+ expect(stored, isNotNull);
+ expect(stored!.depthUnit, DepthUnit.feet);
+ expect(stored.pressureUnit, PressureUnit.psi);
+ expect(stored.weightUnit, WeightUnit.pounds);
+
+ // Current diver persisted to prefs.
+ expect(prefs.getString(currentDiverIdKey), divers.single.id);
+
+ // Backup schedule applied.
+ final backup = container.read(backupSettingsProvider);
+ expect(backup.enabled, isTrue);
+ expect(backup.frequency, BackupFrequency.daily);
+ },
+ );
+
+ test(
+ 'applyFirstRun with empty name throws ArgumentError and writes nothing',
+ () async {
+ final container = makeContainer();
+ final service = container.read(setupApplyServiceProvider);
+ const draft = SetupWizardDraft(mode: SetupWizardMode.firstRun);
+
+ await expectLater(service.applyFirstRun(draft), throwsArgumentError);
+ expect(await DiverRepository().getAllDivers(), isEmpty);
+ },
+ );
+
+ test(
+ 'applySettingsMode updates the current diver via existing setters',
+ () async {
+ final container = makeContainer();
+
+ // Seed a diver + make it current, mirroring a real re-entry session.
+ final repo = DiverRepository();
+ final now = DateTime.now();
+ final diver = await repo.createDiver(
+ Diver(
+ id: '',
+ name: 'Existing',
+ isDefault: true,
+ createdAt: now,
+ updatedAt: now,
+ ),
+ );
+ await container
+ .read(currentDiverIdProvider.notifier)
+ .setCurrentDiver(diver.id);
+ // Let the SettingsNotifier finish its reload before applying, mirroring
+ // a settled re-entry session (bounded poll; see plan Task 5 note).
+ container.read(settingsProvider.notifier);
+ var waited = 0;
+ while (waited < 1000) {
+ await Future.delayed(const Duration(milliseconds: 20));
+ waited += 20;
+ // A loaded notifier persists writes; probe via the repository.
+ final probe = await DiverSettingsRepository().getSettingsForDiver(
+ diver.id,
+ );
+ if (probe != null) break;
+ }
+
+ final service = container.read(setupApplyServiceProvider);
+ const base = SetupWizardDraft(mode: SetupWizardMode.settings);
+ final draft = base.applyingUnitPreset(UnitPreset.imperial);
+ await service.applySettingsMode(draft);
+
+ expect(container.read(settingsProvider).depthUnit, DepthUnit.feet);
+ final stored = await DiverSettingsRepository().getSettingsForDiver(
+ diver.id,
+ );
+ expect(stored!.depthUnit, DepthUnit.feet);
+ expect(stored.volumeUnit, VolumeUnit.cubicFeet);
+ },
+ );
+
+ test(
+ 'settings-mode draft seeds live backup state; Finish preserves it',
+ () async {
+ // A backup schedule is already enabled before the wizard opens.
+ SharedPreferences.setMockInitialValues({
+ 'backup_enabled': true,
+ 'backup_frequency': 'daily',
+ });
+ final seededPrefs = await SharedPreferences.getInstance();
+ final container = ProviderContainer(
+ overrides: [sharedPreferencesProvider.overrideWithValue(seededPrefs)],
+ );
+ addTearDown(container.dispose);
+
+ // The re-entry draft must mirror the live schedule, not defaults.
+ final draft = container.read(
+ setupWizardProvider(SetupWizardMode.settings),
+ );
+ expect(draft.backupEnabled, isTrue);
+ expect(draft.backupFrequency, BackupFrequency.daily);
+
+ // Applying an untouched settings-mode draft must not disable backups
+ // (regression: seeding defaults here silently turned them off).
+ await container.read(setupApplyServiceProvider).applySettingsMode(draft);
+ expect(container.read(backupSettingsProvider).enabled, isTrue);
+ expect(
+ container.read(backupSettingsProvider).frequency,
+ BackupFrequency.daily,
+ );
+ },
+ );
+}
diff --git a/test/features/setup_wizard/domain/setup_wizard_models_test.dart b/test/features/setup_wizard/domain/setup_wizard_models_test.dart
new file mode 100644
index 000000000..a3bf02da6
--- /dev/null
+++ b/test/features/setup_wizard/domain/setup_wizard_models_test.dart
@@ -0,0 +1,103 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'package:submersion/core/constants/units.dart';
+import 'package:submersion/features/settings/presentation/providers/settings_providers.dart';
+import 'package:submersion/features/setup_wizard/domain/setup_wizard_models.dart';
+
+void main() {
+ group('computeSteps', () {
+ test('first run starts with fork only while path undecided', () {
+ const draft = SetupWizardDraft(mode: SetupWizardMode.firstRun);
+ expect(computeSteps(draft), [SetupStepId.welcomeFork]);
+ });
+
+ test('fresh path shows full first-run flow', () {
+ const draft = SetupWizardDraft(
+ mode: SetupWizardMode.firstRun,
+ path: SetupPath.fresh,
+ );
+ expect(computeSteps(draft), [
+ SetupStepId.welcomeFork,
+ SetupStepId.profile,
+ SetupStepId.units,
+ SetupStepId.backupSync,
+ SetupStepId.finish,
+ ]);
+ });
+
+ test('skip setup collapses fresh path to profile then finish', () {
+ const draft = SetupWizardDraft(
+ mode: SetupWizardMode.firstRun,
+ path: SetupPath.fresh,
+ skipRequested: true,
+ );
+ expect(computeSteps(draft), [
+ SetupStepId.welcomeFork,
+ SetupStepId.profile,
+ SetupStepId.finish,
+ ]);
+ });
+
+ test('existing-data path shows choice, then source step', () {
+ const undecided = SetupWizardDraft(
+ mode: SetupWizardMode.firstRun,
+ path: SetupPath.existingData,
+ );
+ expect(computeSteps(undecided), [
+ SetupStepId.welcomeFork,
+ SetupStepId.existingChoice,
+ ]);
+
+ const restore = SetupWizardDraft(
+ mode: SetupWizardMode.firstRun,
+ path: SetupPath.existingData,
+ source: ExistingDataSource.restoreBackup,
+ );
+ expect(computeSteps(restore), [
+ SetupStepId.welcomeFork,
+ SetupStepId.existingChoice,
+ SetupStepId.restore,
+ ]);
+
+ const sync = SetupWizardDraft(
+ mode: SetupWizardMode.firstRun,
+ path: SetupPath.existingData,
+ source: ExistingDataSource.cloudSync,
+ );
+ expect(computeSteps(sync).last, SetupStepId.syncConnect);
+
+ const folder = SetupWizardDraft(
+ mode: SetupWizardMode.firstRun,
+ path: SetupPath.existingData,
+ source: ExistingDataSource.openFolder,
+ );
+ expect(computeSteps(folder).last, SetupStepId.openFolder);
+ });
+
+ test('settings mode hides fork and profile', () {
+ const draft = SetupWizardDraft(mode: SetupWizardMode.settings);
+ expect(computeSteps(draft), [
+ SetupStepId.units,
+ SetupStepId.backupSync,
+ SetupStepId.finish,
+ ]);
+ });
+ });
+
+ group('SetupWizardDraft', () {
+ test('applyUnitPreset imperial sets the six core units', () {
+ const draft = SetupWizardDraft(mode: SetupWizardMode.firstRun);
+ final imperial = draft.applyingUnitPreset(UnitPreset.imperial);
+ expect(imperial.settings.depthUnit, DepthUnit.feet);
+ expect(imperial.settings.temperatureUnit, TemperatureUnit.fahrenheit);
+ expect(imperial.settings.pressureUnit, PressureUnit.psi);
+ expect(imperial.settings.volumeUnit, VolumeUnit.cubicFeet);
+ expect(imperial.settings.weightUnit, WeightUnit.pounds);
+ expect(imperial.settings.altitudeUnit, AltitudeUnit.feet);
+ // sacUnit untouched by preset
+ expect(imperial.settings.sacUnit, const AppSettings().sacUnit);
+ // and back to metric
+ final metric = imperial.applyingUnitPreset(UnitPreset.metric);
+ expect(metric.settings.unitPreset, UnitPreset.metric);
+ });
+ });
+}
diff --git a/test/features/setup_wizard/domain/unit_preset_detector_test.dart b/test/features/setup_wizard/domain/unit_preset_detector_test.dart
new file mode 100644
index 000000000..fc94f07f5
--- /dev/null
+++ b/test/features/setup_wizard/domain/unit_preset_detector_test.dart
@@ -0,0 +1,19 @@
+import 'dart:ui';
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:submersion/features/settings/presentation/providers/settings_providers.dart';
+import 'package:submersion/features/setup_wizard/domain/unit_preset_detector.dart';
+
+void main() {
+ test('US, Liberia, Myanmar map to imperial', () {
+ expect(presetForLocale(const Locale('en', 'US')), UnitPreset.imperial);
+ expect(presetForLocale(const Locale('en', 'LR')), UnitPreset.imperial);
+ expect(presetForLocale(const Locale('my', 'MM')), UnitPreset.imperial);
+ });
+
+ test('everything else maps to metric', () {
+ expect(presetForLocale(const Locale('de', 'DE')), UnitPreset.metric);
+ expect(presetForLocale(const Locale('en', 'GB')), UnitPreset.metric);
+ expect(presetForLocale(const Locale('en')), UnitPreset.metric);
+ });
+}
diff --git a/test/features/setup_wizard/presentation/pages/first_run_redirect_test.dart b/test/features/setup_wizard/presentation/pages/first_run_redirect_test.dart
new file mode 100644
index 000000000..545c349af
--- /dev/null
+++ b/test/features/setup_wizard/presentation/pages/first_run_redirect_test.dart
@@ -0,0 +1,43 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:submersion/core/router/app_router.dart';
+import 'package:submersion/features/divers/presentation/providers/diver_providers.dart';
+import 'package:submersion/features/setup_wizard/presentation/pages/setup_wizard_page.dart';
+import 'package:submersion/l10n/arb/app_localizations.dart';
+
+import '../../../../helpers/mock_providers.dart';
+
+void main() {
+ testWidgets('zero divers redirects initial route to the setup wizard', (
+ tester,
+ ) async {
+ final overrides = await getBaseOverrides();
+ final container = ProviderContainer(
+ overrides: [
+ ...overrides,
+ hasAnyDiversProvider.overrideWith((ref) async => false),
+ ],
+ );
+ addTearDown(container.dispose);
+ final router = container.read(appRouterProvider);
+
+ await tester.pumpWidget(
+ UncontrolledProviderScope(
+ container: container,
+ child: MaterialApp.router(
+ localizationsDelegates: AppLocalizations.localizationsDelegates,
+ supportedLocales: AppLocalizations.supportedLocales,
+ routerConfig: router,
+ ),
+ ),
+ );
+ // OceanBackground animates forever; use fixed pumps, not pumpAndSettle.
+ await tester.pump();
+ await tester.pump(const Duration(milliseconds: 400));
+ await tester.pump(const Duration(milliseconds: 50));
+
+ expect(find.byType(SetupWizardPage), findsOneWidget);
+ expect(find.text('Welcome to Submersion'), findsOneWidget);
+ });
+}
diff --git a/test/features/setup_wizard/presentation/pages/setup_wizard_page_test.dart b/test/features/setup_wizard/presentation/pages/setup_wizard_page_test.dart
new file mode 100644
index 000000000..e7209fe4c
--- /dev/null
+++ b/test/features/setup_wizard/presentation/pages/setup_wizard_page_test.dart
@@ -0,0 +1,303 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:submersion/core/data/repositories/sync_repository.dart';
+import 'package:submersion/core/domain/entities/storage_config.dart';
+import 'package:submersion/core/providers/provider.dart';
+import 'package:submersion/core/services/cloud_storage/cloud_storage_provider.dart';
+import 'package:submersion/core/services/cloud_storage/icloud_native_service.dart';
+import 'package:submersion/core/services/sync/sync_initializer.dart';
+import 'package:submersion/features/setup_wizard/presentation/providers/setup_wizard_providers.dart';
+import 'package:submersion/features/backup/presentation/providers/backup_providers.dart';
+import 'package:submersion/features/settings/presentation/providers/storage_providers.dart';
+import 'package:submersion/features/settings/presentation/providers/sync_providers.dart';
+import 'package:submersion/features/setup_wizard/domain/setup_wizard_models.dart';
+import 'package:submersion/features/setup_wizard/presentation/pages/setup_wizard_page.dart';
+
+import '../../../../helpers/mock_providers.dart';
+import '../../../../helpers/test_app.dart';
+
+/// Idle fakes so the existing-data source steps render without real pickers,
+/// timers, or sync services when driven through the shell.
+class _FakeStorageNotifier extends StateNotifier
+ implements StorageConfigNotifier {
+ _FakeStorageNotifier()
+ : super(const StorageConfigState(config: StorageConfig()));
+ @override
+ dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
+}
+
+class _FakeBackupOp extends StateNotifier
+ implements BackupOperationNotifier {
+ _FakeBackupOp() : super(const BackupOperationState());
+ @override
+ dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
+}
+
+class _FakeSyncInit implements SyncInitializer {
+ @override
+ Future> peerSyncFiles(
+ CloudStorageProvider provider,
+ ) async => const [];
+
+ @override
+ dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
+}
+
+class _FakeSyncNotifier extends StateNotifier
+ implements SyncNotifier {
+ _FakeSyncNotifier() : super(const SyncState());
+ @override
+ dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
+}
+
+/// OceanBackground animates forever, so pumpAndSettle would time out.
+/// Fixed pumps cover the post-frame advance, the 300 ms page transition,
+/// and the setState frame that follows the transition future.
+Future pumpWizard(WidgetTester tester) async {
+ for (var i = 0; i < 10; i++) {
+ await tester.pump(const Duration(milliseconds: 100));
+ }
+}
+
+void main() {
+ testWidgets('first run shows fork; fresh choice walks to profile and back', (
+ tester,
+ ) async {
+ final overrides = await getBaseOverrides();
+ await tester.pumpWidget(
+ testApp(
+ overrides: overrides,
+ child: const SetupWizardPage(mode: SetupWizardMode.firstRun),
+ ),
+ );
+ await pumpWizard(tester);
+
+ expect(find.text('Welcome to Submersion'), findsOneWidget);
+ expect(find.text('Set up a new logbook'), findsOneWidget);
+ expect(find.text('I have existing Submersion data'), findsOneWidget);
+
+ await tester.tap(find.text('Set up a new logbook'));
+ await pumpWizard(tester);
+
+ expect(find.text('Create Your Profile'), findsOneWidget);
+
+ // Next disabled with empty name.
+ final nextFinder = find.widgetWithText(FilledButton, 'Next');
+ expect(tester.widget(nextFinder).onPressed, isNull);
+
+ await tester.enterText(find.byType(TextFormField), 'Eric');
+ await pumpWizard(tester);
+ expect(tester.widget(nextFinder).onPressed, isNotNull);
+
+ await tester.tap(nextFinder);
+ await pumpWizard(tester);
+ // Units placeholder page (real step lands in Task 7).
+ expect(find.text('Units'), findsWidgets);
+
+ await tester.tap(find.widgetWithText(OutlinedButton, 'Back'));
+ await pumpWizard(tester);
+ expect(find.text('Create Your Profile'), findsOneWidget);
+ });
+
+ testWidgets('skip setup jumps from profile straight to finish placeholder', (
+ tester,
+ ) async {
+ final overrides = await getBaseOverrides();
+ await tester.pumpWidget(
+ testApp(
+ overrides: overrides,
+ child: const SetupWizardPage(mode: SetupWizardMode.firstRun),
+ ),
+ );
+ await pumpWizard(tester);
+
+ await tester.ensureVisible(find.text('Skip setup'));
+ await tester.pump();
+ await tester.tap(find.text('Skip setup'));
+ await pumpWizard(tester);
+ expect(find.text('Create Your Profile'), findsOneWidget);
+
+ await tester.enterText(find.byType(TextFormField), 'Eric');
+ await pumpWizard(tester);
+ await tester.tap(find.widgetWithText(FilledButton, 'Next'));
+ await pumpWizard(tester);
+
+ expect(find.text("You're all set"), findsOneWidget);
+ });
+
+ testWidgets('settings mode starts at units with no fork', (tester) async {
+ final overrides = await getBaseOverrides();
+ await tester.pumpWidget(
+ testApp(
+ overrides: overrides,
+ child: const SetupWizardPage(mode: SetupWizardMode.settings),
+ ),
+ );
+ await pumpWizard(tester);
+
+ expect(find.text('Welcome to Submersion'), findsNothing);
+ expect(find.text('Units'), findsWidgets);
+ });
+
+ testWidgets('existing-data steps have a back button to the fork', (
+ tester,
+ ) async {
+ final overrides = await getBaseOverrides();
+ await tester.pumpWidget(
+ testApp(
+ overrides: overrides,
+ child: const SetupWizardPage(mode: SetupWizardMode.firstRun),
+ ),
+ );
+ await pumpWizard(tester);
+
+ await tester.tap(find.text('I have existing Submersion data'));
+ await pumpWizard(tester);
+ expect(find.text('Bring your data'), findsOneWidget);
+
+ // The choice step must be reversible (regression: it had no bottom bar
+ // and no back affordance, stranding the user).
+ final backButton = find.byTooltip('Back');
+ expect(backButton, findsOneWidget);
+
+ await tester.tap(backButton);
+ await pumpWizard(tester);
+ expect(find.text('Set up a new logbook'), findsOneWidget);
+ });
+
+ testWidgets('existing-data path renders each source step, reversibly', (
+ tester,
+ ) async {
+ final overrides = await getBaseOverrides();
+ await tester.pumpWidget(
+ testApp(
+ overrides: [
+ ...overrides,
+ isApplePlatformProvider.overrideWithValue(false),
+ dropboxConfiguredProvider.overrideWithValue(false),
+ iCloudAvailabilityProvider.overrideWith(
+ (ref) async => ICloudAvailability.unsupported,
+ ),
+ backupOperationProvider.overrideWith((ref) => _FakeBackupOp()),
+ storageConfigNotifierProvider.overrideWith(
+ (ref) => _FakeStorageNotifier(),
+ ),
+ syncInitializerProvider.overrideWithValue(_FakeSyncInit()),
+ syncStateProvider.overrideWith((ref) => _FakeSyncNotifier()),
+ ],
+ child: const SetupWizardPage(mode: SetupWizardMode.firstRun),
+ ),
+ );
+ await pumpWizard(tester);
+
+ await tester.tap(find.text('I have existing Submersion data'));
+ await pumpWizard(tester);
+ expect(find.text('Bring your data'), findsOneWidget);
+
+ // Restore source, then back to the choice.
+ await tester.tap(find.text('Restore a backup file'));
+ await pumpWizard(tester);
+ expect(find.text('Restore backup'), findsOneWidget);
+ await tester.tap(find.byTooltip('Back'));
+ await pumpWizard(tester);
+ expect(find.text('Bring your data'), findsOneWidget);
+
+ // Open-folder source, then back.
+ await tester.tap(find.text('Open an existing folder'));
+ await pumpWizard(tester);
+ expect(find.text('Open existing folder'), findsOneWidget);
+ await tester.tap(find.byTooltip('Back'));
+ await pumpWizard(tester);
+
+ // Connect-cloud-sync source renders its connect phase.
+ await tester.tap(find.text('Connect cloud sync'));
+ await pumpWizard(tester);
+ expect(find.widgetWithText(FilledButton, 'Continue'), findsOneWidget);
+ });
+
+ testWidgets('fresh path walks profile, units, backup, then finish', (
+ tester,
+ ) async {
+ final overrides = await getBaseOverrides();
+ await tester.pumpWidget(
+ testApp(
+ overrides: [
+ ...overrides,
+ isApplePlatformProvider.overrideWithValue(false),
+ dropboxConfiguredProvider.overrideWithValue(false),
+ iCloudAvailabilityProvider.overrideWith(
+ (ref) async => ICloudAvailability.unsupported,
+ ),
+ ],
+ child: const SetupWizardPage(mode: SetupWizardMode.firstRun),
+ ),
+ );
+ await pumpWizard(tester);
+
+ await tester.tap(find.text('Set up a new logbook'));
+ await pumpWizard(tester);
+ await tester.enterText(find.byType(TextFormField), 'Eric');
+ await pumpWizard(tester);
+
+ await tester.tap(find.widgetWithText(FilledButton, 'Next'));
+ await pumpWizard(tester); // -> units
+ await tester.tap(find.widgetWithText(FilledButton, 'Next'));
+ await pumpWizard(tester); // -> backup & sync
+ expect(find.text('Backups & Sync'), findsOneWidget);
+
+ await tester.tap(find.widgetWithText(FilledButton, 'Next'));
+ await pumpWizard(tester); // -> finish
+ expect(find.text("You're all set"), findsOneWidget);
+ });
+
+ testWidgets('sync pull with no peer library pivots back to the fresh path', (
+ tester,
+ ) async {
+ late ProviderContainer container;
+ final overrides = await getBaseOverrides();
+ await tester.pumpWidget(
+ testApp(
+ overrides: [
+ ...overrides,
+ isApplePlatformProvider.overrideWithValue(false),
+ dropboxConfiguredProvider.overrideWithValue(false),
+ iCloudAvailabilityProvider.overrideWith(
+ (ref) async => ICloudAvailability.unsupported,
+ ),
+ backupOperationProvider.overrideWith((ref) => _FakeBackupOp()),
+ storageConfigNotifierProvider.overrideWith(
+ (ref) => _FakeStorageNotifier(),
+ ),
+ syncInitializerProvider.overrideWithValue(_FakeSyncInit()),
+ syncStateProvider.overrideWith((ref) => _FakeSyncNotifier()),
+ ],
+ child: Builder(
+ builder: (context) {
+ container = ProviderScope.containerOf(context);
+ return const SetupWizardPage(mode: SetupWizardMode.firstRun);
+ },
+ ),
+ ),
+ );
+ await pumpWizard(tester);
+
+ await tester.tap(find.text('I have existing Submersion data'));
+ await pumpWizard(tester);
+ await tester.tap(find.text('Connect cloud sync'));
+ await pumpWizard(tester);
+
+ // Simulate a completed connect, then pull: no peer library is found.
+ container
+ .read(setupWizardProvider(SetupWizardMode.firstRun).notifier)
+ .setConnectedProvider(CloudProviderType.s3);
+ await pumpWizard(tester);
+ await tester.tap(find.widgetWithText(FilledButton, 'Continue'));
+ await pumpWizard(tester);
+ expect(find.text('No library found'), findsOneWidget);
+
+ // Starting fresh pivots the wizard back to the profile step.
+ await tester.tap(find.widgetWithText(FilledButton, 'Start fresh'));
+ await pumpWizard(tester);
+ expect(find.text('Create Your Profile'), findsOneWidget);
+ });
+}
diff --git a/test/features/setup_wizard/presentation/providers/setup_wizard_providers_test.dart b/test/features/setup_wizard/presentation/providers/setup_wizard_providers_test.dart
new file mode 100644
index 000000000..b815df7c4
--- /dev/null
+++ b/test/features/setup_wizard/presentation/providers/setup_wizard_providers_test.dart
@@ -0,0 +1,76 @@
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:submersion/core/data/repositories/sync_repository.dart';
+import 'package:submersion/features/settings/presentation/providers/settings_providers.dart';
+import 'package:submersion/features/setup_wizard/domain/setup_wizard_models.dart';
+import 'package:submersion/features/setup_wizard/presentation/providers/setup_wizard_providers.dart';
+
+void main() {
+ ProviderContainer makeContainer() {
+ final container = ProviderContainer();
+ addTearDown(container.dispose);
+ return container;
+ }
+
+ test('starts undecided in first-run mode, seeded from defaults', () {
+ final container = makeContainer();
+ final draft = container.read(setupWizardProvider(SetupWizardMode.firstRun));
+ expect(draft.path, SetupPath.undecided);
+ expect(draft.settings, const AppSettings());
+ });
+
+ test('mutators update the draft', () {
+ final container = makeContainer();
+ final notifier = container.read(
+ setupWizardProvider(SetupWizardMode.firstRun).notifier,
+ );
+ notifier.choosePath(SetupPath.fresh);
+ notifier.setName(' Eric ');
+ notifier.applyUnitPreset(UnitPreset.imperial);
+ notifier.setBackupEnabled(true);
+
+ final draft = container.read(setupWizardProvider(SetupWizardMode.firstRun));
+ expect(draft.path, SetupPath.fresh);
+ expect(draft.name, 'Eric');
+ expect(draft.settings.unitPreset, UnitPreset.imperial);
+ expect(draft.backupEnabled, isTrue);
+ expect(computeSteps(draft), contains(SetupStepId.backupSync));
+ });
+
+ test('setConnectedProvider(null) clears a previous connection', () {
+ final container = makeContainer();
+ final notifier = container.read(
+ setupWizardProvider(SetupWizardMode.firstRun).notifier,
+ );
+ notifier.setConnectedProvider(CloudProviderType.s3);
+ expect(
+ container
+ .read(setupWizardProvider(SetupWizardMode.firstRun))
+ .connectedProvider,
+ CloudProviderType.s3,
+ );
+ notifier.setConnectedProvider(null);
+ expect(
+ container
+ .read(setupWizardProvider(SetupWizardMode.firstRun))
+ .connectedProvider,
+ isNull,
+ );
+ });
+
+ test('requestSkip trims the fresh path', () {
+ final container = makeContainer();
+ final notifier = container.read(
+ setupWizardProvider(SetupWizardMode.firstRun).notifier,
+ );
+ notifier.requestSkip();
+ final draft = container.read(setupWizardProvider(SetupWizardMode.firstRun));
+ expect(draft.path, SetupPath.fresh);
+ expect(draft.skipRequested, isTrue);
+ expect(computeSteps(draft), [
+ SetupStepId.welcomeFork,
+ SetupStepId.profile,
+ SetupStepId.finish,
+ ]);
+ });
+}
diff --git a/test/features/setup_wizard/presentation/widgets/steps/backup_sync_step_test.dart b/test/features/setup_wizard/presentation/widgets/steps/backup_sync_step_test.dart
new file mode 100644
index 000000000..2fa94342f
--- /dev/null
+++ b/test/features/setup_wizard/presentation/widgets/steps/backup_sync_step_test.dart
@@ -0,0 +1,261 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:flutter_riverpod/legacy.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:submersion/core/data/repositories/sync_repository.dart'
+ show CloudProviderType;
+import 'package:submersion/core/services/cloud_storage/icloud_native_service.dart';
+import 'package:submersion/core/services/sync/sync_initializer.dart';
+import 'package:submersion/features/backup/domain/entities/backup_settings.dart';
+import 'package:submersion/features/settings/presentation/pages/s3_config_page.dart';
+import 'package:submersion/features/settings/presentation/providers/sync_providers.dart';
+import 'package:submersion/features/setup_wizard/domain/setup_wizard_models.dart';
+import 'package:submersion/features/setup_wizard/presentation/providers/setup_wizard_providers.dart';
+import 'package:submersion/features/setup_wizard/presentation/widgets/steps/backup_sync_step.dart';
+
+import '../../../../../helpers/mock_providers.dart';
+import '../../../../../helpers/test_app.dart';
+
+class _FakeSyncInit implements SyncInitializer {
+ @override
+ Future saveProvider(CloudProviderType? provider) async {}
+
+ @override
+ dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
+}
+
+class _FakeSyncNotifier extends StateNotifier
+ implements SyncNotifier {
+ _FakeSyncNotifier() : super(const SyncState());
+
+ @override
+ Future refreshState() async {}
+
+ @override
+ dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
+}
+
+void main() {
+ testWidgets('backup toggle reveals frequency and updates draft', (
+ tester,
+ ) async {
+ late ProviderContainer container;
+ final overrides = await getBaseOverrides();
+ await tester.pumpWidget(
+ testApp(
+ overrides: [
+ ...overrides,
+ // Deterministic gates: not Apple, no Dropbox key.
+ isApplePlatformProvider.overrideWithValue(false),
+ dropboxConfiguredProvider.overrideWithValue(false),
+ ],
+ child: Builder(
+ builder: (context) {
+ container = ProviderScope.containerOf(context);
+ return const BackupSyncStep(mode: SetupWizardMode.firstRun);
+ },
+ ),
+ ),
+ );
+ await tester.pumpAndSettle();
+
+ expect(find.text('Frequency'), findsNothing);
+ await tester.tap(find.text('Automatic backups'));
+ await tester.pumpAndSettle();
+
+ expect(find.text('Frequency'), findsOneWidget);
+ await tester.tap(find.text('Daily'));
+ await tester.pumpAndSettle();
+
+ final draft = container.read(setupWizardProvider(SetupWizardMode.firstRun));
+ expect(draft.backupEnabled, isTrue);
+ expect(draft.backupFrequency, BackupFrequency.daily);
+ });
+
+ testWidgets('non-Apple platform without Dropbox shows S3 card only', (
+ tester,
+ ) async {
+ final overrides = await getBaseOverrides();
+ await tester.pumpWidget(
+ testApp(
+ overrides: [
+ ...overrides,
+ isApplePlatformProvider.overrideWithValue(false),
+ dropboxConfiguredProvider.overrideWithValue(false),
+ ],
+ child: const BackupSyncStep(mode: SetupWizardMode.firstRun),
+ ),
+ );
+ await tester.pumpAndSettle();
+
+ expect(find.text('iCloud'), findsNothing);
+ expect(find.text('Dropbox'), findsNothing);
+ expect(find.text('S3'), findsOneWidget);
+ expect(find.text('Not connected'), findsOneWidget);
+ });
+
+ testWidgets('tapping S3 opens the config page via the root navigator', (
+ tester,
+ ) async {
+ // testApp provides a plain Navigator but NO GoRouter. The pre-fix
+ // context.push would throw here; the fix uses the root Navigator so the
+ // config page opens without going through the onboarding redirect.
+ final overrides = await getBaseOverrides();
+ await tester.pumpWidget(
+ testApp(
+ overrides: [
+ ...overrides,
+ isApplePlatformProvider.overrideWithValue(false),
+ dropboxConfiguredProvider.overrideWithValue(false),
+ // Simulate S3 having been activated on the config page so the
+ // post-return handler records it in the draft.
+ selectedCloudProviderTypeProvider.overrideWith(
+ (ref) => CloudProviderType.s3,
+ ),
+ ],
+ child: const BackupSyncStep(mode: SetupWizardMode.firstRun),
+ ),
+ );
+ await tester.pumpAndSettle();
+
+ await tester.tap(find.text('S3'));
+ await tester.pumpAndSettle();
+ expect(find.byType(S3ConfigPage), findsOneWidget);
+
+ // Close the config page; the step reads the active provider and reflects
+ // it as connected.
+ await tester.pageBack();
+ await tester.pumpAndSettle();
+ expect(find.text('Connected to S3'), findsOneWidget);
+ });
+
+ const connectedNames = {
+ CloudProviderType.icloud: 'iCloud',
+ CloudProviderType.dropbox: 'Dropbox',
+ CloudProviderType.s3: 'S3',
+ CloudProviderType.googledrive: 'Google Drive',
+ };
+ for (final entry in connectedNames.entries) {
+ testWidgets('settings mode shows connected UI for ${entry.value}', (
+ tester,
+ ) async {
+ final overrides = await getBaseOverrides();
+ await tester.pumpWidget(
+ testApp(
+ overrides: [
+ ...overrides,
+ isApplePlatformProvider.overrideWithValue(false),
+ dropboxConfiguredProvider.overrideWithValue(false),
+ selectedCloudProviderTypeProvider.overrideWith((ref) => entry.key),
+ ],
+ child: const BackupSyncStep(mode: SetupWizardMode.settings),
+ ),
+ );
+ await tester.pumpAndSettle();
+
+ expect(find.text('Connected to ${entry.value}'), findsOneWidget);
+ expect(find.text('Manage in Settings'), findsOneWidget);
+
+ // Toggling the cloud-copy switch drives setCloudBackupEnabled.
+ await tester.tap(find.text('Keep a backup copy in the cloud'));
+ await tester.pumpAndSettle();
+ });
+ }
+
+ testWidgets('first run can disconnect and return to the provider cards', (
+ tester,
+ ) async {
+ late ProviderContainer container;
+ final overrides = await getBaseOverrides();
+ await tester.pumpWidget(
+ testApp(
+ overrides: [
+ ...overrides,
+ isApplePlatformProvider.overrideWithValue(false),
+ dropboxConfiguredProvider.overrideWithValue(false),
+ syncInitializerProvider.overrideWithValue(_FakeSyncInit()),
+ syncStateProvider.overrideWith((ref) => _FakeSyncNotifier()),
+ ],
+ child: Builder(
+ builder: (context) {
+ container = ProviderScope.containerOf(context);
+ return const BackupSyncStep(mode: SetupWizardMode.firstRun);
+ },
+ ),
+ ),
+ );
+ await tester.pumpAndSettle();
+
+ // Simulate a connected provider (e.g. iCloud connected, then a failed
+ // pull left the user here). The cards are hidden.
+ container
+ .read(setupWizardProvider(SetupWizardMode.firstRun).notifier)
+ .setConnectedProvider(CloudProviderType.icloud);
+ await tester.pumpAndSettle();
+ expect(find.text('Connected to iCloud'), findsOneWidget);
+ expect(find.text('S3'), findsNothing);
+
+ // Change provider clears the connection and restores the cards.
+ await tester.tap(find.text('Change provider'));
+ await tester.pumpAndSettle();
+ expect(find.text('Connected to iCloud'), findsNothing);
+ expect(find.text('S3'), findsOneWidget);
+ expect(
+ container
+ .read(setupWizardProvider(SetupWizardMode.firstRun))
+ .connectedProvider,
+ isNull,
+ );
+ });
+
+ testWidgets('iCloud card renders when the platform supports it', (
+ tester,
+ ) async {
+ final overrides = await getBaseOverrides();
+ await tester.pumpWidget(
+ testApp(
+ overrides: [
+ ...overrides,
+ isApplePlatformProvider.overrideWithValue(true),
+ iCloudAvailabilityProvider.overrideWith(
+ (ref) async => ICloudAvailability.available,
+ ),
+ dropboxConfiguredProvider.overrideWithValue(false),
+ ],
+ child: const BackupSyncStep(mode: SetupWizardMode.firstRun),
+ ),
+ );
+ await tester.pumpAndSettle();
+
+ expect(find.text('iCloud'), findsOneWidget);
+ expect(find.text('S3'), findsOneWidget);
+ });
+
+ testWidgets('unsupported iCloud on Apple shows a disabled explanation', (
+ tester,
+ ) async {
+ final overrides = await getBaseOverrides();
+ await tester.pumpWidget(
+ testApp(
+ overrides: [
+ ...overrides,
+ isApplePlatformProvider.overrideWithValue(true),
+ iCloudAvailabilityProvider.overrideWith(
+ (ref) async => ICloudAvailability.unsupported,
+ ),
+ dropboxConfiguredProvider.overrideWithValue(false),
+ ],
+ child: const BackupSyncStep(mode: SetupWizardMode.firstRun),
+ ),
+ );
+ await tester.pumpAndSettle();
+
+ expect(find.text('iCloud'), findsOneWidget);
+ expect(find.text('iCloud is not available on this device'), findsOneWidget);
+ // The disabled tile is not tappable.
+ final tile = tester.widget(
+ find.ancestor(of: find.text('iCloud'), matching: find.byType(ListTile)),
+ );
+ expect(tile.enabled, isFalse);
+ });
+}
diff --git a/test/features/setup_wizard/presentation/widgets/steps/existing_data_steps_test.dart b/test/features/setup_wizard/presentation/widgets/steps/existing_data_steps_test.dart
new file mode 100644
index 000000000..eb01e9215
--- /dev/null
+++ b/test/features/setup_wizard/presentation/widgets/steps/existing_data_steps_test.dart
@@ -0,0 +1,40 @@
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:submersion/features/setup_wizard/domain/setup_wizard_models.dart';
+import 'package:submersion/features/setup_wizard/presentation/providers/setup_wizard_providers.dart';
+import 'package:submersion/features/setup_wizard/presentation/widgets/steps/existing_choice_step.dart';
+
+import '../../../../../helpers/mock_providers.dart';
+import '../../../../../helpers/test_app.dart';
+
+void main() {
+ testWidgets('existing choice cards set the draft source', (tester) async {
+ late ProviderContainer container;
+ final overrides = await getBaseOverrides();
+ var advanced = 0;
+ await tester.pumpWidget(
+ testApp(
+ overrides: overrides,
+ child: Builder(
+ builder: (context) {
+ container = ProviderScope.containerOf(context);
+ return ExistingChoiceStep(
+ mode: SetupWizardMode.firstRun,
+ onChosen: () => advanced++,
+ );
+ },
+ ),
+ ),
+ );
+ await tester.pumpAndSettle();
+
+ expect(find.text('Bring your data'), findsOneWidget);
+ await tester.tap(find.text('Restore a backup file'));
+ await tester.pumpAndSettle();
+
+ final draft = container.read(setupWizardProvider(SetupWizardMode.firstRun));
+ expect(draft.source, ExistingDataSource.restoreBackup);
+ expect(advanced, 1);
+ });
+}
diff --git a/test/features/setup_wizard/presentation/widgets/steps/finish_routes_test.dart b/test/features/setup_wizard/presentation/widgets/steps/finish_routes_test.dart
new file mode 100644
index 000000000..36d0614c0
--- /dev/null
+++ b/test/features/setup_wizard/presentation/widgets/steps/finish_routes_test.dart
@@ -0,0 +1,53 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'package:go_router/go_router.dart';
+import 'package:submersion/core/providers/provider.dart';
+import 'package:submersion/core/router/app_router.dart';
+import 'package:submersion/features/divers/presentation/providers/diver_providers.dart';
+import 'package:submersion/features/setup_wizard/presentation/widgets/steps/finish_step.dart';
+
+import '../../../../../helpers/mock_providers.dart';
+
+/// Collects absolute paths from the route tree, resolving relative child
+/// paths against their parent (go_router stores them relative).
+Set _absolutePaths(List routes, [String prefix = '']) {
+ final out = {};
+ for (final route in routes) {
+ if (route is GoRoute) {
+ final full = route.path.startsWith('/')
+ ? route.path
+ : '$prefix/${route.path}';
+ out.add(full);
+ out.addAll(_absolutePaths(route.routes, full));
+ } else if (route is ShellRoute) {
+ out.addAll(_absolutePaths(route.routes, prefix));
+ }
+ }
+ return out;
+}
+
+void main() {
+ test(
+ 'every finish-screen feature route resolves in the app router',
+ () async {
+ final overrides = await getBaseOverrides();
+ final container = ProviderContainer(
+ overrides: [
+ ...overrides,
+ hasAnyDiversProvider.overrideWith((ref) async => true),
+ ],
+ );
+ addTearDown(container.dispose);
+
+ final router = container.read(appRouterProvider);
+ final paths = _absolutePaths(router.configuration.routes);
+
+ for (final route in kSetupFinishFeatureRoutes) {
+ expect(
+ paths,
+ contains(route),
+ reason: 'Finish-screen link "$route" has no matching route',
+ );
+ }
+ },
+ );
+}
diff --git a/test/features/setup_wizard/presentation/widgets/steps/finish_step_test.dart b/test/features/setup_wizard/presentation/widgets/steps/finish_step_test.dart
new file mode 100644
index 000000000..71bbd9171
--- /dev/null
+++ b/test/features/setup_wizard/presentation/widgets/steps/finish_step_test.dart
@@ -0,0 +1,136 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:go_router/go_router.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+import 'package:submersion/features/divers/data/repositories/diver_repository.dart';
+import 'package:submersion/features/settings/presentation/providers/settings_providers.dart';
+import 'package:submersion/features/setup_wizard/domain/setup_wizard_models.dart';
+import 'package:submersion/features/setup_wizard/presentation/providers/setup_wizard_providers.dart';
+import 'package:submersion/features/setup_wizard/presentation/widgets/steps/finish_step.dart';
+
+import '../../../../../helpers/test_app.dart';
+import '../../../../../helpers/test_database.dart';
+
+void main() {
+ late SharedPreferences prefs;
+
+ setUp(() async {
+ SharedPreferences.setMockInitialValues({});
+ prefs = await SharedPreferences.getInstance();
+ await setUpTestDatabase();
+ });
+
+ tearDown(() async {
+ await tearDownTestDatabase();
+ });
+
+ testWidgets('start logging applies draft and navigates to dashboard', (
+ tester,
+ ) async {
+ var dashboardShown = false;
+ var seeded = false;
+ final router = GoRouter(
+ initialLocation: '/wizard',
+ routes: [
+ GoRoute(
+ path: '/wizard',
+ builder: (context, state) => Consumer(
+ builder: (context, ref, _) {
+ // Watch keeps the autoDispose family instance alive for the
+ // whole test (in production the wizard shell watches it).
+ ref.watch(setupWizardProvider(SetupWizardMode.firstRun));
+ // Seed a completed fresh-path draft after this build frame
+ // (mutating providers during build is forbidden). Once only:
+ // the mutation rebuilds this watcher, which must not reseed.
+ if (!seeded) {
+ seeded = true;
+ Future.microtask(() {
+ final notifier = ref.read(
+ setupWizardProvider(SetupWizardMode.firstRun).notifier,
+ );
+ notifier.choosePath(SetupPath.fresh);
+ notifier.setName('Eric');
+ });
+ }
+ return const Scaffold(
+ body: FinishStep(mode: SetupWizardMode.firstRun),
+ );
+ },
+ ),
+ ),
+ GoRoute(
+ path: '/dashboard',
+ builder: (context, state) {
+ dashboardShown = true;
+ return const Scaffold(body: Text('dashboard'));
+ },
+ ),
+ ],
+ );
+
+ await tester.pumpWidget(
+ testAppRouter(
+ router: router,
+ overrides: [sharedPreferencesProvider.overrideWithValue(prefs)],
+ ),
+ );
+ await tester.pumpAndSettle();
+
+ expect(find.text("You're all set"), findsOneWidget);
+ expect(find.text('Download dives from your dive computer'), findsOneWidget);
+
+ await tester.tap(find.text('Start logging'));
+ await tester.runAsync(() async {
+ // Apply performs real DB writes; let them settle outside fake time.
+ await Future.delayed(const Duration(milliseconds: 300));
+ });
+ await tester.pumpAndSettle();
+
+ expect(dashboardShown, isTrue);
+ final divers = await tester.runAsync(
+ () => DiverRepository().getAllDivers(),
+ );
+ expect(divers, hasLength(1));
+ expect(divers!.single.name, 'Eric');
+ });
+
+ testWidgets('apply failure surfaces an error and re-enables the button', (
+ tester,
+ ) async {
+ var seeded = false;
+ await tester.pumpWidget(
+ testApp(
+ overrides: [sharedPreferencesProvider.overrideWithValue(prefs)],
+ child: Consumer(
+ builder: (context, ref, _) {
+ ref.watch(setupWizardProvider(SetupWizardMode.firstRun));
+ if (!seeded) {
+ seeded = true;
+ // Fresh path but no name -> applyFirstRun throws ArgumentError.
+ Future.microtask(
+ () => ref
+ .read(
+ setupWizardProvider(SetupWizardMode.firstRun).notifier,
+ )
+ .choosePath(SetupPath.fresh),
+ );
+ }
+ return const FinishStep(mode: SetupWizardMode.firstRun);
+ },
+ ),
+ ),
+ );
+ await tester.pumpAndSettle();
+
+ await tester.tap(find.text('Start logging'));
+ await tester.pumpAndSettle();
+
+ expect(find.textContaining('Could not complete setup'), findsOneWidget);
+ // Button re-enabled so the user can retry.
+ final button = tester.widget(
+ find.widgetWithText(FilledButton, 'Start logging'),
+ );
+ expect(button.onPressed, isNotNull);
+ });
+}
diff --git a/test/features/setup_wizard/presentation/widgets/steps/open_folder_step_test.dart b/test/features/setup_wizard/presentation/widgets/steps/open_folder_step_test.dart
new file mode 100644
index 000000000..a0af7950d
--- /dev/null
+++ b/test/features/setup_wizard/presentation/widgets/steps/open_folder_step_test.dart
@@ -0,0 +1,112 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:submersion/core/domain/entities/storage_config.dart';
+import 'package:submersion/core/providers/provider.dart';
+import 'package:submersion/core/services/database_location_service.dart';
+import 'package:submersion/core/services/database_migration_service.dart';
+import 'package:submersion/features/settings/presentation/providers/storage_providers.dart';
+import 'package:submersion/features/setup_wizard/presentation/widgets/steps/open_folder_step.dart';
+
+import '../../../../../helpers/test_app.dart';
+
+/// Minimal fake so the step's folder-adoption flow can be driven without a
+/// real picker or database. Only the three methods the step calls are
+/// implemented; everything else is unused.
+class _FakeStorageNotifier extends StateNotifier
+ implements StorageConfigNotifier {
+ _FakeStorageNotifier()
+ : super(const StorageConfigState(config: StorageConfig()));
+
+ FolderPickResultWithBookmark? pickResult;
+ ExistingDatabaseInfo? existingInfo;
+ MigrationResult? switchResult;
+
+ @override
+ Future pickCustomFolder({
+ Object? chooser,
+ }) async => pickResult;
+
+ @override
+ Future checkForExistingDatabase(
+ String folderPath,
+ ) async => existingInfo;
+
+ @override
+ Future switchToExistingDatabase(String folderPath) async =>
+ switchResult!;
+
+ @override
+ dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
+}
+
+const _existing = ExistingDatabaseInfo(
+ path: '/tmp/x/submersion.db',
+ userCount: 1,
+ diveCount: 5,
+ siteCount: 2,
+ tripCount: 0,
+ buddyCount: 0,
+ fileSize: 1024,
+);
+
+void main() {
+ late _FakeStorageNotifier fake;
+
+ setUp(() => fake = _FakeStorageNotifier());
+
+ Widget build() => testApp(
+ overrides: [storageConfigNotifierProvider.overrideWith((ref) => fake)],
+ child: const OpenFolderStep(),
+ );
+
+ testWidgets('idle shows the choose-folder button', (tester) async {
+ await tester.pumpWidget(build());
+ expect(find.text('Choose folder'), findsOneWidget);
+ });
+
+ testWidgets('cancelled folder pick returns to idle', (tester) async {
+ fake.pickResult = null;
+ await tester.pumpWidget(build());
+ await tester.tap(find.text('Choose folder'));
+ await tester.pumpAndSettle();
+ expect(find.text('Choose folder'), findsOneWidget);
+ });
+
+ testWidgets('folder without a database shows the not-found message', (
+ tester,
+ ) async {
+ fake.pickResult = const FolderPickResultWithBookmark(path: '/tmp/x');
+ fake.existingInfo = null;
+ await tester.pumpWidget(build());
+ await tester.tap(find.text('Choose folder'));
+ await tester.pumpAndSettle();
+ expect(
+ find.text('The selected folder does not contain a Submersion database.'),
+ findsOneWidget,
+ );
+ });
+
+ testWidgets('existing database + success surfaces no error', (tester) async {
+ fake.pickResult = const FolderPickResultWithBookmark(path: '/tmp/x');
+ fake.existingInfo = _existing;
+ fake.switchResult = MigrationResult.success(
+ oldPath: '/a',
+ newPath: '/tmp/x',
+ );
+ await tester.pumpWidget(build());
+ await tester.tap(find.text('Choose folder'));
+ await tester.pumpAndSettle();
+ // restartApp() was invoked (harmless ValueNotifier bump); no error shown.
+ expect(find.textContaining('does not contain'), findsNothing);
+ });
+
+ testWidgets('switch failure shows the error message', (tester) async {
+ fake.pickResult = const FolderPickResultWithBookmark(path: '/tmp/x');
+ fake.existingInfo = _existing;
+ fake.switchResult = MigrationResult.failure('disk error');
+ await tester.pumpWidget(build());
+ await tester.tap(find.text('Choose folder'));
+ await tester.pumpAndSettle();
+ expect(find.text('disk error'), findsOneWidget);
+ });
+}
diff --git a/test/features/setup_wizard/presentation/widgets/steps/restore_step_test.dart b/test/features/setup_wizard/presentation/widgets/steps/restore_step_test.dart
new file mode 100644
index 000000000..1e10408f7
--- /dev/null
+++ b/test/features/setup_wizard/presentation/widgets/steps/restore_step_test.dart
@@ -0,0 +1,121 @@
+import 'dart:io';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:submersion/core/providers/provider.dart';
+import 'package:submersion/features/backup/presentation/pages/restore_complete_page.dart';
+import 'package:submersion/features/backup/presentation/providers/backup_providers.dart';
+import 'package:submersion/features/backup/presentation/widgets/restore_confirmation_dialog.dart';
+import 'package:submersion/features/setup_wizard/presentation/widgets/steps/restore_step.dart';
+
+import '../../../../../helpers/test_app.dart';
+
+class _FakeBackupOp extends StateNotifier
+ implements BackupOperationNotifier {
+ _FakeBackupOp(super.state);
+
+ void push(BackupOperationState next) => state = next;
+
+ @override
+ dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
+}
+
+void main() {
+ Widget build(_FakeBackupOp fake) => testApp(
+ overrides: [backupOperationProvider.overrideWith((ref) => fake)],
+ child: const RestoreStep(),
+ );
+
+ testWidgets('idle shows the choose-file button', (tester) async {
+ await tester.pumpWidget(build(_FakeBackupOp(const BackupOperationState())));
+ expect(find.text('Choose backup file'), findsOneWidget);
+ });
+
+ testWidgets('cancelled picker returns to idle without crashing', (
+ tester,
+ ) async {
+ await tester.pumpWidget(build(_FakeBackupOp(const BackupOperationState())));
+ // FilePicker returns no selection under test; the step must handle it.
+ await tester.tap(find.text('Choose backup file'));
+ await tester.pumpAndSettle();
+ expect(find.text('Restore backup'), findsOneWidget);
+ expect(find.text('Choose backup file'), findsOneWidget);
+ });
+
+ testWidgets('in-progress shows a spinner and message', (tester) async {
+ await tester.pumpWidget(
+ build(
+ _FakeBackupOp(
+ const BackupOperationState(
+ status: BackupOperationStatus.inProgress,
+ message: 'Restoring backup...',
+ ),
+ ),
+ ),
+ );
+ expect(find.byType(CircularProgressIndicator), findsOneWidget);
+ expect(find.text('Restoring backup...'), findsOneWidget);
+ expect(find.text('Choose backup file'), findsNothing);
+ });
+
+ testWidgets('error state shows the message and keeps the button', (
+ tester,
+ ) async {
+ await tester.pumpWidget(
+ build(
+ _FakeBackupOp(
+ const BackupOperationState(
+ status: BackupOperationStatus.error,
+ message: 'Restore failed: bad file',
+ ),
+ ),
+ ),
+ );
+ expect(find.text('Restore failed: bad file'), findsOneWidget);
+ expect(find.text('Choose backup file'), findsOneWidget);
+ });
+
+ testWidgets('picking a file opens the restore confirmation dialog', (
+ tester,
+ ) async {
+ final dir = Directory.systemTemp.createTempSync('restore_step_test');
+ addTearDown(() => dir.deleteSync(recursive: true));
+ final backup = File('${dir.path}/backup.db')..writeAsBytesSync([1, 2, 3]);
+
+ await tester.pumpWidget(
+ testApp(
+ overrides: [
+ backupOperationProvider.overrideWith(
+ (ref) => _FakeBackupOp(const BackupOperationState()),
+ ),
+ ],
+ child: RestoreStep(
+ pickBackupFile: () async => (path: backup.path, name: 'backup.db'),
+ ),
+ ),
+ );
+
+ await tester.runAsync(() async {
+ await tester.tap(find.text('Choose backup file'));
+ await Future.delayed(const Duration(milliseconds: 50));
+ });
+ await tester.pumpAndSettle();
+
+ expect(find.byType(RestoreConfirmationDialog), findsOneWidget);
+ });
+
+ testWidgets('restoreComplete transition shows the completion page', (
+ tester,
+ ) async {
+ final fake = _FakeBackupOp(const BackupOperationState());
+ await tester.pumpWidget(build(fake));
+ await tester.pumpAndSettle();
+
+ fake.push(
+ const BackupOperationState(status: BackupOperationStatus.restoreComplete),
+ );
+ await tester.pumpAndSettle();
+
+ expect(find.byType(RestoreCompletePage), findsOneWidget);
+ });
+}
diff --git a/test/features/setup_wizard/presentation/widgets/steps/sync_connect_step_test.dart b/test/features/setup_wizard/presentation/widgets/steps/sync_connect_step_test.dart
new file mode 100644
index 000000000..69ed418ce
--- /dev/null
+++ b/test/features/setup_wizard/presentation/widgets/steps/sync_connect_step_test.dart
@@ -0,0 +1,313 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:go_router/go_router.dart';
+import 'package:submersion/core/data/repositories/sync_repository.dart'
+ show CloudProviderType;
+import 'package:submersion/core/providers/provider.dart';
+import 'package:submersion/core/services/cloud_storage/cloud_storage_provider.dart';
+import 'package:submersion/core/services/cloud_storage/icloud_native_service.dart';
+import 'package:submersion/core/services/sync/sync_initializer.dart';
+import 'package:submersion/features/divers/presentation/providers/diver_providers.dart';
+import 'package:submersion/features/settings/presentation/providers/sync_providers.dart';
+import 'package:submersion/features/setup_wizard/domain/setup_wizard_models.dart';
+import 'package:submersion/features/setup_wizard/presentation/providers/setup_wizard_providers.dart';
+import 'package:submersion/features/setup_wizard/presentation/widgets/steps/sync_connect_step.dart';
+
+import '../../../../../helpers/mock_providers.dart';
+import '../../../../../helpers/test_app.dart';
+import '../../../../../helpers/test_database.dart';
+
+class _FakeSyncInit implements SyncInitializer {
+ List peers = [];
+ bool throwOnPeers = false;
+
+ @override
+ Future> peerSyncFiles(
+ CloudStorageProvider provider,
+ ) async {
+ if (throwOnPeers) throw Exception('network down');
+ return peers;
+ }
+
+ @override
+ dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
+}
+
+class _FakeSyncNotifier extends StateNotifier
+ implements SyncNotifier {
+ _FakeSyncNotifier({this.failSync = false}) : super(const SyncState());
+
+ final bool failSync;
+
+ @override
+ Future performSync({bool auto = false}) async {
+ if (failSync) {
+ state = const SyncState(status: SyncStatus.error, message: 'sync boom');
+ }
+ }
+
+ @override
+ dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
+}
+
+void main() {
+ late _FakeSyncInit syncInit;
+
+ setUp(() async {
+ syncInit = _FakeSyncInit();
+ await setUpTestDatabase();
+ });
+
+ tearDown(() async => tearDownTestDatabase());
+
+ Future> overrides({
+ bool hasDivers = true,
+ bool failSync = false,
+ }) async {
+ final base = await getBaseOverrides();
+ return [
+ ...base,
+ isApplePlatformProvider.overrideWithValue(false),
+ dropboxConfiguredProvider.overrideWithValue(false),
+ iCloudAvailabilityProvider.overrideWith(
+ (ref) async => ICloudAvailability.unsupported,
+ ),
+ syncInitializerProvider.overrideWithValue(syncInit),
+ syncStateProvider.overrideWith(
+ (ref) => _FakeSyncNotifier(failSync: failSync),
+ ),
+ hasAnyDiversProvider.overrideWith((ref) async => hasDivers),
+ ];
+ }
+
+ testWidgets('connect phase gates Continue on a connected provider', (
+ tester,
+ ) async {
+ late ProviderContainer container;
+ await tester.pumpWidget(
+ testApp(
+ overrides: await overrides(),
+ child: Builder(
+ builder: (context) {
+ container = ProviderScope.containerOf(context);
+ return SyncConnectStep(
+ mode: SetupWizardMode.firstRun,
+ onNoLibrary: () {},
+ );
+ },
+ ),
+ ),
+ );
+ await tester.pumpAndSettle();
+
+ final continueButton = find.widgetWithText(FilledButton, 'Continue');
+ expect(tester.widget(continueButton).onPressed, isNull);
+
+ container
+ .read(setupWizardProvider(SetupWizardMode.firstRun).notifier)
+ .setConnectedProvider(CloudProviderType.s3);
+ await tester.pumpAndSettle();
+ expect(tester.widget(continueButton).onPressed, isNotNull);
+ });
+
+ testWidgets('no peer library shows the no-library screen and pivots fresh', (
+ tester,
+ ) async {
+ syncInit.peers = [];
+ var pivoted = 0;
+ late ProviderContainer container;
+ await tester.pumpWidget(
+ testApp(
+ overrides: await overrides(),
+ child: Builder(
+ builder: (context) {
+ container = ProviderScope.containerOf(context);
+ return SyncConnectStep(
+ mode: SetupWizardMode.firstRun,
+ onNoLibrary: () => pivoted++,
+ );
+ },
+ ),
+ ),
+ );
+ await tester.pumpAndSettle();
+ container
+ .read(setupWizardProvider(SetupWizardMode.firstRun).notifier)
+ .setConnectedProvider(CloudProviderType.s3);
+ await tester.pumpAndSettle();
+
+ await tester.tap(find.widgetWithText(FilledButton, 'Continue'));
+ await tester.pumpAndSettle();
+
+ expect(find.text('No library found'), findsOneWidget);
+ await tester.tap(find.widgetWithText(FilledButton, 'Start fresh'));
+ expect(pivoted, 1);
+ });
+
+ testWidgets('a failing pull returns to the connect UI with an error', (
+ tester,
+ ) async {
+ syncInit.throwOnPeers = true;
+ late ProviderContainer container;
+ await tester.pumpWidget(
+ testApp(
+ overrides: await overrides(),
+ child: Builder(
+ builder: (context) {
+ container = ProviderScope.containerOf(context);
+ return SyncConnectStep(
+ mode: SetupWizardMode.firstRun,
+ onNoLibrary: () {},
+ );
+ },
+ ),
+ ),
+ );
+ await tester.pumpAndSettle();
+ container
+ .read(setupWizardProvider(SetupWizardMode.firstRun).notifier)
+ .setConnectedProvider(CloudProviderType.s3);
+ await tester.pumpAndSettle();
+
+ await tester.tap(find.widgetWithText(FilledButton, 'Continue'));
+ await tester.pumpAndSettle();
+
+ // Error surfaced and the connect UI restored (Continue button present).
+ expect(find.textContaining('Could not connect'), findsOneWidget);
+ expect(find.widgetWithText(FilledButton, 'Continue'), findsOneWidget);
+ });
+
+ testWidgets('a failed sync returns to connect instead of "no library"', (
+ tester,
+ ) async {
+ syncInit.peers = [
+ CloudFileInfo(
+ id: 'a',
+ name: 'peer.manifest.json',
+ modifiedTime: DateTime(2026),
+ ),
+ ];
+ late ProviderContainer container;
+ await tester.pumpWidget(
+ testApp(
+ overrides: await overrides(failSync: true),
+ child: Builder(
+ builder: (context) {
+ container = ProviderScope.containerOf(context);
+ return SyncConnectStep(
+ mode: SetupWizardMode.firstRun,
+ onNoLibrary: () {},
+ );
+ },
+ ),
+ ),
+ );
+ await tester.pumpAndSettle();
+ container
+ .read(setupWizardProvider(SetupWizardMode.firstRun).notifier)
+ .setConnectedProvider(CloudProviderType.s3);
+ await tester.pumpAndSettle();
+
+ await tester.tap(find.widgetWithText(FilledButton, 'Continue'));
+ await tester.pumpAndSettle();
+
+ expect(find.textContaining('Could not connect'), findsOneWidget);
+ expect(find.text('No library found'), findsNothing);
+ expect(find.widgetWithText(FilledButton, 'Continue'), findsOneWidget);
+ });
+
+ testWidgets('peer library pulls and shows the adopted screen', (
+ tester,
+ ) async {
+ syncInit.peers = [
+ CloudFileInfo(
+ id: 'a',
+ name: 'peer.manifest.json',
+ modifiedTime: DateTime(2026),
+ ),
+ ];
+ late ProviderContainer container;
+ await tester.pumpWidget(
+ testApp(
+ overrides: await overrides(hasDivers: true),
+ child: Builder(
+ builder: (context) {
+ container = ProviderScope.containerOf(context);
+ return SyncConnectStep(
+ mode: SetupWizardMode.firstRun,
+ onNoLibrary: () {},
+ );
+ },
+ ),
+ ),
+ );
+ await tester.pumpAndSettle();
+ container
+ .read(setupWizardProvider(SetupWizardMode.firstRun).notifier)
+ .setConnectedProvider(CloudProviderType.s3);
+ await tester.pumpAndSettle();
+
+ await tester.runAsync(() async {
+ await tester.tap(find.widgetWithText(FilledButton, 'Continue'));
+ await Future.delayed(const Duration(milliseconds: 50));
+ });
+ await tester.pumpAndSettle();
+
+ expect(find.text('Library adopted'), findsOneWidget);
+ });
+
+ testWidgets('adopted screen Continue navigates to the dashboard', (
+ tester,
+ ) async {
+ syncInit.peers = [
+ CloudFileInfo(
+ id: 'a',
+ name: 'peer.manifest.json',
+ modifiedTime: DateTime(2026),
+ ),
+ ];
+ var dashboard = false;
+ ProviderContainer? container;
+ final router = GoRouter(
+ initialLocation: '/w',
+ routes: [
+ GoRoute(
+ path: '/w',
+ builder: (context, state) {
+ container = ProviderScope.containerOf(context);
+ return Scaffold(
+ body: SyncConnectStep(
+ mode: SetupWizardMode.firstRun,
+ onNoLibrary: () {},
+ ),
+ );
+ },
+ ),
+ GoRoute(
+ path: '/dashboard',
+ builder: (context, state) {
+ dashboard = true;
+ return const Scaffold(body: Text('dash'));
+ },
+ ),
+ ],
+ );
+ await tester.pumpWidget(
+ testAppRouter(router: router, overrides: await overrides()),
+ );
+ await tester.pumpAndSettle();
+ container!
+ .read(setupWizardProvider(SetupWizardMode.firstRun).notifier)
+ .setConnectedProvider(CloudProviderType.s3);
+ await tester.pumpAndSettle();
+ await tester.runAsync(() async {
+ await tester.tap(find.widgetWithText(FilledButton, 'Continue'));
+ await Future.delayed(const Duration(milliseconds: 50));
+ });
+ await tester.pumpAndSettle();
+
+ await tester.tap(find.widgetWithText(FilledButton, 'Continue'));
+ await tester.pumpAndSettle();
+ expect(dashboard, isTrue);
+ });
+}
diff --git a/test/features/setup_wizard/presentation/widgets/steps/units_step_test.dart b/test/features/setup_wizard/presentation/widgets/steps/units_step_test.dart
new file mode 100644
index 000000000..0d28d0372
--- /dev/null
+++ b/test/features/setup_wizard/presentation/widgets/steps/units_step_test.dart
@@ -0,0 +1,175 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:submersion/core/constants/units.dart';
+import 'package:submersion/features/settings/presentation/providers/settings_providers.dart';
+import 'package:submersion/features/setup_wizard/domain/setup_wizard_models.dart';
+import 'package:submersion/features/setup_wizard/presentation/providers/setup_wizard_providers.dart';
+import 'package:submersion/features/setup_wizard/presentation/widgets/steps/units_step.dart';
+
+import '../../../../../helpers/test_app.dart';
+
+void main() {
+ testWidgets('preset toggle drives the draft units', (tester) async {
+ tester.binding.platformDispatcher.localeTestValue = const Locale(
+ 'de',
+ 'DE',
+ );
+ addTearDown(tester.binding.platformDispatcher.clearLocaleTestValue);
+
+ late ProviderContainer container;
+ await tester.pumpWidget(
+ testApp(
+ // UI pinned to English; the device locale set above only drives
+ // presetForLocale, not the labels this test taps.
+ locale: const Locale('en'),
+ child: Builder(
+ builder: (context) {
+ container = ProviderScope.containerOf(context);
+ return const UnitsStep(mode: SetupWizardMode.firstRun);
+ },
+ ),
+ ),
+ );
+ await tester.pumpAndSettle();
+
+ await tester.tap(find.text('Imperial'));
+ await tester.pumpAndSettle();
+
+ final draft = container.read(setupWizardProvider(SetupWizardMode.firstRun));
+ expect(draft.settings.unitPreset, UnitPreset.imperial);
+ expect(draft.settings.depthUnit, DepthUnit.feet);
+ });
+
+ testWidgets('advanced expander exposes per-unit overrides', (tester) async {
+ tester.binding.platformDispatcher.localeTestValue = const Locale(
+ 'de',
+ 'DE',
+ );
+ addTearDown(tester.binding.platformDispatcher.clearLocaleTestValue);
+
+ late ProviderContainer container;
+ await tester.pumpWidget(
+ testApp(
+ // UI pinned to English; the device locale set above only drives
+ // presetForLocale, not the labels this test taps.
+ locale: const Locale('en'),
+ child: Builder(
+ builder: (context) {
+ container = ProviderScope.containerOf(context);
+ return const UnitsStep(mode: SetupWizardMode.firstRun);
+ },
+ ),
+ ),
+ );
+ await tester.pumpAndSettle();
+
+ await tester.tap(find.text('Fine-tune units'));
+ await tester.pumpAndSettle();
+
+ // Depth row: switch to feet only.
+ await tester.ensureVisible(
+ find.byKey(const ValueKey('setup-unit-depth-ft')),
+ );
+ await tester.tap(find.byKey(const ValueKey('setup-unit-depth-ft')));
+ await tester.pumpAndSettle();
+
+ final draft = container.read(setupWizardProvider(SetupWizardMode.firstRun));
+ expect(draft.settings.depthUnit, DepthUnit.feet);
+ expect(draft.settings.temperatureUnit, TemperatureUnit.celsius);
+ expect(draft.settings.unitPreset, UnitPreset.custom);
+ });
+
+ testWidgets('US device locale preselects imperial once', (tester) async {
+ tester.binding.platformDispatcher.localeTestValue = const Locale(
+ 'en',
+ 'US',
+ );
+ addTearDown(tester.binding.platformDispatcher.clearLocaleTestValue);
+
+ late ProviderContainer container;
+ await tester.pumpWidget(
+ testApp(
+ // UI pinned to English; the device locale set above only drives
+ // presetForLocale, not the labels this test taps.
+ locale: const Locale('en'),
+ child: Builder(
+ builder: (context) {
+ container = ProviderScope.containerOf(context);
+ return const UnitsStep(mode: SetupWizardMode.firstRun);
+ },
+ ),
+ ),
+ );
+ await tester.pumpAndSettle();
+
+ final draft = container.read(setupWizardProvider(SetupWizardMode.firstRun));
+ expect(draft.settings.unitPreset, UnitPreset.imperial);
+ });
+
+ testWidgets('each per-unit control writes to the draft', (tester) async {
+ tester.binding.platformDispatcher.localeTestValue = const Locale(
+ 'de',
+ 'DE',
+ );
+ addTearDown(tester.binding.platformDispatcher.clearLocaleTestValue);
+
+ late ProviderContainer container;
+ await tester.pumpWidget(
+ testApp(
+ locale: const Locale('en'),
+ child: Builder(
+ builder: (context) {
+ container = ProviderScope.containerOf(context);
+ return const UnitsStep(mode: SetupWizardMode.firstRun);
+ },
+ ),
+ ),
+ );
+ await tester.pumpAndSettle();
+ await tester.tap(find.text('Fine-tune units'));
+ await tester.pumpAndSettle();
+
+ Future tapUnit(String key) async {
+ final finder = find.byKey(ValueKey(key));
+ await tester.ensureVisible(finder);
+ await tester.tap(finder);
+ await tester.pumpAndSettle();
+ }
+
+ // Flip each unit row's non-default segment; each fires its onChanged.
+ await tapUnit('setup-unit-depth-ft');
+ await tapUnit('setup-unit-temperature-f');
+ await tapUnit('setup-unit-pressure-psi');
+ await tapUnit('setup-unit-volume-cuft');
+ await tapUnit('setup-unit-weight-lbs');
+ await tapUnit('setup-unit-altitude-ft');
+ await tapUnit('setup-unit-sac-pressuremin');
+
+ final s = container
+ .read(setupWizardProvider(SetupWizardMode.firstRun))
+ .settings;
+ expect(s.depthUnit, DepthUnit.feet);
+ expect(s.temperatureUnit, TemperatureUnit.fahrenheit);
+ expect(s.pressureUnit, PressureUnit.psi);
+ expect(s.volumeUnit, VolumeUnit.cubicFeet);
+ expect(s.weightUnit, WeightUnit.pounds);
+ expect(s.altitudeUnit, AltitudeUnit.feet);
+ expect(s.sacUnit, SacUnit.pressurePerMin);
+
+ // Date-format dropdown fires its onChanged too.
+ final dropdown = find.byType(DropdownButton);
+ await tester.ensureVisible(dropdown);
+ await tester.tap(dropdown);
+ await tester.pumpAndSettle();
+ await tester.tap(find.text('YYYY-MM-DD').last);
+ await tester.pumpAndSettle();
+ expect(
+ container
+ .read(setupWizardProvider(SetupWizardMode.firstRun))
+ .settings
+ .dateFormat,
+ DateFormatPreference.yyyymmdd,
+ );
+ });
+}
diff --git a/test/helpers/test_app.dart b/test/helpers/test_app.dart
index 97d652aee..917f5f632 100644
--- a/test/helpers/test_app.dart
+++ b/test/helpers/test_app.dart
@@ -14,10 +14,18 @@ import 'package:submersion/l10n/arb/app_localizations.dart';
/// Callers should pass the return values of `.overrideWithValue(...)` or
/// `.overrideWith(...)` -- the Riverpod `Override` type is sealed and not
/// re-exported, so we accept `dynamic` here.
-Widget testApp({required Widget child, List? overrides}) {
+/// Pass [locale] to pin the UI language deterministically (defaults to
+/// MaterialApp's platform-locale resolution). Useful when a test drives the
+/// device locale for other reasons but still asserts on English labels.
+Widget testApp({
+ required Widget child,
+ List? overrides,
+ Locale? locale,
+}) {
return ProviderScope(
overrides: overrides?.cast() ?? [],
child: MaterialApp(
+ locale: locale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: Scaffold(body: child),