feat(train): interval plans, synced and sealed, with an editor and templates (ADR 0030, D42)
Train's server gains the synced `plan` table, a sealed payload alone; the migration `plans` only adds it, its space index and the foreign key to its sync space, and a person's deletion takes it with their space. The app keeps plans as `plan` rows sealed for their row: a name, what they are for, whether a step off target is told, and blocks of steps repeated, each ending after a time, a distance or a lap tap and aiming at a pace, speed, heart rate or zone, or the rower's split, watts or stroke rate. Home's Plans tab lists them, a new one starts from nothing or from a template (4×4 minutes, Tabata, 10×400 m), the editor changes steps on a sheet, and a deletion asks first and can be undone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
fa7680821d
commit
f406ddcc59
48 changed files with 23608 additions and 10 deletions
|
|
@ -3,10 +3,10 @@
|
|||
What holds for every app and package of this repository is in the [root AGENTS.md](../../AGENTS.md); it applies here in full, and Learn's patterns (`apps/learn/AGENTS.md`) wherever Train needs the same thing.
|
||||
|
||||
- **A shell on the shared packages**, Todo's copied and renamed (decided 2026-10-28, ADR 0030, D12; what Train is to do: ADR 0030): signing in, the instances, the account and the look work, and since step 5a (28 Oct 2026) a person's workouts are kept and synced with `serverpod_offline_sync`, every row sealed (ADR 0030, D7, D14, D15), and on Android a workout is recorded (5b: ADR 0030, D13, D16, D18 to D21), and since 5c home lists them and each has its page with its map (D22 to D24). Packages: `halkyon_train_server`, `halkyon_train_client` (generated, never edited), `halkyon_train_flutter`; app ID `app.halkyon.train` on every platform; no web build. Licences as Learn's (server SSPL, client and app AGPL; `LICENSE.md`).
|
||||
- **The server** includes `halkyon_base` (ADR 0014) and configures it in `server.dart` (`trainBase()`, `lib/src/train_base.dart`): its basics say it serves `train` (never change it once apps use it), sign-in mails name Halkyon Train, the source is this repository, `minimumAppBuild` 1, and `deleteTrainData` (`lib/src/train_data.dart`) as its `deletePersonData` hook, which deletes the person's sync spaces and so, by the sync engine's cascades, every row of theirs (`test/integration/workout/person_deletion_test.dart`). Authentication through the module's builders: a password, SimpleLogin on the register, email codes proving an address, server-side sessions; no anonymous accounts (ADR 0020, D25), nor GitHub (D36). **Accounts** (ADR 0016): Train holds its own register when it runs alone, and is a member of the instance's register (Learn's server on learn.halkyon.eu) where the operator sets `ACCOUNT_REGISTER_URL` (`trainBase(accounts: AccountsMode.fromEnvironment())`) with the instance's shared `INSTANCE_SECRET` (`instanceSecret`); then sign-in, handles and profiles are the register's, the email settings are the register's alone, and an identified person's deletion goes to the register first. `addInstanceRoutes` serves the calls between the services on its web server, and the server doesn't start without the secret where it needs it. `schedulePruning` (`lib/src/pruning.dart`) keeps the module's pruning scheduled, one entry each however often the server starts (and cancels the one under the pruning's name before ADR 0020, D25): the anonymous accounts left from before daily, sign-up attempts and codes hourly, as the register the deletions its members haven't confirmed hourly, as a member the people the register no longer knows daily (each a no-op in the other mode). Session logs are never persisted, in any run mode. **The Loop's firmware relay** (ADR 0030, D10, D40, `lib/src/firmware/`): the `firmware` endpoint asks Polar's firmware service in the person's place and hands on the file, kept in memory a day per hardware and version (`FirmwareRelay`); on by default, off where the operator sets `LOOP_FIRMWARE_RELAY=off`, the app then offering the direct way alone; tested on a fake Polar (`test/integration/firmware/`). Migrations (2026-10-28): an initial one with Serverpod's tables, its auth modules' and the module's as they stand (Todo's grew one by one with the module; Train starts where they ended), then `workouts`, purely additive: the sync engine's tables and Train's first three, then `profile` and `days` (Train's `day`, `day_series` and `night`), each purely additive. **Synced models** (`database: sync`, the module `offline_sync`, `experimental_features: databaseSync` in `config/generator.yaml`): `content_key` (the person's key sealed to the account), `workout` and `workout_series` (a workout's series in chunks of at most ten minutes, deleted with it), `profile` (the body's figures, one row a person), `day`, `day_series` (a day's 24/7 heart rate in chunks of three hours, deleted with it) and `night` (one row a person and date each, ADR 0030, D37), each a sealed `payload` with its `keyId` and `nonce`, nothing else readable. Tests (`test/integration/train_server_test.dart`) call the module's endpoints through the server's own dispatch (`halkyon_base.<endpoint>`), since the generated test tools expose the project's endpoints alone.
|
||||
- **The server** includes `halkyon_base` (ADR 0014) and configures it in `server.dart` (`trainBase()`, `lib/src/train_base.dart`): its basics say it serves `train` (never change it once apps use it), sign-in mails name Halkyon Train, the source is this repository, `minimumAppBuild` 1, and `deleteTrainData` (`lib/src/train_data.dart`) as its `deletePersonData` hook, which deletes the person's sync spaces and so, by the sync engine's cascades, every row of theirs (`test/integration/workout/person_deletion_test.dart`). Authentication through the module's builders: a password, SimpleLogin on the register, email codes proving an address, server-side sessions; no anonymous accounts (ADR 0020, D25), nor GitHub (D36). **Accounts** (ADR 0016): Train holds its own register when it runs alone, and is a member of the instance's register (Learn's server on learn.halkyon.eu) where the operator sets `ACCOUNT_REGISTER_URL` (`trainBase(accounts: AccountsMode.fromEnvironment())`) with the instance's shared `INSTANCE_SECRET` (`instanceSecret`); then sign-in, handles and profiles are the register's, the email settings are the register's alone, and an identified person's deletion goes to the register first. `addInstanceRoutes` serves the calls between the services on its web server, and the server doesn't start without the secret where it needs it. `schedulePruning` (`lib/src/pruning.dart`) keeps the module's pruning scheduled, one entry each however often the server starts (and cancels the one under the pruning's name before ADR 0020, D25): the anonymous accounts left from before daily, sign-up attempts and codes hourly, as the register the deletions its members haven't confirmed hourly, as a member the people the register no longer knows daily (each a no-op in the other mode). Session logs are never persisted, in any run mode. **The Loop's firmware relay** (ADR 0030, D10, D40, `lib/src/firmware/`): the `firmware` endpoint asks Polar's firmware service in the person's place and hands on the file, kept in memory a day per hardware and version (`FirmwareRelay`); on by default, off where the operator sets `LOOP_FIRMWARE_RELAY=off`, the app then offering the direct way alone; tested on a fake Polar (`test/integration/firmware/`). Migrations (2026-10-28): an initial one with Serverpod's tables, its auth modules' and the module's as they stand (Todo's grew one by one with the module; Train starts where they ended), then `workouts`, purely additive: the sync engine's tables and Train's first three, then `profile`, `days` (Train's `day`, `day_series` and `night`) and `plans` (`plan`, D42), each purely additive. **Synced models** (`database: sync`, the module `offline_sync`, `experimental_features: databaseSync` in `config/generator.yaml`): `content_key` (the person's key sealed to the account), `workout` and `workout_series` (a workout's series in chunks of at most ten minutes, deleted with it), `profile` (the body's figures, one row a person), `day`, `day_series` (a day's 24/7 heart rate in chunks of three hours, deleted with it) and `night` (one row a person and date each, ADR 0030, D37) and `plan` (an interval plan, D42), each a sealed `payload` with its `keyId` and `nonce`, nothing else readable. Tests (`test/integration/train_server_test.dart`) call the module's endpoints through the server's own dispatch (`halkyon_base.<endpoint>`), since the generated test tools expose the project's endpoints alone.
|
||||
- **The app** is halkyon_app's `InstanceGate` and `SessionGate` with Train's frame (`trainFrame`: "Train" under "Halkyon", its icon, `train.example.org`, its own sentence) and `ServerpodInstanceChecker(app: 'train')`; `TrainDependencies` (`AppDependencies<TrainPerson>`) on one instance, `TrainPerson` per person: their own sync database in their folder (`sync.sqlite`, halkyon_app's `PersonFolders`, one person's at a time), halkyon_app's `SyncService` behind `AppCompatibility` (the module's basics' `minimumAppBuild`, against the build `package_info_plus` reads), their key (`TrainKeys`, `lib/src/core/privacy/`: one key per person, IDs `train1.<ms>.<nonce>`, the oldest-made writing, kept in `content_key` and opened only in Train's form, ADR 0030, D15) and their workouts (`WorkoutRepository`, `lib/src/feature/workout/`: halkyon_app's `PayloadEnvelope` seals a workout for its row and a series chunk for its row and its workout; payloads are versioned JSON, a series' columns delta-coded, gzipped before sealing; a deletion blanks every payload first); the gateways' tests run on a sync database opened without a server, the repository's on memory gateways and real libsodium; a person's app whose home lists their workouts (`WorkoutsCubit`, with `TrainPerson.deletions`, halkyon_app's `DelayedDeletions`) and opens each one's page (`WorkoutCubit`: `analyse` in `workout_analysis.dart` reads its series into the track's pieces, splits, elevation and charts, drawn by `workout_chart.dart`; the map with flutter_map on the instance's source, `MapSourceRepository` asking `map.source`, its tiles from `TrainPerson.mapTiles`, vector_map_tiles in the app and none in widget tests), and the account sheet: halkyon_app's `AccountTiles` (whose account on which instance, Switch account, Sign out, Delete account, which asks first) in Train's words, and halkyon_app's About (`showAppAbout`: the source of what the instance runs, from `TrainDependencies.sourceCodeUrl`, and the licence list). Texts through gen-l10n in British English (the template), German (informal du), an empty `en_US` and `en_GB` (`test/l10n/arb_files_test.dart` keeps them in step). The look follows the system's theme mode and language until Train has settings. Countries in where an instance keeps its data are named by halkyon_app's CLDR names (`AssetCountryNames`), its language picked by halkyon_app's `resolveAppLocale`. Widget tests start Train's whole tree with halkyon_app's fakes (`test/app/train_app_test.dart`).
|
||||
- **Recording** (ADR 0030, D13, D16, D18 to D21, D26 to D28) is Android's alone for now (iOS waits for a Mac, D25) (`TrainPerson.recording` null elsewhere): `RecorderService` (Kotlin, `android/app/src/main/kotlin/app/halkyon/train/`), a foreground service of the types `location` and `health` started from the open app, takes fixes from LocationManager's GPS (no Play services), steps from the step detector the heart rate from the strap and a row from the rower the person chose (ADR 0030, D26 to D28: `NotifyLink.kt` on Android's own Bluetooth API, no package, `HeartRateMeasurement.kt` parsing 0x2A37 and `RowerData.kt` FTMS's Rower Data 0x2AD1; `DeviceScanner.kt` finds them for the sheets over `…/recorder/straps` and `…/recorder/rowers`; each remembered in the person's `strap.json` and `rower.json`, `DeviceStore`), decides auto-pause (`AutoPause.kt`) and appends each sample as a line to the person's `recording.txt` (format in `recording_file.dart`), flushed every 5 s; `MainActivity` asks the permissions (never background location) and bridges `app.halkyon.train/recorder` (methods) and `…/recorder/lines` (the file so far, then each line). The app (`lib/src/feature/recording/`) follows those lines with `RecordingBloc`, works out what the screen and the notification show (`liveOf`, the saved totals' functions), and saves a recording as a workout and its series (`recordedWorkout`) or throws it away; one cut off, or stopped and not saved, is found as the person's app opens. Tests: the Dart side with `test/support/fake_recorder_platform.dart`; the Kotlin auto-pause and the strap's and the rower's parsers on the JVM, `JAVA_HOME=<Android Studio's jbr> ./gradlew :app:testDebugUnitTest` in `android/` after a first `flutter build apk` (which writes `gradlew`), not part of `tool/check.dart`, as CI's runner has no Android SDK.
|
||||
- **Health Connect** (ADR 0030, D30, D31) is Android's (`TrainPerson.health` null elsewhere): `HealthConnectBridge.kt` on AndroidX's `connect-client` 1.1.0 over `app.halkyon.train/health` (its records' mapping in `HealthRecords.kt`, tested on the JVM; the rationale's links from Health Connect in the manifest, `…/health/rationale` to the app), `HealthPlatform` in Dart (`lib/src/feature/health/`), `HealthConnect` per person: with writing on, every workout written as `healthRecordsOf` makes it (client IDs from the workout's ID, a version counted up as it changes, a fingerprint deciding whether it did), written again or deleted as the workouts change and as the app opens, what was written kept in the person's `health_connect.json` (`HealthStore`, never synced); with reading on, the sessions of the apps the person turned on brought in as workouts through the changes token (`importedWorkout` in `health_import.dart`, the workout's `origin` in its payload; a double of one Train has brings at most its heart rate, `isDouble`; one deleted in Train is remembered so it stays out; a track only when asked for on its page, `bringInTrack`), as the app opens and comes to the front (`HealthConnectListener`, which opens the rationale too); the screen from the account sheet (`HealthScreen`, `HealthCubit`). Tests on `test/support/fake_health_platform.dart`. Train needs Android 8 (API 26) for it.
|
||||
- **The Polar Loop** (ADR 0030, D32 to D36) is Android's (`TrainPerson.loop` null elsewhere): `LoopBridge.kt` on Polar's BLE SDK 8.3.0 (from JitPack, a repository asked for Polar's group alone, its files pinned by SHA-256 in `android/gradle/verification-metadata.xml`; a new version needs its checksums there) over `app.halkyon.train/loop`, `…/loop/state` and `…/loop/search`, the first-time use's figures mapped and checked in `LoopSetup.kt` (tested on the JVM); `LoopPlatform` in Dart (`lib/src/feature/loop/`), `PolarLoop` per person: the band remembered in the person's `loop.json` (`LoopStore`, never synced) with what it was set up with and what it told, connected while the app is open, its clock set as each connection is ready, a changed profile offered on the band's screen and sent only when the person says (D35), *Let the band go* a factory reset after asking; its days and nights (D37, D38, `lib/src/feature/day/`) read as each connection is ready (every half hour at most, or when the person pulls *Days*) and by a WorkManager job every four hours while the app is closed (`LoopWorker.kt`, into the person's `loop_days.txt`, which the app takes in as it opens, `LoopLines`), `LoopDays.kt` turning the SDK's answers into lines of JSON (tested on the JVM), `DayRepository` sealing them as `day`, `night` and `day_series` rows (payloads in `day_payload.dart`), and what is sealed, up to yesterday, deleted on the band by the next read (`KnownLoop.keptUntil`), never today; its training sessions (D39) read with the days and kept as workouts of origin *Polar Loop* (`loop_workouts.dart`: `takeLoopSessions`, Polar's sport 1 and 2 as running and cycling, the rest as `Activity.other`, which the edit sheet changes; a double of a recorded workout gives it its heart rate), remembered in `loop.json` (`takenSessions`), written to Health Connect as Train's own; its firmware (D10, D40) asked for on the day's first connection and on the band's screen, from Polar directly (the SDK) or through the instance as the person chose (`KnownLoop.firmwareVia`, `FirmwareRelayGateway` on the server's `firmware` endpoint), updated only after asking, the band read and kept first and set up again after (`PolarLoop.updateFirmware`, `LoopFirmware.kt`); its live heart rate (D41) read by the recording service through the same SDK (`PolarSdk.kt`, one for the process, held by the bridge and by a recording; `LoopHeart.kt`) while no strap sends, the file's `loop` lines its state; home's *Days* tab and a day's page (`DaysCubit`, `DayCubit`); `loopFiguresOf` fills Polar's defaults for the band alone (`loop_figures.dart`), the setup sheet asks for what the profile lacks with `ProfileForm(only: …)`. The person's body's figures are the synced, sealed `profile` (D33, *Your body*, `lib/src/feature/profile/`). Polar's SDK isn't free software (D36): the app's AGPL carries a section 7 permission for it (`halkyon_train_flutter/LICENSE-polar-exception.md`), its notices are in the licence list (`registerPolarLicense`, on Android), and an F-Droid listing of Train declares `NonFreeDep`. Never ask the SDK for `FEATURE_TELEMETRY`. Tests on `test/support/fake_loop_platform.dart`.
|
||||
- **The Polar Loop** (ADR 0030, D32 to D36) is Android's (`TrainPerson.loop` null elsewhere): `LoopBridge.kt` on Polar's BLE SDK 8.3.0 (from JitPack, a repository asked for Polar's group alone, its files pinned by SHA-256 in `android/gradle/verification-metadata.xml`; a new version needs its checksums there) over `app.halkyon.train/loop`, `…/loop/state` and `…/loop/search`, the first-time use's figures mapped and checked in `LoopSetup.kt` (tested on the JVM); `LoopPlatform` in Dart (`lib/src/feature/loop/`), `PolarLoop` per person: the band remembered in the person's `loop.json` (`LoopStore`, never synced) with what it was set up with and what it told, connected while the app is open, its clock set as each connection is ready, a changed profile offered on the band's screen and sent only when the person says (D35), *Let the band go* a factory reset after asking; its days and nights (D37, D38, `lib/src/feature/day/`) read as each connection is ready (every half hour at most, or when the person pulls *Days*) and by a WorkManager job every four hours while the app is closed (`LoopWorker.kt`, into the person's `loop_days.txt`, which the app takes in as it opens, `LoopLines`), `LoopDays.kt` turning the SDK's answers into lines of JSON (tested on the JVM), `DayRepository` sealing them as `day`, `night` and `day_series` rows (payloads in `day_payload.dart`), and what is sealed, up to yesterday, deleted on the band by the next read (`KnownLoop.keptUntil`), never today; its training sessions (D39) read with the days and kept as workouts of origin *Polar Loop* (`loop_workouts.dart`: `takeLoopSessions`, Polar's sport 1 and 2 as running and cycling, the rest as `Activity.other`, which the edit sheet changes; a double of a recorded workout gives it its heart rate), remembered in `loop.json` (`takenSessions`), written to Health Connect as Train's own; its firmware (D10, D40) asked for on the day's first connection and on the band's screen, from Polar directly (the SDK) or through the instance as the person chose (`KnownLoop.firmwareVia`, `FirmwareRelayGateway` on the server's `firmware` endpoint), updated only after asking, the band read and kept first and set up again after (`PolarLoop.updateFirmware`, `LoopFirmware.kt`); its live heart rate (D41) read by the recording service through the same SDK (`PolarSdk.kt`, one for the process, held by the bridge and by a recording; `LoopHeart.kt`) while no strap sends, the file's `loop` lines its state; home's *Days* tab and a day's page (`DaysCubit`, `DayCubit`); `loopFiguresOf` fills Polar's defaults for the band alone (`loop_figures.dart`), the setup sheet asks for what the profile lacks with `ProfileForm(only: …)`. **Interval plans** (D42, `lib/src/feature/plan/`): home's *Plans* tab (`PlansCubit`, deletions undone for a moment as workouts' are), a plan's editor (`PlanEditorCubit`, `PlanScreen`, a step on `showStepSheet`) and three templates (`plan_templates.dart`); `PlanRepository` seals each as a `plan` row (payload in `plan_payload.dart`, the step's JSON shared with the recorder). The person's body's figures are the synced, sealed `profile` (D33, *Your body*, `lib/src/feature/profile/`). Polar's SDK isn't free software (D36): the app's AGPL carries a section 7 permission for it (`halkyon_train_flutter/LICENSE-polar-exception.md`), its notices are in the licence list (`registerPolarLicense`, on Android), and an F-Droid listing of Train declares `NonFreeDep`. Never ask the SDK for `FEATURE_TELEMETRY`. Tests on `test/support/fake_loop_platform.dart`.
|
||||
- **The icon** is Lucide's `bike` fading from cobalt through violet and strawberry into carrot (ADR 0031), drawn with the other apps' by the repository's `tool/app_icons.py` (`python3 -I tool/app_icons.py train` from the root): `assets/icon/halkyon_train_icon.png`, Lucide's ISC licence beside it, and every platform's launcher icon through flutter_launcher_icons. `registerLucideLicense` puts Lucide's notice in the licence list.
|
||||
- **`serverpod start`** runs in `apps/train/halkyon_train_server` (it launches the app from `lib/driver.dart`); `.mcp.json`'s `--server-dir` names it when a session works on Train. Its tests need throwaway passwords in its git-ignored `config/passwords.yaml` (`database`, `serviceSecret`, `serverSideSessionKeyHashPepper`, `emailSignInPepper`) or from the environment, as CI gives them. Train's development server has ports of its own (API 8180, Insights 8181, web 8182, PostgreSQL 8190; the app's `assets/config.json`), so it runs beside Learn's, as a member of Learn's register; its tests use `config/test.yaml`'s ports, as Learn's and Todo's do, which Melos runs one package at a time.
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,770 @@
|
|||
{
|
||||
"__className__": "serverpod.DatabaseDefinition",
|
||||
"schemaVersion": 2,
|
||||
"moduleName": "halkyon_train",
|
||||
"tables": [
|
||||
{
|
||||
"__className__": "serverpod.TableDefinition",
|
||||
"name": "content_key",
|
||||
"dartName": "ContentKey",
|
||||
"module": "halkyon_train",
|
||||
"schema": "public",
|
||||
"columns": [
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "id",
|
||||
"fieldName": "id",
|
||||
"columnType": 7,
|
||||
"isNullable": false,
|
||||
"columnDefault": "random_v7",
|
||||
"dartType": "UuidValue?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "spaceId",
|
||||
"fieldName": "spaceId",
|
||||
"columnType": 6,
|
||||
"isNullable": true,
|
||||
"dartType": "int?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "dataKeyId",
|
||||
"fieldName": "dataKeyId",
|
||||
"columnType": 0,
|
||||
"isNullable": false,
|
||||
"dartType": "String"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "sealedKey",
|
||||
"fieldName": "sealedKey",
|
||||
"columnType": 5,
|
||||
"isNullable": false,
|
||||
"dartType": "dart:typed_data:ByteData"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"__className__": "serverpod.ForeignKeyDefinition",
|
||||
"constraintName": "content_key_fk_0",
|
||||
"columns": [
|
||||
"spaceId"
|
||||
],
|
||||
"referenceTable": "offline_sync_spaces",
|
||||
"referenceTableSchema": "public",
|
||||
"referenceColumns": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": 3,
|
||||
"onDelete": 4
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"__className__": "serverpod.IndexDefinition",
|
||||
"indexName": "content_key_space_idx",
|
||||
"elements": [
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "spaceId"
|
||||
}
|
||||
],
|
||||
"type": "btree",
|
||||
"isUnique": false,
|
||||
"isPrimary": false
|
||||
}
|
||||
],
|
||||
"managed": true
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.TableDefinition",
|
||||
"name": "day",
|
||||
"dartName": "Day",
|
||||
"module": "halkyon_train",
|
||||
"schema": "public",
|
||||
"columns": [
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "id",
|
||||
"fieldName": "id",
|
||||
"columnType": 7,
|
||||
"isNullable": false,
|
||||
"columnDefault": "random_v7",
|
||||
"dartType": "UuidValue?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "spaceId",
|
||||
"fieldName": "spaceId",
|
||||
"columnType": 6,
|
||||
"isNullable": true,
|
||||
"dartType": "int?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "keyId",
|
||||
"fieldName": "keyId",
|
||||
"columnType": 0,
|
||||
"isNullable": true,
|
||||
"dartType": "String?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "payload",
|
||||
"fieldName": "payload",
|
||||
"columnType": 5,
|
||||
"isNullable": false,
|
||||
"dartType": "dart:typed_data:ByteData"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "nonce",
|
||||
"fieldName": "nonce",
|
||||
"columnType": 5,
|
||||
"isNullable": true,
|
||||
"dartType": "dart:typed_data:ByteData?"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"__className__": "serverpod.ForeignKeyDefinition",
|
||||
"constraintName": "day_fk_0",
|
||||
"columns": [
|
||||
"spaceId"
|
||||
],
|
||||
"referenceTable": "offline_sync_spaces",
|
||||
"referenceTableSchema": "public",
|
||||
"referenceColumns": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": 3,
|
||||
"onDelete": 4
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"__className__": "serverpod.IndexDefinition",
|
||||
"indexName": "day_space_idx",
|
||||
"elements": [
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "spaceId"
|
||||
}
|
||||
],
|
||||
"type": "btree",
|
||||
"isUnique": false,
|
||||
"isPrimary": false
|
||||
}
|
||||
],
|
||||
"managed": true
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.TableDefinition",
|
||||
"name": "day_series",
|
||||
"dartName": "DaySeries",
|
||||
"module": "halkyon_train",
|
||||
"schema": "public",
|
||||
"columns": [
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "id",
|
||||
"fieldName": "id",
|
||||
"columnType": 7,
|
||||
"isNullable": false,
|
||||
"columnDefault": "random_v7",
|
||||
"dartType": "UuidValue?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "spaceId",
|
||||
"fieldName": "spaceId",
|
||||
"columnType": 6,
|
||||
"isNullable": true,
|
||||
"dartType": "int?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "dayId",
|
||||
"fieldName": "dayId",
|
||||
"columnType": 7,
|
||||
"isNullable": false,
|
||||
"dartType": "UuidValue"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "keyId",
|
||||
"fieldName": "keyId",
|
||||
"columnType": 0,
|
||||
"isNullable": true,
|
||||
"dartType": "String?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "payload",
|
||||
"fieldName": "payload",
|
||||
"columnType": 5,
|
||||
"isNullable": false,
|
||||
"dartType": "dart:typed_data:ByteData"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "nonce",
|
||||
"fieldName": "nonce",
|
||||
"columnType": 5,
|
||||
"isNullable": true,
|
||||
"dartType": "dart:typed_data:ByteData?"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"__className__": "serverpod.ForeignKeyDefinition",
|
||||
"constraintName": "day_series_fk_0",
|
||||
"columns": [
|
||||
"spaceId"
|
||||
],
|
||||
"referenceTable": "offline_sync_spaces",
|
||||
"referenceTableSchema": "public",
|
||||
"referenceColumns": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": 3,
|
||||
"onDelete": 4
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ForeignKeyDefinition",
|
||||
"constraintName": "day_series_fk_1",
|
||||
"columns": [
|
||||
"dayId"
|
||||
],
|
||||
"referenceTable": "day",
|
||||
"referenceTableSchema": "public",
|
||||
"referenceColumns": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": 3,
|
||||
"onDelete": 4,
|
||||
"deferrable": "initiallyDeferred"
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"__className__": "serverpod.IndexDefinition",
|
||||
"indexName": "day_series_space_idx",
|
||||
"elements": [
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "spaceId"
|
||||
}
|
||||
],
|
||||
"type": "btree",
|
||||
"isUnique": false,
|
||||
"isPrimary": false
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.IndexDefinition",
|
||||
"indexName": "day_series_day_idx",
|
||||
"elements": [
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "dayId"
|
||||
}
|
||||
],
|
||||
"type": "btree",
|
||||
"isUnique": false,
|
||||
"isPrimary": false
|
||||
}
|
||||
],
|
||||
"managed": true
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.TableDefinition",
|
||||
"name": "night",
|
||||
"dartName": "Night",
|
||||
"module": "halkyon_train",
|
||||
"schema": "public",
|
||||
"columns": [
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "id",
|
||||
"fieldName": "id",
|
||||
"columnType": 7,
|
||||
"isNullable": false,
|
||||
"columnDefault": "random_v7",
|
||||
"dartType": "UuidValue?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "spaceId",
|
||||
"fieldName": "spaceId",
|
||||
"columnType": 6,
|
||||
"isNullable": true,
|
||||
"dartType": "int?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "keyId",
|
||||
"fieldName": "keyId",
|
||||
"columnType": 0,
|
||||
"isNullable": true,
|
||||
"dartType": "String?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "payload",
|
||||
"fieldName": "payload",
|
||||
"columnType": 5,
|
||||
"isNullable": false,
|
||||
"dartType": "dart:typed_data:ByteData"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "nonce",
|
||||
"fieldName": "nonce",
|
||||
"columnType": 5,
|
||||
"isNullable": true,
|
||||
"dartType": "dart:typed_data:ByteData?"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"__className__": "serverpod.ForeignKeyDefinition",
|
||||
"constraintName": "night_fk_0",
|
||||
"columns": [
|
||||
"spaceId"
|
||||
],
|
||||
"referenceTable": "offline_sync_spaces",
|
||||
"referenceTableSchema": "public",
|
||||
"referenceColumns": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": 3,
|
||||
"onDelete": 4
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"__className__": "serverpod.IndexDefinition",
|
||||
"indexName": "night_space_idx",
|
||||
"elements": [
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "spaceId"
|
||||
}
|
||||
],
|
||||
"type": "btree",
|
||||
"isUnique": false,
|
||||
"isPrimary": false
|
||||
}
|
||||
],
|
||||
"managed": true
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.TableDefinition",
|
||||
"name": "plan",
|
||||
"dartName": "Plan",
|
||||
"module": "halkyon_train",
|
||||
"schema": "public",
|
||||
"columns": [
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "id",
|
||||
"fieldName": "id",
|
||||
"columnType": 7,
|
||||
"isNullable": false,
|
||||
"columnDefault": "random_v7",
|
||||
"dartType": "UuidValue?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "spaceId",
|
||||
"fieldName": "spaceId",
|
||||
"columnType": 6,
|
||||
"isNullable": true,
|
||||
"dartType": "int?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "keyId",
|
||||
"fieldName": "keyId",
|
||||
"columnType": 0,
|
||||
"isNullable": true,
|
||||
"dartType": "String?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "payload",
|
||||
"fieldName": "payload",
|
||||
"columnType": 5,
|
||||
"isNullable": false,
|
||||
"dartType": "dart:typed_data:ByteData"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "nonce",
|
||||
"fieldName": "nonce",
|
||||
"columnType": 5,
|
||||
"isNullable": true,
|
||||
"dartType": "dart:typed_data:ByteData?"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"__className__": "serverpod.ForeignKeyDefinition",
|
||||
"constraintName": "plan_fk_0",
|
||||
"columns": [
|
||||
"spaceId"
|
||||
],
|
||||
"referenceTable": "offline_sync_spaces",
|
||||
"referenceTableSchema": "public",
|
||||
"referenceColumns": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": 3,
|
||||
"onDelete": 4
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"__className__": "serverpod.IndexDefinition",
|
||||
"indexName": "plan_space_idx",
|
||||
"elements": [
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "spaceId"
|
||||
}
|
||||
],
|
||||
"type": "btree",
|
||||
"isUnique": false,
|
||||
"isPrimary": false
|
||||
}
|
||||
],
|
||||
"managed": true
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.TableDefinition",
|
||||
"name": "profile",
|
||||
"dartName": "Profile",
|
||||
"module": "halkyon_train",
|
||||
"schema": "public",
|
||||
"columns": [
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "id",
|
||||
"fieldName": "id",
|
||||
"columnType": 7,
|
||||
"isNullable": false,
|
||||
"columnDefault": "random_v7",
|
||||
"dartType": "UuidValue?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "spaceId",
|
||||
"fieldName": "spaceId",
|
||||
"columnType": 6,
|
||||
"isNullable": true,
|
||||
"dartType": "int?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "keyId",
|
||||
"fieldName": "keyId",
|
||||
"columnType": 0,
|
||||
"isNullable": true,
|
||||
"dartType": "String?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "payload",
|
||||
"fieldName": "payload",
|
||||
"columnType": 5,
|
||||
"isNullable": false,
|
||||
"dartType": "dart:typed_data:ByteData"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "nonce",
|
||||
"fieldName": "nonce",
|
||||
"columnType": 5,
|
||||
"isNullable": true,
|
||||
"dartType": "dart:typed_data:ByteData?"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"__className__": "serverpod.ForeignKeyDefinition",
|
||||
"constraintName": "profile_fk_0",
|
||||
"columns": [
|
||||
"spaceId"
|
||||
],
|
||||
"referenceTable": "offline_sync_spaces",
|
||||
"referenceTableSchema": "public",
|
||||
"referenceColumns": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": 3,
|
||||
"onDelete": 4
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"__className__": "serverpod.IndexDefinition",
|
||||
"indexName": "profile_space_idx",
|
||||
"elements": [
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "spaceId"
|
||||
}
|
||||
],
|
||||
"type": "btree",
|
||||
"isUnique": false,
|
||||
"isPrimary": false
|
||||
}
|
||||
],
|
||||
"managed": true
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.TableDefinition",
|
||||
"name": "workout",
|
||||
"dartName": "Workout",
|
||||
"module": "halkyon_train",
|
||||
"schema": "public",
|
||||
"columns": [
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "id",
|
||||
"fieldName": "id",
|
||||
"columnType": 7,
|
||||
"isNullable": false,
|
||||
"columnDefault": "random_v7",
|
||||
"dartType": "UuidValue?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "spaceId",
|
||||
"fieldName": "spaceId",
|
||||
"columnType": 6,
|
||||
"isNullable": true,
|
||||
"dartType": "int?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "keyId",
|
||||
"fieldName": "keyId",
|
||||
"columnType": 0,
|
||||
"isNullable": true,
|
||||
"dartType": "String?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "payload",
|
||||
"fieldName": "payload",
|
||||
"columnType": 5,
|
||||
"isNullable": false,
|
||||
"dartType": "dart:typed_data:ByteData"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "nonce",
|
||||
"fieldName": "nonce",
|
||||
"columnType": 5,
|
||||
"isNullable": true,
|
||||
"dartType": "dart:typed_data:ByteData?"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"__className__": "serverpod.ForeignKeyDefinition",
|
||||
"constraintName": "workout_fk_0",
|
||||
"columns": [
|
||||
"spaceId"
|
||||
],
|
||||
"referenceTable": "offline_sync_spaces",
|
||||
"referenceTableSchema": "public",
|
||||
"referenceColumns": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": 3,
|
||||
"onDelete": 4
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"__className__": "serverpod.IndexDefinition",
|
||||
"indexName": "workout_space_idx",
|
||||
"elements": [
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "spaceId"
|
||||
}
|
||||
],
|
||||
"type": "btree",
|
||||
"isUnique": false,
|
||||
"isPrimary": false
|
||||
}
|
||||
],
|
||||
"managed": true
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.TableDefinition",
|
||||
"name": "workout_series",
|
||||
"dartName": "WorkoutSeries",
|
||||
"module": "halkyon_train",
|
||||
"schema": "public",
|
||||
"columns": [
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "id",
|
||||
"fieldName": "id",
|
||||
"columnType": 7,
|
||||
"isNullable": false,
|
||||
"columnDefault": "random_v7",
|
||||
"dartType": "UuidValue?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "spaceId",
|
||||
"fieldName": "spaceId",
|
||||
"columnType": 6,
|
||||
"isNullable": true,
|
||||
"dartType": "int?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "workoutId",
|
||||
"fieldName": "workoutId",
|
||||
"columnType": 7,
|
||||
"isNullable": false,
|
||||
"dartType": "UuidValue"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "keyId",
|
||||
"fieldName": "keyId",
|
||||
"columnType": 0,
|
||||
"isNullable": true,
|
||||
"dartType": "String?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "payload",
|
||||
"fieldName": "payload",
|
||||
"columnType": 5,
|
||||
"isNullable": false,
|
||||
"dartType": "dart:typed_data:ByteData"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "nonce",
|
||||
"fieldName": "nonce",
|
||||
"columnType": 5,
|
||||
"isNullable": true,
|
||||
"dartType": "dart:typed_data:ByteData?"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"__className__": "serverpod.ForeignKeyDefinition",
|
||||
"constraintName": "workout_series_fk_0",
|
||||
"columns": [
|
||||
"spaceId"
|
||||
],
|
||||
"referenceTable": "offline_sync_spaces",
|
||||
"referenceTableSchema": "public",
|
||||
"referenceColumns": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": 3,
|
||||
"onDelete": 4
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ForeignKeyDefinition",
|
||||
"constraintName": "workout_series_fk_1",
|
||||
"columns": [
|
||||
"workoutId"
|
||||
],
|
||||
"referenceTable": "workout",
|
||||
"referenceTableSchema": "public",
|
||||
"referenceColumns": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": 3,
|
||||
"onDelete": 4,
|
||||
"deferrable": "initiallyDeferred"
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"__className__": "serverpod.IndexDefinition",
|
||||
"indexName": "workout_series_space_idx",
|
||||
"elements": [
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "spaceId"
|
||||
}
|
||||
],
|
||||
"type": "btree",
|
||||
"isUnique": false,
|
||||
"isPrimary": false
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.IndexDefinition",
|
||||
"indexName": "workout_series_workout_idx",
|
||||
"elements": [
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "workoutId"
|
||||
}
|
||||
],
|
||||
"type": "btree",
|
||||
"isUnique": false,
|
||||
"isPrimary": false
|
||||
}
|
||||
],
|
||||
"managed": true
|
||||
}
|
||||
],
|
||||
"installedModules": [
|
||||
{
|
||||
"__className__": "serverpod.DatabaseMigrationVersion",
|
||||
"module": "halkyon_base",
|
||||
"version": "20261026140329453-assistant-budgets"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.DatabaseMigrationVersion",
|
||||
"module": "serverpod",
|
||||
"version": "20260824182259319"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.DatabaseMigrationVersion",
|
||||
"module": "serverpod_auth_idp",
|
||||
"version": "20260924105404509"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.DatabaseMigrationVersion",
|
||||
"module": "serverpod_offline_sync",
|
||||
"version": "20260914143806119"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.DatabaseMigrationVersion",
|
||||
"module": "halkyon_oidc",
|
||||
"version": "20261023180644988"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.DatabaseMigrationVersion",
|
||||
"module": "serverpod_auth_core",
|
||||
"version": "20260924105232991"
|
||||
}
|
||||
],
|
||||
"migrationApiVersion": 1
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,94 @@
|
|||
{
|
||||
"__className__": "serverpod.DatabaseMigration",
|
||||
"actions": [
|
||||
{
|
||||
"__className__": "serverpod.DatabaseMigrationAction",
|
||||
"type": "createTable",
|
||||
"createTable": {
|
||||
"__className__": "serverpod.TableDefinition",
|
||||
"name": "plan",
|
||||
"dartName": "Plan",
|
||||
"module": "halkyon_train",
|
||||
"schema": "public",
|
||||
"columns": [
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "id",
|
||||
"fieldName": "id",
|
||||
"columnType": 7,
|
||||
"isNullable": false,
|
||||
"columnDefault": "random_v7",
|
||||
"dartType": "UuidValue?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "spaceId",
|
||||
"fieldName": "spaceId",
|
||||
"columnType": 6,
|
||||
"isNullable": true,
|
||||
"dartType": "int?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "keyId",
|
||||
"fieldName": "keyId",
|
||||
"columnType": 0,
|
||||
"isNullable": true,
|
||||
"dartType": "String?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "payload",
|
||||
"fieldName": "payload",
|
||||
"columnType": 5,
|
||||
"isNullable": false,
|
||||
"dartType": "dart:typed_data:ByteData"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "nonce",
|
||||
"fieldName": "nonce",
|
||||
"columnType": 5,
|
||||
"isNullable": true,
|
||||
"dartType": "dart:typed_data:ByteData?"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"__className__": "serverpod.ForeignKeyDefinition",
|
||||
"constraintName": "plan_fk_0",
|
||||
"columns": [
|
||||
"spaceId"
|
||||
],
|
||||
"referenceTable": "offline_sync_spaces",
|
||||
"referenceTableSchema": "public",
|
||||
"referenceColumns": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": 3,
|
||||
"onDelete": 4
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"__className__": "serverpod.IndexDefinition",
|
||||
"indexName": "plan_space_idx",
|
||||
"elements": [
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "spaceId"
|
||||
}
|
||||
],
|
||||
"type": "btree",
|
||||
"isUnique": false,
|
||||
"isPrimary": false
|
||||
}
|
||||
],
|
||||
"managed": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"warnings": [],
|
||||
"migrationApiVersion": 1
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import 'package:serverpod_database/serverpod_database.dart';
|
|||
part '20261028101012641-workouts/migration.dart';
|
||||
part '20261028164640000-profile/migration.dart';
|
||||
part '20261028181405893-days/migration.dart';
|
||||
part '20261028211508606-plans/migration.dart';
|
||||
|
||||
/// Migration registry for the client-side database.
|
||||
class MigrationRegistry {
|
||||
|
|
@ -16,6 +17,7 @@ class MigrationRegistry {
|
|||
_Migration20261028101012641_workouts(),
|
||||
_Migration20261028164640000_profile(),
|
||||
_Migration20261028181405893_days(),
|
||||
_Migration20261028211508606_plans(),
|
||||
];
|
||||
|
||||
/// List of all client-side database migration versions.
|
||||
|
|
|
|||
670
apps/train/halkyon_train_client/lib/src/protocol/plan/plan.dart
Normal file
670
apps/train/halkyon_train_client/lib/src/protocol/plan/plan.dart
Normal file
|
|
@ -0,0 +1,670 @@
|
|||
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
|
||||
/* To generate run: "serverpod generate" */
|
||||
|
||||
// ignore_for_file: implementation_imports
|
||||
// ignore_for_file: library_private_types_in_public_api
|
||||
// ignore_for_file: non_constant_identifier_names
|
||||
// ignore_for_file: public_member_api_docs
|
||||
// ignore_for_file: type_literal_in_constant_pattern
|
||||
// ignore_for_file: use_super_parameters
|
||||
// ignore_for_file: invalid_use_of_internal_member
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'dart:typed_data' as _idt;
|
||||
import 'package:serverpod_client/serverpod_client.dart' as _isc;
|
||||
import 'package:serverpod_database/serverpod_database.dart' as _isd;
|
||||
|
||||
/// An interval plan (ADR 0030, D42): its name and steps, each ending after a
|
||||
/// time, a distance or a lap tap and aiming at a target, in its sealed
|
||||
/// payload (D7); the instance sees that it exists, its size and when it
|
||||
/// changed. Changed when edited, the last writer winning. Synced.
|
||||
abstract class Plan
|
||||
implements _isd.TableRow<_isc.UuidValue?>, _isc.ProtocolSerialization {
|
||||
Plan._({
|
||||
this.id,
|
||||
this.spaceId,
|
||||
this.keyId,
|
||||
required this.payload,
|
||||
this.nonce,
|
||||
});
|
||||
|
||||
factory Plan({
|
||||
_isc.UuidValue? id,
|
||||
int? spaceId,
|
||||
String? keyId,
|
||||
required _idt.ByteData payload,
|
||||
_idt.ByteData? nonce,
|
||||
}) = _PlanImpl;
|
||||
|
||||
factory Plan.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
return Plan(
|
||||
id: jsonSerialization['id'] == null
|
||||
? null
|
||||
: _isc.UuidValueJsonExtension.fromJson(jsonSerialization['id']),
|
||||
spaceId: jsonSerialization['spaceId'] as int?,
|
||||
keyId: jsonSerialization['keyId'] as String?,
|
||||
payload: _isc.ByteDataJsonExtension.fromJson(
|
||||
jsonSerialization['payload'],
|
||||
),
|
||||
nonce: jsonSerialization['nonce'] == null
|
||||
? null
|
||||
: _isc.ByteDataJsonExtension.fromJson(jsonSerialization['nonce']),
|
||||
);
|
||||
}
|
||||
|
||||
static final t = PlanTable();
|
||||
|
||||
static const db = PlanRepository._();
|
||||
|
||||
@override
|
||||
_isc.UuidValue? id;
|
||||
|
||||
/// The space owning this row. Maintained by the sync engine.
|
||||
int? spaceId;
|
||||
|
||||
/// The key it is sealed with (ADR 0002's envelope); never null in Train.
|
||||
String? keyId;
|
||||
|
||||
/// The content, sealed: UTF-8 JSON with a `schemaVersion` once opened.
|
||||
_idt.ByteData payload;
|
||||
|
||||
/// The sealing's nonce (ADR 0002's envelope).
|
||||
_idt.ByteData? nonce;
|
||||
|
||||
@override
|
||||
_isd.Table<_isc.UuidValue?> get table => t;
|
||||
|
||||
/// Returns a shallow copy of this [Plan]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_isc.useResult
|
||||
Plan copyWith({
|
||||
_isc.UuidValue? id,
|
||||
int? spaceId,
|
||||
String? keyId,
|
||||
_idt.ByteData? payload,
|
||||
_idt.ByteData? nonce,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'Plan',
|
||||
if (id != null) 'id': id?.toJson(),
|
||||
if (spaceId != null) 'spaceId': spaceId,
|
||||
if (keyId != null) 'keyId': keyId,
|
||||
'payload': payload.toJson(),
|
||||
if (nonce != null) 'nonce': nonce?.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJsonForProtocol() {
|
||||
return {
|
||||
'__className__': 'Plan',
|
||||
if (id != null) 'id': id?.toJson(),
|
||||
if (spaceId != null) 'spaceId': spaceId,
|
||||
if (keyId != null) 'keyId': keyId,
|
||||
'payload': payload.toJson(),
|
||||
if (nonce != null) 'nonce': nonce?.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
static PlanInclude include() {
|
||||
return PlanInclude._();
|
||||
}
|
||||
|
||||
static PlanIncludeList includeList({
|
||||
_isd.WhereExpressionBuilder<PlanTable>? where,
|
||||
int? limit,
|
||||
int? offset,
|
||||
_isd.OrderByBuilder<PlanTable>? orderBy,
|
||||
_isd.OrderByListBuilder<PlanTable>? orderByList,
|
||||
PlanInclude? include,
|
||||
}) {
|
||||
return PlanIncludeList._(
|
||||
where: where,
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
orderBy: orderBy?.call(Plan.t),
|
||||
orderByList: orderByList?.call(Plan.t),
|
||||
include: include,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return _isc.SerializationManager.encode(this);
|
||||
}
|
||||
}
|
||||
|
||||
class _Undefined {}
|
||||
|
||||
class _PlanImpl extends Plan {
|
||||
_PlanImpl({
|
||||
_isc.UuidValue? id,
|
||||
int? spaceId,
|
||||
String? keyId,
|
||||
required _idt.ByteData payload,
|
||||
_idt.ByteData? nonce,
|
||||
}) : super._(
|
||||
id: id,
|
||||
spaceId: spaceId,
|
||||
keyId: keyId,
|
||||
payload: payload,
|
||||
nonce: nonce,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [Plan]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_isc.useResult
|
||||
@override
|
||||
Plan copyWith({
|
||||
Object? id = _Undefined,
|
||||
Object? spaceId = _Undefined,
|
||||
Object? keyId = _Undefined,
|
||||
_idt.ByteData? payload,
|
||||
Object? nonce = _Undefined,
|
||||
}) {
|
||||
return Plan(
|
||||
id: id is _isc.UuidValue? ? id : this.id,
|
||||
spaceId: spaceId is int? ? spaceId : this.spaceId,
|
||||
keyId: keyId is String? ? keyId : this.keyId,
|
||||
payload: payload ?? this.payload.clone(),
|
||||
nonce: nonce is _idt.ByteData? ? nonce : this.nonce?.clone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PlanUpdateTable extends _isd.UpdateTable<PlanTable> {
|
||||
PlanUpdateTable(super.table);
|
||||
|
||||
_isd.ColumnValue<int, int> spaceId(int? value) => _isd.ColumnValue(
|
||||
table.spaceId,
|
||||
value,
|
||||
);
|
||||
|
||||
_isd.ColumnValue<String, String> keyId(String? value) => _isd.ColumnValue(
|
||||
table.keyId,
|
||||
value,
|
||||
);
|
||||
|
||||
_isd.ColumnValue<_idt.ByteData, _idt.ByteData> payload(_idt.ByteData value) =>
|
||||
_isd.ColumnValue(
|
||||
table.payload,
|
||||
value,
|
||||
);
|
||||
|
||||
_isd.ColumnValue<_idt.ByteData, _idt.ByteData> nonce(_idt.ByteData? value) =>
|
||||
_isd.ColumnValue(
|
||||
table.nonce,
|
||||
value,
|
||||
);
|
||||
}
|
||||
|
||||
class PlanTable extends _isd.Table<_isc.UuidValue?> {
|
||||
PlanTable({super.tableRelation}) : super(tableName: 'plan') {
|
||||
updateTable = PlanUpdateTable(this);
|
||||
spaceId = _isd.ColumnInt(
|
||||
'spaceId',
|
||||
this,
|
||||
);
|
||||
keyId = _isd.ColumnString(
|
||||
'keyId',
|
||||
this,
|
||||
);
|
||||
payload = _isd.ColumnByteData(
|
||||
'payload',
|
||||
this,
|
||||
);
|
||||
nonce = _isd.ColumnByteData(
|
||||
'nonce',
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
late final PlanUpdateTable updateTable;
|
||||
|
||||
/// The space owning this row. Maintained by the sync engine.
|
||||
late final _isd.ColumnInt spaceId;
|
||||
|
||||
/// The key it is sealed with (ADR 0002's envelope); never null in Train.
|
||||
late final _isd.ColumnString keyId;
|
||||
|
||||
/// The content, sealed: UTF-8 JSON with a `schemaVersion` once opened.
|
||||
late final _isd.ColumnByteData payload;
|
||||
|
||||
/// The sealing's nonce (ADR 0002's envelope).
|
||||
late final _isd.ColumnByteData nonce;
|
||||
|
||||
@override
|
||||
List<_isd.Column> get columns => [
|
||||
id,
|
||||
spaceId,
|
||||
keyId,
|
||||
payload,
|
||||
nonce,
|
||||
];
|
||||
}
|
||||
|
||||
class PlanInclude extends _isd.IncludeObject {
|
||||
PlanInclude._();
|
||||
|
||||
@override
|
||||
Map<String, _isd.Include?> get includes => {};
|
||||
|
||||
@override
|
||||
_isd.Table<_isc.UuidValue?> get table => Plan.t;
|
||||
}
|
||||
|
||||
class PlanIncludeList extends _isd.IncludeList {
|
||||
PlanIncludeList._({
|
||||
_isd.WhereExpressionBuilder<PlanTable>? where,
|
||||
super.limit,
|
||||
super.offset,
|
||||
super.orderBy,
|
||||
super.orderByList,
|
||||
super.include,
|
||||
}) {
|
||||
super.where = where?.call(Plan.t);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, _isd.Include?> get includes => include?.includes ?? {};
|
||||
|
||||
@override
|
||||
_isd.Table<_isc.UuidValue?> get table => Plan.t;
|
||||
}
|
||||
|
||||
class PlanRepository {
|
||||
const PlanRepository._();
|
||||
|
||||
/// Returns a list of [Plan]s matching the given query parameters.
|
||||
///
|
||||
/// Use [where] to specify which items to include in the return value.
|
||||
/// If none is specified, all items will be returned.
|
||||
///
|
||||
/// To specify the order of the items use [orderBy] or [orderByList]
|
||||
/// when sorting by multiple columns.
|
||||
///
|
||||
/// The maximum number of items can be set by [limit]. If no limit is set,
|
||||
/// all items matching the query will be returned.
|
||||
///
|
||||
/// [offset] defines how many items to skip, after which [limit] (or all)
|
||||
/// items are read from the database.
|
||||
///
|
||||
/// ```dart
|
||||
/// var persons = await Persons.db.find(
|
||||
/// session,
|
||||
/// where: (t) => t.lastName.equals('Jones'),
|
||||
/// orderBy: (t) => t.firstName,
|
||||
/// limit: 100,
|
||||
/// );
|
||||
/// ```
|
||||
Future<List<Plan>> find(
|
||||
_isd.DatabaseSession session, {
|
||||
_isd.WhereExpressionBuilder<PlanTable>? where,
|
||||
int? limit,
|
||||
int? offset,
|
||||
_isd.OrderByBuilder<PlanTable>? orderBy,
|
||||
_isd.OrderByListBuilder<PlanTable>? orderByList,
|
||||
_isd.Transaction? transaction,
|
||||
_isd.LockMode? lockMode,
|
||||
_isd.LockBehavior? lockBehavior,
|
||||
}) async {
|
||||
return session.db.find<Plan>(
|
||||
where: where?.call(Plan.t),
|
||||
orderBy: orderBy?.call(Plan.t),
|
||||
orderByList: orderByList?.call(Plan.t),
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
transaction: transaction,
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns the first matching [Plan] matching the given query parameters.
|
||||
///
|
||||
/// Use [where] to specify which items to include in the return value.
|
||||
/// If none is specified, all items will be returned.
|
||||
///
|
||||
/// To specify the order use [orderBy] or [orderByList]
|
||||
/// when sorting by multiple columns.
|
||||
///
|
||||
/// [offset] defines how many items to skip, after which the next one will be picked.
|
||||
///
|
||||
/// ```dart
|
||||
/// var youngestPerson = await Persons.db.findFirstRow(
|
||||
/// session,
|
||||
/// where: (t) => t.lastName.equals('Jones'),
|
||||
/// orderBy: (t) => t.age,
|
||||
/// );
|
||||
/// ```
|
||||
Future<Plan?> findFirstRow(
|
||||
_isd.DatabaseSession session, {
|
||||
_isd.WhereExpressionBuilder<PlanTable>? where,
|
||||
int? offset,
|
||||
_isd.OrderByBuilder<PlanTable>? orderBy,
|
||||
_isd.OrderByListBuilder<PlanTable>? orderByList,
|
||||
_isd.Transaction? transaction,
|
||||
_isd.LockMode? lockMode,
|
||||
_isd.LockBehavior? lockBehavior,
|
||||
}) async {
|
||||
return session.db.findFirstRow<Plan>(
|
||||
where: where?.call(Plan.t),
|
||||
orderBy: orderBy?.call(Plan.t),
|
||||
orderByList: orderByList?.call(Plan.t),
|
||||
offset: offset,
|
||||
transaction: transaction,
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
);
|
||||
}
|
||||
|
||||
/// Finds a single [Plan] by its [id] or null if no such row exists.
|
||||
Future<Plan?> findById(
|
||||
_isd.DatabaseSession session,
|
||||
_isc.UuidValue id, {
|
||||
_isd.Transaction? transaction,
|
||||
_isd.LockMode? lockMode,
|
||||
_isd.LockBehavior? lockBehavior,
|
||||
}) async {
|
||||
return session.db.findById<Plan>(
|
||||
id,
|
||||
transaction: transaction,
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
);
|
||||
}
|
||||
|
||||
/// Inserts all [Plan]s in the list and returns the inserted rows.
|
||||
///
|
||||
/// The returned [Plan]s will have their `id` fields set.
|
||||
///
|
||||
/// This is an atomic operation, meaning that if one of the rows fails to
|
||||
/// insert, none of the rows will be inserted.
|
||||
///
|
||||
/// If [ignoreConflicts] is set to `true`, rows that conflict with existing
|
||||
/// rows are silently skipped, and only the successfully inserted rows are
|
||||
/// returned.
|
||||
///
|
||||
/// If [noReturn] is set to `true`, the inserted rows are not read back from
|
||||
/// the database and an empty list is returned. This avoids the overhead of
|
||||
/// transferring and deserializing the rows when the result is not needed.
|
||||
Future<List<Plan>> insert(
|
||||
_isd.DatabaseSession session,
|
||||
List<Plan> rows, {
|
||||
_isd.Transaction? transaction,
|
||||
bool ignoreConflicts = false,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.insert<Plan>(
|
||||
rows,
|
||||
transaction: transaction,
|
||||
ignoreConflicts: ignoreConflicts,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Inserts a single [Plan] and returns the inserted row.
|
||||
///
|
||||
/// The returned [Plan] will have its `id` field set.
|
||||
Future<Plan> insertRow(
|
||||
_isd.DatabaseSession session,
|
||||
Plan row, {
|
||||
_isd.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.insertRow<Plan>(
|
||||
row,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Upserts all [Plan]s in the list and returns the resulting rows.
|
||||
///
|
||||
/// If a row conflicts on the given [conflictColumns], the existing row is
|
||||
/// updated with the new values. Otherwise, a new row is inserted.
|
||||
///
|
||||
/// If [updateColumns] is provided, only those columns will be updated on
|
||||
/// conflict. If null, all non-conflict, non-id columns are updated.
|
||||
///
|
||||
/// If [updateWhere] is provided, the update only applies to rows matching the
|
||||
/// given expression. Conflicting rows that don't match are skipped and not
|
||||
/// returned, so the resulting list may be shorter than [rows].
|
||||
///
|
||||
/// The returned [Plan]s will have their `id` fields set.
|
||||
///
|
||||
/// This is an atomic operation, meaning that if one of the rows fails,
|
||||
/// none of the rows will be affected.
|
||||
///
|
||||
/// If [noReturn] is set to `true`, the resulting rows are not read back from
|
||||
/// the database and an empty list is returned. This avoids the overhead of
|
||||
/// transferring and deserializing the rows when the result is not needed.
|
||||
Future<List<Plan>> upsert(
|
||||
_isd.DatabaseSession session,
|
||||
List<Plan> rows, {
|
||||
required _isd.ColumnSelections<PlanTable> conflictColumns,
|
||||
_isd.ColumnSelections<PlanTable>? updateColumns,
|
||||
_isd.WhereExpressionBuilder<PlanTable>? updateWhere,
|
||||
_isd.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.upsert<Plan>(
|
||||
rows,
|
||||
conflictColumns: conflictColumns(Plan.t),
|
||||
updateColumns: updateColumns?.call(Plan.t),
|
||||
updateWhere: updateWhere?.call(Plan.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Upserts a single [Plan] and returns the resulting row.
|
||||
///
|
||||
/// If the row conflicts on the given [conflictColumns], the existing row is
|
||||
/// updated. Otherwise, a new row is inserted.
|
||||
///
|
||||
/// If [updateColumns] is provided, only those columns will be updated on
|
||||
/// conflict. If null, all non-conflict, non-id columns are updated.
|
||||
///
|
||||
/// If [updateWhere] is provided, the update only applies when the existing
|
||||
/// row matches the expression. Returns `null` if no row was affected — for
|
||||
/// example when [updateWhere] does not match the conflicting row.
|
||||
///
|
||||
/// The returned [Plan] will have its `id` field set.
|
||||
Future<Plan?> upsertRow(
|
||||
_isd.DatabaseSession session,
|
||||
Plan row, {
|
||||
required _isd.ColumnSelections<PlanTable> conflictColumns,
|
||||
_isd.ColumnSelections<PlanTable>? updateColumns,
|
||||
_isd.WhereExpressionBuilder<PlanTable>? updateWhere,
|
||||
_isd.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.upsertRow<Plan>(
|
||||
row,
|
||||
conflictColumns: conflictColumns(Plan.t),
|
||||
updateColumns: updateColumns?.call(Plan.t),
|
||||
updateWhere: updateWhere?.call(Plan.t),
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates all [Plan]s in the list and returns the updated rows. If
|
||||
/// [columns] is provided, only those columns will be updated. Defaults to
|
||||
/// all columns.
|
||||
/// This is an atomic operation, meaning that if one of the rows fails to
|
||||
/// update, none of the rows will be updated.
|
||||
///
|
||||
/// If [noReturn] is set to `true`, the updated rows are not read back from
|
||||
/// the database and an empty list is returned. This avoids the overhead of
|
||||
/// transferring and deserializing the rows when the result is not needed.
|
||||
Future<List<Plan>> update(
|
||||
_isd.DatabaseSession session,
|
||||
List<Plan> rows, {
|
||||
_isd.ColumnSelections<PlanTable>? columns,
|
||||
_isd.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.update<Plan>(
|
||||
rows,
|
||||
columns: columns?.call(Plan.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates a single [Plan]. The row needs to have its id set.
|
||||
/// Optionally, a list of [columns] can be provided to only update those
|
||||
/// columns. Defaults to all columns.
|
||||
Future<Plan> updateRow(
|
||||
_isd.DatabaseSession session,
|
||||
Plan row, {
|
||||
_isd.ColumnSelections<PlanTable>? columns,
|
||||
_isd.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.updateRow<Plan>(
|
||||
row,
|
||||
columns: columns?.call(Plan.t),
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates a single [Plan] by its [id] with the specified [columnValues].
|
||||
/// Returns the updated row or null if no row with the given id exists.
|
||||
Future<Plan?> updateById(
|
||||
_isd.DatabaseSession session,
|
||||
_isc.UuidValue id, {
|
||||
required _isd.ColumnValueListBuilder<PlanUpdateTable> columnValues,
|
||||
_isd.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.updateById<Plan>(
|
||||
id,
|
||||
columnValues: columnValues(Plan.t.updateTable),
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates all [Plan]s matching the [where] expression with the specified [columnValues].
|
||||
/// Returns the list of updated rows.
|
||||
///
|
||||
/// If [noReturn] is set to `true`, the updated rows are not read back from
|
||||
/// the database and an empty list is returned. This avoids the overhead of
|
||||
/// transferring and deserializing the rows when the result is not needed.
|
||||
Future<List<Plan>> updateWhere(
|
||||
_isd.DatabaseSession session, {
|
||||
required _isd.ColumnValueListBuilder<PlanUpdateTable> columnValues,
|
||||
required _isd.WhereExpressionBuilder<PlanTable> where,
|
||||
int? limit,
|
||||
int? offset,
|
||||
_isd.OrderByBuilder<PlanTable>? orderBy,
|
||||
_isd.OrderByListBuilder<PlanTable>? orderByList,
|
||||
_isd.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.updateWhere<Plan>(
|
||||
columnValues: columnValues(Plan.t.updateTable),
|
||||
where: where(Plan.t),
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
orderBy: orderBy?.call(Plan.t),
|
||||
orderByList: orderByList?.call(Plan.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Deletes all [Plan]s in the list and returns the deleted rows.
|
||||
///
|
||||
/// To specify the order of the returned rows use [orderBy] or [orderByList]
|
||||
/// when sorting by multiple columns.
|
||||
///
|
||||
/// This is an atomic operation, meaning that if one of the rows fail to
|
||||
/// be deleted, none of the rows will be deleted.
|
||||
///
|
||||
/// If [noReturn] is set to `true`, the deleted rows are not read back from
|
||||
/// the database and an empty list is returned. This avoids the overhead of
|
||||
/// transferring and deserializing the rows when the result is not needed.
|
||||
Future<List<Plan>> delete(
|
||||
_isd.DatabaseSession session,
|
||||
List<Plan> rows, {
|
||||
_isd.OrderByBuilder<PlanTable>? orderBy,
|
||||
_isd.OrderByListBuilder<PlanTable>? orderByList,
|
||||
_isd.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.delete<Plan>(
|
||||
rows,
|
||||
orderBy: orderBy?.call(Plan.t),
|
||||
orderByList: orderByList?.call(Plan.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Deletes a single [Plan].
|
||||
Future<Plan> deleteRow(
|
||||
_isd.DatabaseSession session,
|
||||
Plan row, {
|
||||
_isd.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.deleteRow<Plan>(
|
||||
row,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Deletes all rows matching the [where] expression.
|
||||
///
|
||||
/// To specify the order of the returned rows use [orderBy] or [orderByList]
|
||||
/// when sorting by multiple columns.
|
||||
///
|
||||
/// If [noReturn] is set to `true`, the deleted rows are not read back from
|
||||
/// the database and an empty list is returned. This avoids the overhead of
|
||||
/// transferring and deserializing the rows when the result is not needed.
|
||||
Future<List<Plan>> deleteWhere(
|
||||
_isd.DatabaseSession session, {
|
||||
required _isd.WhereExpressionBuilder<PlanTable> where,
|
||||
_isd.OrderByBuilder<PlanTable>? orderBy,
|
||||
_isd.OrderByListBuilder<PlanTable>? orderByList,
|
||||
_isd.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.deleteWhere<Plan>(
|
||||
where: where(Plan.t),
|
||||
orderBy: orderBy?.call(Plan.t),
|
||||
orderByList: orderByList?.call(Plan.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Counts the number of rows matching the [where] expression. If omitted,
|
||||
/// will return the count of all rows in the table.
|
||||
Future<int> count(
|
||||
_isd.DatabaseSession session, {
|
||||
_isd.WhereExpressionBuilder<PlanTable>? where,
|
||||
int? limit,
|
||||
_isd.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.count<Plan>(
|
||||
where: where?.call(Plan.t),
|
||||
limit: limit,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Acquires row-level locks on [Plan] rows matching the [where] expression.
|
||||
Future<void> lockRows(
|
||||
_isd.DatabaseSession session, {
|
||||
required _isd.WhereExpressionBuilder<PlanTable> where,
|
||||
required _isd.LockMode lockMode,
|
||||
required _isd.Transaction transaction,
|
||||
_isd.LockBehavior lockBehavior = _isd.LockBehavior.wait,
|
||||
}) async {
|
||||
return session.db.lockRows<Plan>(
|
||||
where: where(Plan.t),
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -27,6 +27,7 @@ import 'day/night.dart' as _i17cgxdf;
|
|||
import 'firmware/loop_firmware.dart' as _i4qollgq;
|
||||
import 'firmware/loop_firmware_refused.dart' as _ipcwdy0l;
|
||||
import 'map/map_source.dart' as _ikw0qg8x;
|
||||
import 'plan/plan.dart' as _i45prw73;
|
||||
import 'privacy/content_key.dart' as _imed3x5d;
|
||||
import 'profile/profile.dart' as _ia5j8fch;
|
||||
import 'workout/workout.dart' as _i5b9b17z;
|
||||
|
|
@ -37,6 +38,7 @@ export 'day/night.dart';
|
|||
export 'firmware/loop_firmware.dart';
|
||||
export 'firmware/loop_firmware_refused.dart';
|
||||
export 'map/map_source.dart';
|
||||
export 'plan/plan.dart';
|
||||
export 'privacy/content_key.dart';
|
||||
export 'profile/profile.dart';
|
||||
export 'workout/workout.dart';
|
||||
|
|
@ -344,6 +346,73 @@ class Protocol extends _isd.DatabaseSerializationManager {
|
|||
],
|
||||
managed: true,
|
||||
),
|
||||
_isd.TableDefinition(
|
||||
name: 'plan',
|
||||
dartName: 'Plan',
|
||||
schema: 'public',
|
||||
module: 'halkyon_train',
|
||||
columns: [
|
||||
_isd.ColumnDefinition(
|
||||
name: 'id',
|
||||
columnType: _isd.ColumnType.uuid,
|
||||
isNullable: false,
|
||||
dartType: 'UuidValue?',
|
||||
columnDefault: 'random_v7',
|
||||
),
|
||||
_isd.ColumnDefinition(
|
||||
name: 'spaceId',
|
||||
columnType: _isd.ColumnType.bigint,
|
||||
isNullable: true,
|
||||
dartType: 'int?',
|
||||
),
|
||||
_isd.ColumnDefinition(
|
||||
name: 'keyId',
|
||||
columnType: _isd.ColumnType.text,
|
||||
isNullable: true,
|
||||
dartType: 'String?',
|
||||
),
|
||||
_isd.ColumnDefinition(
|
||||
name: 'payload',
|
||||
columnType: _isd.ColumnType.bytea,
|
||||
isNullable: false,
|
||||
dartType: 'dart:typed_data:ByteData',
|
||||
),
|
||||
_isd.ColumnDefinition(
|
||||
name: 'nonce',
|
||||
columnType: _isd.ColumnType.bytea,
|
||||
isNullable: true,
|
||||
dartType: 'dart:typed_data:ByteData?',
|
||||
),
|
||||
],
|
||||
foreignKeys: [
|
||||
_isd.ForeignKeyDefinition(
|
||||
constraintName: 'plan_fk_0',
|
||||
columns: ['spaceId'],
|
||||
referenceTable: 'offline_sync_spaces',
|
||||
referenceTableSchema: 'public',
|
||||
referenceColumns: ['id'],
|
||||
onUpdate: _isd.ForeignKeyAction.noAction,
|
||||
onDelete: _isd.ForeignKeyAction.cascade,
|
||||
matchType: null,
|
||||
),
|
||||
],
|
||||
indexes: [
|
||||
_isd.IndexDefinition(
|
||||
indexName: 'plan_space_idx',
|
||||
tableSpace: null,
|
||||
elements: [
|
||||
_isd.IndexElementDefinition(
|
||||
type: _isd.IndexElementDefinitionType.column,
|
||||
definition: 'spaceId',
|
||||
),
|
||||
],
|
||||
type: 'btree',
|
||||
isUnique: false,
|
||||
isPrimary: false,
|
||||
),
|
||||
],
|
||||
managed: true,
|
||||
),
|
||||
_isd.TableDefinition(
|
||||
name: 'profile',
|
||||
dartName: 'Profile',
|
||||
|
|
@ -642,6 +711,9 @@ class Protocol extends _isd.DatabaseSerializationManager {
|
|||
if (t == _ikw0qg8x.MapSource) {
|
||||
return _ikw0qg8x.MapSource.fromJson(data) as T;
|
||||
}
|
||||
if (t == _i45prw73.Plan) {
|
||||
return _i45prw73.Plan.fromJson(data) as T;
|
||||
}
|
||||
if (t == _imed3x5d.ContentKey) {
|
||||
return _imed3x5d.ContentKey.fromJson(data) as T;
|
||||
}
|
||||
|
|
@ -675,6 +747,9 @@ class Protocol extends _isd.DatabaseSerializationManager {
|
|||
if (t == _isc.getType<_ikw0qg8x.MapSource?>()) {
|
||||
return (data != null ? _ikw0qg8x.MapSource.fromJson(data) : null) as T;
|
||||
}
|
||||
if (t == _isc.getType<_i45prw73.Plan?>()) {
|
||||
return (data != null ? _i45prw73.Plan.fromJson(data) : null) as T;
|
||||
}
|
||||
if (t == _isc.getType<_imed3x5d.ContentKey?>()) {
|
||||
return (data != null ? _imed3x5d.ContentKey.fromJson(data) : null) as T;
|
||||
}
|
||||
|
|
@ -714,6 +789,7 @@ class Protocol extends _isd.DatabaseSerializationManager {
|
|||
_i4qollgq.LoopFirmware => 'LoopFirmware',
|
||||
_ipcwdy0l.LoopFirmwareRefused => 'LoopFirmwareRefused',
|
||||
_ikw0qg8x.MapSource => 'MapSource',
|
||||
_i45prw73.Plan => 'Plan',
|
||||
_imed3x5d.ContentKey => 'ContentKey',
|
||||
_ia5j8fch.Profile => 'Profile',
|
||||
_i5b9b17z.Workout => 'Workout',
|
||||
|
|
@ -747,6 +823,8 @@ class Protocol extends _isd.DatabaseSerializationManager {
|
|||
return 'LoopFirmwareRefused';
|
||||
case _ikw0qg8x.MapSource():
|
||||
return 'MapSource';
|
||||
case _i45prw73.Plan():
|
||||
return 'Plan';
|
||||
case _imed3x5d.ContentKey():
|
||||
return 'ContentKey';
|
||||
case _ia5j8fch.Profile():
|
||||
|
|
@ -809,6 +887,9 @@ class Protocol extends _isd.DatabaseSerializationManager {
|
|||
if (dataClassName == 'MapSource') {
|
||||
return deserialize<_ikw0qg8x.MapSource>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'Plan') {
|
||||
return deserialize<_i45prw73.Plan>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'ContentKey') {
|
||||
return deserialize<_imed3x5d.ContentKey>(data['data']);
|
||||
}
|
||||
|
|
@ -906,6 +987,8 @@ class Protocol extends _isd.DatabaseSerializationManager {
|
|||
return _ir1ofny6.DaySeries.t;
|
||||
case _i17cgxdf.Night:
|
||||
return _i17cgxdf.Night.t;
|
||||
case _i45prw73.Plan:
|
||||
return _i45prw73.Plan.t;
|
||||
case _imed3x5d.ContentKey:
|
||||
return _imed3x5d.ContentKey.t;
|
||||
case _ia5j8fch.Profile:
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import 'package:serverpod_database/serverpod_database.dart' as _isd;
|
|||
import 'day/day.dart' as _ikmv9f3y;
|
||||
import 'day/day_series.dart' as _ir1ofny6;
|
||||
import 'day/night.dart' as _i17cgxdf;
|
||||
import 'plan/plan.dart' as _i45prw73;
|
||||
import 'privacy/content_key.dart' as _imed3x5d;
|
||||
import 'profile/profile.dart' as _ia5j8fch;
|
||||
import 'workout/workout.dart' as _i5b9b17z;
|
||||
|
|
@ -27,6 +28,7 @@ final List<_isd.Table> syncTables = [
|
|||
_ikmv9f3y.Day.t,
|
||||
_ir1ofny6.DaySeries.t,
|
||||
_i17cgxdf.Night.t,
|
||||
_i45prw73.Plan.t,
|
||||
_ia5j8fch.Profile.t,
|
||||
_i5b9b17z.Workout.t,
|
||||
_i92mefto.WorkoutSeries.t,
|
||||
|
|
|
|||
|
|
@ -241,6 +241,76 @@
|
|||
"loopFormSave": "Band einrichten",
|
||||
"homeWorkouts": "Workouts",
|
||||
"homeDays": "Tage",
|
||||
"homePlans": "Pläne",
|
||||
"plansNew": "Neuer Plan",
|
||||
"plansNewEmpty": "Leer",
|
||||
"plansEmptyTitle": "Noch keine Pläne",
|
||||
"plansEmptyHint": "Die Schritte eines Plans führen dich beim Aufzeichnen durch Intervalle, mit Ton, Vibration und Stimme bei jedem Wechsel. Beginne mit einer Vorlage oder leer.",
|
||||
"planLocked": "Ein Plan, der sich hier nicht öffnet",
|
||||
"planTemplateFourByFour": "4×4 Minuten",
|
||||
"planTemplateTabata": "Tabata",
|
||||
"planTemplateFourHundreds": "10×400 m",
|
||||
"planTitle": "Plan",
|
||||
"planNewTitle": "Neuer Plan",
|
||||
"planName": "Name",
|
||||
"planActivity": "Für",
|
||||
"planActivityAny": "Jede Aktivität",
|
||||
"planOffTarget": "Sagen, wenn ich vom Ziel abweiche",
|
||||
"planOffTargetHint": "Ein kurzer Hinweis, höchstens alle 30 Sekunden.",
|
||||
"planSave": "Speichern",
|
||||
"planNotSaved": "Der Plan wurde nicht gespeichert.",
|
||||
"planAddStep": "Schritt hinzufügen",
|
||||
"planAddRepeat": "Wiederholung hinzufügen",
|
||||
"planRepeat": "{count}×",
|
||||
"planFewer": "Weniger Runden",
|
||||
"planMore": "Mehr Runden",
|
||||
"planBlockMenu": "Mehr",
|
||||
"planMoveUp": "Nach oben",
|
||||
"planMoveDown": "Nach unten",
|
||||
"planRemove": "Entfernen",
|
||||
"planDelete": "Löschen",
|
||||
"planDeleteTitle": "Diesen Plan löschen?",
|
||||
"planDeleteMessage": "Er wird auf all deinen Geräten gelöscht; Trainings, die mit ihm aufgezeichnet wurden, behalten ihre Schritte.",
|
||||
"planDeleted": "Plan gelöscht",
|
||||
"planSteps": "{count, plural, =1{1 Schritt} other{{count} Schritte}}",
|
||||
"stepTitle": "Schritt",
|
||||
"stepKind": "Art",
|
||||
"stepWarmUp": "Aufwärmen",
|
||||
"stepWork": "Belastung",
|
||||
"stepRecovery": "Erholung",
|
||||
"stepCoolDown": "Auslaufen",
|
||||
"stepEnds": "Endet",
|
||||
"stepEndTime": "Nach einer Zeit",
|
||||
"stepEndDistance": "Nach einer Strecke",
|
||||
"stepEndLap": "Mit der Rundentaste",
|
||||
"stepTimeLabel": "Zeit (min:s)",
|
||||
"stepMetresLabel": "Meter",
|
||||
"stepTarget": "Ziel",
|
||||
"stepZone": "Zone",
|
||||
"stepDone": "Fertig",
|
||||
"stepMinutes": "{minutes} min",
|
||||
"stepSeconds": "{seconds} s",
|
||||
"stepMinutesSeconds": "{minutes} min {seconds} s",
|
||||
"stepMetres": "{metres} m",
|
||||
"stepKilometres": "{km} km",
|
||||
"stepUntilLap": "bis zur Rundentaste",
|
||||
"targetNone": "Keins",
|
||||
"targetFrom": "Von",
|
||||
"targetTo": "Bis",
|
||||
"targetKindPace": "Tempo (pro km)",
|
||||
"targetKindSpeed": "Geschwindigkeit (km/h)",
|
||||
"targetKindHeartRate": "Herzfrequenz (Schläge/min)",
|
||||
"targetKindHeartZone": "Herzfrequenzzone",
|
||||
"targetKindSplit": "Split (pro 500 m)",
|
||||
"targetKindWatts": "Leistung (W)",
|
||||
"targetKindStrokeRate": "Schlagzahl (pro min)",
|
||||
"targetPace": "{from}–{to} pro km",
|
||||
"targetSpeed": "{from}–{to} km/h",
|
||||
"targetHeartRate": "{from}–{to} Schläge/min",
|
||||
"targetZone": "Zone {zone}",
|
||||
"targetSplit": "{from}–{to} pro 500 m",
|
||||
"targetWatts": "{from}–{to} W",
|
||||
"targetStrokeRate": "{from}–{to} Schläge/min",
|
||||
"daysEmptyTitle": "Noch keine Tage",
|
||||
"daysEmptyHint": "Die Tage und Nächte, die dein Polar Loop zählt, erscheinen hier.",
|
||||
"daysSteps": "{steps, plural, =1{1 Schritt} other{{steps} Schritte}}",
|
||||
|
|
|
|||
|
|
@ -300,6 +300,90 @@
|
|||
"loopFormSave": "Set up the band",
|
||||
"homeWorkouts": "Workouts",
|
||||
"homeDays": "Days",
|
||||
"homePlans": "Plans",
|
||||
"plansNew": "New plan",
|
||||
"plansNewEmpty": "From nothing",
|
||||
"plansEmptyTitle": "No plans yet",
|
||||
"plansEmptyHint": "A plan's steps lead you through intervals as you record, with a tone, a vibration and a voice at each change. Start from a template, or from nothing.",
|
||||
"planLocked": "A plan that doesn't open here",
|
||||
"planTemplateFourByFour": "4×4 minutes",
|
||||
"planTemplateTabata": "Tabata",
|
||||
"planTemplateFourHundreds": "10×400 m",
|
||||
"planTitle": "Plan",
|
||||
"planNewTitle": "New plan",
|
||||
"planName": "Name",
|
||||
"planActivity": "For",
|
||||
"planActivityAny": "Any activity",
|
||||
"planOffTarget": "Tell me when I'm off target",
|
||||
"planOffTargetHint": "A short cue at most every 30 seconds.",
|
||||
"planSave": "Save",
|
||||
"planNotSaved": "The plan wasn't saved.",
|
||||
"planAddStep": "Add a step",
|
||||
"planAddRepeat": "Add a repeat",
|
||||
"planRepeat": "{count}×",
|
||||
"@planRepeat": {"placeholders": {"count": {"type": "int"}}},
|
||||
"planFewer": "Fewer rounds",
|
||||
"planMore": "More rounds",
|
||||
"planBlockMenu": "More",
|
||||
"planMoveUp": "Move up",
|
||||
"planMoveDown": "Move down",
|
||||
"planRemove": "Remove",
|
||||
"planDelete": "Delete",
|
||||
"planDeleteTitle": "Delete this plan?",
|
||||
"planDeleteMessage": "It's deleted on all your devices; workouts recorded with it keep their steps.",
|
||||
"planDeleted": "Plan deleted",
|
||||
"planSteps": "{count, plural, =1{1 step} other{{count} steps}}",
|
||||
"@planSteps": {"placeholders": {"count": {"type": "int"}}},
|
||||
"stepTitle": "Step",
|
||||
"stepKind": "Kind",
|
||||
"stepWarmUp": "Warm-up",
|
||||
"stepWork": "Work",
|
||||
"stepRecovery": "Recovery",
|
||||
"stepCoolDown": "Cool-down",
|
||||
"stepEnds": "Ends",
|
||||
"stepEndTime": "After a time",
|
||||
"stepEndDistance": "After a distance",
|
||||
"stepEndLap": "With a lap tap",
|
||||
"stepTimeLabel": "Time (min:s)",
|
||||
"stepMetresLabel": "Metres",
|
||||
"stepTarget": "Target",
|
||||
"stepZone": "Zone",
|
||||
"stepDone": "Done",
|
||||
"stepMinutes": "{minutes} min",
|
||||
"@stepMinutes": {"placeholders": {"minutes": {"type": "int"}}},
|
||||
"stepSeconds": "{seconds} s",
|
||||
"@stepSeconds": {"placeholders": {"seconds": {"type": "int"}}},
|
||||
"stepMinutesSeconds": "{minutes} min {seconds} s",
|
||||
"@stepMinutesSeconds": {"placeholders": {"minutes": {"type": "int"}, "seconds": {"type": "int"}}},
|
||||
"stepMetres": "{metres} m",
|
||||
"@stepMetres": {"placeholders": {"metres": {"type": "int"}}},
|
||||
"stepKilometres": "{km} km",
|
||||
"@stepKilometres": {"placeholders": {"km": {"type": "String"}}},
|
||||
"stepUntilLap": "until a lap tap",
|
||||
"targetNone": "None",
|
||||
"targetFrom": "From",
|
||||
"targetTo": "To",
|
||||
"targetKindPace": "Pace (per km)",
|
||||
"targetKindSpeed": "Speed (km/h)",
|
||||
"targetKindHeartRate": "Heart rate (bpm)",
|
||||
"targetKindHeartZone": "Heart-rate zone",
|
||||
"targetKindSplit": "Split (per 500 m)",
|
||||
"targetKindWatts": "Power (W)",
|
||||
"targetKindStrokeRate": "Stroke rate (per min)",
|
||||
"targetPace": "{from}–{to} per km",
|
||||
"@targetPace": {"placeholders": {"from": {"type": "String"}, "to": {"type": "String"}}},
|
||||
"targetSpeed": "{from}–{to} km/h",
|
||||
"@targetSpeed": {"placeholders": {"from": {"type": "String"}, "to": {"type": "String"}}},
|
||||
"targetHeartRate": "{from}–{to} bpm",
|
||||
"@targetHeartRate": {"placeholders": {"from": {"type": "String"}, "to": {"type": "String"}}},
|
||||
"targetZone": "Zone {zone}",
|
||||
"@targetZone": {"placeholders": {"zone": {"type": "int"}}},
|
||||
"targetSplit": "{from}–{to} per 500 m",
|
||||
"@targetSplit": {"placeholders": {"from": {"type": "String"}, "to": {"type": "String"}}},
|
||||
"targetWatts": "{from}–{to} W",
|
||||
"@targetWatts": {"placeholders": {"from": {"type": "String"}, "to": {"type": "String"}}},
|
||||
"targetStrokeRate": "{from}–{to} strokes/min",
|
||||
"@targetStrokeRate": {"placeholders": {"from": {"type": "String"}, "to": {"type": "String"}}},
|
||||
"daysEmptyTitle": "No days yet",
|
||||
"daysEmptyHint": "The days and nights your Polar Loop counts show here.",
|
||||
"daysSteps": "{steps, plural, =1{1 step} other{{steps} steps}}",
|
||||
|
|
|
|||
|
|
@ -1468,6 +1468,426 @@ abstract class AppLocalizations {
|
|||
/// **'Days'**
|
||||
String get homeDays;
|
||||
|
||||
/// No description provided for @homePlans.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Plans'**
|
||||
String get homePlans;
|
||||
|
||||
/// No description provided for @plansNew.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'New plan'**
|
||||
String get plansNew;
|
||||
|
||||
/// No description provided for @plansNewEmpty.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'From nothing'**
|
||||
String get plansNewEmpty;
|
||||
|
||||
/// No description provided for @plansEmptyTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No plans yet'**
|
||||
String get plansEmptyTitle;
|
||||
|
||||
/// No description provided for @plansEmptyHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'A plan\'s steps lead you through intervals as you record, with a tone, a vibration and a voice at each change. Start from a template, or from nothing.'**
|
||||
String get plansEmptyHint;
|
||||
|
||||
/// No description provided for @planLocked.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'A plan that doesn\'t open here'**
|
||||
String get planLocked;
|
||||
|
||||
/// No description provided for @planTemplateFourByFour.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'4×4 minutes'**
|
||||
String get planTemplateFourByFour;
|
||||
|
||||
/// No description provided for @planTemplateTabata.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Tabata'**
|
||||
String get planTemplateTabata;
|
||||
|
||||
/// No description provided for @planTemplateFourHundreds.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'10×400 m'**
|
||||
String get planTemplateFourHundreds;
|
||||
|
||||
/// No description provided for @planTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Plan'**
|
||||
String get planTitle;
|
||||
|
||||
/// No description provided for @planNewTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'New plan'**
|
||||
String get planNewTitle;
|
||||
|
||||
/// No description provided for @planName.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Name'**
|
||||
String get planName;
|
||||
|
||||
/// No description provided for @planActivity.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'For'**
|
||||
String get planActivity;
|
||||
|
||||
/// No description provided for @planActivityAny.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Any activity'**
|
||||
String get planActivityAny;
|
||||
|
||||
/// No description provided for @planOffTarget.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Tell me when I\'m off target'**
|
||||
String get planOffTarget;
|
||||
|
||||
/// No description provided for @planOffTargetHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'A short cue at most every 30 seconds.'**
|
||||
String get planOffTargetHint;
|
||||
|
||||
/// No description provided for @planSave.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Save'**
|
||||
String get planSave;
|
||||
|
||||
/// No description provided for @planNotSaved.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The plan wasn\'t saved.'**
|
||||
String get planNotSaved;
|
||||
|
||||
/// No description provided for @planAddStep.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Add a step'**
|
||||
String get planAddStep;
|
||||
|
||||
/// No description provided for @planAddRepeat.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Add a repeat'**
|
||||
String get planAddRepeat;
|
||||
|
||||
/// No description provided for @planRepeat.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{count}×'**
|
||||
String planRepeat(int count);
|
||||
|
||||
/// No description provided for @planFewer.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Fewer rounds'**
|
||||
String get planFewer;
|
||||
|
||||
/// No description provided for @planMore.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'More rounds'**
|
||||
String get planMore;
|
||||
|
||||
/// No description provided for @planBlockMenu.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'More'**
|
||||
String get planBlockMenu;
|
||||
|
||||
/// No description provided for @planMoveUp.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Move up'**
|
||||
String get planMoveUp;
|
||||
|
||||
/// No description provided for @planMoveDown.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Move down'**
|
||||
String get planMoveDown;
|
||||
|
||||
/// No description provided for @planRemove.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Remove'**
|
||||
String get planRemove;
|
||||
|
||||
/// No description provided for @planDelete.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Delete'**
|
||||
String get planDelete;
|
||||
|
||||
/// No description provided for @planDeleteTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Delete this plan?'**
|
||||
String get planDeleteTitle;
|
||||
|
||||
/// No description provided for @planDeleteMessage.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'It\'s deleted on all your devices; workouts recorded with it keep their steps.'**
|
||||
String get planDeleteMessage;
|
||||
|
||||
/// No description provided for @planDeleted.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Plan deleted'**
|
||||
String get planDeleted;
|
||||
|
||||
/// No description provided for @planSteps.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{count, plural, =1{1 step} other{{count} steps}}'**
|
||||
String planSteps(int count);
|
||||
|
||||
/// No description provided for @stepTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Step'**
|
||||
String get stepTitle;
|
||||
|
||||
/// No description provided for @stepKind.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Kind'**
|
||||
String get stepKind;
|
||||
|
||||
/// No description provided for @stepWarmUp.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Warm-up'**
|
||||
String get stepWarmUp;
|
||||
|
||||
/// No description provided for @stepWork.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Work'**
|
||||
String get stepWork;
|
||||
|
||||
/// No description provided for @stepRecovery.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Recovery'**
|
||||
String get stepRecovery;
|
||||
|
||||
/// No description provided for @stepCoolDown.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Cool-down'**
|
||||
String get stepCoolDown;
|
||||
|
||||
/// No description provided for @stepEnds.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Ends'**
|
||||
String get stepEnds;
|
||||
|
||||
/// No description provided for @stepEndTime.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'After a time'**
|
||||
String get stepEndTime;
|
||||
|
||||
/// No description provided for @stepEndDistance.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'After a distance'**
|
||||
String get stepEndDistance;
|
||||
|
||||
/// No description provided for @stepEndLap.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'With a lap tap'**
|
||||
String get stepEndLap;
|
||||
|
||||
/// No description provided for @stepTimeLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Time (min:s)'**
|
||||
String get stepTimeLabel;
|
||||
|
||||
/// No description provided for @stepMetresLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Metres'**
|
||||
String get stepMetresLabel;
|
||||
|
||||
/// No description provided for @stepTarget.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Target'**
|
||||
String get stepTarget;
|
||||
|
||||
/// No description provided for @stepZone.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Zone'**
|
||||
String get stepZone;
|
||||
|
||||
/// No description provided for @stepDone.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Done'**
|
||||
String get stepDone;
|
||||
|
||||
/// No description provided for @stepMinutes.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{minutes} min'**
|
||||
String stepMinutes(int minutes);
|
||||
|
||||
/// No description provided for @stepSeconds.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{seconds} s'**
|
||||
String stepSeconds(int seconds);
|
||||
|
||||
/// No description provided for @stepMinutesSeconds.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{minutes} min {seconds} s'**
|
||||
String stepMinutesSeconds(int minutes, int seconds);
|
||||
|
||||
/// No description provided for @stepMetres.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{metres} m'**
|
||||
String stepMetres(int metres);
|
||||
|
||||
/// No description provided for @stepKilometres.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{km} km'**
|
||||
String stepKilometres(String km);
|
||||
|
||||
/// No description provided for @stepUntilLap.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'until a lap tap'**
|
||||
String get stepUntilLap;
|
||||
|
||||
/// No description provided for @targetNone.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'None'**
|
||||
String get targetNone;
|
||||
|
||||
/// No description provided for @targetFrom.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'From'**
|
||||
String get targetFrom;
|
||||
|
||||
/// No description provided for @targetTo.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'To'**
|
||||
String get targetTo;
|
||||
|
||||
/// No description provided for @targetKindPace.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Pace (per km)'**
|
||||
String get targetKindPace;
|
||||
|
||||
/// No description provided for @targetKindSpeed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Speed (km/h)'**
|
||||
String get targetKindSpeed;
|
||||
|
||||
/// No description provided for @targetKindHeartRate.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Heart rate (bpm)'**
|
||||
String get targetKindHeartRate;
|
||||
|
||||
/// No description provided for @targetKindHeartZone.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Heart-rate zone'**
|
||||
String get targetKindHeartZone;
|
||||
|
||||
/// No description provided for @targetKindSplit.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Split (per 500 m)'**
|
||||
String get targetKindSplit;
|
||||
|
||||
/// No description provided for @targetKindWatts.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Power (W)'**
|
||||
String get targetKindWatts;
|
||||
|
||||
/// No description provided for @targetKindStrokeRate.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Stroke rate (per min)'**
|
||||
String get targetKindStrokeRate;
|
||||
|
||||
/// No description provided for @targetPace.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{from}–{to} per km'**
|
||||
String targetPace(String from, String to);
|
||||
|
||||
/// No description provided for @targetSpeed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{from}–{to} km/h'**
|
||||
String targetSpeed(String from, String to);
|
||||
|
||||
/// No description provided for @targetHeartRate.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{from}–{to} bpm'**
|
||||
String targetHeartRate(String from, String to);
|
||||
|
||||
/// No description provided for @targetZone.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Zone {zone}'**
|
||||
String targetZone(int zone);
|
||||
|
||||
/// No description provided for @targetSplit.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{from}–{to} per 500 m'**
|
||||
String targetSplit(String from, String to);
|
||||
|
||||
/// No description provided for @targetWatts.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{from}–{to} W'**
|
||||
String targetWatts(String from, String to);
|
||||
|
||||
/// No description provided for @targetStrokeRate.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{from}–{to} strokes/min'**
|
||||
String targetStrokeRate(String from, String to);
|
||||
|
||||
/// No description provided for @daysEmptyTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
|
|||
|
|
@ -834,6 +834,253 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get homeDays => 'Tage';
|
||||
|
||||
@override
|
||||
String get homePlans => 'Pläne';
|
||||
|
||||
@override
|
||||
String get plansNew => 'Neuer Plan';
|
||||
|
||||
@override
|
||||
String get plansNewEmpty => 'Leer';
|
||||
|
||||
@override
|
||||
String get plansEmptyTitle => 'Noch keine Pläne';
|
||||
|
||||
@override
|
||||
String get plansEmptyHint =>
|
||||
'Die Schritte eines Plans führen dich beim Aufzeichnen durch Intervalle, mit Ton, Vibration und Stimme bei jedem Wechsel. Beginne mit einer Vorlage oder leer.';
|
||||
|
||||
@override
|
||||
String get planLocked => 'Ein Plan, der sich hier nicht öffnet';
|
||||
|
||||
@override
|
||||
String get planTemplateFourByFour => '4×4 Minuten';
|
||||
|
||||
@override
|
||||
String get planTemplateTabata => 'Tabata';
|
||||
|
||||
@override
|
||||
String get planTemplateFourHundreds => '10×400 m';
|
||||
|
||||
@override
|
||||
String get planTitle => 'Plan';
|
||||
|
||||
@override
|
||||
String get planNewTitle => 'Neuer Plan';
|
||||
|
||||
@override
|
||||
String get planName => 'Name';
|
||||
|
||||
@override
|
||||
String get planActivity => 'Für';
|
||||
|
||||
@override
|
||||
String get planActivityAny => 'Jede Aktivität';
|
||||
|
||||
@override
|
||||
String get planOffTarget => 'Sagen, wenn ich vom Ziel abweiche';
|
||||
|
||||
@override
|
||||
String get planOffTargetHint =>
|
||||
'Ein kurzer Hinweis, höchstens alle 30 Sekunden.';
|
||||
|
||||
@override
|
||||
String get planSave => 'Speichern';
|
||||
|
||||
@override
|
||||
String get planNotSaved => 'Der Plan wurde nicht gespeichert.';
|
||||
|
||||
@override
|
||||
String get planAddStep => 'Schritt hinzufügen';
|
||||
|
||||
@override
|
||||
String get planAddRepeat => 'Wiederholung hinzufügen';
|
||||
|
||||
@override
|
||||
String planRepeat(int count) {
|
||||
return '$count×';
|
||||
}
|
||||
|
||||
@override
|
||||
String get planFewer => 'Weniger Runden';
|
||||
|
||||
@override
|
||||
String get planMore => 'Mehr Runden';
|
||||
|
||||
@override
|
||||
String get planBlockMenu => 'Mehr';
|
||||
|
||||
@override
|
||||
String get planMoveUp => 'Nach oben';
|
||||
|
||||
@override
|
||||
String get planMoveDown => 'Nach unten';
|
||||
|
||||
@override
|
||||
String get planRemove => 'Entfernen';
|
||||
|
||||
@override
|
||||
String get planDelete => 'Löschen';
|
||||
|
||||
@override
|
||||
String get planDeleteTitle => 'Diesen Plan löschen?';
|
||||
|
||||
@override
|
||||
String get planDeleteMessage =>
|
||||
'Er wird auf all deinen Geräten gelöscht; Trainings, die mit ihm aufgezeichnet wurden, behalten ihre Schritte.';
|
||||
|
||||
@override
|
||||
String get planDeleted => 'Plan gelöscht';
|
||||
|
||||
@override
|
||||
String planSteps(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count Schritte',
|
||||
one: '1 Schritt',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get stepTitle => 'Schritt';
|
||||
|
||||
@override
|
||||
String get stepKind => 'Art';
|
||||
|
||||
@override
|
||||
String get stepWarmUp => 'Aufwärmen';
|
||||
|
||||
@override
|
||||
String get stepWork => 'Belastung';
|
||||
|
||||
@override
|
||||
String get stepRecovery => 'Erholung';
|
||||
|
||||
@override
|
||||
String get stepCoolDown => 'Auslaufen';
|
||||
|
||||
@override
|
||||
String get stepEnds => 'Endet';
|
||||
|
||||
@override
|
||||
String get stepEndTime => 'Nach einer Zeit';
|
||||
|
||||
@override
|
||||
String get stepEndDistance => 'Nach einer Strecke';
|
||||
|
||||
@override
|
||||
String get stepEndLap => 'Mit der Rundentaste';
|
||||
|
||||
@override
|
||||
String get stepTimeLabel => 'Zeit (min:s)';
|
||||
|
||||
@override
|
||||
String get stepMetresLabel => 'Meter';
|
||||
|
||||
@override
|
||||
String get stepTarget => 'Ziel';
|
||||
|
||||
@override
|
||||
String get stepZone => 'Zone';
|
||||
|
||||
@override
|
||||
String get stepDone => 'Fertig';
|
||||
|
||||
@override
|
||||
String stepMinutes(int minutes) {
|
||||
return '$minutes min';
|
||||
}
|
||||
|
||||
@override
|
||||
String stepSeconds(int seconds) {
|
||||
return '$seconds s';
|
||||
}
|
||||
|
||||
@override
|
||||
String stepMinutesSeconds(int minutes, int seconds) {
|
||||
return '$minutes min $seconds s';
|
||||
}
|
||||
|
||||
@override
|
||||
String stepMetres(int metres) {
|
||||
return '$metres m';
|
||||
}
|
||||
|
||||
@override
|
||||
String stepKilometres(String km) {
|
||||
return '$km km';
|
||||
}
|
||||
|
||||
@override
|
||||
String get stepUntilLap => 'bis zur Rundentaste';
|
||||
|
||||
@override
|
||||
String get targetNone => 'Keins';
|
||||
|
||||
@override
|
||||
String get targetFrom => 'Von';
|
||||
|
||||
@override
|
||||
String get targetTo => 'Bis';
|
||||
|
||||
@override
|
||||
String get targetKindPace => 'Tempo (pro km)';
|
||||
|
||||
@override
|
||||
String get targetKindSpeed => 'Geschwindigkeit (km/h)';
|
||||
|
||||
@override
|
||||
String get targetKindHeartRate => 'Herzfrequenz (Schläge/min)';
|
||||
|
||||
@override
|
||||
String get targetKindHeartZone => 'Herzfrequenzzone';
|
||||
|
||||
@override
|
||||
String get targetKindSplit => 'Split (pro 500 m)';
|
||||
|
||||
@override
|
||||
String get targetKindWatts => 'Leistung (W)';
|
||||
|
||||
@override
|
||||
String get targetKindStrokeRate => 'Schlagzahl (pro min)';
|
||||
|
||||
@override
|
||||
String targetPace(String from, String to) {
|
||||
return '$from–$to pro km';
|
||||
}
|
||||
|
||||
@override
|
||||
String targetSpeed(String from, String to) {
|
||||
return '$from–$to km/h';
|
||||
}
|
||||
|
||||
@override
|
||||
String targetHeartRate(String from, String to) {
|
||||
return '$from–$to Schläge/min';
|
||||
}
|
||||
|
||||
@override
|
||||
String targetZone(int zone) {
|
||||
return 'Zone $zone';
|
||||
}
|
||||
|
||||
@override
|
||||
String targetSplit(String from, String to) {
|
||||
return '$from–$to pro 500 m';
|
||||
}
|
||||
|
||||
@override
|
||||
String targetWatts(String from, String to) {
|
||||
return '$from–$to W';
|
||||
}
|
||||
|
||||
@override
|
||||
String targetStrokeRate(String from, String to) {
|
||||
return '$from–$to Schläge/min';
|
||||
}
|
||||
|
||||
@override
|
||||
String get daysEmptyTitle => 'Noch keine Tage';
|
||||
|
||||
|
|
|
|||
|
|
@ -828,6 +828,252 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get homeDays => 'Days';
|
||||
|
||||
@override
|
||||
String get homePlans => 'Plans';
|
||||
|
||||
@override
|
||||
String get plansNew => 'New plan';
|
||||
|
||||
@override
|
||||
String get plansNewEmpty => 'From nothing';
|
||||
|
||||
@override
|
||||
String get plansEmptyTitle => 'No plans yet';
|
||||
|
||||
@override
|
||||
String get plansEmptyHint =>
|
||||
'A plan\'s steps lead you through intervals as you record, with a tone, a vibration and a voice at each change. Start from a template, or from nothing.';
|
||||
|
||||
@override
|
||||
String get planLocked => 'A plan that doesn\'t open here';
|
||||
|
||||
@override
|
||||
String get planTemplateFourByFour => '4×4 minutes';
|
||||
|
||||
@override
|
||||
String get planTemplateTabata => 'Tabata';
|
||||
|
||||
@override
|
||||
String get planTemplateFourHundreds => '10×400 m';
|
||||
|
||||
@override
|
||||
String get planTitle => 'Plan';
|
||||
|
||||
@override
|
||||
String get planNewTitle => 'New plan';
|
||||
|
||||
@override
|
||||
String get planName => 'Name';
|
||||
|
||||
@override
|
||||
String get planActivity => 'For';
|
||||
|
||||
@override
|
||||
String get planActivityAny => 'Any activity';
|
||||
|
||||
@override
|
||||
String get planOffTarget => 'Tell me when I\'m off target';
|
||||
|
||||
@override
|
||||
String get planOffTargetHint => 'A short cue at most every 30 seconds.';
|
||||
|
||||
@override
|
||||
String get planSave => 'Save';
|
||||
|
||||
@override
|
||||
String get planNotSaved => 'The plan wasn\'t saved.';
|
||||
|
||||
@override
|
||||
String get planAddStep => 'Add a step';
|
||||
|
||||
@override
|
||||
String get planAddRepeat => 'Add a repeat';
|
||||
|
||||
@override
|
||||
String planRepeat(int count) {
|
||||
return '$count×';
|
||||
}
|
||||
|
||||
@override
|
||||
String get planFewer => 'Fewer rounds';
|
||||
|
||||
@override
|
||||
String get planMore => 'More rounds';
|
||||
|
||||
@override
|
||||
String get planBlockMenu => 'More';
|
||||
|
||||
@override
|
||||
String get planMoveUp => 'Move up';
|
||||
|
||||
@override
|
||||
String get planMoveDown => 'Move down';
|
||||
|
||||
@override
|
||||
String get planRemove => 'Remove';
|
||||
|
||||
@override
|
||||
String get planDelete => 'Delete';
|
||||
|
||||
@override
|
||||
String get planDeleteTitle => 'Delete this plan?';
|
||||
|
||||
@override
|
||||
String get planDeleteMessage =>
|
||||
'It\'s deleted on all your devices; workouts recorded with it keep their steps.';
|
||||
|
||||
@override
|
||||
String get planDeleted => 'Plan deleted';
|
||||
|
||||
@override
|
||||
String planSteps(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count steps',
|
||||
one: '1 step',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get stepTitle => 'Step';
|
||||
|
||||
@override
|
||||
String get stepKind => 'Kind';
|
||||
|
||||
@override
|
||||
String get stepWarmUp => 'Warm-up';
|
||||
|
||||
@override
|
||||
String get stepWork => 'Work';
|
||||
|
||||
@override
|
||||
String get stepRecovery => 'Recovery';
|
||||
|
||||
@override
|
||||
String get stepCoolDown => 'Cool-down';
|
||||
|
||||
@override
|
||||
String get stepEnds => 'Ends';
|
||||
|
||||
@override
|
||||
String get stepEndTime => 'After a time';
|
||||
|
||||
@override
|
||||
String get stepEndDistance => 'After a distance';
|
||||
|
||||
@override
|
||||
String get stepEndLap => 'With a lap tap';
|
||||
|
||||
@override
|
||||
String get stepTimeLabel => 'Time (min:s)';
|
||||
|
||||
@override
|
||||
String get stepMetresLabel => 'Metres';
|
||||
|
||||
@override
|
||||
String get stepTarget => 'Target';
|
||||
|
||||
@override
|
||||
String get stepZone => 'Zone';
|
||||
|
||||
@override
|
||||
String get stepDone => 'Done';
|
||||
|
||||
@override
|
||||
String stepMinutes(int minutes) {
|
||||
return '$minutes min';
|
||||
}
|
||||
|
||||
@override
|
||||
String stepSeconds(int seconds) {
|
||||
return '$seconds s';
|
||||
}
|
||||
|
||||
@override
|
||||
String stepMinutesSeconds(int minutes, int seconds) {
|
||||
return '$minutes min $seconds s';
|
||||
}
|
||||
|
||||
@override
|
||||
String stepMetres(int metres) {
|
||||
return '$metres m';
|
||||
}
|
||||
|
||||
@override
|
||||
String stepKilometres(String km) {
|
||||
return '$km km';
|
||||
}
|
||||
|
||||
@override
|
||||
String get stepUntilLap => 'until a lap tap';
|
||||
|
||||
@override
|
||||
String get targetNone => 'None';
|
||||
|
||||
@override
|
||||
String get targetFrom => 'From';
|
||||
|
||||
@override
|
||||
String get targetTo => 'To';
|
||||
|
||||
@override
|
||||
String get targetKindPace => 'Pace (per km)';
|
||||
|
||||
@override
|
||||
String get targetKindSpeed => 'Speed (km/h)';
|
||||
|
||||
@override
|
||||
String get targetKindHeartRate => 'Heart rate (bpm)';
|
||||
|
||||
@override
|
||||
String get targetKindHeartZone => 'Heart-rate zone';
|
||||
|
||||
@override
|
||||
String get targetKindSplit => 'Split (per 500 m)';
|
||||
|
||||
@override
|
||||
String get targetKindWatts => 'Power (W)';
|
||||
|
||||
@override
|
||||
String get targetKindStrokeRate => 'Stroke rate (per min)';
|
||||
|
||||
@override
|
||||
String targetPace(String from, String to) {
|
||||
return '$from–$to per km';
|
||||
}
|
||||
|
||||
@override
|
||||
String targetSpeed(String from, String to) {
|
||||
return '$from–$to km/h';
|
||||
}
|
||||
|
||||
@override
|
||||
String targetHeartRate(String from, String to) {
|
||||
return '$from–$to bpm';
|
||||
}
|
||||
|
||||
@override
|
||||
String targetZone(int zone) {
|
||||
return 'Zone $zone';
|
||||
}
|
||||
|
||||
@override
|
||||
String targetSplit(String from, String to) {
|
||||
return '$from–$to per 500 m';
|
||||
}
|
||||
|
||||
@override
|
||||
String targetWatts(String from, String to) {
|
||||
return '$from–$to W';
|
||||
}
|
||||
|
||||
@override
|
||||
String targetStrokeRate(String from, String to) {
|
||||
return '$from–$to strokes/min';
|
||||
}
|
||||
|
||||
@override
|
||||
String get daysEmptyTitle => 'No days yet';
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'package:halkyon_train_flutter/src/app/train_frame.dart';
|
|||
import 'package:halkyon_train_flutter/src/app/train_languages.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/day/bloc/days_cubit.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/health/widget/health_screen.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/plan/bloc/plans_cubit.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/recording/bloc/recording_bloc.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/recording/widget/recording_texts.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/workout/bloc/workouts_cubit.dart';
|
||||
|
|
@ -91,6 +92,12 @@ class const TrainPersonApp({
|
|||
return DaysCubit(train.days, train.loop)..open();
|
||||
},
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) {
|
||||
final train = PersonScope.of<TrainPerson>(context);
|
||||
return PlansCubit(train.plans, train.planDeletions)..open();
|
||||
},
|
||||
),
|
||||
],
|
||||
child: _Recording(child: child!),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ import 'package:halkyon_train_flutter/src/feature/loop/data/loop_store.dart';
|
|||
import 'package:halkyon_train_flutter/src/feature/loop/data/polar_loop.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/map/data/map_source_repository.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/map/widget/map_tiles.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/plan/data/plan_repository.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/plan/data/sync_plan_gateway.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/profile/data/profile_repository.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/profile/data/sync_profile_gateway.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/recording/data/android_recorder_platform.dart';
|
||||
|
|
@ -62,6 +64,9 @@ class TrainPerson(
|
|||
/// every device of theirs, read on the phone holding the band.
|
||||
required final DayRepository days,
|
||||
|
||||
/// Their interval plans (D42), sealed and synced.
|
||||
required final PlanRepository plans,
|
||||
|
||||
/// The instance's map source (D22).
|
||||
required final MapSourceRepository maps,
|
||||
|
||||
|
|
@ -85,10 +90,14 @@ class TrainPerson(
|
|||
/// person's data, the workouts then kept.
|
||||
late final deletions = DelayedDeletions(workouts.delete, logName: 'Workouts');
|
||||
|
||||
/// Plans deleted, to be undone for a moment, as workouts are.
|
||||
late final planDeletions = DelayedDeletions(plans.delete, logName: 'Plans');
|
||||
|
||||
/// Closes what was opened for the person.
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
await deletions.dispose();
|
||||
await planDeletions.dispose();
|
||||
await health?.dispose();
|
||||
await loop?.dispose();
|
||||
await _onDispose?.call();
|
||||
|
|
@ -330,6 +339,7 @@ Future<TrainPerson> _openPersonData(
|
|||
keys,
|
||||
person: person.authUserId,
|
||||
);
|
||||
final plans = PlanRepository(SyncPlanGateway(db, sync), keys);
|
||||
// Writes what changed while the app was closed, as it opens (D31).
|
||||
final health = Platform.isAndroid
|
||||
? (HealthConnect(
|
||||
|
|
@ -356,6 +366,7 @@ Future<TrainPerson> _openPersonData(
|
|||
workouts: workouts,
|
||||
profile: profile,
|
||||
days: days,
|
||||
plans: plans,
|
||||
maps: MapSourceRepository(ServerpodMapSourceGateway(client)),
|
||||
recording: recorder != null
|
||||
? (
|
||||
|
|
@ -372,6 +383,7 @@ Future<TrainPerson> _openPersonData(
|
|||
await workouts.dispose();
|
||||
await profile.dispose();
|
||||
await days.dispose();
|
||||
await plans.dispose();
|
||||
await sync.dispose();
|
||||
await db.close();
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,151 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:halkyon_train_flutter/src/core/privacy/train_keys.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/plan/data/plan_repository.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/plan/model/plan.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/workout/model/workout.dart';
|
||||
|
||||
/// What a new step is, and a new repeat: a minute's work, and four rounds
|
||||
/// of a minute's work and a minute's recovery.
|
||||
const _newStep = PlanStep(
|
||||
kind: StepKind.work,
|
||||
end: EndAfterTime(Duration(minutes: 1)),
|
||||
);
|
||||
const _newRepeat = PlanBlock(
|
||||
repeat: 4,
|
||||
steps: [
|
||||
_newStep,
|
||||
PlanStep(kind: StepKind.recovery, end: EndAfterTime(Duration(minutes: 1))),
|
||||
],
|
||||
);
|
||||
|
||||
/// A plan being made or changed (ADR 0030, D42): its name, what it is for,
|
||||
/// its blocks and steps, kept once saved; [id] null for a new one.
|
||||
class PlanEditorCubit(
|
||||
final PlanRepository _plans, {
|
||||
required final PlanContent plan,
|
||||
final String? id,
|
||||
}) extends Cubit<PlanEditorState> {
|
||||
this : super(PlanEditorState(plan: plan, id: id));
|
||||
|
||||
void rename(String name) => _edit(
|
||||
(plan) => _plan(plan, name: name),
|
||||
);
|
||||
|
||||
void setActivity(Activity? activity) => _edit(
|
||||
(plan) => PlanContent(
|
||||
name: plan.name,
|
||||
blocks: plan.blocks,
|
||||
activity: activity,
|
||||
offTargetCues: plan.offTargetCues,
|
||||
),
|
||||
);
|
||||
|
||||
void setOffTargetCues(bool on) =>
|
||||
_edit((plan) => _plan(plan, offTargetCues: on));
|
||||
|
||||
/// Adds a step: at the end of block [block], or as a block of its own.
|
||||
void addStep({int? block}) => _blocks((blocks) {
|
||||
if (block == null) {
|
||||
blocks.add(const PlanBlock(steps: [_newStep]));
|
||||
} else {
|
||||
blocks[block] = PlanBlock(
|
||||
repeat: blocks[block].repeat,
|
||||
steps: [...blocks[block].steps, _newStep],
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
void addRepeat() => _blocks((blocks) => blocks.add(_newRepeat));
|
||||
|
||||
void setStep(int block, int index, PlanStep step) => _blocks((blocks) {
|
||||
blocks[block] = PlanBlock(
|
||||
repeat: blocks[block].repeat,
|
||||
steps: [...blocks[block].steps]..[index] = step,
|
||||
);
|
||||
});
|
||||
|
||||
/// Removes a step; its block goes with its last.
|
||||
void removeStep(int block, int index) => _blocks((blocks) {
|
||||
final steps = [...blocks[block].steps]..removeAt(index);
|
||||
if (steps.isEmpty) {
|
||||
blocks.removeAt(block);
|
||||
} else {
|
||||
blocks[block] = PlanBlock(repeat: blocks[block].repeat, steps: steps);
|
||||
}
|
||||
});
|
||||
|
||||
/// Sets how often block [block] runs, once at least.
|
||||
void setRepeat(int block, int repeat) => _blocks((blocks) {
|
||||
blocks[block] = PlanBlock(
|
||||
repeat: repeat < 1 ? 1 : repeat,
|
||||
steps: blocks[block].steps,
|
||||
);
|
||||
});
|
||||
|
||||
void removeBlock(int block) => _blocks((blocks) => blocks.removeAt(block));
|
||||
|
||||
/// Moves block [block] up or down by one.
|
||||
void moveBlock(int block, {required bool up}) => _blocks((blocks) {
|
||||
final to = up ? block - 1 : block + 1;
|
||||
if (to >= 0 && to < blocks.length) {
|
||||
blocks.insert(to, blocks.removeAt(block));
|
||||
}
|
||||
});
|
||||
|
||||
/// Keeps the plan; false where it can't be (no name, no step, or this
|
||||
/// device can't seal).
|
||||
Future<bool> save() async {
|
||||
final plan = state.plan;
|
||||
if (!state.canSave) {
|
||||
return false;
|
||||
}
|
||||
final named = _plan(plan, name: plan.name.trim());
|
||||
try {
|
||||
if (state.id case final id?) {
|
||||
await _plans.update(id, named);
|
||||
} else {
|
||||
emit(PlanEditorState(plan: named, id: await _plans.save(named)));
|
||||
}
|
||||
} on KeysMissing {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void _blocks(void Function(List<PlanBlock> blocks) change) => _edit((plan) {
|
||||
final blocks = [...plan.blocks];
|
||||
change(blocks);
|
||||
return PlanContent(
|
||||
name: plan.name,
|
||||
blocks: blocks,
|
||||
activity: plan.activity,
|
||||
offTargetCues: plan.offTargetCues,
|
||||
);
|
||||
});
|
||||
|
||||
void _edit(PlanContent Function(PlanContent plan) change) =>
|
||||
emit(PlanEditorState(plan: change(state.plan), id: state.id));
|
||||
|
||||
static PlanContent _plan(
|
||||
PlanContent plan, {
|
||||
String? name,
|
||||
bool? offTargetCues,
|
||||
}) => PlanContent(
|
||||
name: name ?? plan.name,
|
||||
blocks: plan.blocks,
|
||||
activity: plan.activity,
|
||||
offTargetCues: offTargetCues ?? plan.offTargetCues,
|
||||
);
|
||||
}
|
||||
|
||||
final class const PlanEditorState({
|
||||
required final PlanContent plan,
|
||||
final String? id,
|
||||
}) with Equatable {
|
||||
/// Whether it has what a plan needs: a name and a step.
|
||||
bool get canSave => plan.name.trim().isNotEmpty && plan.blocks.isNotEmpty;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [plan, id];
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/plan/data/plan_repository.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/plan/model/stored_plan.dart';
|
||||
|
||||
/// The person's plans as home lists them (ADR 0030, D42): by name, those
|
||||
/// this device can't open last; read again whenever they change, here or on
|
||||
/// another device. A deletion leaves the list at once and can be undone for
|
||||
/// a moment ([DelayedDeletions]).
|
||||
class PlansCubit(
|
||||
final PlanRepository _plans,
|
||||
final DelayedDeletions _deletions,
|
||||
) extends Cubit<PlansState> {
|
||||
this : super(const PlansState());
|
||||
|
||||
StreamSubscription<void>? _changes;
|
||||
StreamSubscription<Set<String>>? _pending;
|
||||
|
||||
Future<void> open() async {
|
||||
_changes = _plans.changes.listen((_) => unawaited(_read()));
|
||||
_pending = _deletions.watchPending().listen((pending) {
|
||||
if (!isClosed) {
|
||||
emit(state._with(pending: pending));
|
||||
}
|
||||
});
|
||||
await _read();
|
||||
}
|
||||
|
||||
void delete(String id) => _deletions.delete(id);
|
||||
|
||||
void undo(String id) => _deletions.undo(id);
|
||||
|
||||
Future<void> _read() async {
|
||||
final all = await _plans.all();
|
||||
final opened =
|
||||
[
|
||||
for (final plan in all)
|
||||
if (plan.content != null) plan,
|
||||
]..sort(
|
||||
(a, b) => a.content!.name.toLowerCase().compareTo(
|
||||
b.content!.name.toLowerCase(),
|
||||
),
|
||||
);
|
||||
if (!isClosed) {
|
||||
emit(
|
||||
state._with(
|
||||
all: [
|
||||
...opened,
|
||||
for (final plan in all)
|
||||
if (plan.content == null) plan,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await _changes?.cancel();
|
||||
await _pending?.cancel();
|
||||
await super.close();
|
||||
}
|
||||
}
|
||||
|
||||
final class const PlansState({
|
||||
final bool loaded = false,
|
||||
final List<StoredPlan> _all = const [],
|
||||
final Set<String> _pending = const {},
|
||||
}) with Equatable {
|
||||
/// The plans to list: all but those being deleted.
|
||||
List<StoredPlan> get visible => [
|
||||
for (final plan in _all)
|
||||
if (!_pending.contains(plan.id)) plan,
|
||||
];
|
||||
|
||||
PlansState _with({List<StoredPlan>? all, Set<String>? pending}) => PlansState(
|
||||
loaded: loaded || all != null,
|
||||
all: all ?? _all,
|
||||
pending: pending ?? _pending,
|
||||
);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [loaded, _all, _pending];
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
|
||||
/// A `plan` row as kept: its id and its envelope, sealed (ADR 0030, D7).
|
||||
typedef PlanRow = ({String id, Envelope envelope});
|
||||
|
||||
/// Where the person's interval plans are kept: their synced `plan` rows
|
||||
/// (D42). The repository seals and opens; this only keeps.
|
||||
abstract interface class PlanGateway() {
|
||||
Future<List<PlanRow>> plans();
|
||||
|
||||
Future<void> insert(PlanRow plan);
|
||||
|
||||
/// Replaces the plan [id]'s envelope (an edit); nothing for one gone.
|
||||
Future<void> update(String id, Envelope envelope);
|
||||
|
||||
/// Deletes the plan [id], its payload blanked first (ADR 0002, rule 2).
|
||||
Future<void> delete(String id);
|
||||
|
||||
/// Fires when a sync brought changes from the person's other devices.
|
||||
Stream<void> get remoteChanges;
|
||||
}
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:halkyon_train_flutter/src/feature/plan/model/plan.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/workout/model/workout.dart';
|
||||
|
||||
/// The schema of a plan's payload this app writes and reads.
|
||||
const planSchemaVersion = 1;
|
||||
|
||||
/// [content] as its row's payload, before sealing: UTF-8 JSON, a step's end
|
||||
/// as `{by: time, seconds}`, `{by: distance, metres}` or `{by: lap}`.
|
||||
ByteData encodePlan(PlanContent content) => ByteData.sublistView(
|
||||
Uint8List.fromList(
|
||||
utf8.encode(
|
||||
jsonEncode({
|
||||
'schemaVersion': planSchemaVersion,
|
||||
'name': content.name,
|
||||
'activity': ?content.activity?.name,
|
||||
'offTargetCues': content.offTargetCues,
|
||||
'blocks': [
|
||||
for (final block in content.blocks)
|
||||
{
|
||||
'repeat': block.repeat,
|
||||
'steps': [for (final step in block.steps) planStepJson(step)],
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
/// [step] as JSON, as a plan's payload and the recorder take it.
|
||||
Map<String, Object?> planStepJson(PlanStep step) => {
|
||||
'kind': step.kind.name,
|
||||
'end': switch (step.end) {
|
||||
EndAfterTime(:final time) => {'by': 'time', 'seconds': time.inSeconds},
|
||||
EndAfterDistance(:final metres) => {'by': 'distance', 'metres': metres},
|
||||
EndWithLap() => {'by': 'lap'},
|
||||
},
|
||||
if (step.target case final target?)
|
||||
'target': {
|
||||
'kind': target.kind.name,
|
||||
'low': target.low,
|
||||
'high': target.high,
|
||||
},
|
||||
};
|
||||
|
||||
/// The plan [payload] holds, opened; null for one of a newer schema or one
|
||||
/// it can't read. A step it can't read is left out; a target it doesn't
|
||||
/// know is none.
|
||||
PlanContent? decodePlan(ByteData payload) {
|
||||
try {
|
||||
final json = jsonDecode(
|
||||
utf8.decode(Uint8List.sublistView(payload)),
|
||||
) as Map<String, Object?>;
|
||||
final version = json['schemaVersion']! as int;
|
||||
if (version > planSchemaVersion) {
|
||||
log('A plan of schema $version skipped', name: 'Plan');
|
||||
return null;
|
||||
}
|
||||
return PlanContent(
|
||||
name: json['name']! as String,
|
||||
activity: Activity.values.asNameMap()[json['activity']],
|
||||
offTargetCues: json['offTargetCues'] as bool? ?? true,
|
||||
blocks: [
|
||||
for (final block in json['blocks']! as List<Object?>)
|
||||
if (block
|
||||
case {
|
||||
'repeat': final int repeat,
|
||||
'steps': final List<Object?> steps,
|
||||
}
|
||||
when repeat > 0)
|
||||
PlanBlock(
|
||||
repeat: repeat,
|
||||
steps: [for (final step in steps) ?_stepOf(step)],
|
||||
),
|
||||
],
|
||||
);
|
||||
} on Object catch (error) {
|
||||
log('A plan not read: ${error.runtimeType}', name: 'Plan');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
PlanStep? _stepOf(Object? json) {
|
||||
if (json is! Map<String, Object?>) {
|
||||
return null;
|
||||
}
|
||||
final kind = StepKind.values.asNameMap()[json['kind']];
|
||||
final end = switch (json['end']) {
|
||||
{'by': 'time', 'seconds': final int seconds} when seconds > 0 =>
|
||||
EndAfterTime(Duration(seconds: seconds)),
|
||||
{'by': 'distance', 'metres': final int metres} when metres > 0 =>
|
||||
EndAfterDistance(metres),
|
||||
{'by': 'lap'} => const EndWithLap(),
|
||||
_ => null,
|
||||
};
|
||||
if (kind == null || end == null) {
|
||||
return null;
|
||||
}
|
||||
return PlanStep(
|
||||
kind: kind,
|
||||
end: end,
|
||||
target: switch (json['target']) {
|
||||
{
|
||||
'kind': final String name,
|
||||
'low': final num low,
|
||||
'high': final num high,
|
||||
}
|
||||
when TargetKind.values.asNameMap()[name] != null =>
|
||||
Target(
|
||||
TargetKind.values.byName(name),
|
||||
low.toDouble(),
|
||||
high.toDouble(),
|
||||
),
|
||||
_ => null,
|
||||
},
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue