Files
GameTime/lib/infrastructure/local/drift_repositories.dart
Blomios 75f52200a2 feat(exécution): refonte des chronos de séance et carte exercice actif (ticket #90)
Réorganise l'écran d'exécution autour d'une carte "Exercice actif" sous
le compteur SÉRIE X/Y (nom, médias, temps de série, action "Démarrer
l'exercice"). Le timer de série devient un état persistant dédié (plus
un DateTime UI volatile), pause-aware, et "Démarrer l'exercice" lance
en une action tous les chronos qui doivent démarrer en début de série
(série, première étape temps, score chrono si stopwatch).

Implémentation restée non commitée depuis sa réalisation ; QA a validé
les vérifications statiques exécutables en sandbox (dart analyze,
git diff --check). flutter analyze / flutter test restent à relancer
dans un environnement avec cache Flutter SDK accessible.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 22:26:28 +02:00

3885 lines
135 KiB
Dart

import 'dart:convert';
import 'package:drift/drift.dart';
import '../../application/application.dart';
import '../../domain/domain.dart' as domain;
import 'app_database.dart' as db;
final class DriftExerciseRepository implements ExerciseRepository {
const DriftExerciseRepository(this.database);
final db.AppDatabase database;
@override
Future<domain.Exercise?> findById(String id) async {
final row = await (database.select(
database.exercises,
)..where((table) => table.id.equals(id))).getSingleOrNull();
return row == null
? null
: _exerciseFromRow(
row,
await _imageMediaIdsForExercise(id),
await _stepsForExercise(id),
);
}
@override
Future<List<domain.Exercise>> listActive() async {
final rows =
await (database.select(database.exercises)
..where(
(table) => table.deletedAt.isNull() & table.archivedAt.isNull(),
)
..orderBy([(table) => OrderingTerm.asc(table.name)]))
.get();
final exercises = <domain.Exercise>[];
for (final row in rows) {
exercises.add(
_exerciseFromRow(
row,
await _imageMediaIdsForExercise(row.id),
await _stepsForExercise(row.id),
),
);
}
return exercises;
}
@override
Future<bool> isReferencedByProgram(String id) async {
final row =
await (database.select(database.programExercises)
..where(
(table) =>
table.deletedAt.isNull() &
table.sourceExerciseId.equals(id),
)
..limit(1))
.getSingleOrNull();
return row != null;
}
@override
Future<void> save(domain.Exercise exercise) async {
await database.transaction(() async {
await _upsertWithChangeLog(
database: database,
tableName: 'exercises',
entityType: 'Exercise',
metadata: exercise.metadata,
write: () => database
.into(database.exercises)
.insertOnConflictUpdate(_exerciseCompanion(exercise)),
);
await _replaceExerciseImages(database, exercise);
await _replaceExerciseSteps(database, exercise);
});
}
Future<List<String>> _imageMediaIdsForExercise(String exerciseId) async {
final rows =
await (database.select(database.exerciseImages)
..where(
(table) =>
table.exerciseId.equals(exerciseId) &
table.deletedAt.isNull(),
)
..orderBy([(table) => OrderingTerm.asc(table.position)]))
.get();
return rows.map((row) => row.mediaAssetId).toList();
}
Future<List<domain.ExerciseStep>> _stepsForExercise(String exerciseId) async {
final rows =
await (database.select(database.exerciseSteps)
..where(
(table) =>
table.exerciseId.equals(exerciseId) &
table.deletedAt.isNull(),
)
..orderBy([(table) => OrderingTerm.asc(table.position)]))
.get();
return rows.map(_exerciseStepFromRow).toList();
}
}
final class DriftMediaAssetRepository implements MediaAssetRepository {
const DriftMediaAssetRepository(this.database);
final db.AppDatabase database;
@override
Future<domain.MediaAsset?> findById(String id) async {
final row = await (database.select(
database.mediaAssets,
)..where((table) => table.id.equals(id))).getSingleOrNull();
return row == null ? null : _mediaAssetFromRow(row);
}
@override
Future<List<domain.MediaAsset>> listActive() async {
final rows = await (database.select(
database.mediaAssets,
)..where((table) => table.deletedAt.isNull())).get();
return rows.map(_mediaAssetFromRow).toList();
}
@override
Future<void> save(domain.MediaAsset mediaAsset) async {
await _upsertWithChangeLog(
database: database,
tableName: 'media_assets',
entityType: 'MediaAsset',
metadata: mediaAsset.metadata,
write: () => database
.into(database.mediaAssets)
.insertOnConflictUpdate(_mediaAssetCompanion(mediaAsset)),
);
}
}
final class DriftOnlineAccountRepository implements OnlineAccountRepository {
const DriftOnlineAccountRepository(this.database);
final db.AppDatabase database;
@override
Future<domain.UserAccountSession?> currentSession() async {
final row =
await (database.select(database.onlineAccountSessions)
..orderBy([(table) => OrderingTerm.desc(table.updatedAt)])
..limit(1))
.getSingleOrNull();
return row == null ? null : _userAccountSessionFromRow(row);
}
@override
Future<void> saveSession(domain.UserAccountSession session) async {
await database
.into(database.onlineAccountSessions)
.insertOnConflictUpdate(_userAccountSessionCompanion(session));
}
@override
Future<void> clearSession() async {
await database.delete(database.onlineAccountSessions).go();
}
}
final class DriftSyncMetadataRepository implements SyncMetadataRepository {
const DriftSyncMetadataRepository(this.database);
static const _id = 'singleton';
final db.AppDatabase database;
@override
Future<SyncMetadataSnapshot> read() async {
final row = await (database.select(
database.syncMetadataEntries,
)..where((table) => table.id.equals(_id))).getSingleOrNull();
if (row == null) {
return const SyncMetadataSnapshot();
}
return SyncMetadataSnapshot(
serverCursor: row.serverCursor,
lastSuccessfulSyncAt: _utcOrNull(row.lastSuccessfulSyncAt),
lastAttemptAt: _utcOrNull(row.lastAttemptAt),
lastFailureAt: _utcOrNull(row.lastFailureAt),
status: _onlineSyncStatusFromDb(row.status),
pendingPushCount: row.pendingPushCount,
);
}
@override
Future<void> save(SyncMetadataSnapshot metadata) async {
await database
.into(database.syncMetadataEntries)
.insertOnConflictUpdate(
db.SyncMetadataEntriesCompanion.insert(
id: const Value(_id),
serverCursor: Value<String?>(metadata.serverCursor),
lastSuccessfulSyncAt: Value<DateTime?>(
_utcOrNull(metadata.lastSuccessfulSyncAt),
),
lastAttemptAt: Value<DateTime?>(_utcOrNull(metadata.lastAttemptAt)),
lastFailureAt: Value<DateTime?>(_utcOrNull(metadata.lastFailureAt)),
status: Value(_onlineSyncStatusToDb(metadata.status)),
pendingPushCount: Value<int?>(metadata.pendingPushCount),
),
);
}
}
final class DriftRemoteResourceMappingRepository
implements RemoteResourceMappingRepository {
const DriftRemoteResourceMappingRepository(this.database);
final db.AppDatabase database;
@override
Future<RemoteResourceMapping?> find({
required SyncResourceType resourceType,
required String clientId,
}) async {
final row =
await (database.select(database.remoteResourceMappings)..where(
(table) =>
table.resourceType.equals(
_syncResourceTypeToDb(resourceType),
) &
table.clientId.equals(clientId),
))
.getSingleOrNull();
return row == null ? null : _remoteResourceMappingFromRow(row);
}
@override
Future<void> save(RemoteResourceMapping mapping) async {
await database
.into(database.remoteResourceMappings)
.insertOnConflictUpdate(_remoteResourceMappingCompanion(mapping));
}
}
final class DriftLocalSyncChangeRepository
implements LocalSyncChangeRepository {
const DriftLocalSyncChangeRepository(this.database);
final db.AppDatabase database;
@override
Future<List<PendingSyncChange>> listPendingChanges() async {
final rows =
await (database.select(database.changeLogEntries)
..where(
(table) =>
table.syncedAt.isNull() &
table.entityType.isIn(_syncableEntityTypes),
)
..orderBy([(table) => OrderingTerm.asc(table.createdAt)]))
.get();
final grouped = <String, List<db.ChangeLogEntry>>{};
for (final row in rows) {
grouped
.putIfAbsent('${row.entityType}:${row.entityId}', () => [])
.add(row);
}
final changes = <PendingSyncChange>[];
for (final group in grouped.values) {
group.sort((left, right) => left.createdAt.compareTo(right.createdAt));
final latest = group.last;
final item = await _pushItemForChange(latest);
if (item == null) {
continue;
}
changes.add(
PendingSyncChange(
changeLogIds: group.map((row) => row.id).toList(),
item: item,
),
);
}
return changes;
}
@override
Future<void> markChangesSynced(
List<String> changeLogIds,
DateTime syncedAt,
) async {
if (changeLogIds.isEmpty) {
return;
}
await (database.update(
database.changeLogEntries,
)..where((table) => table.id.isIn(changeLogIds))).write(
db.ChangeLogEntriesCompanion(
syncedAt: Value<DateTime?>(syncedAt.toUtc()),
),
);
}
@override
Future<bool> applyRemoteItem(RemoteSyncedItem item) async {
final localUpdatedAt = await _localUpdatedAt(
item.resourceType,
item.clientId,
);
if (localUpdatedAt != null &&
!item.clientUpdatedAt.isAfter(localUpdatedAt)) {
return false;
}
if (item.deletedAt != null) {
return _applyRemoteDelete(item);
}
return _applyRemotePayload(item);
}
Future<RemoteSyncPushItem?> _pushItemForChange(
db.ChangeLogEntry change,
) async {
final resourceType = _syncResourceTypeFromEntityType(change.entityType);
if (resourceType == null) {
return null;
}
final snapshot = await _localSnapshot(resourceType, change.entityId);
return RemoteSyncPushItem(
resourceType: resourceType,
clientId: change.entityId,
schemaVersion: snapshot.schemaVersion,
clientUpdatedAt: snapshot.updatedAt,
deletedAt: snapshot.deletedAt,
payload: snapshot.payload,
);
}
Future<_LocalSyncSnapshot> _localSnapshot(
SyncResourceType resourceType,
String id,
) async {
return switch (resourceType) {
SyncResourceType.exercise => _exerciseSnapshot(id),
SyncResourceType.program => _programSnapshot(id),
SyncResourceType.workoutTemplate => _workoutTemplateSnapshot(id),
SyncResourceType.workoutHistory => _workoutHistorySnapshot(id),
SyncResourceType.mediaAsset => _mediaAssetSnapshot(id),
};
}
Future<DateTime?> _localUpdatedAt(
SyncResourceType resourceType,
String id,
) async {
final tableName = _tableNameForResourceType(resourceType);
final row = await database
.customSelect(
'SELECT updated_at FROM $tableName WHERE id = ? LIMIT 1',
variables: [Variable<String>(id)],
)
.getSingleOrNull();
final value = row?.data['updated_at'];
return value is DateTime ? value.toUtc() : null;
}
Future<bool> _applyRemoteDelete(RemoteSyncedItem item) async {
final tableName = _tableNameForResourceType(item.resourceType);
final updated = await database.customUpdate(
'UPDATE $tableName SET deleted_at = ?, updated_at = ?, '
"sync_state = 'deleted' WHERE id = ?",
variables: [
Variable<DateTime>(item.deletedAt!.toUtc()),
Variable<DateTime>(item.clientUpdatedAt.toUtc()),
Variable<String>(item.clientId),
],
);
return updated > 0;
}
Future<bool> _applyRemotePayload(RemoteSyncedItem item) async {
switch (item.resourceType) {
case SyncResourceType.exercise:
await database
.into(database.exercises)
.insertOnConflictUpdate(
_exerciseCompanion(_exerciseFromPayload(item)),
);
return true;
case SyncResourceType.mediaAsset:
await database
.into(database.mediaAssets)
.insertOnConflictUpdate(
_mediaAssetCompanion(_mediaAssetFromPayload(item)),
);
return true;
case SyncResourceType.program:
final program = _programFromPayload(item);
await database.transaction(() async {
await database
.into(database.programs)
.insertOnConflictUpdate(_programCompanion(program));
for (final exercise in program.exercises) {
await database
.into(database.programExercises)
.insertOnConflictUpdate(_programExerciseCompanion(exercise));
}
});
return true;
case SyncResourceType.workoutTemplate:
final template = _workoutTemplateFromPayload(item);
await database.transaction(() async {
await database
.into(database.workoutTemplates)
.insertOnConflictUpdate(_workoutTemplateCompanion(template));
for (final program in template.programs) {
await database
.into(database.workoutTemplatePrograms)
.insertOnConflictUpdate(
_workoutTemplateProgramCompanion(program),
);
}
for (final override in template.overrides) {
await database
.into(database.workoutTemplateExerciseOverrides)
.insertOnConflictUpdate(
_workoutTemplateExerciseOverrideCompanion(override),
);
}
});
return true;
case SyncResourceType.workoutHistory:
return false;
}
}
Future<_LocalSyncSnapshot> _exerciseSnapshot(String id) async {
final row = await (database.select(
database.exercises,
)..where((table) => table.id.equals(id))).getSingle();
final exercise = _exerciseFromRow(
row,
await DriftExerciseRepository(database)._imageMediaIdsForExercise(id),
await DriftExerciseRepository(database)._stepsForExercise(id),
);
return _LocalSyncSnapshot.fromMetadata(
exercise.metadata,
_exercisePayload(exercise),
);
}
Future<_LocalSyncSnapshot> _mediaAssetSnapshot(String id) async {
final row = await (database.select(
database.mediaAssets,
)..where((table) => table.id.equals(id))).getSingle();
final asset = _mediaAssetFromRow(row);
return _LocalSyncSnapshot.fromMetadata(
asset.metadata,
_mediaAssetPayload(asset),
);
}
Future<_LocalSyncSnapshot> _programSnapshot(String id) async {
final program = await DriftProgramRepository(database).findById(id);
if (program == null) {
return _deletedFallbackSnapshot(SyncResourceType.program, id);
}
return _LocalSyncSnapshot.fromMetadata(
program.metadata,
_programPayload(program),
);
}
Future<_LocalSyncSnapshot> _workoutTemplateSnapshot(String id) async {
final template = await DriftWorkoutTemplateRepository(
database,
).findById(id);
if (template == null) {
return _deletedFallbackSnapshot(SyncResourceType.workoutTemplate, id);
}
return _LocalSyncSnapshot.fromMetadata(
template.metadata,
_workoutTemplatePayload(template),
);
}
Future<_LocalSyncSnapshot> _workoutHistorySnapshot(String id) async {
final history = await DriftWorkoutHistoryRepository(database).findById(id);
if (history == null) {
return _deletedFallbackSnapshot(SyncResourceType.workoutHistory, id);
}
return _LocalSyncSnapshot.fromMetadata(
history.metadata,
_workoutHistoryPayload(history),
);
}
Future<_LocalSyncSnapshot> _deletedFallbackSnapshot(
SyncResourceType resourceType,
String id,
) async {
final tableName = _tableNameForResourceType(resourceType);
final row = await database
.customSelect(
'SELECT updated_at, deleted_at, schema_version FROM $tableName '
'WHERE id = ? LIMIT 1',
variables: [Variable<String>(id)],
)
.getSingle();
return _LocalSyncSnapshot(
schemaVersion: row.data['schema_version'] as int? ?? 1,
updatedAt: (row.data['updated_at'] as DateTime).toUtc(),
deletedAt: (row.data['deleted_at'] as DateTime?)?.toUtc(),
payload: {'id': id},
);
}
}
final class DriftShareInboxRepository implements ShareInboxRepository {
const DriftShareInboxRepository(this.database);
final db.AppDatabase database;
@override
Future<List<domain.ShareInboxItem>> listAll() async {
final rows = await (database.select(
database.shareInboxItems,
)..orderBy([(table) => OrderingTerm.desc(table.createdAt)])).get();
return rows.map(_shareInboxItemFromRow).toList(growable: false);
}
@override
Future<domain.ShareInboxItem?> findByShareId(String shareId) async {
final row = await (database.select(
database.shareInboxItems,
)..where((table) => table.shareId.equals(shareId))).getSingleOrNull();
return row == null ? null : _shareInboxItemFromRow(row);
}
@override
Future<void> upsert(domain.ShareInboxItem item) async {
await database
.into(database.shareInboxItems)
.insertOnConflictUpdate(_shareInboxItemCompanion(item));
}
@override
Future<void> upsertAll(List<domain.ShareInboxItem> items) async {
await database.batch((batch) {
batch.insertAllOnConflictUpdate(
database.shareInboxItems,
items.map(_shareInboxItemCompanion).toList(),
);
});
}
@override
Future<void> markStatus(
String shareId,
domain.ShareInboxStatus status,
DateTime respondedAt,
) async {
await (database.update(
database.shareInboxItems,
)..where((table) => table.shareId.equals(shareId))).write(
db.ShareInboxItemsCompanion(
status: Value<String>(_shareInboxStatusToDb(status)),
respondedAt: Value<DateTime?>(respondedAt.toUtc()),
updatedAt: Value<DateTime>(respondedAt.toUtc()),
),
);
}
}
final class DriftPendingShareActionRepository
implements PendingShareActionRepository {
const DriftPendingShareActionRepository(this.database);
final db.AppDatabase database;
@override
Future<List<domain.PendingShareAction>> listPending() async {
final rows =
await (database.select(database.pendingShareActions)
..where(
(table) => table.status.isIn([
_pendingShareActionStatusToDb(
domain.PendingShareActionStatus.pending,
),
_pendingShareActionStatusToDb(
domain.PendingShareActionStatus.failed,
),
]),
)
..orderBy([(table) => OrderingTerm.asc(table.createdAt)]))
.get();
return rows.map(_pendingShareActionFromRow).toList(growable: false);
}
@override
Future<void> add(domain.PendingShareAction action) async {
await database
.into(database.pendingShareActions)
.insertOnConflictUpdate(_pendingShareActionCompanion(action));
}
@override
Future<void> markSucceeded(String id, DateTime attemptedAt) async {
await _markActionAttempt(
id,
attemptedAt,
domain.PendingShareActionStatus.succeeded,
);
}
@override
Future<void> markFailed(String id, DateTime attemptedAt) async {
await _markActionAttempt(
id,
attemptedAt,
domain.PendingShareActionStatus.failed,
);
}
Future<void> _markActionAttempt(
String id,
DateTime attemptedAt,
domain.PendingShareActionStatus status,
) async {
final row = await (database.select(
database.pendingShareActions,
)..where((table) => table.id.equals(id))).getSingleOrNull();
await (database.update(
database.pendingShareActions,
)..where((table) => table.id.equals(id))).write(
db.PendingShareActionsCompanion(
lastAttemptAt: Value<DateTime?>(attemptedAt.toUtc()),
attemptCount: Value<int>((row?.attemptCount ?? 0) + 1),
status: Value<String>(_pendingShareActionStatusToDb(status)),
),
);
}
}
final class DriftProgramRepository implements ProgramRepository {
const DriftProgramRepository(this.database);
final db.AppDatabase database;
@override
Future<domain.Program?> findById(String id) async {
final query = database.select(database.programs)
..where((table) => table.id.equals(id) & table.deletedAt.isNull());
final row = await query.getSingleOrNull();
if (row == null) {
return null;
}
final exercises =
await (database.select(database.programExercises)
..where(
(table) =>
table.programId.equals(id) & table.deletedAt.isNull(),
)
..orderBy([(table) => OrderingTerm.asc(table.position)]))
.get();
return _programFromRow(
row,
exercises.map(_programExerciseFromRow).toList(),
);
}
@override
Future<List<domain.Program>> listActive() async {
final rows =
await (database.select(database.programs)
..where((table) => table.deletedAt.isNull())
..orderBy([(table) => OrderingTerm.asc(table.name)]))
.get();
final programs = <domain.Program>[];
for (final row in rows) {
final exercises =
await (database.select(database.programExercises)
..where(
(table) =>
table.programId.equals(row.id) & table.deletedAt.isNull(),
)
..orderBy([(table) => OrderingTerm.asc(table.position)]))
.get();
programs.add(
_programFromRow(row, exercises.map(_programExerciseFromRow).toList()),
);
}
return programs;
}
@override
Future<void> save(domain.Program program) async {
await database.transaction(() async {
await _upsertWithChangeLog(
database: database,
tableName: 'programs',
entityType: 'Program',
metadata: program.metadata,
write: () => database
.into(database.programs)
.insertOnConflictUpdate(_programCompanion(program)),
);
for (final exercise in program.exercises) {
await saveExercise(exercise);
}
});
}
@override
Future<void> saveExercise(domain.ProgramExercise exercise) async {
await _upsertWithChangeLog(
database: database,
tableName: 'program_exercises',
entityType: 'ProgramExercise',
metadata: exercise.metadata,
write: () => database
.into(database.programExercises)
.insertOnConflictUpdate(_programExerciseCompanion(exercise)),
);
}
@override
Future<void> replaceExercises(
domain.Program program,
DateTime deletedAt,
) async {
await database.transaction(() async {
await save(program);
final activeIds = program.exercises
.map((exercise) => exercise.metadata.id)
.toSet();
final staleQuery = database.select(database.programExercises);
if (activeIds.isEmpty) {
staleQuery.where(
(table) =>
table.programId.equals(program.metadata.id) &
table.deletedAt.isNull(),
);
} else {
staleQuery.where(
(table) =>
table.programId.equals(program.metadata.id) &
table.deletedAt.isNull() &
table.id.isNotIn(activeIds),
);
}
await _softDeleteProgramExerciseRows(
database,
await staleQuery.get(),
deletedAt,
);
});
}
}
final class DriftWorkoutTemplateRepository
implements WorkoutTemplateRepository {
const DriftWorkoutTemplateRepository(this.database);
final db.AppDatabase database;
@override
Future<domain.WorkoutTemplate?> findById(String id) async {
final row =
await (database.select(
database.workoutTemplates,
)..where((table) => table.id.equals(id) & table.deletedAt.isNull()))
.getSingleOrNull();
if (row == null) {
return null;
}
final programs =
await (database.select(database.workoutTemplatePrograms)
..where(
(table) =>
table.workoutTemplateId.equals(id) &
table.deletedAt.isNull(),
)
..orderBy([(table) => OrderingTerm.asc(table.position)]))
.get();
final programIds = programs.map((program) => program.id).toSet();
final overrides = programIds.isEmpty
? <db.WorkoutTemplateExerciseOverride>[]
: await (database.select(database.workoutTemplateExerciseOverrides)
..where(
(table) =>
table.workoutTemplateProgramId.isIn(programIds) &
table.deletedAt.isNull(),
))
.get();
return _workoutTemplateFromRow(
row,
programs.map(_workoutTemplateProgramFromRow).toList(),
overrides.map(_workoutTemplateExerciseOverrideFromRow).toList(),
);
}
@override
Future<List<domain.WorkoutTemplate>> listActive() async {
final rows =
await (database.select(database.workoutTemplates)
..where((table) => table.deletedAt.isNull())
..orderBy([(table) => OrderingTerm.asc(table.name)]))
.get();
final templates = <domain.WorkoutTemplate>[];
for (final row in rows) {
final template = await findById(row.id);
if (template != null) {
templates.add(template);
}
}
return templates;
}
@override
Future<void> save(domain.WorkoutTemplate template) async {
await database.transaction(() async {
await _upsertWithChangeLog(
database: database,
tableName: 'workout_templates',
entityType: 'WorkoutTemplate',
metadata: template.metadata,
write: () => database
.into(database.workoutTemplates)
.insertOnConflictUpdate(_workoutTemplateCompanion(template)),
);
for (final program in template.programs) {
await saveProgram(program);
}
for (final override in template.overrides) {
await saveOverride(override);
}
});
}
@override
Future<void> saveProgram(domain.WorkoutTemplateProgram program) async {
await _upsertWithChangeLog(
database: database,
tableName: 'workout_template_programs',
entityType: 'WorkoutTemplateProgram',
metadata: program.metadata,
write: () => database
.into(database.workoutTemplatePrograms)
.insertOnConflictUpdate(_workoutTemplateProgramCompanion(program)),
);
}
@override
Future<void> saveOverride(
domain.WorkoutTemplateExerciseOverride override,
) async {
await _upsertWithChangeLog(
database: database,
tableName: 'workout_template_exercise_overrides',
entityType: 'WorkoutTemplateExerciseOverride',
metadata: override.metadata,
write: () => database
.into(database.workoutTemplateExerciseOverrides)
.insertOnConflictUpdate(
_workoutTemplateExerciseOverrideCompanion(override),
),
);
}
@override
Future<void> replaceComposition(
domain.WorkoutTemplate template,
DateTime deletedAt,
) async {
await database.transaction(() async {
final previousPrograms =
await (database.select(database.workoutTemplatePrograms)..where(
(table) =>
table.workoutTemplateId.equals(template.metadata.id) &
table.deletedAt.isNull(),
))
.get();
final previousProgramIds = previousPrograms
.map((program) => program.id)
.toSet();
await save(template);
final activeProgramIds = template.programs
.map((program) => program.metadata.id)
.toSet();
final staleProgramsQuery = database.select(
database.workoutTemplatePrograms,
);
if (activeProgramIds.isEmpty) {
staleProgramsQuery.where(
(table) =>
table.workoutTemplateId.equals(template.metadata.id) &
table.deletedAt.isNull(),
);
} else {
staleProgramsQuery.where(
(table) =>
table.workoutTemplateId.equals(template.metadata.id) &
table.deletedAt.isNull() &
table.id.isNotIn(activeProgramIds),
);
}
await _softDeleteWorkoutTemplateProgramRows(
database,
await staleProgramsQuery.get(),
deletedAt,
);
final activeOverrideIds = template.overrides
.map((override) => override.metadata.id)
.toSet();
final relevantProgramIds = {...previousProgramIds, ...activeProgramIds};
if (relevantProgramIds.isEmpty) {
return;
}
final staleOverridesQuery = database.select(
database.workoutTemplateExerciseOverrides,
);
if (activeOverrideIds.isEmpty) {
staleOverridesQuery.where(
(table) =>
table.workoutTemplateProgramId.isIn(relevantProgramIds) &
table.deletedAt.isNull(),
);
} else {
staleOverridesQuery.where(
(table) =>
table.workoutTemplateProgramId.isIn(relevantProgramIds) &
table.deletedAt.isNull() &
table.id.isNotIn(activeOverrideIds),
);
}
await _softDeleteWorkoutTemplateExerciseOverrideRows(
database,
await staleOverridesQuery.get(),
deletedAt,
);
});
}
}
final class DriftActiveSessionRepository implements ActiveSessionRepository {
const DriftActiveSessionRepository(this.database);
final db.AppDatabase database;
@override
Future<domain.ActiveWorkoutSession?> findById(String id) async {
final row = await (database.select(
database.activeWorkoutSessions,
)..where((table) => table.id.equals(id))).getSingleOrNull();
return row == null ? null : _activeWorkoutSessionFromRow(row);
}
@override
Future<domain.ActiveWorkoutSession?> findOpen() async {
final row =
await (database.select(database.activeWorkoutSessions)
..where(
(table) =>
table.deletedAt.isNull() &
table.status.isIn(['running', 'paused', 'savedExit']),
)
..orderBy([(table) => OrderingTerm.desc(table.updatedAt)])
..limit(1))
.getSingleOrNull();
return row == null ? null : _activeWorkoutSessionFromRow(row);
}
@override
Future<void> save(domain.ActiveWorkoutSession session) async {
await _upsertWithChangeLog(
database: database,
tableName: 'active_workout_sessions',
entityType: 'ActiveWorkoutSession',
metadata: session.metadata,
write: () => database
.into(database.activeWorkoutSessions)
.insertOnConflictUpdate(_activeWorkoutSessionCompanion(session)),
);
}
@override
Future<void> saveSetResult(domain.ActiveSetResult result) async {
await _upsertWithChangeLog(
database: database,
tableName: 'active_set_results',
entityType: 'ActiveSetResult',
metadata: result.metadata,
write: () => database
.into(database.activeSetResults)
.insertOnConflictUpdate(_activeSetResultCompanion(result)),
);
}
@override
Future<void> saveSetTimerState(domain.ActiveSetTimerState state) async {
await _upsertWithChangeLog(
database: database,
tableName: 'active_set_timer_states',
entityType: 'ActiveSetTimerState',
metadata: state.metadata,
write: () => _upsertActiveSetTimerState(database, state),
);
}
@override
Future<void> saveRestState(domain.ActiveRestState restState) async {
await _upsertWithChangeLog(
database: database,
tableName: 'active_rest_states',
entityType: 'ActiveRestState',
metadata: restState.metadata,
write: () async {
await database
.into(database.activeRestStates)
.insertOnConflictUpdate(_activeRestStateCompanion(restState));
await _updateActiveRestPauseFields(database, restState);
},
);
}
@override
Future<domain.ActiveRestState?> findRestStateById(String id) async {
final row = await database
.customSelect(
'SELECT * FROM active_rest_states WHERE id = ? LIMIT 1',
variables: [Variable<String>(id)],
)
.getSingleOrNull();
return row == null ? null : _activeRestStateFromCustomRow(row);
}
@override
Future<void> saveScoreStopwatchState(
domain.ActiveScoreStopwatchState state,
) async {
await _upsertWithChangeLog(
database: database,
tableName: 'active_score_stopwatch_states',
entityType: 'ActiveScoreStopwatchState',
metadata: state.metadata,
write: () => database
.into(database.activeScoreStopwatchStates)
.insertOnConflictUpdate(_activeScoreStopwatchStateCompanion(state)),
);
}
@override
Future<void> saveExerciseStepProgressState(
domain.ActiveExerciseStepProgressState state,
) async {
await _upsertWithChangeLog(
database: database,
tableName: 'active_exercise_step_progress_states',
entityType: 'ActiveExerciseStepProgressState',
metadata: state.metadata,
write: () => database
.into(database.activeExerciseStepProgressStates)
.insertOnConflictUpdate(
_activeExerciseStepProgressStateCompanion(state),
),
);
}
@override
Future<void> saveExerciseStepResult(
domain.ActiveExerciseStepResult result,
) async {
await _upsertWithChangeLog(
database: database,
tableName: 'active_exercise_step_results',
entityType: 'ActiveExerciseStepResult',
metadata: result.metadata,
write: () => database
.into(database.activeExerciseStepResults)
.insertOnConflictUpdate(_activeExerciseStepResultCompanion(result)),
);
}
@override
Future<void> deleteScoreStopwatchState({
required String sessionId,
required int programIndex,
required int exerciseIndex,
required int setIndex,
required DateTime deletedAt,
}) async {
final row =
await (database.select(database.activeScoreStopwatchStates)..where(
(table) =>
table.activeWorkoutSessionId.equals(sessionId) &
table.programIndex.equals(programIndex) &
table.exerciseIndex.equals(exerciseIndex) &
table.setIndex.equals(setIndex) &
table.deletedAt.isNull(),
))
.getSingleOrNull();
if (row == null) {
return;
}
final revision = row.localRevision + 1;
await (database.delete(
database.activeScoreStopwatchStates,
)..where((table) => table.id.equals(row.id))).go();
await _writeChangeLog(
database: database,
entityType: 'ActiveScoreStopwatchState',
entityId: row.id,
operation: 'delete',
localRevision: revision,
originDeviceId: row.originDeviceId,
createdAt: deletedAt,
);
}
@override
Future<domain.ActiveScoreStopwatchState?> findScoreStopwatchState({
required String sessionId,
required int programIndex,
required int exerciseIndex,
required int setIndex,
}) async {
final row =
await (database.select(database.activeScoreStopwatchStates)..where(
(table) =>
table.activeWorkoutSessionId.equals(sessionId) &
table.programIndex.equals(programIndex) &
table.exerciseIndex.equals(exerciseIndex) &
table.setIndex.equals(setIndex) &
table.deletedAt.isNull(),
))
.getSingleOrNull();
return row == null ? null : _activeScoreStopwatchStateFromRow(row);
}
@override
Future<domain.ActiveSetTimerState?> findSetTimerState({
required String sessionId,
required int programIndex,
required int exerciseIndex,
required int setIndex,
}) async {
final row = await database
.customSelect(
'SELECT * FROM active_set_timer_states '
'WHERE active_workout_session_id = ? '
'AND program_index = ? '
'AND exercise_index = ? '
'AND set_index = ? '
'AND deleted_at IS NULL '
'LIMIT 1',
variables: [
Variable<String>(sessionId),
Variable<int>(programIndex),
Variable<int>(exerciseIndex),
Variable<int>(setIndex),
],
)
.getSingleOrNull();
return row == null ? null : _activeSetTimerStateFromCustomRow(row);
}
@override
Future<domain.ActiveExerciseStepProgressState?>
findExerciseStepProgressState({
required String sessionId,
required int programIndex,
required int exerciseIndex,
required int setIndex,
}) async {
final row =
await (database.select(database.activeExerciseStepProgressStates)
..where(
(table) =>
table.activeWorkoutSessionId.equals(sessionId) &
table.programIndex.equals(programIndex) &
table.exerciseIndex.equals(exerciseIndex) &
table.setIndex.equals(setIndex) &
table.deletedAt.isNull(),
))
.getSingleOrNull();
return row == null ? null : _activeExerciseStepProgressStateFromRow(row);
}
@override
Future<List<domain.ActiveSetResult>> listSetResults(String sessionId) async {
final rows =
await (database.select(database.activeSetResults)
..where((table) => table.activeWorkoutSessionId.equals(sessionId))
..orderBy([
(table) => OrderingTerm.asc(table.programIndex),
(table) => OrderingTerm.asc(table.exerciseIndex),
(table) => OrderingTerm.asc(table.setIndex),
]))
.get();
return rows.map(_activeSetResultFromRow).toList();
}
@override
Future<List<domain.ActiveSetTimerState>> listSetTimerStates(
String sessionId,
) async {
final rows = await database
.customSelect(
'SELECT * FROM active_set_timer_states '
'WHERE active_workout_session_id = ? '
'AND deleted_at IS NULL '
'ORDER BY program_index, exercise_index, set_index',
variables: [Variable<String>(sessionId)],
)
.get();
return rows.map(_activeSetTimerStateFromCustomRow).toList();
}
@override
Future<List<domain.ActiveExerciseStepProgressState>>
listExerciseStepProgressStates(String sessionId) async {
final rows =
await (database.select(database.activeExerciseStepProgressStates)
..where(
(table) =>
table.activeWorkoutSessionId.equals(sessionId) &
table.deletedAt.isNull(),
)
..orderBy([
(table) => OrderingTerm.asc(table.programIndex),
(table) => OrderingTerm.asc(table.exerciseIndex),
(table) => OrderingTerm.asc(table.setIndex),
]))
.get();
return rows.map(_activeExerciseStepProgressStateFromRow).toList();
}
@override
Future<List<domain.ActiveExerciseStepResult>> listExerciseStepResults(
String sessionId,
) async {
final rows =
await (database.select(database.activeExerciseStepResults)
..where(
(table) =>
table.activeWorkoutSessionId.equals(sessionId) &
table.deletedAt.isNull(),
)
..orderBy([
(table) => OrderingTerm.asc(table.programIndex),
(table) => OrderingTerm.asc(table.exerciseIndex),
(table) => OrderingTerm.asc(table.setIndex),
(table) => OrderingTerm.asc(table.passageIndex),
(table) => OrderingTerm.asc(table.stepIndex),
]))
.get();
return rows.map(_activeExerciseStepResultFromRow).toList();
}
@override
Future<List<domain.ActiveRestState>> listRestStates(String sessionId) async {
final rows = await database
.customSelect(
'SELECT * FROM active_rest_states '
'WHERE active_workout_session_id = ? '
'ORDER BY started_at',
variables: [Variable<String>(sessionId)],
)
.get();
return rows.map(_activeRestStateFromCustomRow).toList();
}
@override
Future<List<domain.ActiveScoreStopwatchState>> listScoreStopwatchStates(
String sessionId,
) async {
final rows =
await (database.select(database.activeScoreStopwatchStates)
..where(
(table) =>
table.activeWorkoutSessionId.equals(sessionId) &
table.deletedAt.isNull(),
)
..orderBy([
(table) => OrderingTerm.asc(table.programIndex),
(table) => OrderingTerm.asc(table.exerciseIndex),
(table) => OrderingTerm.asc(table.setIndex),
]))
.get();
return rows.map(_activeScoreStopwatchStateFromRow).toList();
}
}
final class DriftWorkoutHistoryRepository implements WorkoutHistoryRepository {
const DriftWorkoutHistoryRepository(this.database);
final db.AppDatabase database;
@override
Future<domain.WorkoutHistory?> findById(String id) async {
final row =
await (database.select(
database.workoutHistories,
)..where((table) => table.id.equals(id) & table.deletedAt.isNull()))
.getSingleOrNull();
if (row == null) {
return null;
}
final results =
await (database.select(database.workoutHistorySetResults)
..where(
(table) =>
table.workoutHistoryId.equals(id) &
table.deletedAt.isNull(),
)
..orderBy([
(table) => OrderingTerm.asc(table.programIndex),
(table) => OrderingTerm.asc(table.exerciseIndex),
(table) => OrderingTerm.asc(table.setIndex),
]))
.get();
final stepResults =
await (database.select(database.workoutHistoryStepResults)
..where(
(table) =>
table.workoutHistoryId.equals(id) &
table.deletedAt.isNull(),
)
..orderBy([
(table) => OrderingTerm.asc(table.programIndex),
(table) => OrderingTerm.asc(table.exerciseIndex),
(table) => OrderingTerm.asc(table.setIndex),
(table) => OrderingTerm.asc(table.passageIndex),
(table) => OrderingTerm.asc(table.stepIndex),
]))
.get();
return _workoutHistoryFromRow(
row,
results.map(_workoutHistorySetResultFromRow).toList(),
stepResults.map(_workoutHistoryStepResultFromRow).toList(),
);
}
@override
Future<List<domain.WorkoutHistory>> listActive() async {
final rows =
await (database.select(database.workoutHistories)
..where((table) => table.deletedAt.isNull())
..orderBy([(table) => OrderingTerm.desc(table.startedAt)]))
.get();
final histories = <domain.WorkoutHistory>[];
for (final row in rows) {
final history = await findById(row.id);
if (history != null) {
histories.add(history);
}
}
return histories;
}
@override
Future<void> save(domain.WorkoutHistory history) async {
await database.transaction(() async {
await _upsertWithChangeLog(
database: database,
tableName: 'workout_history',
entityType: 'WorkoutHistory',
metadata: history.metadata,
write: () => database
.into(database.workoutHistories)
.insertOnConflictUpdate(_workoutHistoryCompanion(history)),
);
for (final result in history.results) {
await saveSetResult(result);
}
for (final result in history.stepResults) {
await saveStepResult(result);
}
});
}
@override
Future<void> saveSetResult(domain.WorkoutHistorySetResult result) async {
await _upsertWithChangeLog(
database: database,
tableName: 'workout_history_set_results',
entityType: 'WorkoutHistorySetResult',
metadata: result.metadata,
write: () => database
.into(database.workoutHistorySetResults)
.insertOnConflictUpdate(_workoutHistorySetResultCompanion(result)),
);
}
@override
Future<void> saveStepResult(domain.WorkoutHistoryStepResult result) async {
await _upsertWithChangeLog(
database: database,
tableName: 'workout_history_step_results',
entityType: 'WorkoutHistoryStepResult',
metadata: result.metadata,
write: () => database
.into(database.workoutHistoryStepResults)
.insertOnConflictUpdate(_workoutHistoryStepResultCompanion(result)),
);
}
@override
Future<void> delete(String id, DateTime deletedAt) async {
await database.transaction(() async {
final history = await (database.select(
database.workoutHistories,
)..where((table) => table.id.equals(id))).getSingle();
final revision = history.localRevision + 1;
await (database.update(
database.workoutHistories,
)..where((table) => table.id.equals(id))).write(
db.WorkoutHistoriesCompanion(
deletedAt: Value(deletedAt.toUtc()),
updatedAt: Value(deletedAt.toUtc()),
localRevision: Value(revision),
syncState: const Value('deleted'),
),
);
await _writeChangeLog(
database: database,
entityType: 'WorkoutHistory',
entityId: id,
operation: 'softDelete',
localRevision: revision,
originDeviceId: history.originDeviceId,
createdAt: deletedAt,
);
final resultRows =
await (database.select(database.workoutHistorySetResults)..where(
(table) =>
table.workoutHistoryId.equals(id) &
table.deletedAt.isNull(),
))
.get();
await _softDeleteWorkoutHistorySetResultRows(
database,
resultRows,
deletedAt,
);
final stepResultRows =
await (database.select(database.workoutHistoryStepResults)..where(
(table) =>
table.workoutHistoryId.equals(id) &
table.deletedAt.isNull(),
))
.get();
await _softDeleteWorkoutHistoryStepResultRows(
database,
stepResultRows,
deletedAt,
);
});
}
}
Future<void> _upsertWithChangeLog({
required db.AppDatabase database,
required String tableName,
required String entityType,
required domain.EntityMetadata metadata,
required Future<void> Function() write,
}) async {
await database.transaction(() async {
final operation = await _operationForMutation(
database: database,
tableName: tableName,
entityId: metadata.id,
deletedAt: metadata.deletedAt,
);
await write();
await _writeChangeLog(
database: database,
entityType: entityType,
entityId: metadata.id,
operation: operation,
localRevision: metadata.localRevision,
originDeviceId: metadata.originDeviceId,
createdAt: metadata.updatedAt,
);
});
}
Future<void> _replaceExerciseImages(
db.AppDatabase database,
domain.Exercise exercise,
) async {
final rows = await (database.select(
database.exerciseImages,
)..where((table) => table.exerciseId.equals(exercise.metadata.id))).get();
final activeRowsByMediaId = {
for (final row in rows)
if (row.deletedAt == null) row.mediaAssetId: row,
};
final desiredIds = exercise.imageMediaIds.toSet();
final removedRows = activeRowsByMediaId.values
.where((row) => !desiredIds.contains(row.mediaAssetId))
.toList();
await _softDeleteExerciseImageRows(
database,
removedRows,
exercise.metadata.updatedAt,
);
for (var index = 0; index < exercise.imageMediaIds.length; index++) {
final mediaId = exercise.imageMediaIds[index];
final existing = activeRowsByMediaId[mediaId];
final metadata = existing == null
? domain.EntityMetadata(
id: _exerciseImageId(exercise.metadata.id, mediaId),
createdAt: exercise.metadata.updatedAt,
updatedAt: exercise.metadata.updatedAt,
originDeviceId: exercise.metadata.originDeviceId,
)
: _metadataFromRow(existing).touch(exercise.metadata.updatedAt);
await _upsertWithChangeLog(
database: database,
tableName: 'exercise_images',
entityType: 'ExerciseImage',
metadata: metadata,
write: () => database
.into(database.exerciseImages)
.insertOnConflictUpdate(
db.ExerciseImagesCompanion(
id: Value(metadata.id),
createdAt: Value(metadata.createdAt.toUtc()),
updatedAt: Value(metadata.updatedAt.toUtc()),
deletedAt: Value(_utcOrNull(metadata.deletedAt)),
schemaVersion: Value(metadata.schemaVersion),
syncState: Value(_syncStateToDb(metadata.syncState)),
localRevision: Value(metadata.localRevision),
originDeviceId: Value(metadata.originDeviceId),
futureOwnerProfileId: Value(metadata.futureOwnerProfileId),
lastSyncedAt: Value(_utcOrNull(metadata.lastSyncedAt)),
remoteRevision: Value(metadata.remoteRevision),
exerciseId: Value(exercise.metadata.id),
mediaAssetId: Value(mediaId),
position: Value(index),
),
),
);
}
}
Future<void> _replaceExerciseSteps(
db.AppDatabase database,
domain.Exercise exercise,
) async {
final rows = await (database.select(
database.exerciseSteps,
)..where((table) => table.exerciseId.equals(exercise.metadata.id))).get();
final activeRowsByStepId = {
for (final row in rows)
if (row.deletedAt == null) row.id: row,
};
final desiredIds = exercise.steps.map((step) => step.id).toSet();
final removedRows = activeRowsByStepId.values
.where((row) => !desiredIds.contains(row.id))
.toList();
await _softDeleteExerciseStepRows(
database,
removedRows,
exercise.metadata.updatedAt,
);
for (final step in exercise.steps) {
final existing = activeRowsByStepId[step.id];
final metadata = existing == null
? domain.EntityMetadata(
id: step.id,
createdAt: exercise.metadata.updatedAt,
updatedAt: exercise.metadata.updatedAt,
originDeviceId: exercise.metadata.originDeviceId,
)
: _metadataFromRow(existing).touch(exercise.metadata.updatedAt);
await _upsertWithChangeLog(
database: database,
tableName: 'exercise_steps',
entityType: 'ExerciseStep',
metadata: metadata,
write: () => database
.into(database.exerciseSteps)
.insertOnConflictUpdate(
db.ExerciseStepsCompanion(
id: Value(metadata.id),
createdAt: Value(metadata.createdAt.toUtc()),
updatedAt: Value(metadata.updatedAt.toUtc()),
deletedAt: Value(_utcOrNull(metadata.deletedAt)),
schemaVersion: Value(metadata.schemaVersion),
syncState: Value(_syncStateToDb(metadata.syncState)),
localRevision: Value(metadata.localRevision),
originDeviceId: Value(metadata.originDeviceId),
futureOwnerProfileId: Value(metadata.futureOwnerProfileId),
lastSyncedAt: Value(_utcOrNull(metadata.lastSyncedAt)),
remoteRevision: Value(metadata.remoteRevision),
exerciseId: Value(exercise.metadata.id),
position: Value(step.position),
name: Value(step.name),
type: Value(_exerciseStepTypeToDb(step.type)),
defaultTargetValue: Value(step.defaultTargetValue),
hasScore: Value(step.hasScore),
scoreInputMode: Value(
step.hasScore ? _scoreInputModeToDb(step.scoreInputMode) : null,
),
scoreLabel: Value(step.scoreLabel),
scoreUnit: Value(step.scoreUnit),
defaultTargetScore: Value(step.defaultTargetScore),
defaultTargetScoreTimeMs: Value(step.defaultTargetScoreTimeMs),
),
),
);
}
}
Future<void> _softDeleteExerciseImageRows(
db.AppDatabase database,
List<db.ExerciseImage> rows,
DateTime deletedAt,
) async {
for (final row in rows) {
final revision = row.localRevision + 1;
await (database.update(
database.exerciseImages,
)..where((table) => table.id.equals(row.id))).write(
db.ExerciseImagesCompanion(
deletedAt: Value(deletedAt.toUtc()),
updatedAt: Value(deletedAt.toUtc()),
localRevision: Value(revision),
syncState: const Value('deleted'),
),
);
await _writeChangeLog(
database: database,
entityType: 'ExerciseImage',
entityId: row.id,
operation: 'softDelete',
localRevision: revision,
originDeviceId: row.originDeviceId,
createdAt: deletedAt,
);
}
}
Future<void> _softDeleteExerciseStepRows(
db.AppDatabase database,
List<db.ExerciseStep> rows,
DateTime deletedAt,
) async {
for (final row in rows) {
final revision = row.localRevision + 1;
await (database.update(
database.exerciseSteps,
)..where((table) => table.id.equals(row.id))).write(
db.ExerciseStepsCompanion(
deletedAt: Value(deletedAt.toUtc()),
updatedAt: Value(deletedAt.toUtc()),
localRevision: Value(revision),
syncState: const Value('deleted'),
),
);
await _writeChangeLog(
database: database,
entityType: 'ExerciseStep',
entityId: row.id,
operation: 'softDelete',
localRevision: revision,
originDeviceId: row.originDeviceId,
createdAt: deletedAt,
);
}
}
String _exerciseImageId(String exerciseId, String mediaId) {
return 'exercise-image:$exerciseId:$mediaId';
}
Future<String> _operationForMutation({
required db.AppDatabase database,
required String tableName,
required String entityId,
required DateTime? deletedAt,
}) async {
final existing = await database
.customSelect(
'SELECT deleted_at FROM $tableName WHERE id = ? LIMIT 1',
variables: [Variable<String>(entityId)],
)
.getSingleOrNull();
if (existing == null) {
return 'insert';
}
final wasDeleted = existing.data['deleted_at'] != null;
if (deletedAt != null) {
return 'softDelete';
}
if (wasDeleted) {
return 'restore';
}
return 'update';
}
Future<void> _writeChangeLog({
required db.AppDatabase database,
required String entityType,
required String entityId,
required String operation,
required int localRevision,
required String originDeviceId,
required DateTime createdAt,
}) async {
final id = [
entityType,
entityId,
operation,
localRevision,
createdAt.microsecondsSinceEpoch,
].join(':');
await database
.into(database.changeLogEntries)
.insertOnConflictUpdate(
db.ChangeLogEntriesCompanion.insert(
id: id,
entityType: entityType,
entityId: entityId,
operation: operation,
localRevision: localRevision,
originDeviceId: originDeviceId,
createdAt: createdAt.toUtc(),
),
);
}
Future<void> _softDeleteProgramExerciseRows(
db.AppDatabase database,
List<db.ProgramExercise> rows,
DateTime deletedAt,
) async {
for (final row in rows) {
final revision = row.localRevision + 1;
await (database.update(
database.programExercises,
)..where((table) => table.id.equals(row.id))).write(
db.ProgramExercisesCompanion(
deletedAt: Value(deletedAt.toUtc()),
updatedAt: Value(deletedAt.toUtc()),
localRevision: Value(revision),
syncState: const Value('deleted'),
),
);
await _writeChangeLog(
database: database,
entityType: 'ProgramExercise',
entityId: row.id,
operation: 'softDelete',
localRevision: revision,
originDeviceId: row.originDeviceId,
createdAt: deletedAt,
);
}
}
Future<void> _softDeleteWorkoutTemplateProgramRows(
db.AppDatabase database,
List<db.WorkoutTemplateProgram> rows,
DateTime deletedAt,
) async {
for (final row in rows) {
final revision = row.localRevision + 1;
await (database.update(
database.workoutTemplatePrograms,
)..where((table) => table.id.equals(row.id))).write(
db.WorkoutTemplateProgramsCompanion(
deletedAt: Value(deletedAt.toUtc()),
updatedAt: Value(deletedAt.toUtc()),
localRevision: Value(revision),
syncState: const Value('deleted'),
),
);
await _writeChangeLog(
database: database,
entityType: 'WorkoutTemplateProgram',
entityId: row.id,
operation: 'softDelete',
localRevision: revision,
originDeviceId: row.originDeviceId,
createdAt: deletedAt,
);
}
}
Future<void> _softDeleteWorkoutTemplateExerciseOverrideRows(
db.AppDatabase database,
List<db.WorkoutTemplateExerciseOverride> rows,
DateTime deletedAt,
) async {
for (final row in rows) {
final revision = row.localRevision + 1;
await (database.update(
database.workoutTemplateExerciseOverrides,
)..where((table) => table.id.equals(row.id))).write(
db.WorkoutTemplateExerciseOverridesCompanion(
deletedAt: Value(deletedAt.toUtc()),
updatedAt: Value(deletedAt.toUtc()),
localRevision: Value(revision),
syncState: const Value('deleted'),
),
);
await _writeChangeLog(
database: database,
entityType: 'WorkoutTemplateExerciseOverride',
entityId: row.id,
operation: 'softDelete',
localRevision: revision,
originDeviceId: row.originDeviceId,
createdAt: deletedAt,
);
}
}
Future<void> _softDeleteWorkoutHistorySetResultRows(
db.AppDatabase database,
List<db.WorkoutHistorySetResult> rows,
DateTime deletedAt,
) async {
for (final row in rows) {
final revision = row.localRevision + 1;
await (database.update(
database.workoutHistorySetResults,
)..where((table) => table.id.equals(row.id))).write(
db.WorkoutHistorySetResultsCompanion(
deletedAt: Value(deletedAt.toUtc()),
updatedAt: Value(deletedAt.toUtc()),
localRevision: Value(revision),
syncState: const Value('deleted'),
),
);
await _writeChangeLog(
database: database,
entityType: 'WorkoutHistorySetResult',
entityId: row.id,
operation: 'softDelete',
localRevision: revision,
originDeviceId: row.originDeviceId,
createdAt: deletedAt,
);
}
}
Future<void> _softDeleteWorkoutHistoryStepResultRows(
db.AppDatabase database,
List<db.WorkoutHistoryStepResult> rows,
DateTime deletedAt,
) async {
for (final row in rows) {
final revision = row.localRevision + 1;
await (database.update(
database.workoutHistoryStepResults,
)..where((table) => table.id.equals(row.id))).write(
db.WorkoutHistoryStepResultsCompanion(
deletedAt: Value(deletedAt.toUtc()),
updatedAt: Value(deletedAt.toUtc()),
localRevision: Value(revision),
syncState: const Value('deleted'),
),
);
await _writeChangeLog(
database: database,
entityType: 'WorkoutHistoryStepResult',
entityId: row.id,
operation: 'softDelete',
localRevision: revision,
originDeviceId: row.originDeviceId,
createdAt: deletedAt,
);
}
}
DateTime _utc(DateTime value) => value.toUtc();
DateTime? _utcOrNull(DateTime? value) => value?.toUtc();
domain.EntityMetadata _metadataFromRow(dynamic row) {
return domain.EntityMetadata(
id: row.id as String,
createdAt: _utc(row.createdAt as DateTime),
updatedAt: _utc(row.updatedAt as DateTime),
deletedAt: _utcOrNull(row.deletedAt as DateTime?),
schemaVersion: row.schemaVersion as int,
syncState: _syncStateFromDb(row.syncState as String),
localRevision: row.localRevision as int,
originDeviceId: row.originDeviceId as String,
futureOwnerProfileId: row.futureOwnerProfileId as String?,
lastSyncedAt: _utcOrNull(row.lastSyncedAt as DateTime?),
remoteRevision: row.remoteRevision as String?,
);
}
domain.EntityMetadata _metadataFromData(Map<String, dynamic> data) {
return domain.EntityMetadata(
id: data['id'] as String,
createdAt: _dateTimeFromData(data, 'created_at'),
updatedAt: _dateTimeFromData(data, 'updated_at'),
deletedAt: _dateTimeOrNullFromData(data, 'deleted_at'),
schemaVersion: data['schema_version'] as int,
syncState: _syncStateFromDb(data['sync_state'] as String),
localRevision: data['local_revision'] as int,
originDeviceId: data['origin_device_id'] as String,
futureOwnerProfileId: data['future_owner_profile_id'] as String?,
lastSyncedAt: _dateTimeOrNullFromData(data, 'last_synced_at'),
remoteRevision: data['remote_revision'] as String?,
);
}
DateTime _dateTimeFromData(Map<String, dynamic> data, String key) {
final value = data[key];
if (value is DateTime) {
return value.toUtc();
}
if (value is int) {
return DateTime.fromMillisecondsSinceEpoch(value, isUtc: true);
}
throw domain.DomainException('Invalid timestamp column: $key');
}
DateTime? _dateTimeOrNullFromData(Map<String, dynamic> data, String key) {
final value = data[key];
if (value == null) {
return null;
}
if (value is DateTime) {
return value.toUtc();
}
if (value is int) {
return DateTime.fromMillisecondsSinceEpoch(value, isUtc: true);
}
throw domain.DomainException('Invalid timestamp column: $key');
}
List<dynamic> _metadataValues(domain.EntityMetadata metadata) => [
Value(metadata.id),
Value(metadata.createdAt.toUtc()),
Value(metadata.updatedAt.toUtc()),
Value(_utcOrNull(metadata.deletedAt)),
Value(metadata.schemaVersion),
Value(_syncStateToDb(metadata.syncState)),
Value(metadata.localRevision),
Value(metadata.originDeviceId),
Value(metadata.futureOwnerProfileId),
Value(_utcOrNull(metadata.lastSyncedAt)),
Value(metadata.remoteRevision),
];
db.ExercisesCompanion _exerciseCompanion(domain.Exercise exercise) {
final values = _metadataValues(exercise.metadata);
return db.ExercisesCompanion(
id: values[0] as Value<String>,
createdAt: values[1] as Value<DateTime>,
updatedAt: values[2] as Value<DateTime>,
deletedAt: values[3] as Value<DateTime?>,
schemaVersion: values[4] as Value<int>,
syncState: values[5] as Value<String>,
localRevision: values[6] as Value<int>,
originDeviceId: values[7] as Value<String>,
futureOwnerProfileId: values[8] as Value<String?>,
lastSyncedAt: values[9] as Value<DateTime?>,
remoteRevision: values[10] as Value<String?>,
name: Value(exercise.name),
description: Value(exercise.description),
iconMediaId: Value(exercise.iconMediaId),
videoMediaId: Value(exercise.videoMediaId),
hasTimeMeasure: Value(exercise.hasTimeMeasure),
hasRepsMeasure: Value(exercise.hasRepsMeasure),
hasScoreMeasure: Value(exercise.hasScoreMeasure),
scoreInputMode: Value(_scoreInputModeToDb(exercise.scoreInputMode)),
scoreLabel: Value(exercise.scoreLabel),
scoreUnit: Value(exercise.scoreUnit),
defaultTargetTimeSeconds: Value(exercise.defaultTargetTimeSeconds),
defaultTargetReps: Value(exercise.defaultTargetReps),
defaultTargetScore: Value(exercise.defaultTargetScore),
defaultTargetScoreTimeMs: Value(exercise.defaultTargetScoreTimeMs),
autoStartNextTimedStep: Value(exercise.autoStartNextTimedStep),
archivedAt: Value(_utcOrNull(exercise.archivedAt)),
);
}
domain.Exercise _exerciseFromRow(
db.Exercise row,
List<String> imageMediaIds,
List<domain.ExerciseStep> steps,
) {
return domain.Exercise(
metadata: _metadataFromRow(row),
name: row.name,
description: row.description,
imageMediaIds: imageMediaIds,
iconMediaId: row.iconMediaId,
videoMediaId: row.videoMediaId,
hasTimeMeasure: row.hasTimeMeasure,
hasRepsMeasure: row.hasRepsMeasure,
hasScoreMeasure: row.hasScoreMeasure,
scoreInputMode: _scoreInputModeFromDb(row.scoreInputMode),
scoreLabel: row.scoreLabel,
scoreUnit: row.scoreUnit,
defaultTargetTimeSeconds: row.defaultTargetTimeSeconds,
defaultTargetReps: row.defaultTargetReps,
defaultTargetScore: row.defaultTargetScore,
defaultTargetScoreTimeMs: row.defaultTargetScoreTimeMs,
autoStartNextTimedStep: row.autoStartNextTimedStep,
steps: steps,
archivedAt: _utcOrNull(row.archivedAt),
);
}
domain.ExerciseStep _exerciseStepFromRow(db.ExerciseStep row) {
return domain.ExerciseStep(
id: row.id,
position: row.position,
name: row.name,
type: _exerciseStepTypeFromDb(row.type),
defaultTargetValue: row.defaultTargetValue,
hasScore: row.hasScore,
scoreInputMode: row.scoreInputMode == null
? domain.ScoreInputMode.manual
: _scoreInputModeFromDb(row.scoreInputMode!),
scoreLabel: row.scoreLabel,
scoreUnit: row.scoreUnit,
defaultTargetScore: row.defaultTargetScore,
defaultTargetScoreTimeMs: row.defaultTargetScoreTimeMs,
);
}
db.MediaAssetsCompanion _mediaAssetCompanion(domain.MediaAsset asset) {
final values = _metadataValues(asset.metadata);
return db.MediaAssetsCompanion(
id: values[0] as Value<String>,
createdAt: values[1] as Value<DateTime>,
updatedAt: values[2] as Value<DateTime>,
deletedAt: values[3] as Value<DateTime?>,
schemaVersion: values[4] as Value<int>,
syncState: values[5] as Value<String>,
localRevision: values[6] as Value<int>,
originDeviceId: values[7] as Value<String>,
futureOwnerProfileId: values[8] as Value<String?>,
lastSyncedAt: values[9] as Value<DateTime?>,
remoteRevision: values[10] as Value<String?>,
kind: Value(_mediaKindToDb(asset.kind)),
localUri: Value(asset.localUri),
mimeType: Value(asset.mimeType),
sizeBytes: Value(asset.sizeBytes),
width: Value(asset.width),
height: Value(asset.height),
durationMs: Value(asset.durationMs),
checksum: Value(asset.checksum),
remoteUri: Value(asset.remoteUri),
thumbnailLocalUri: Value(asset.thumbnailLocalUri),
);
}
domain.MediaAsset _mediaAssetFromRow(db.MediaAsset row) {
return domain.MediaAsset(
metadata: _metadataFromRow(row),
kind: _mediaKindFromDb(row.kind),
localUri: row.localUri,
mimeType: row.mimeType,
sizeBytes: row.sizeBytes,
width: row.width,
height: row.height,
durationMs: row.durationMs,
checksum: row.checksum,
remoteUri: row.remoteUri,
thumbnailLocalUri: row.thumbnailLocalUri,
);
}
db.OnlineAccountSessionsCompanion _userAccountSessionCompanion(
domain.UserAccountSession session,
) {
return db.OnlineAccountSessionsCompanion(
id: Value(session.id),
serverUserId: Value(session.serverUserId),
email: Value(session.email),
displayName: Value<String?>(session.displayName),
isLoggedIn: Value(session.isLoggedIn),
createdAt: Value(session.createdAt.toUtc()),
updatedAt: Value(session.updatedAt.toUtc()),
lastAuthenticatedAt: Value<DateTime?>(
_utcOrNull(session.lastAuthenticatedAt),
),
);
}
domain.UserAccountSession _userAccountSessionFromRow(
db.OnlineAccountSession row,
) {
return domain.UserAccountSession(
id: row.id,
serverUserId: row.serverUserId,
email: row.email,
displayName: row.displayName,
isLoggedIn: row.isLoggedIn,
createdAt: _utc(row.createdAt),
updatedAt: _utc(row.updatedAt),
lastAuthenticatedAt: _utcOrNull(row.lastAuthenticatedAt),
);
}
db.ShareInboxItemsCompanion _shareInboxItemCompanion(
domain.ShareInboxItem item,
) {
final updatedAt = item.respondedAt ?? item.createdAt;
return db.ShareInboxItemsCompanion.insert(
shareId: item.shareId,
senderUserId: item.senderUserId,
resourceType: _shareResourceTypeToDb(item.resourceType),
payloadJson: item.payloadJson,
status: _shareInboxStatusToDb(item.status),
createdAt: item.createdAt.toUtc(),
updatedAt: updatedAt.toUtc(),
respondedAt: Value<DateTime?>(_utcOrNull(item.respondedAt)),
);
}
domain.ShareInboxItem _shareInboxItemFromRow(db.ShareInboxItem row) {
return domain.ShareInboxItem(
shareId: row.shareId,
senderUserId: row.senderUserId,
resourceType: _shareResourceTypeFromDb(row.resourceType),
payloadJson: row.payloadJson,
status: _shareInboxStatusFromDb(row.status),
createdAt: _utc(row.createdAt),
respondedAt: _utcOrNull(row.respondedAt),
);
}
db.PendingShareActionsCompanion _pendingShareActionCompanion(
domain.PendingShareAction action,
) {
return db.PendingShareActionsCompanion.insert(
id: action.id,
actionType: _pendingShareActionTypeToDb(action.actionType),
shareId: Value<String?>(action.shareId),
resourceType: Value<String?>(
action.resourceType == null
? null
: _shareResourceTypeToDb(action.resourceType!),
),
payloadJson: Value<String?>(action.payloadJson),
recipientEmailsJson: Value<String?>(action.recipientEmailsJson),
createdAt: action.createdAt.toUtc(),
lastAttemptAt: Value<DateTime?>(_utcOrNull(action.lastAttemptAt)),
attemptCount: action.attemptCount,
status: _pendingShareActionStatusToDb(action.status),
);
}
domain.PendingShareAction _pendingShareActionFromRow(
db.PendingShareAction row,
) {
return domain.PendingShareAction(
id: row.id,
actionType: _pendingShareActionTypeFromDb(row.actionType),
shareId: row.shareId,
resourceType: row.resourceType == null
? null
: _shareResourceTypeFromDb(row.resourceType!),
payloadJson: row.payloadJson,
recipientEmailsJson: row.recipientEmailsJson,
createdAt: _utc(row.createdAt),
lastAttemptAt: _utcOrNull(row.lastAttemptAt),
attemptCount: row.attemptCount,
status: _pendingShareActionStatusFromDb(row.status),
);
}
db.ProgramsCompanion _programCompanion(domain.Program program) {
final values = _metadataValues(program.metadata);
return db.ProgramsCompanion(
id: values[0] as Value<String>,
createdAt: values[1] as Value<DateTime>,
updatedAt: values[2] as Value<DateTime>,
deletedAt: values[3] as Value<DateTime?>,
schemaVersion: values[4] as Value<int>,
syncState: values[5] as Value<String>,
localRevision: values[6] as Value<int>,
originDeviceId: values[7] as Value<String>,
futureOwnerProfileId: values[8] as Value<String?>,
lastSyncedAt: values[9] as Value<DateTime?>,
remoteRevision: values[10] as Value<String?>,
name: Value(program.name),
defaultRestSeconds: Value(program.defaultRestSeconds),
);
}
domain.Program _programFromRow(
db.Program row,
List<domain.ProgramExercise> exercises,
) {
return domain.Program(
metadata: _metadataFromRow(row),
name: row.name,
defaultRestSeconds: row.defaultRestSeconds,
exercises: exercises,
);
}
db.ProgramExercisesCompanion _programExerciseCompanion(
domain.ProgramExercise exercise,
) {
final values = _metadataValues(exercise.metadata);
return db.ProgramExercisesCompanion(
id: values[0] as Value<String>,
createdAt: values[1] as Value<DateTime>,
updatedAt: values[2] as Value<DateTime>,
deletedAt: values[3] as Value<DateTime?>,
schemaVersion: values[4] as Value<int>,
syncState: values[5] as Value<String>,
localRevision: values[6] as Value<int>,
originDeviceId: values[7] as Value<String>,
futureOwnerProfileId: values[8] as Value<String?>,
lastSyncedAt: values[9] as Value<DateTime?>,
remoteRevision: values[10] as Value<String?>,
programId: Value(exercise.programId),
sourceExerciseId: Value(exercise.sourceExerciseId),
position: Value(exercise.position),
exerciseNameSnapshot: Value(exercise.exerciseNameSnapshot),
exerciseDescriptionSnapshot: Value(exercise.exerciseDescriptionSnapshot),
exerciseImageMediaIdSnapshot: Value(exercise.exerciseImageMediaIdSnapshot),
exerciseImageMediaIdsSnapshotJson: Value(
_encodeImageMediaIdsSnapshot(exercise.exerciseImageMediaIdsSnapshot),
),
exerciseStepsSnapshotJson: Value(
_encodeExerciseStepsSnapshot(exercise.exerciseStepsSnapshot),
),
autoStartNextTimedStepSnapshot: Value(
exercise.autoStartNextTimedStepSnapshot,
),
autoStartNextTimedStepOverride: Value(
exercise.autoStartNextTimedStepOverride,
),
exerciseVideoMediaIdSnapshot: Value(exercise.exerciseVideoMediaIdSnapshot),
exerciseArchivedSnapshot: Value(exercise.exerciseArchivedSnapshot),
availableTimeSnapshot: Value(exercise.availableTimeSnapshot),
availableRepsSnapshot: Value(exercise.availableRepsSnapshot),
availableScoreSnapshot: Value(exercise.availableScoreSnapshot),
scoreInputModeSnapshot: Value(
_scoreInputModeToDb(exercise.scoreInputModeSnapshot),
),
scoreLabelSnapshot: Value(exercise.scoreLabelSnapshot),
scoreUnitSnapshot: Value(exercise.scoreUnitSnapshot),
setsCount: Value(exercise.setsCount),
timeEnabled: Value(exercise.timeEnabled),
repsEnabled: Value(exercise.repsEnabled),
scoreEnabled: Value(exercise.scoreEnabled),
targetTimeSeconds: Value(exercise.targetTimeSeconds),
targetReps: Value(exercise.targetReps),
targetScore: Value(exercise.targetScore),
targetScoreTimeMs: Value(exercise.targetScoreTimeMs),
restSecondsOverride: Value(exercise.restSecondsOverride),
);
}
domain.ProgramExercise _programExerciseFromRow(db.ProgramExercise row) {
return domain.ProgramExercise(
metadata: _metadataFromRow(row),
programId: row.programId,
sourceExerciseId: row.sourceExerciseId,
position: row.position,
exerciseNameSnapshot: row.exerciseNameSnapshot,
exerciseDescriptionSnapshot: row.exerciseDescriptionSnapshot,
exerciseImageMediaIdSnapshot: row.exerciseImageMediaIdSnapshot,
exerciseImageMediaIdsSnapshot: _decodeImageMediaIdsSnapshot(
row.exerciseImageMediaIdsSnapshotJson,
row.exerciseImageMediaIdSnapshot,
),
exerciseStepsSnapshot: _decodeExerciseStepsSnapshot(
row.exerciseStepsSnapshotJson,
),
autoStartNextTimedStepSnapshot: row.autoStartNextTimedStepSnapshot,
autoStartNextTimedStepOverride: row.autoStartNextTimedStepOverride,
exerciseVideoMediaIdSnapshot: row.exerciseVideoMediaIdSnapshot,
exerciseArchivedSnapshot: row.exerciseArchivedSnapshot,
availableTimeSnapshot: row.availableTimeSnapshot,
availableRepsSnapshot: row.availableRepsSnapshot,
availableScoreSnapshot: row.availableScoreSnapshot,
scoreInputModeSnapshot: _scoreInputModeFromDb(row.scoreInputModeSnapshot),
scoreLabelSnapshot: row.scoreLabelSnapshot,
scoreUnitSnapshot: row.scoreUnitSnapshot,
setsCount: row.setsCount,
timeEnabled: row.timeEnabled,
repsEnabled: row.repsEnabled,
scoreEnabled: row.scoreEnabled,
targetTimeSeconds: row.targetTimeSeconds,
targetReps: row.targetReps,
targetScore: row.targetScore,
targetScoreTimeMs: row.targetScoreTimeMs,
restSecondsOverride: row.restSecondsOverride,
);
}
db.WorkoutTemplatesCompanion _workoutTemplateCompanion(
domain.WorkoutTemplate template,
) {
final values = _metadataValues(template.metadata);
return db.WorkoutTemplatesCompanion(
id: values[0] as Value<String>,
createdAt: values[1] as Value<DateTime>,
updatedAt: values[2] as Value<DateTime>,
deletedAt: values[3] as Value<DateTime?>,
schemaVersion: values[4] as Value<int>,
syncState: values[5] as Value<String>,
localRevision: values[6] as Value<int>,
originDeviceId: values[7] as Value<String>,
futureOwnerProfileId: values[8] as Value<String?>,
lastSyncedAt: values[9] as Value<DateTime?>,
remoteRevision: values[10] as Value<String?>,
name: Value(template.name),
lastStartedAt: Value(_utcOrNull(template.lastStartedAt)),
);
}
domain.WorkoutTemplate _workoutTemplateFromRow(
db.WorkoutTemplate row,
List<domain.WorkoutTemplateProgram> programs,
List<domain.WorkoutTemplateExerciseOverride> overrides,
) {
return domain.WorkoutTemplate(
metadata: _metadataFromRow(row),
name: row.name,
lastStartedAt: _utcOrNull(row.lastStartedAt),
programs: programs,
overrides: overrides,
);
}
db.WorkoutTemplateProgramsCompanion _workoutTemplateProgramCompanion(
domain.WorkoutTemplateProgram program,
) {
final values = _metadataValues(program.metadata);
return db.WorkoutTemplateProgramsCompanion(
id: values[0] as Value<String>,
createdAt: values[1] as Value<DateTime>,
updatedAt: values[2] as Value<DateTime>,
deletedAt: values[3] as Value<DateTime?>,
schemaVersion: values[4] as Value<int>,
syncState: values[5] as Value<String>,
localRevision: values[6] as Value<int>,
originDeviceId: values[7] as Value<String>,
futureOwnerProfileId: values[8] as Value<String?>,
lastSyncedAt: values[9] as Value<DateTime?>,
remoteRevision: values[10] as Value<String?>,
workoutTemplateId: Value(program.workoutTemplateId),
sourceProgramId: Value(program.sourceProgramId),
position: Value(program.position),
programNameSnapshot: Value(program.programNameSnapshot),
defaultRestSecondsSnapshot: Value(program.defaultRestSecondsSnapshot),
programSnapshotJson: Value(program.programSnapshotJson),
);
}
domain.WorkoutTemplateProgram _workoutTemplateProgramFromRow(
db.WorkoutTemplateProgram row,
) {
return domain.WorkoutTemplateProgram(
metadata: _metadataFromRow(row),
workoutTemplateId: row.workoutTemplateId,
sourceProgramId: row.sourceProgramId,
position: row.position,
programNameSnapshot: row.programNameSnapshot,
defaultRestSecondsSnapshot: row.defaultRestSecondsSnapshot,
programSnapshotJson: row.programSnapshotJson,
);
}
db.WorkoutTemplateExerciseOverridesCompanion
_workoutTemplateExerciseOverrideCompanion(
domain.WorkoutTemplateExerciseOverride override,
) {
final values = _metadataValues(override.metadata);
return db.WorkoutTemplateExerciseOverridesCompanion(
id: values[0] as Value<String>,
createdAt: values[1] as Value<DateTime>,
updatedAt: values[2] as Value<DateTime>,
deletedAt: values[3] as Value<DateTime?>,
schemaVersion: values[4] as Value<int>,
syncState: values[5] as Value<String>,
localRevision: values[6] as Value<int>,
originDeviceId: values[7] as Value<String>,
futureOwnerProfileId: values[8] as Value<String?>,
lastSyncedAt: values[9] as Value<DateTime?>,
remoteRevision: values[10] as Value<String?>,
workoutTemplateProgramId: Value(override.workoutTemplateProgramId),
snapshotProgramExerciseId: Value(override.snapshotProgramExerciseId),
setsCountOverride: Value(override.setsCountOverride),
targetTimeSecondsOverride: Value(override.targetTimeSecondsOverride),
targetRepsOverride: Value(override.targetRepsOverride),
targetScoreOverride: Value(override.targetScoreOverride),
targetScoreTimeMsOverride: Value(override.targetScoreTimeMsOverride),
autoStartNextTimedStepOverride: Value(
override.autoStartNextTimedStepOverride,
),
);
}
domain.WorkoutTemplateExerciseOverride _workoutTemplateExerciseOverrideFromRow(
db.WorkoutTemplateExerciseOverride row,
) {
return domain.WorkoutTemplateExerciseOverride(
metadata: _metadataFromRow(row),
workoutTemplateProgramId: row.workoutTemplateProgramId,
snapshotProgramExerciseId: row.snapshotProgramExerciseId,
setsCountOverride: row.setsCountOverride,
targetTimeSecondsOverride: row.targetTimeSecondsOverride,
targetRepsOverride: row.targetRepsOverride,
targetScoreOverride: row.targetScoreOverride,
targetScoreTimeMsOverride: row.targetScoreTimeMsOverride,
autoStartNextTimedStepOverride: row.autoStartNextTimedStepOverride,
);
}
db.ActiveWorkoutSessionsCompanion _activeWorkoutSessionCompanion(
domain.ActiveWorkoutSession session,
) {
final values = _metadataValues(session.metadata);
return db.ActiveWorkoutSessionsCompanion(
id: values[0] as Value<String>,
createdAt: values[1] as Value<DateTime>,
updatedAt: values[2] as Value<DateTime>,
deletedAt: values[3] as Value<DateTime?>,
schemaVersion: values[4] as Value<int>,
syncState: values[5] as Value<String>,
localRevision: values[6] as Value<int>,
originDeviceId: values[7] as Value<String>,
futureOwnerProfileId: values[8] as Value<String?>,
lastSyncedAt: values[9] as Value<DateTime?>,
remoteRevision: values[10] as Value<String?>,
sourceWorkoutTemplateId: Value(session.sourceWorkoutTemplateId),
status: Value(_activeStatusToDb(session.status)),
startedAt: Value(session.startedAt.toUtc()),
pausedAt: Value(_utcOrNull(session.pausedAt)),
endedAt: Value(_utcOrNull(session.endedAt)),
lastPersistedAt: Value(session.lastPersistedAt.toUtc()),
elapsedActiveMs: Value(session.elapsedActiveMs),
currentProgramIndex: Value(session.currentProgramIndex),
currentExerciseIndex: Value(session.currentExerciseIndex),
currentSetIndex: Value(session.currentSetIndex),
resolvedTemplateSnapshotJson: Value(session.resolvedTemplateSnapshotJson),
);
}
domain.ActiveWorkoutSession _activeWorkoutSessionFromRow(
db.ActiveWorkoutSession row,
) {
return domain.ActiveWorkoutSession(
metadata: _metadataFromRow(row),
sourceWorkoutTemplateId: row.sourceWorkoutTemplateId,
status: _activeStatusFromDb(row.status),
startedAt: _utc(row.startedAt),
pausedAt: _utcOrNull(row.pausedAt),
endedAt: _utcOrNull(row.endedAt),
lastPersistedAt: _utc(row.lastPersistedAt),
elapsedActiveMs: row.elapsedActiveMs,
currentProgramIndex: row.currentProgramIndex,
currentExerciseIndex: row.currentExerciseIndex,
currentSetIndex: row.currentSetIndex,
resolvedTemplateSnapshotJson: row.resolvedTemplateSnapshotJson,
);
}
db.ActiveSetResultsCompanion _activeSetResultCompanion(
domain.ActiveSetResult result,
) {
final values = _metadataValues(result.metadata);
return db.ActiveSetResultsCompanion(
id: values[0] as Value<String>,
createdAt: values[1] as Value<DateTime>,
updatedAt: values[2] as Value<DateTime>,
deletedAt: values[3] as Value<DateTime?>,
schemaVersion: values[4] as Value<int>,
syncState: values[5] as Value<String>,
localRevision: values[6] as Value<int>,
originDeviceId: values[7] as Value<String>,
futureOwnerProfileId: values[8] as Value<String?>,
lastSyncedAt: values[9] as Value<DateTime?>,
remoteRevision: values[10] as Value<String?>,
activeWorkoutSessionId: Value(result.activeWorkoutSessionId),
programSnapshotId: Value(result.programSnapshotId),
exerciseSnapshotId: Value(result.exerciseSnapshotId),
programIndex: Value(result.programIndex),
exerciseIndex: Value(result.exerciseIndex),
setIndex: Value(result.setIndex),
startedAt: Value(_utcOrNull(result.startedAt)),
completedAt: Value(_utcOrNull(result.completedAt)),
actualTimeMs: Value(result.actualTimeMs),
actualReps: Value(result.actualReps),
actualScore: Value(result.actualScore),
actualScoreTimeMs: Value(result.actualScoreTimeMs),
scoreInputModeSnapshot: Value(
_scoreInputModeToDb(result.scoreInputModeSnapshot),
),
scoreLabelSnapshot: Value(result.scoreLabelSnapshot),
scoreUnitSnapshot: Value(result.scoreUnitSnapshot),
note: Value(result.note),
status: Value(_setResultStatusToDb(result.status)),
);
}
domain.ActiveSetResult _activeSetResultFromRow(db.ActiveSetResult row) {
return domain.ActiveSetResult(
metadata: _metadataFromRow(row),
activeWorkoutSessionId: row.activeWorkoutSessionId,
programSnapshotId: row.programSnapshotId,
exerciseSnapshotId: row.exerciseSnapshotId,
programIndex: row.programIndex,
exerciseIndex: row.exerciseIndex,
setIndex: row.setIndex,
startedAt: _utcOrNull(row.startedAt),
completedAt: _utcOrNull(row.completedAt),
actualTimeMs: row.actualTimeMs,
actualReps: row.actualReps,
actualScore: row.actualScore,
actualScoreTimeMs: row.actualScoreTimeMs,
scoreInputModeSnapshot: _scoreInputModeFromDb(row.scoreInputModeSnapshot),
scoreLabelSnapshot: row.scoreLabelSnapshot,
scoreUnitSnapshot: row.scoreUnitSnapshot,
note: row.note,
status: _setResultStatusFromDb(row.status),
);
}
db.ActiveRestStatesCompanion _activeRestStateCompanion(
domain.ActiveRestState restState,
) {
final values = _metadataValues(restState.metadata);
return db.ActiveRestStatesCompanion(
id: values[0] as Value<String>,
createdAt: values[1] as Value<DateTime>,
updatedAt: values[2] as Value<DateTime>,
deletedAt: values[3] as Value<DateTime?>,
schemaVersion: values[4] as Value<int>,
syncState: values[5] as Value<String>,
localRevision: values[6] as Value<int>,
originDeviceId: values[7] as Value<String>,
futureOwnerProfileId: values[8] as Value<String?>,
lastSyncedAt: values[9] as Value<DateTime?>,
remoteRevision: values[10] as Value<String?>,
activeWorkoutSessionId: Value(restState.activeWorkoutSessionId),
afterProgramIndex: Value(restState.afterProgramIndex),
afterExerciseIndex: Value(restState.afterExerciseIndex),
afterSetIndex: Value(restState.afterSetIndex),
plannedRestSeconds: Value(restState.plannedRestSeconds),
adjustedRestSeconds: Value(restState.adjustedRestSeconds),
startedAt: Value(restState.startedAt.toUtc()),
endedAt: Value(_utcOrNull(restState.endedAt)),
skippedAt: Value(_utcOrNull(restState.skippedAt)),
);
}
Future<void> _updateActiveRestPauseFields(
db.AppDatabase database,
domain.ActiveRestState restState,
) async {
await database.customUpdate(
'UPDATE active_rest_states SET paused_at = ?, accumulated_paused_ms = ? '
'WHERE id = ?',
variables: [
Variable<DateTime>(_utcOrNull(restState.pausedAt)),
Variable<int>(restState.accumulatedPausedMs),
Variable<String>(restState.metadata.id),
],
);
}
Future<void> _upsertActiveSetTimerState(
db.AppDatabase database,
domain.ActiveSetTimerState state,
) async {
final metadata = state.metadata;
await database.customInsert(
'INSERT INTO active_set_timer_states ('
'id, created_at, updated_at, deleted_at, schema_version, sync_state, '
'local_revision, origin_device_id, future_owner_profile_id, '
'last_synced_at, remote_revision, active_workout_session_id, '
'program_index, exercise_index, set_index, status, started_at, '
'accumulated_ms, stopped_at, skipped_at'
') VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) '
'ON CONFLICT(active_workout_session_id, program_index, exercise_index, '
'set_index) DO UPDATE SET '
'id = excluded.id, '
'created_at = excluded.created_at, '
'updated_at = excluded.updated_at, '
'deleted_at = excluded.deleted_at, '
'schema_version = excluded.schema_version, '
'sync_state = excluded.sync_state, '
'local_revision = excluded.local_revision, '
'origin_device_id = excluded.origin_device_id, '
'future_owner_profile_id = excluded.future_owner_profile_id, '
'last_synced_at = excluded.last_synced_at, '
'remote_revision = excluded.remote_revision, '
'active_workout_session_id = excluded.active_workout_session_id, '
'program_index = excluded.program_index, '
'exercise_index = excluded.exercise_index, '
'set_index = excluded.set_index, '
'status = excluded.status, '
'started_at = excluded.started_at, '
'accumulated_ms = excluded.accumulated_ms, '
'stopped_at = excluded.stopped_at, '
'skipped_at = excluded.skipped_at',
variables: [
Variable<String>(metadata.id),
Variable<DateTime>(metadata.createdAt.toUtc()),
Variable<DateTime>(metadata.updatedAt.toUtc()),
Variable<DateTime>(_utcOrNull(metadata.deletedAt)),
Variable<int>(metadata.schemaVersion),
Variable<String>(_syncStateToDb(metadata.syncState)),
Variable<int>(metadata.localRevision),
Variable<String>(metadata.originDeviceId),
Variable<String>(metadata.futureOwnerProfileId),
Variable<DateTime>(_utcOrNull(metadata.lastSyncedAt)),
Variable<String>(metadata.remoteRevision),
Variable<String>(state.activeWorkoutSessionId),
Variable<int>(state.programIndex),
Variable<int>(state.exerciseIndex),
Variable<int>(state.setIndex),
Variable<String>(_setTimerStatusToDb(state.status)),
Variable<DateTime>(_utcOrNull(state.startedAt)),
Variable<int>(state.accumulatedMs),
Variable<DateTime>(_utcOrNull(state.stoppedAt)),
Variable<DateTime>(_utcOrNull(state.skippedAt)),
],
);
}
domain.ActiveSetTimerState _activeSetTimerStateFromCustomRow(QueryRow row) {
final data = row.data;
return domain.ActiveSetTimerState(
metadata: _metadataFromData(data),
activeWorkoutSessionId: data['active_workout_session_id'] as String,
programIndex: data['program_index'] as int,
exerciseIndex: data['exercise_index'] as int,
setIndex: data['set_index'] as int,
status: _setTimerStatusFromDb(data['status'] as String),
startedAt: _dateTimeOrNullFromData(data, 'started_at'),
accumulatedMs: data['accumulated_ms'] as int,
stoppedAt: _dateTimeOrNullFromData(data, 'stopped_at'),
skippedAt: _dateTimeOrNullFromData(data, 'skipped_at'),
);
}
domain.ActiveRestState _activeRestStateFromCustomRow(QueryRow row) {
final data = row.data;
return domain.ActiveRestState(
metadata: _metadataFromData(data),
activeWorkoutSessionId: data['active_workout_session_id'] as String,
afterProgramIndex: data['after_program_index'] as int,
afterExerciseIndex: data['after_exercise_index'] as int,
afterSetIndex: data['after_set_index'] as int,
plannedRestSeconds: data['planned_rest_seconds'] as int,
adjustedRestSeconds: data['adjusted_rest_seconds'] as int,
startedAt: _dateTimeFromData(data, 'started_at'),
endedAt: _dateTimeOrNullFromData(data, 'ended_at'),
skippedAt: _dateTimeOrNullFromData(data, 'skipped_at'),
pausedAt: _dateTimeOrNullFromData(data, 'paused_at'),
accumulatedPausedMs: data['accumulated_paused_ms'] as int? ?? 0,
);
}
db.ActiveScoreStopwatchStatesCompanion _activeScoreStopwatchStateCompanion(
domain.ActiveScoreStopwatchState state,
) {
final values = _metadataValues(state.metadata);
return db.ActiveScoreStopwatchStatesCompanion(
id: values[0] as Value<String>,
createdAt: values[1] as Value<DateTime>,
updatedAt: values[2] as Value<DateTime>,
deletedAt: values[3] as Value<DateTime?>,
schemaVersion: values[4] as Value<int>,
syncState: values[5] as Value<String>,
localRevision: values[6] as Value<int>,
originDeviceId: values[7] as Value<String>,
futureOwnerProfileId: values[8] as Value<String?>,
lastSyncedAt: values[9] as Value<DateTime?>,
remoteRevision: values[10] as Value<String?>,
activeWorkoutSessionId: Value(state.activeWorkoutSessionId),
programIndex: Value(state.programIndex),
exerciseIndex: Value(state.exerciseIndex),
setIndex: Value(state.setIndex),
status: Value(_scoreStopwatchStatusToDb(state.status)),
startedAt: Value(state.startedAt.toUtc()),
accumulatedMs: Value(state.accumulatedMs),
stoppedAt: Value(_utcOrNull(state.stoppedAt)),
);
}
domain.ActiveScoreStopwatchState _activeScoreStopwatchStateFromRow(
db.ActiveScoreStopwatchState row,
) {
return domain.ActiveScoreStopwatchState(
metadata: _metadataFromRow(row),
activeWorkoutSessionId: row.activeWorkoutSessionId,
programIndex: row.programIndex,
exerciseIndex: row.exerciseIndex,
setIndex: row.setIndex,
status: _scoreStopwatchStatusFromDb(row.status),
startedAt: _utc(row.startedAt),
accumulatedMs: row.accumulatedMs,
stoppedAt: _utcOrNull(row.stoppedAt),
);
}
db.ActiveExerciseStepProgressStatesCompanion
_activeExerciseStepProgressStateCompanion(
domain.ActiveExerciseStepProgressState state,
) {
final values = _metadataValues(state.metadata);
return db.ActiveExerciseStepProgressStatesCompanion(
id: values[0] as Value<String>,
createdAt: values[1] as Value<DateTime>,
updatedAt: values[2] as Value<DateTime>,
deletedAt: values[3] as Value<DateTime?>,
schemaVersion: values[4] as Value<int>,
syncState: values[5] as Value<String>,
localRevision: values[6] as Value<int>,
originDeviceId: values[7] as Value<String>,
futureOwnerProfileId: values[8] as Value<String?>,
lastSyncedAt: values[9] as Value<DateTime?>,
remoteRevision: values[10] as Value<String?>,
activeWorkoutSessionId: Value(state.activeWorkoutSessionId),
programIndex: Value(state.programIndex),
exerciseIndex: Value(state.exerciseIndex),
setIndex: Value(state.setIndex),
currentPassageIndex: Value(state.currentPassageIndex),
currentStepIndex: Value(state.currentStepIndex),
currentStepSnapshotId: Value(state.currentStepSnapshotId),
status: Value(_exerciseStepProgressStatusToDb(state.status)),
startedAt: Value<DateTime?>(_utcOrNull(state.startedAt)),
accumulatedMs: Value(state.accumulatedMs),
lastTransitionAt: Value(state.lastTransitionAt.toUtc()),
);
}
domain.ActiveExerciseStepProgressState _activeExerciseStepProgressStateFromRow(
db.ActiveExerciseStepProgressState row,
) {
return domain.ActiveExerciseStepProgressState(
metadata: _metadataFromRow(row),
activeWorkoutSessionId: row.activeWorkoutSessionId,
programIndex: row.programIndex,
exerciseIndex: row.exerciseIndex,
setIndex: row.setIndex,
currentPassageIndex: row.currentPassageIndex,
currentStepIndex: row.currentStepIndex,
currentStepSnapshotId: row.currentStepSnapshotId,
status: _exerciseStepProgressStatusFromDb(row.status),
startedAt: _utcOrNull(row.startedAt),
accumulatedMs: row.accumulatedMs,
lastTransitionAt: _utc(row.lastTransitionAt),
);
}
db.ActiveExerciseStepResultsCompanion _activeExerciseStepResultCompanion(
domain.ActiveExerciseStepResult result,
) {
final values = _metadataValues(result.metadata);
return db.ActiveExerciseStepResultsCompanion(
id: values[0] as Value<String>,
createdAt: values[1] as Value<DateTime>,
updatedAt: values[2] as Value<DateTime>,
deletedAt: values[3] as Value<DateTime?>,
schemaVersion: values[4] as Value<int>,
syncState: values[5] as Value<String>,
localRevision: values[6] as Value<int>,
originDeviceId: values[7] as Value<String>,
futureOwnerProfileId: values[8] as Value<String?>,
lastSyncedAt: values[9] as Value<DateTime?>,
remoteRevision: values[10] as Value<String?>,
activeWorkoutSessionId: Value(result.activeWorkoutSessionId),
programSnapshotId: Value(result.programSnapshotId),
exerciseSnapshotId: Value(result.exerciseSnapshotId),
programIndex: Value(result.programIndex),
exerciseIndex: Value(result.exerciseIndex),
setIndex: Value(result.setIndex),
passageIndex: Value(result.passageIndex),
stepIndex: Value(result.stepIndex),
stepSnapshotId: Value(result.stepSnapshotId),
stepNameSnapshot: Value(result.stepNameSnapshot),
stepTypeSnapshot: Value(_exerciseStepTypeToDb(result.stepTypeSnapshot)),
targetValueSnapshot: Value(result.targetValueSnapshot),
hasScoreSnapshot: Value(result.hasScoreSnapshot),
scoreInputModeSnapshot: Value<String?>(
result.scoreInputModeSnapshot == null
? null
: _scoreInputModeToDb(result.scoreInputModeSnapshot!),
),
scoreLabelSnapshot: Value<String?>(result.scoreLabelSnapshot),
scoreUnitSnapshot: Value<String?>(result.scoreUnitSnapshot),
targetScoreSnapshot: Value<double?>(result.targetScoreSnapshot),
targetScoreTimeMsSnapshot: Value<int?>(result.targetScoreTimeMsSnapshot),
status: Value(_setResultStatusToDb(result.status)),
startedAt: Value<DateTime?>(_utcOrNull(result.startedAt)),
completedAt: Value<DateTime?>(_utcOrNull(result.completedAt)),
actualTimeMs: Value<int?>(result.actualTimeMs),
actualReps: Value<int?>(result.actualReps),
actualScore: Value<double?>(result.actualScore),
actualScoreTimeMs: Value<int?>(result.actualScoreTimeMs),
note: Value<String?>(result.note),
);
}
domain.ActiveExerciseStepResult _activeExerciseStepResultFromRow(
db.ActiveExerciseStepResult row,
) {
return domain.ActiveExerciseStepResult(
metadata: _metadataFromRow(row),
activeWorkoutSessionId: row.activeWorkoutSessionId,
programSnapshotId: row.programSnapshotId,
exerciseSnapshotId: row.exerciseSnapshotId,
programIndex: row.programIndex,
exerciseIndex: row.exerciseIndex,
setIndex: row.setIndex,
passageIndex: row.passageIndex,
stepIndex: row.stepIndex,
stepSnapshotId: row.stepSnapshotId,
stepNameSnapshot: row.stepNameSnapshot,
stepTypeSnapshot: _exerciseStepTypeFromDb(row.stepTypeSnapshot),
targetValueSnapshot: row.targetValueSnapshot,
hasScoreSnapshot: row.hasScoreSnapshot,
scoreInputModeSnapshot: row.scoreInputModeSnapshot == null
? null
: _scoreInputModeFromDb(row.scoreInputModeSnapshot!),
scoreLabelSnapshot: row.scoreLabelSnapshot,
scoreUnitSnapshot: row.scoreUnitSnapshot,
targetScoreSnapshot: row.targetScoreSnapshot,
targetScoreTimeMsSnapshot: row.targetScoreTimeMsSnapshot,
status: _setResultStatusFromDb(row.status),
startedAt: _utcOrNull(row.startedAt),
completedAt: _utcOrNull(row.completedAt),
actualTimeMs: row.actualTimeMs,
actualReps: row.actualReps,
actualScore: row.actualScore,
actualScoreTimeMs: row.actualScoreTimeMs,
note: row.note,
);
}
db.WorkoutHistoriesCompanion _workoutHistoryCompanion(
domain.WorkoutHistory history,
) {
final values = _metadataValues(history.metadata);
return db.WorkoutHistoriesCompanion(
id: values[0] as Value<String>,
createdAt: values[1] as Value<DateTime>,
updatedAt: values[2] as Value<DateTime>,
deletedAt: values[3] as Value<DateTime?>,
schemaVersion: values[4] as Value<int>,
syncState: values[5] as Value<String>,
localRevision: values[6] as Value<int>,
originDeviceId: values[7] as Value<String>,
futureOwnerProfileId: values[8] as Value<String?>,
lastSyncedAt: values[9] as Value<DateTime?>,
remoteRevision: values[10] as Value<String?>,
sourceWorkoutTemplateId: Value(history.sourceWorkoutTemplateId),
sourceActiveWorkoutSessionId: Value(history.sourceActiveWorkoutSessionId),
nameSnapshot: Value(history.nameSnapshot),
startedAt: Value(history.startedAt.toUtc()),
endedAt: Value(history.endedAt.toUtc()),
totalActiveMs: Value(history.totalActiveMs),
completed: Value(history.completed),
historySnapshotJson: Value(history.historySnapshotJson),
);
}
db.WorkoutHistorySetResultsCompanion _workoutHistorySetResultCompanion(
domain.WorkoutHistorySetResult result,
) {
final values = _metadataValues(result.metadata);
return db.WorkoutHistorySetResultsCompanion(
id: values[0] as Value<String>,
createdAt: values[1] as Value<DateTime>,
updatedAt: values[2] as Value<DateTime>,
deletedAt: values[3] as Value<DateTime?>,
schemaVersion: values[4] as Value<int>,
syncState: values[5] as Value<String>,
localRevision: values[6] as Value<int>,
originDeviceId: values[7] as Value<String>,
futureOwnerProfileId: values[8] as Value<String?>,
lastSyncedAt: values[9] as Value<DateTime?>,
remoteRevision: values[10] as Value<String?>,
workoutHistoryId: Value(result.workoutHistoryId),
programSnapshotId: Value(result.programSnapshotId),
exerciseSnapshotId: Value(result.exerciseSnapshotId),
programIndex: Value(result.programIndex),
exerciseIndex: Value(result.exerciseIndex),
setIndex: Value(result.setIndex),
programNameSnapshot: Value(result.programNameSnapshot),
exerciseNameSnapshot: Value(result.exerciseNameSnapshot),
timeEnabledSnapshot: Value(result.timeEnabledSnapshot),
repsEnabledSnapshot: Value(result.repsEnabledSnapshot),
scoreEnabledSnapshot: Value(result.scoreEnabledSnapshot),
scoreInputModeSnapshot: Value(
_scoreInputModeToDb(result.scoreInputModeSnapshot),
),
targetTimeSecondsSnapshot: Value(result.targetTimeSecondsSnapshot),
targetRepsSnapshot: Value(result.targetRepsSnapshot),
targetScoreSnapshot: Value(result.targetScoreSnapshot),
targetScoreTimeMsSnapshot: Value(result.targetScoreTimeMsSnapshot),
actualTimeMs: Value(result.actualTimeMs),
actualReps: Value(result.actualReps),
actualScore: Value(result.actualScore),
actualScoreTimeMs: Value(result.actualScoreTimeMs),
scoreLabelSnapshot: Value(result.scoreLabelSnapshot),
scoreUnitSnapshot: Value(result.scoreUnitSnapshot),
startedAt: Value(_utcOrNull(result.startedAt)),
completedAt: Value(_utcOrNull(result.completedAt)),
status: Value(_setResultStatusToDb(result.status)),
);
}
db.WorkoutHistoryStepResultsCompanion _workoutHistoryStepResultCompanion(
domain.WorkoutHistoryStepResult result,
) {
final values = _metadataValues(result.metadata);
return db.WorkoutHistoryStepResultsCompanion(
id: values[0] as Value<String>,
createdAt: values[1] as Value<DateTime>,
updatedAt: values[2] as Value<DateTime>,
deletedAt: values[3] as Value<DateTime?>,
schemaVersion: values[4] as Value<int>,
syncState: values[5] as Value<String>,
localRevision: values[6] as Value<int>,
originDeviceId: values[7] as Value<String>,
futureOwnerProfileId: values[8] as Value<String?>,
lastSyncedAt: values[9] as Value<DateTime?>,
remoteRevision: values[10] as Value<String?>,
workoutHistoryId: Value(result.workoutHistoryId),
programSnapshotId: Value(result.programSnapshotId),
exerciseSnapshotId: Value(result.exerciseSnapshotId),
programIndex: Value(result.programIndex),
exerciseIndex: Value(result.exerciseIndex),
setIndex: Value(result.setIndex),
passageIndex: Value(result.passageIndex),
stepIndex: Value(result.stepIndex),
stepSnapshotId: Value(result.stepSnapshotId),
stepNameSnapshot: Value(result.stepNameSnapshot),
stepTypeSnapshot: Value(_exerciseStepTypeToDb(result.stepTypeSnapshot)),
targetValueSnapshot: Value(result.targetValueSnapshot),
hasScoreSnapshot: Value(result.hasScoreSnapshot),
scoreInputModeSnapshot: Value<String?>(
result.scoreInputModeSnapshot == null
? null
: _scoreInputModeToDb(result.scoreInputModeSnapshot!),
),
scoreLabelSnapshot: Value<String?>(result.scoreLabelSnapshot),
scoreUnitSnapshot: Value<String?>(result.scoreUnitSnapshot),
targetScoreSnapshot: Value<double?>(result.targetScoreSnapshot),
targetScoreTimeMsSnapshot: Value<int?>(result.targetScoreTimeMsSnapshot),
status: Value(_setResultStatusToDb(result.status)),
startedAt: Value<DateTime?>(_utcOrNull(result.startedAt)),
completedAt: Value<DateTime?>(_utcOrNull(result.completedAt)),
actualTimeMs: Value<int?>(result.actualTimeMs),
actualReps: Value<int?>(result.actualReps),
actualScore: Value<double?>(result.actualScore),
actualScoreTimeMs: Value<int?>(result.actualScoreTimeMs),
note: Value<String?>(result.note),
);
}
domain.WorkoutHistory _workoutHistoryFromRow(
db.WorkoutHistory row,
List<domain.WorkoutHistorySetResult> results,
List<domain.WorkoutHistoryStepResult> stepResults,
) {
return domain.WorkoutHistory(
metadata: _metadataFromRow(row),
sourceWorkoutTemplateId: row.sourceWorkoutTemplateId,
sourceActiveWorkoutSessionId: row.sourceActiveWorkoutSessionId,
nameSnapshot: row.nameSnapshot,
startedAt: _utc(row.startedAt),
endedAt: _utc(row.endedAt),
totalActiveMs: row.totalActiveMs,
completed: row.completed,
historySnapshotJson: row.historySnapshotJson,
results: results,
stepResults: stepResults,
);
}
domain.WorkoutHistorySetResult _workoutHistorySetResultFromRow(
db.WorkoutHistorySetResult row,
) {
return domain.WorkoutHistorySetResult(
metadata: _metadataFromRow(row),
workoutHistoryId: row.workoutHistoryId,
programSnapshotId: row.programSnapshotId,
exerciseSnapshotId: row.exerciseSnapshotId,
programIndex: row.programIndex,
exerciseIndex: row.exerciseIndex,
setIndex: row.setIndex,
programNameSnapshot: row.programNameSnapshot,
exerciseNameSnapshot: row.exerciseNameSnapshot,
timeEnabledSnapshot: row.timeEnabledSnapshot,
repsEnabledSnapshot: row.repsEnabledSnapshot,
scoreEnabledSnapshot: row.scoreEnabledSnapshot,
scoreInputModeSnapshot: _scoreInputModeFromDb(row.scoreInputModeSnapshot),
targetTimeSecondsSnapshot: row.targetTimeSecondsSnapshot,
targetRepsSnapshot: row.targetRepsSnapshot,
targetScoreSnapshot: row.targetScoreSnapshot,
targetScoreTimeMsSnapshot: row.targetScoreTimeMsSnapshot,
actualTimeMs: row.actualTimeMs,
actualReps: row.actualReps,
actualScore: row.actualScore,
actualScoreTimeMs: row.actualScoreTimeMs,
scoreLabelSnapshot: row.scoreLabelSnapshot,
scoreUnitSnapshot: row.scoreUnitSnapshot,
startedAt: _utcOrNull(row.startedAt),
completedAt: _utcOrNull(row.completedAt),
status: _setResultStatusFromDb(row.status),
);
}
domain.WorkoutHistoryStepResult _workoutHistoryStepResultFromRow(
db.WorkoutHistoryStepResult row,
) {
return domain.WorkoutHistoryStepResult(
metadata: _metadataFromRow(row),
workoutHistoryId: row.workoutHistoryId,
programSnapshotId: row.programSnapshotId,
exerciseSnapshotId: row.exerciseSnapshotId,
programIndex: row.programIndex,
exerciseIndex: row.exerciseIndex,
setIndex: row.setIndex,
passageIndex: row.passageIndex,
stepIndex: row.stepIndex,
stepSnapshotId: row.stepSnapshotId,
stepNameSnapshot: row.stepNameSnapshot,
stepTypeSnapshot: _exerciseStepTypeFromDb(row.stepTypeSnapshot),
targetValueSnapshot: row.targetValueSnapshot,
hasScoreSnapshot: row.hasScoreSnapshot,
scoreInputModeSnapshot: row.scoreInputModeSnapshot == null
? null
: _scoreInputModeFromDb(row.scoreInputModeSnapshot!),
scoreLabelSnapshot: row.scoreLabelSnapshot,
scoreUnitSnapshot: row.scoreUnitSnapshot,
targetScoreSnapshot: row.targetScoreSnapshot,
targetScoreTimeMsSnapshot: row.targetScoreTimeMsSnapshot,
status: _setResultStatusFromDb(row.status),
startedAt: _utcOrNull(row.startedAt),
completedAt: _utcOrNull(row.completedAt),
actualTimeMs: row.actualTimeMs,
actualReps: row.actualReps,
actualScore: row.actualScore,
actualScoreTimeMs: row.actualScoreTimeMs,
note: row.note,
);
}
db.RemoteResourceMappingsCompanion _remoteResourceMappingCompanion(
RemoteResourceMapping mapping,
) {
return db.RemoteResourceMappingsCompanion.insert(
id: _remoteResourceMappingId(mapping.resourceType, mapping.clientId),
resourceType: _syncResourceTypeToDb(mapping.resourceType),
clientId: mapping.clientId,
serverId: mapping.serverId,
serverUpdatedAt: mapping.serverUpdatedAt.toUtc(),
);
}
RemoteResourceMapping _remoteResourceMappingFromRow(
db.RemoteResourceMapping row,
) {
return RemoteResourceMapping(
resourceType: _syncResourceTypeFromDb(row.resourceType),
clientId: row.clientId,
serverId: row.serverId,
serverUpdatedAt: _utc(row.serverUpdatedAt),
);
}
String _remoteResourceMappingId(
SyncResourceType resourceType,
String clientId,
) {
return '${_syncResourceTypeToDb(resourceType)}:$clientId';
}
final class _LocalSyncSnapshot {
const _LocalSyncSnapshot({
required this.schemaVersion,
required this.updatedAt,
required this.deletedAt,
required this.payload,
});
factory _LocalSyncSnapshot.fromMetadata(
domain.EntityMetadata metadata,
Map<String, Object?> payload,
) {
return _LocalSyncSnapshot(
schemaVersion: metadata.schemaVersion,
updatedAt: metadata.updatedAt.toUtc(),
deletedAt: _utcOrNull(metadata.deletedAt),
payload: payload,
);
}
final int schemaVersion;
final DateTime updatedAt;
final DateTime? deletedAt;
final Map<String, Object?> payload;
}
Map<String, Object?> _metadataPayload(domain.EntityMetadata metadata) => {
'id': metadata.id,
'createdAt': metadata.createdAt.toUtc().toIso8601String(),
'updatedAt': metadata.updatedAt.toUtc().toIso8601String(),
'deletedAt': metadata.deletedAt?.toUtc().toIso8601String(),
'schemaVersion': metadata.schemaVersion,
'syncState': metadata.syncState.name,
'localRevision': metadata.localRevision,
'originDeviceId': metadata.originDeviceId,
};
Map<String, Object?> _exercisePayload(domain.Exercise exercise) => {
'metadata': _metadataPayload(exercise.metadata),
'id': exercise.metadata.id,
'name': exercise.name,
'description': exercise.description,
'imageMediaIds': exercise.imageMediaIds,
'iconMediaId': exercise.iconMediaId,
'videoMediaId': exercise.videoMediaId,
'hasTimeMeasure': exercise.hasTimeMeasure,
'hasRepsMeasure': exercise.hasRepsMeasure,
'hasScoreMeasure': exercise.hasScoreMeasure,
'scoreInputMode': exercise.scoreInputMode.name,
'scoreLabel': exercise.scoreLabel,
'scoreUnit': exercise.scoreUnit,
'defaultTargetTimeSeconds': exercise.defaultTargetTimeSeconds,
'defaultTargetReps': exercise.defaultTargetReps,
'defaultTargetScore': exercise.defaultTargetScore,
'defaultTargetScoreTimeMs': exercise.defaultTargetScoreTimeMs,
'autoStartNextTimedStep': exercise.autoStartNextTimedStep,
'steps': exercise.steps.map((step) => step.toSnapshotJson()).toList(),
'archivedAt': exercise.archivedAt?.toUtc().toIso8601String(),
};
Map<String, Object?> _mediaAssetPayload(domain.MediaAsset asset) => {
'metadata': _metadataPayload(asset.metadata),
'id': asset.metadata.id,
'kind': _mediaKindToDb(asset.kind),
'localUri': asset.localUri,
'mimeType': asset.mimeType,
'sizeBytes': asset.sizeBytes,
'width': asset.width,
'height': asset.height,
'durationMs': asset.durationMs,
'checksum': asset.checksum,
'remoteUri': asset.remoteUri,
'thumbnailLocalUri': asset.thumbnailLocalUri,
};
Map<String, Object?> _programPayload(domain.Program program) => {
'metadata': _metadataPayload(program.metadata),
'id': program.metadata.id,
'name': program.name,
'defaultRestSeconds': program.defaultRestSeconds,
'exercises': program.exercises
.map((exercise) => exercise.toSnapshotJson())
.toList(),
};
Map<String, Object?> _workoutTemplatePayload(domain.WorkoutTemplate template) =>
{
'metadata': _metadataPayload(template.metadata),
'id': template.metadata.id,
'name': template.name,
'lastStartedAt': template.lastStartedAt?.toUtc().toIso8601String(),
'programs': template.programs
.map(
(program) => {
'id': program.metadata.id,
'sourceProgramId': program.sourceProgramId,
'position': program.position,
'programNameSnapshot': program.programNameSnapshot,
'defaultRestSecondsSnapshot': program.defaultRestSecondsSnapshot,
'programSnapshotJson': program.programSnapshotJson,
},
)
.toList(),
'overrides': template.overrides
.map(
(override) => {
'id': override.metadata.id,
'workoutTemplateProgramId': override.workoutTemplateProgramId,
'snapshotProgramExerciseId': override.snapshotProgramExerciseId,
'setsCountOverride': override.setsCountOverride,
'targetTimeSecondsOverride': override.targetTimeSecondsOverride,
'targetRepsOverride': override.targetRepsOverride,
'targetScoreOverride': override.targetScoreOverride,
'targetScoreTimeMsOverride': override.targetScoreTimeMsOverride,
'autoStartNextTimedStepOverride':
override.autoStartNextTimedStepOverride,
},
)
.toList(),
};
Map<String, Object?> _workoutHistoryPayload(domain.WorkoutHistory history) => {
'metadata': _metadataPayload(history.metadata),
'id': history.metadata.id,
'sourceWorkoutTemplateId': history.sourceWorkoutTemplateId,
'sourceActiveWorkoutSessionId': history.sourceActiveWorkoutSessionId,
'nameSnapshot': history.nameSnapshot,
'startedAt': history.startedAt.toUtc().toIso8601String(),
'endedAt': history.endedAt.toUtc().toIso8601String(),
'totalActiveMs': history.totalActiveMs,
'completed': history.completed,
'historySnapshotJson': history.historySnapshotJson,
};
domain.Exercise _exerciseFromPayload(RemoteSyncedItem item) {
final payload = item.payload;
return domain.Exercise(
metadata: _metadataFromPayload(item),
name: _stringFromPayload(payload, 'name', item.clientId),
description: payload['description'] as String?,
imageMediaIds: _stringListFromPayload(payload['imageMediaIds']),
iconMediaId: payload['iconMediaId'] as String?,
videoMediaId: payload['videoMediaId'] as String?,
hasTimeMeasure: payload['hasTimeMeasure'] == true,
hasRepsMeasure: payload['hasRepsMeasure'] == true,
hasScoreMeasure: payload['hasScoreMeasure'] == true,
scoreInputMode: _scoreInputModeFromDb(
payload['scoreInputMode'] as String? ?? 'manual',
),
scoreLabel: payload['scoreLabel'] as String?,
scoreUnit: payload['scoreUnit'] as String?,
defaultTargetTimeSeconds: payload['defaultTargetTimeSeconds'] as int?,
defaultTargetReps: payload['defaultTargetReps'] as int?,
defaultTargetScore: (payload['defaultTargetScore'] as num?)?.toDouble(),
defaultTargetScoreTimeMs: payload['defaultTargetScoreTimeMs'] as int?,
autoStartNextTimedStep: payload['autoStartNextTimedStep'] as bool? ?? true,
steps: _stepsFromPayload(payload['steps']),
archivedAt: _dateTimeFromPayload(payload['archivedAt']),
);
}
domain.MediaAsset _mediaAssetFromPayload(RemoteSyncedItem item) {
final payload = item.payload;
return domain.MediaAsset(
metadata: _metadataFromPayload(item),
kind: _mediaKindFromDb(payload['kind'] as String? ?? 'image'),
localUri: _stringFromPayload(payload, 'localUri', ''),
mimeType: payload['mimeType'] as String?,
sizeBytes: payload['sizeBytes'] as int?,
width: payload['width'] as int?,
height: payload['height'] as int?,
durationMs: payload['durationMs'] as int?,
checksum: payload['checksum'] as String?,
remoteUri: payload['remoteUri'] as String?,
thumbnailLocalUri: payload['thumbnailLocalUri'] as String?,
);
}
domain.Program _programFromPayload(RemoteSyncedItem item) {
final payload = item.payload;
final metadata = _metadataFromPayload(item);
return domain.Program(
metadata: metadata,
name: _stringFromPayload(payload, 'name', item.clientId),
defaultRestSeconds: payload['defaultRestSeconds'] as int? ?? 0,
exercises: _programExercisesFromPayload(payload['exercises'], metadata),
);
}
domain.WorkoutTemplate _workoutTemplateFromPayload(RemoteSyncedItem item) {
final payload = item.payload;
final metadata = _metadataFromPayload(item);
return domain.WorkoutTemplate(
metadata: metadata,
name: _stringFromPayload(payload, 'name', item.clientId),
lastStartedAt: _dateTimeFromPayload(payload['lastStartedAt']),
programs: _workoutTemplateProgramsFromPayload(
payload['programs'],
metadata,
),
overrides: _workoutTemplateOverridesFromPayload(
payload['overrides'],
metadata,
),
);
}
domain.EntityMetadata _metadataFromPayload(RemoteSyncedItem item) {
final metadata = item.payload['metadata'];
final map = metadata is Map ? Map<String, Object?>.from(metadata) : null;
return domain.EntityMetadata(
id: item.clientId,
createdAt: _dateTimeFromPayload(map?['createdAt']) ?? item.clientUpdatedAt,
updatedAt: item.clientUpdatedAt,
deletedAt: item.deletedAt,
schemaVersion: item.schemaVersion,
syncState: item.deletedAt == null
? domain.SyncState.synced
: domain.SyncState.deleted,
localRevision: map?['localRevision'] as int? ?? 0,
originDeviceId: map?['originDeviceId'] as String? ?? 'remote',
);
}
List<domain.ProgramExercise> _programExercisesFromPayload(
Object? value,
domain.EntityMetadata parentMetadata,
) {
if (value is! List) {
return const [];
}
return value
.whereType<Map>()
.map((entry) {
final map = Map<String, Object?>.from(entry);
final id = _stringFromPayload(map, 'id', 'program-exercise');
return domain.ProgramExercise(
metadata: _childMetadataFromPayload(map, id, parentMetadata),
programId: parentMetadata.id,
sourceExerciseId: null,
position: map['position'] as int? ?? 0,
exerciseNameSnapshot: _stringFromPayload(
map,
'exerciseNameSnapshot',
id,
),
exerciseDescriptionSnapshot:
map['exerciseDescriptionSnapshot'] as String?,
exerciseImageMediaIdSnapshot:
map['exerciseImageMediaIdSnapshot'] as String?,
exerciseImageMediaIdsSnapshot: _stringListFromPayload(
map['exerciseImageMediaIdsSnapshot'] ??
map['imageMediaIdsSnapshot'],
),
exerciseVideoMediaIdSnapshot:
map['exerciseVideoMediaIdSnapshot'] as String?,
exerciseStepsSnapshot: _stepsFromPayload(
map['exerciseStepsSnapshot'],
),
autoStartNextTimedStepSnapshot:
map['autoStartNextTimedStepSnapshot'] as bool? ?? true,
autoStartNextTimedStepOverride:
map['autoStartNextTimedStepOverride'] as bool?,
exerciseArchivedSnapshot: map['exerciseArchivedSnapshot'] == true,
availableTimeSnapshot: map['availableTimeSnapshot'] == true,
availableRepsSnapshot: map['availableRepsSnapshot'] == true,
availableScoreSnapshot: map['availableScoreSnapshot'] == true,
scoreInputModeSnapshot: _scoreInputModeFromDb(
map['scoreInputModeSnapshot'] as String? ?? 'manual',
),
scoreLabelSnapshot: map['scoreLabelSnapshot'] as String?,
scoreUnitSnapshot: map['scoreUnitSnapshot'] as String?,
setsCount: map['setsCount'] as int? ?? 1,
timeEnabled: map['timeEnabled'] == true,
repsEnabled: map['repsEnabled'] == true,
scoreEnabled: map['scoreEnabled'] == true,
targetTimeSeconds: map['targetTimeSeconds'] as int?,
targetReps: map['targetReps'] as int?,
targetScore: (map['targetScore'] as num?)?.toDouble(),
targetScoreTimeMs: map['targetScoreTimeMs'] as int?,
restSecondsOverride: map['restSecondsOverride'] as int?,
);
})
.toList(growable: false);
}
List<domain.WorkoutTemplateProgram> _workoutTemplateProgramsFromPayload(
Object? value,
domain.EntityMetadata parentMetadata,
) {
if (value is! List) {
return const [];
}
return value
.whereType<Map>()
.map((entry) {
final map = Map<String, Object?>.from(entry);
final id = _stringFromPayload(map, 'id', 'template-program');
return domain.WorkoutTemplateProgram(
metadata: _childMetadataFromPayload(map, id, parentMetadata),
workoutTemplateId: parentMetadata.id,
sourceProgramId: null,
position: map['position'] as int? ?? 0,
programNameSnapshot: _stringFromPayload(
map,
'programNameSnapshot',
id,
),
defaultRestSecondsSnapshot:
map['defaultRestSecondsSnapshot'] as int? ?? 0,
programSnapshotJson:
map['programSnapshotJson'] as String? ?? '{"exercises":[]}',
);
})
.toList(growable: false);
}
List<domain.WorkoutTemplateExerciseOverride>
_workoutTemplateOverridesFromPayload(
Object? value,
domain.EntityMetadata parentMetadata,
) {
if (value is! List) {
return const [];
}
return value
.whereType<Map>()
.map((entry) {
final map = Map<String, Object?>.from(entry);
final id = _stringFromPayload(map, 'id', 'template-override');
return domain.WorkoutTemplateExerciseOverride(
metadata: _childMetadataFromPayload(map, id, parentMetadata),
workoutTemplateProgramId: _stringFromPayload(
map,
'workoutTemplateProgramId',
'',
),
snapshotProgramExerciseId: _stringFromPayload(
map,
'snapshotProgramExerciseId',
'',
),
setsCountOverride: map['setsCountOverride'] as int?,
targetTimeSecondsOverride: map['targetTimeSecondsOverride'] as int?,
targetRepsOverride: map['targetRepsOverride'] as int?,
targetScoreOverride: (map['targetScoreOverride'] as num?)?.toDouble(),
targetScoreTimeMsOverride: map['targetScoreTimeMsOverride'] as int?,
autoStartNextTimedStepOverride:
map['autoStartNextTimedStepOverride'] as bool?,
);
})
.toList(growable: false);
}
domain.EntityMetadata _childMetadataFromPayload(
Map<String, Object?> payload,
String id,
domain.EntityMetadata parentMetadata,
) {
final metadata = payload['metadata'];
final map = metadata is Map ? Map<String, Object?>.from(metadata) : null;
return domain.EntityMetadata(
id: id,
createdAt:
_dateTimeFromPayload(map?['createdAt']) ?? parentMetadata.createdAt,
updatedAt:
_dateTimeFromPayload(map?['updatedAt']) ?? parentMetadata.updatedAt,
deletedAt: _dateTimeFromPayload(map?['deletedAt']),
schemaVersion:
map?['schemaVersion'] as int? ?? parentMetadata.schemaVersion,
syncState: parentMetadata.syncState,
localRevision: map?['localRevision'] as int? ?? 0,
originDeviceId:
map?['originDeviceId'] as String? ?? parentMetadata.originDeviceId,
);
}
String _stringFromPayload(
Map<String, Object?> payload,
String key,
String fallback,
) {
final value = payload[key];
return value is String && value.trim().isNotEmpty ? value : fallback;
}
List<String> _stringListFromPayload(Object? value) {
if (value is! List) {
return const [];
}
return value.whereType<String>().toList(growable: false);
}
List<domain.ExerciseStep> _stepsFromPayload(Object? value) {
if (value is! List) {
return const [];
}
return value
.map((raw) {
final json = Map<String, Object?>.from(raw as Map);
return domain.ExerciseStep(
id: _requiredString(json, 'id'),
position: _requiredInt(json, 'position'),
name: _requiredString(json, 'name'),
type: _exerciseStepTypeFromDb(_requiredString(json, 'type')),
defaultTargetValue: _requiredInt(json, 'defaultTargetValue'),
hasScore: _requiredBool(json, 'hasScore'),
scoreInputMode: _scoreInputModeFromDb(
_optionalString(json, 'scoreInputMode') ?? 'manual',
),
scoreLabel: _optionalString(json, 'scoreLabel'),
scoreUnit: _optionalString(json, 'scoreUnit'),
defaultTargetScore: _optionalDouble(json, 'defaultTargetScore'),
defaultTargetScoreTimeMs: _optionalInt(
json,
'defaultTargetScoreTimeMs',
),
);
})
.toList(growable: false);
}
DateTime? _dateTimeFromPayload(Object? value) {
return value is String ? DateTime.tryParse(value)?.toUtc() : null;
}
const _syncableEntityTypes = [
'Exercise',
'Program',
'WorkoutTemplate',
'WorkoutHistory',
'MediaAsset',
];
SyncResourceType? _syncResourceTypeFromEntityType(String entityType) {
return switch (entityType) {
'Exercise' => SyncResourceType.exercise,
'Program' => SyncResourceType.program,
'WorkoutTemplate' => SyncResourceType.workoutTemplate,
'WorkoutHistory' => SyncResourceType.workoutHistory,
'MediaAsset' => SyncResourceType.mediaAsset,
_ => null,
};
}
String _tableNameForResourceType(SyncResourceType type) => switch (type) {
SyncResourceType.exercise => 'exercises',
SyncResourceType.program => 'programs',
SyncResourceType.workoutTemplate => 'workout_templates',
SyncResourceType.workoutHistory => 'workout_history',
SyncResourceType.mediaAsset => 'media_assets',
};
String _syncStateToDb(domain.SyncState state) => switch (state) {
domain.SyncState.localOnly => 'localOnly',
domain.SyncState.dirty => 'dirty',
domain.SyncState.synced => 'synced',
domain.SyncState.deleted => 'deleted',
};
domain.SyncState _syncStateFromDb(String value) => switch (value) {
'localOnly' => domain.SyncState.localOnly,
'dirty' => domain.SyncState.dirty,
'synced' => domain.SyncState.synced,
'deleted' => domain.SyncState.deleted,
_ => throw domain.DomainException('Unknown sync state: $value'),
};
String _syncResourceTypeToDb(SyncResourceType type) => switch (type) {
SyncResourceType.exercise => 'exercise',
SyncResourceType.program => 'program',
SyncResourceType.workoutTemplate => 'workoutTemplate',
SyncResourceType.workoutHistory => 'workoutHistory',
SyncResourceType.mediaAsset => 'mediaAsset',
};
SyncResourceType _syncResourceTypeFromDb(String value) => switch (value) {
'exercise' => SyncResourceType.exercise,
'program' => SyncResourceType.program,
'workoutTemplate' => SyncResourceType.workoutTemplate,
'workoutHistory' => SyncResourceType.workoutHistory,
'mediaAsset' => SyncResourceType.mediaAsset,
_ => throw domain.DomainException('Unknown sync resource type: $value'),
};
String _onlineSyncStatusToDb(OnlineSyncStatus status) => switch (status) {
OnlineSyncStatus.idle => 'idle',
OnlineSyncStatus.syncing => 'syncing',
OnlineSyncStatus.success => 'success',
OnlineSyncStatus.failure => 'failure',
};
OnlineSyncStatus _onlineSyncStatusFromDb(String value) => switch (value) {
'idle' => OnlineSyncStatus.idle,
'syncing' => OnlineSyncStatus.syncing,
'success' => OnlineSyncStatus.success,
'failure' => OnlineSyncStatus.failure,
_ => throw domain.DomainException('Unknown sync status: $value'),
};
String _shareResourceTypeToDb(domain.ShareResourceType type) => switch (type) {
domain.ShareResourceType.program => 'program',
domain.ShareResourceType.workoutTemplate => 'workoutTemplate',
};
domain.ShareResourceType _shareResourceTypeFromDb(String value) =>
switch (value) {
'program' => domain.ShareResourceType.program,
'workoutTemplate' => domain.ShareResourceType.workoutTemplate,
_ => throw domain.DomainException('Unknown share resource type: $value'),
};
String _shareInboxStatusToDb(domain.ShareInboxStatus status) =>
switch (status) {
domain.ShareInboxStatus.pending => 'pending',
domain.ShareInboxStatus.accepted => 'accepted',
domain.ShareInboxStatus.declined => 'declined',
domain.ShareInboxStatus.revoked => 'revoked',
};
domain.ShareInboxStatus _shareInboxStatusFromDb(String value) =>
switch (value) {
'pending' => domain.ShareInboxStatus.pending,
'accepted' => domain.ShareInboxStatus.accepted,
'declined' => domain.ShareInboxStatus.declined,
'revoked' => domain.ShareInboxStatus.revoked,
_ => throw domain.DomainException('Unknown share inbox status: $value'),
};
String _pendingShareActionTypeToDb(domain.PendingShareActionType type) =>
switch (type) {
domain.PendingShareActionType.send => 'send',
domain.PendingShareActionType.accept => 'accept',
domain.PendingShareActionType.decline => 'decline',
domain.PendingShareActionType.revoke => 'revoke',
};
domain.PendingShareActionType _pendingShareActionTypeFromDb(String value) =>
switch (value) {
'send' => domain.PendingShareActionType.send,
'accept' => domain.PendingShareActionType.accept,
'decline' => domain.PendingShareActionType.decline,
'revoke' => domain.PendingShareActionType.revoke,
_ => throw domain.DomainException('Unknown share action type: $value'),
};
String _pendingShareActionStatusToDb(domain.PendingShareActionStatus status) =>
switch (status) {
domain.PendingShareActionStatus.pending => 'pending',
domain.PendingShareActionStatus.succeeded => 'succeeded',
domain.PendingShareActionStatus.failed => 'failed',
};
domain.PendingShareActionStatus _pendingShareActionStatusFromDb(String value) =>
switch (value) {
'pending' => domain.PendingShareActionStatus.pending,
'succeeded' => domain.PendingShareActionStatus.succeeded,
'failed' => domain.PendingShareActionStatus.failed,
_ => throw domain.DomainException('Unknown share action status: $value'),
};
String _activeStatusToDb(domain.ActiveWorkoutStatus status) => switch (status) {
domain.ActiveWorkoutStatus.running => 'running',
domain.ActiveWorkoutStatus.paused => 'paused',
domain.ActiveWorkoutStatus.savedExit => 'savedExit',
domain.ActiveWorkoutStatus.completed => 'completed',
domain.ActiveWorkoutStatus.abandoned => 'abandoned',
};
String _mediaKindToDb(domain.MediaKind kind) => switch (kind) {
domain.MediaKind.image => 'image',
domain.MediaKind.video => 'video',
};
domain.MediaKind _mediaKindFromDb(String value) => switch (value) {
'image' => domain.MediaKind.image,
'video' => domain.MediaKind.video,
_ => throw domain.DomainException('Unknown media kind: $value'),
};
String _scoreInputModeToDb(domain.ScoreInputMode mode) => switch (mode) {
domain.ScoreInputMode.manual => 'manual',
domain.ScoreInputMode.stopwatch => 'stopwatch',
};
domain.ScoreInputMode _scoreInputModeFromDb(String value) => switch (value) {
'manual' => domain.ScoreInputMode.manual,
'stopwatch' => domain.ScoreInputMode.stopwatch,
_ => throw domain.DomainException('Unknown score input mode: $value'),
};
String _exerciseStepTypeToDb(domain.ExerciseStepType type) => switch (type) {
domain.ExerciseStepType.time => 'time',
domain.ExerciseStepType.reps => 'reps',
};
domain.ExerciseStepType _exerciseStepTypeFromDb(String value) =>
switch (value) {
'time' => domain.ExerciseStepType.time,
'reps' => domain.ExerciseStepType.reps,
_ => throw domain.DomainException('Unknown exercise step type: $value'),
};
String _setResultStatusToDb(domain.SetResultStatus status) => switch (status) {
domain.SetResultStatus.completed => 'completed',
domain.SetResultStatus.skipped => 'skipped',
};
domain.SetResultStatus _setResultStatusFromDb(String value) => switch (value) {
'completed' => domain.SetResultStatus.completed,
'skipped' => domain.SetResultStatus.skipped,
_ => throw domain.DomainException('Unknown set result status: $value'),
};
String _scoreStopwatchStatusToDb(domain.ActiveScoreStopwatchStatus status) =>
switch (status) {
domain.ActiveScoreStopwatchStatus.running => 'running',
domain.ActiveScoreStopwatchStatus.paused => 'paused',
domain.ActiveScoreStopwatchStatus.stopped => 'stopped',
};
domain.ActiveScoreStopwatchStatus _scoreStopwatchStatusFromDb(String value) =>
switch (value) {
'running' => domain.ActiveScoreStopwatchStatus.running,
'paused' => domain.ActiveScoreStopwatchStatus.paused,
'stopped' => domain.ActiveScoreStopwatchStatus.stopped,
_ => throw domain.DomainException(
'Unknown active score stopwatch status: $value',
),
};
String _setTimerStatusToDb(domain.ActiveSetTimerStatus status) =>
switch (status) {
domain.ActiveSetTimerStatus.running => 'running',
domain.ActiveSetTimerStatus.paused => 'paused',
domain.ActiveSetTimerStatus.stopped => 'stopped',
domain.ActiveSetTimerStatus.skipped => 'skipped',
};
domain.ActiveSetTimerStatus _setTimerStatusFromDb(String value) =>
switch (value) {
'running' => domain.ActiveSetTimerStatus.running,
'paused' => domain.ActiveSetTimerStatus.paused,
'stopped' => domain.ActiveSetTimerStatus.stopped,
'skipped' => domain.ActiveSetTimerStatus.skipped,
_ => throw domain.DomainException(
'Unknown active set timer status: $value',
),
};
String _exerciseStepProgressStatusToDb(
domain.ActiveExerciseStepProgressStatus status,
) => switch (status) {
domain.ActiveExerciseStepProgressStatus.notStarted => 'notStarted',
domain.ActiveExerciseStepProgressStatus.waitingManual => 'waitingManual',
domain.ActiveExerciseStepProgressStatus.runningTimer => 'runningTimer',
domain.ActiveExerciseStepProgressStatus.pausedTimer => 'pausedTimer',
domain.ActiveExerciseStepProgressStatus.stoppedTimer => 'stoppedTimer',
domain.ActiveExerciseStepProgressStatus.sequenceComplete =>
'sequenceComplete',
};
domain.ActiveExerciseStepProgressStatus _exerciseStepProgressStatusFromDb(
String value,
) => switch (value) {
'notStarted' => domain.ActiveExerciseStepProgressStatus.notStarted,
'waitingManual' => domain.ActiveExerciseStepProgressStatus.waitingManual,
'runningTimer' => domain.ActiveExerciseStepProgressStatus.runningTimer,
'pausedTimer' => domain.ActiveExerciseStepProgressStatus.pausedTimer,
'stoppedTimer' => domain.ActiveExerciseStepProgressStatus.stoppedTimer,
'sequenceComplete' =>
domain.ActiveExerciseStepProgressStatus.sequenceComplete,
_ => throw domain.DomainException(
'Unknown active exercise step progress status: $value',
),
};
String? _encodeImageMediaIdsSnapshot(List<String> imageMediaIds) {
return imageMediaIds.isEmpty ? null : jsonEncode(imageMediaIds);
}
List<String> _decodeImageMediaIdsSnapshot(
String? encoded,
String? fallbackImageMediaId,
) {
if (encoded == null || encoded.trim().isEmpty) {
return [if (fallbackImageMediaId != null) fallbackImageMediaId];
}
final decoded = jsonDecode(encoded);
if (decoded is! List) {
throw const domain.DomainException(
'Invalid exercise image media ids snapshot.',
);
}
return decoded
.whereType<String>()
.where((id) => id.trim().isNotEmpty)
.toList(growable: false);
}
String? _encodeExerciseStepsSnapshot(List<domain.ExerciseStep> steps) {
return steps.isEmpty
? null
: jsonEncode(steps.map((step) => step.toSnapshotJson()).toList());
}
List<domain.ExerciseStep> _decodeExerciseStepsSnapshot(String? encoded) {
if (encoded == null || encoded.trim().isEmpty) {
return const [];
}
final decoded = jsonDecode(encoded);
if (decoded is! List) {
throw const domain.DomainException('Invalid exercise steps snapshot.');
}
return decoded
.map((item) {
if (item is! Map) {
throw const domain.DomainException('Invalid exercise step snapshot.');
}
final json = Map<String, Object?>.from(item);
return domain.ExerciseStep(
id: _requiredString(json, 'id'),
position: _requiredInt(json, 'position'),
name: _requiredString(json, 'name'),
type: _exerciseStepTypeFromDb(_requiredString(json, 'type')),
defaultTargetValue: _requiredInt(json, 'defaultTargetValue'),
hasScore: _requiredBool(json, 'hasScore'),
scoreInputMode: _scoreInputModeFromDb(
_optionalString(json, 'scoreInputMode') ?? 'manual',
),
scoreLabel: _optionalString(json, 'scoreLabel'),
scoreUnit: _optionalString(json, 'scoreUnit'),
defaultTargetScore: _optionalDouble(json, 'defaultTargetScore'),
defaultTargetScoreTimeMs: _optionalInt(
json,
'defaultTargetScoreTimeMs',
),
);
})
.toList(growable: false);
}
String _requiredString(Map<String, Object?> json, String key) {
final value = json[key];
if (value is String && value.trim().isNotEmpty) {
return value;
}
throw domain.DomainException('$key must be a non-empty string.');
}
String? _optionalString(Map<String, Object?> json, String key) {
final value = json[key];
if (value == null) {
return null;
}
return value is String ? value : null;
}
int _requiredInt(Map<String, Object?> json, String key) {
final value = json[key];
if (value is int) {
return value;
}
throw domain.DomainException('$key must be an integer.');
}
int? _optionalInt(Map<String, Object?> json, String key) {
final value = json[key];
return value is int ? value : null;
}
double? _optionalDouble(Map<String, Object?> json, String key) {
final value = json[key];
return (value as num?)?.toDouble();
}
bool _requiredBool(Map<String, Object?> json, String key) {
final value = json[key];
if (value is bool) {
return value;
}
throw domain.DomainException('$key must be a boolean.');
}
domain.ActiveWorkoutStatus _activeStatusFromDb(String value) => switch (value) {
'running' => domain.ActiveWorkoutStatus.running,
'paused' => domain.ActiveWorkoutStatus.paused,
'savedExit' => domain.ActiveWorkoutStatus.savedExit,
'completed' => domain.ActiveWorkoutStatus.completed,
'abandoned' => domain.ActiveWorkoutStatus.abandoned,
_ => throw domain.DomainException('Unknown active workout status: $value'),
};