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 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), await _exerciseStarterMetadata(id), ); } @override Future> 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 = []; for (final row in rows) { exercises.add( _exerciseFromRow( row, await _imageMediaIdsForExercise(row.id), await _stepsForExercise(row.id), await _exerciseStarterMetadata(row.id), ), ); } return exercises; } @override Future 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 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 _writeExerciseStarterMetadata(database, exercise); await _replaceExerciseImages(database, exercise); await _replaceExerciseSteps(database, exercise); }); } Future> _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> _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(); } Future<_ExerciseStarterMetadata> _exerciseStarterMetadata( String exerciseId, ) async { final row = await database .customSelect( 'SELECT category, is_example FROM exercises WHERE id = ? LIMIT 1', variables: [Variable(exerciseId)], ) .getSingleOrNull(); if (row == null) { return const _ExerciseStarterMetadata(); } return _ExerciseStarterMetadata( category: _exerciseCategoryFromDb( row.readNullable('category') ?? 'uncategorized', ), isExample: (row.readNullable('is_example') ?? 0) == 1, ); } } final class DriftStarterSeedRepository implements StarterSeedStateRepository, StarterContentRepository { const DriftStarterSeedRepository(this.database); static const _starterSeedKey = 'starter'; final db.AppDatabase database; @override Future readAppliedStarterSeedVersion() async { final row = await database .customSelect( 'SELECT version FROM local_seed_metadata WHERE key = ? LIMIT 1', variables: [Variable(_starterSeedKey)], ) .getSingleOrNull(); return row?.read('version') ?? 0; } @override Future writeAppliedStarterSeedVersion( int version, DateTime appliedAt, ) async { await database.customStatement( 'INSERT INTO local_seed_metadata (key, version, applied_at) ' 'VALUES (?, ?, ?) ' 'ON CONFLICT(key) DO UPDATE SET ' 'version = excluded.version, applied_at = excluded.applied_at', [_starterSeedKey, version, appliedAt.toUtc().millisecondsSinceEpoch], ); } @override Future isLocalContentEmpty() async { final exerciseCount = await _tableCount(database.exercises.actualTableName); final programCount = await _tableCount(database.programs.actualTableName); final templateCount = await _tableCount( database.workoutTemplates.actualTableName, ); return exerciseCount == 0 && programCount == 0 && templateCount == 0; } @override Future insertStarterContent(StarterContent content) async { await database.transaction(() async { for (final exercise in content.exercises) { await database .into(database.exercises) .insertOnConflictUpdate(_exerciseCompanion(exercise)); await _writeExerciseStarterMetadata(database, exercise); await _replaceExerciseSteps(database, exercise); } await database .into(database.programs) .insertOnConflictUpdate(_programCompanion(content.program)); await _writeProgramStarterMetadata(database, content.program); for (final exercise in content.program.exercises) { await database .into(database.programExercises) .insertOnConflictUpdate(_programExerciseCompanion(exercise)); } await database .into(database.workoutTemplates) .insertOnConflictUpdate( _workoutTemplateCompanion(content.workoutTemplate), ); await _writeWorkoutTemplateStarterMetadata( database, content.workoutTemplate, ); for (final program in content.workoutTemplate.programs) { await database .into(database.workoutTemplatePrograms) .insertOnConflictUpdate(_workoutTemplateProgramCompanion(program)); } }); } Future _tableCount(String tableName) async { final row = await database .customSelect('SELECT COUNT(*) AS count FROM $tableName') .getSingle(); return row.read('count'); } } final class DriftMediaAssetRepository implements MediaAssetRepository { const DriftMediaAssetRepository(this.database); final db.AppDatabase database; @override Future 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> listActive() async { final rows = await (database.select( database.mediaAssets, )..where((table) => table.deletedAt.isNull())).get(); return rows.map(_mediaAssetFromRow).toList(); } @override Future 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 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 saveSession(domain.UserAccountSession session) async { await database .into(database.onlineAccountSessions) .insertOnConflictUpdate(_userAccountSessionCompanion(session)); } @override Future 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 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 save(SyncMetadataSnapshot metadata) async { await database .into(database.syncMetadataEntries) .insertOnConflictUpdate( db.SyncMetadataEntriesCompanion.insert( id: const Value(_id), serverCursor: Value(metadata.serverCursor), lastSuccessfulSyncAt: Value( _utcOrNull(metadata.lastSuccessfulSyncAt), ), lastAttemptAt: Value(_utcOrNull(metadata.lastAttemptAt)), lastFailureAt: Value(_utcOrNull(metadata.lastFailureAt)), status: Value(_onlineSyncStatusToDb(metadata.status)), pendingPushCount: Value(metadata.pendingPushCount), ), ); } } final class DriftRemoteResourceMappingRepository implements RemoteResourceMappingRepository { const DriftRemoteResourceMappingRepository(this.database); final db.AppDatabase database; @override Future 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 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> 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 = >{}; for (final row in rows) { grouped .putIfAbsent('${row.entityType}:${row.entityId}', () => []) .add(row); } final changes = []; 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 markChangesSynced( List changeLogIds, DateTime syncedAt, ) async { if (changeLogIds.isEmpty) { return; } await (database.update( database.changeLogEntries, )..where((table) => table.id.isIn(changeLogIds))).write( db.ChangeLogEntriesCompanion( syncedAt: Value(syncedAt.toUtc()), ), ); } @override Future 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 _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 _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(id)], ) .getSingleOrNull(); final value = row?.data['updated_at']; return value is DateTime ? value.toUtc() : null; } Future _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(item.deletedAt!.toUtc()), Variable(item.clientUpdatedAt.toUtc()), Variable(item.clientId), ], ); return updated > 0; } Future _applyRemotePayload(RemoteSyncedItem item) async { switch (item.resourceType) { case SyncResourceType.exercise: final exercise = _exerciseFromPayload(item); await database .into(database.exercises) .insertOnConflictUpdate(_exerciseCompanion(exercise)); await _writeExerciseStarterMetadata(database, exercise); 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)); await _writeProgramStarterMetadata(database, 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)); await _writeWorkoutTemplateStarterMetadata(database, 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), await DriftExerciseRepository(database)._exerciseStarterMetadata(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(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> 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 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 upsert(domain.ShareInboxItem item) async { await database .into(database.shareInboxItems) .insertOnConflictUpdate(_shareInboxItemCompanion(item)); } @override Future upsertAll(List items) async { await database.batch((batch) { batch.insertAllOnConflictUpdate( database.shareInboxItems, items.map(_shareInboxItemCompanion).toList(), ); }); } @override Future 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(_shareInboxStatusToDb(status)), respondedAt: Value(respondedAt.toUtc()), updatedAt: Value(respondedAt.toUtc()), ), ); } } final class DriftPendingShareActionRepository implements PendingShareActionRepository { const DriftPendingShareActionRepository(this.database); final db.AppDatabase database; @override Future> 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 add(domain.PendingShareAction action) async { await database .into(database.pendingShareActions) .insertOnConflictUpdate(_pendingShareActionCompanion(action)); } @override Future markSucceeded(String id, DateTime attemptedAt) async { await _markActionAttempt( id, attemptedAt, domain.PendingShareActionStatus.succeeded, ); } @override Future markFailed(String id, DateTime attemptedAt) async { await _markActionAttempt( id, attemptedAt, domain.PendingShareActionStatus.failed, ); } Future _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(attemptedAt.toUtc()), attemptCount: Value((row?.attemptCount ?? 0) + 1), status: Value(_pendingShareActionStatusToDb(status)), ), ); } } final class DriftProgramRepository implements ProgramRepository { const DriftProgramRepository(this.database); final db.AppDatabase database; @override Future 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(), isExample: await _isProgramExample(id), ); } @override Future> listActive() async { final rows = await (database.select(database.programs) ..where((table) => table.deletedAt.isNull()) ..orderBy([(table) => OrderingTerm.asc(table.name)])) .get(); final programs = []; 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(), isExample: await _isProgramExample(row.id), ), ); } return programs; } @override Future 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)), ); await _writeProgramStarterMetadata(database, program); for (final exercise in program.exercises) { await saveExercise(exercise); } }); } @override Future 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 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, ); }); } Future _isProgramExample(String id) async { final row = await database .customSelect( 'SELECT is_example FROM programs WHERE id = ? LIMIT 1', variables: [Variable(id)], ) .getSingleOrNull(); return (row?.readNullable('is_example') ?? 0) == 1; } } final class DriftWorkoutTemplateRepository implements WorkoutTemplateRepository { const DriftWorkoutTemplateRepository(this.database); final db.AppDatabase database; @override Future 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 ? [] : 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(), isExample: await _isWorkoutTemplateExample(id), ); } @override Future> listActive() async { final rows = await (database.select(database.workoutTemplates) ..where((table) => table.deletedAt.isNull()) ..orderBy([(table) => OrderingTerm.asc(table.name)])) .get(); final templates = []; for (final row in rows) { final template = await findById(row.id); if (template != null) { templates.add(template); } } return templates; } @override Future 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)), ); await _writeWorkoutTemplateStarterMetadata(database, template); for (final program in template.programs) { await saveProgram(program); } for (final override in template.overrides) { await saveOverride(override); } }); } @override Future 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 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 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, ); }); } Future _isWorkoutTemplateExample(String id) async { final row = await database .customSelect( 'SELECT is_example FROM workout_templates WHERE id = ? LIMIT 1', variables: [Variable(id)], ) .getSingleOrNull(); return (row?.readNullable('is_example') ?? 0) == 1; } } final class DriftActiveSessionRepository implements ActiveSessionRepository { const DriftActiveSessionRepository(this.database); final db.AppDatabase database; @override Future 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 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 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 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 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 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 findRestStateById(String id) async { final row = await database .customSelect( 'SELECT * FROM active_rest_states WHERE id = ? LIMIT 1', variables: [Variable(id)], ) .getSingleOrNull(); return row == null ? null : _activeRestStateFromCustomRow(row); } @override Future 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 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 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 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 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 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(sessionId), Variable(programIndex), Variable(exerciseIndex), Variable(setIndex), ], ) .getSingleOrNull(); return row == null ? null : _activeSetTimerStateFromCustomRow(row); } @override Future 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> 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> 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(sessionId)], ) .get(); return rows.map(_activeSetTimerStateFromCustomRow).toList(); } @override Future> 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> 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> 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(sessionId)], ) .get(); return rows.map(_activeRestStateFromCustomRow).toList(); } @override Future> 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 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> listActive() async { final rows = await (database.select(database.workoutHistories) ..where((table) => table.deletedAt.isNull()) ..orderBy([(table) => OrderingTerm.desc(table.startedAt)])) .get(); final histories = []; for (final row in rows) { final history = await findById(row.id); if (history != null) { histories.add(history); } } return histories; } @override Future 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 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 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 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 _upsertWithChangeLog({ required db.AppDatabase database, required String tableName, required String entityType, required domain.EntityMetadata metadata, required Future 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 _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 _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 _softDeleteExerciseImageRows( db.AppDatabase database, List 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 _softDeleteExerciseStepRows( db.AppDatabase database, List 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'; } final class _ExerciseStarterMetadata { const _ExerciseStarterMetadata({ this.category = domain.ExerciseCategory.uncategorized, this.isExample = false, }); final domain.ExerciseCategory category; final bool isExample; } Future _writeExerciseStarterMetadata( db.AppDatabase database, domain.Exercise exercise, ) async { await database.customStatement( 'UPDATE exercises SET category = ?, is_example = ? WHERE id = ?', [ _exerciseCategoryToDb(exercise.category), exercise.isExample ? 1 : 0, exercise.metadata.id, ], ); } Future _writeProgramStarterMetadata( db.AppDatabase database, domain.Program program, ) async { await database.customStatement( 'UPDATE programs SET is_example = ? WHERE id = ?', [program.isExample ? 1 : 0, program.metadata.id], ); } Future _writeWorkoutTemplateStarterMetadata( db.AppDatabase database, domain.WorkoutTemplate template, ) async { await database.customStatement( 'UPDATE workout_templates SET is_example = ? WHERE id = ?', [template.isExample ? 1 : 0, template.metadata.id], ); } Future _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(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 _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 _softDeleteProgramExerciseRows( db.AppDatabase database, List 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 _softDeleteWorkoutTemplateProgramRows( db.AppDatabase database, List 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 _softDeleteWorkoutTemplateExerciseOverrideRows( db.AppDatabase database, List 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 _softDeleteWorkoutHistorySetResultRows( db.AppDatabase database, List 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 _softDeleteWorkoutHistoryStepResultRows( db.AppDatabase database, List 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 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 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 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 _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, createdAt: values[1] as Value, updatedAt: values[2] as Value, deletedAt: values[3] as Value, schemaVersion: values[4] as Value, syncState: values[5] as Value, localRevision: values[6] as Value, originDeviceId: values[7] as Value, futureOwnerProfileId: values[8] as Value, lastSyncedAt: values[9] as Value, remoteRevision: values[10] as Value, 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 imageMediaIds, List steps, _ExerciseStarterMetadata starterMetadata, ) { 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, category: starterMetadata.category, isExample: starterMetadata.isExample, 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, createdAt: values[1] as Value, updatedAt: values[2] as Value, deletedAt: values[3] as Value, schemaVersion: values[4] as Value, syncState: values[5] as Value, localRevision: values[6] as Value, originDeviceId: values[7] as Value, futureOwnerProfileId: values[8] as Value, lastSyncedAt: values[9] as Value, remoteRevision: values[10] as Value, 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(session.displayName), isLoggedIn: Value(session.isLoggedIn), createdAt: Value(session.createdAt.toUtc()), updatedAt: Value(session.updatedAt.toUtc()), lastAuthenticatedAt: Value( _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(_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(action.shareId), resourceType: Value( action.resourceType == null ? null : _shareResourceTypeToDb(action.resourceType!), ), payloadJson: Value(action.payloadJson), recipientEmailsJson: Value(action.recipientEmailsJson), createdAt: action.createdAt.toUtc(), lastAttemptAt: Value(_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, createdAt: values[1] as Value, updatedAt: values[2] as Value, deletedAt: values[3] as Value, schemaVersion: values[4] as Value, syncState: values[5] as Value, localRevision: values[6] as Value, originDeviceId: values[7] as Value, futureOwnerProfileId: values[8] as Value, lastSyncedAt: values[9] as Value, remoteRevision: values[10] as Value, name: Value(program.name), defaultRestSeconds: Value(program.defaultRestSeconds), ); } domain.Program _programFromRow( db.Program row, List exercises, { bool isExample = false, }) { return domain.Program( metadata: _metadataFromRow(row), name: row.name, defaultRestSeconds: row.defaultRestSeconds, isExample: isExample, exercises: exercises, ); } db.ProgramExercisesCompanion _programExerciseCompanion( domain.ProgramExercise exercise, ) { final values = _metadataValues(exercise.metadata); return db.ProgramExercisesCompanion( id: values[0] as Value, createdAt: values[1] as Value, updatedAt: values[2] as Value, deletedAt: values[3] as Value, schemaVersion: values[4] as Value, syncState: values[5] as Value, localRevision: values[6] as Value, originDeviceId: values[7] as Value, futureOwnerProfileId: values[8] as Value, lastSyncedAt: values[9] as Value, remoteRevision: values[10] as Value, 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, createdAt: values[1] as Value, updatedAt: values[2] as Value, deletedAt: values[3] as Value, schemaVersion: values[4] as Value, syncState: values[5] as Value, localRevision: values[6] as Value, originDeviceId: values[7] as Value, futureOwnerProfileId: values[8] as Value, lastSyncedAt: values[9] as Value, remoteRevision: values[10] as Value, name: Value(template.name), lastStartedAt: Value(_utcOrNull(template.lastStartedAt)), ); } domain.WorkoutTemplate _workoutTemplateFromRow( db.WorkoutTemplate row, List programs, List overrides, { bool isExample = false, }) { return domain.WorkoutTemplate( metadata: _metadataFromRow(row), name: row.name, lastStartedAt: _utcOrNull(row.lastStartedAt), isExample: isExample, programs: programs, overrides: overrides, ); } db.WorkoutTemplateProgramsCompanion _workoutTemplateProgramCompanion( domain.WorkoutTemplateProgram program, ) { final values = _metadataValues(program.metadata); return db.WorkoutTemplateProgramsCompanion( id: values[0] as Value, createdAt: values[1] as Value, updatedAt: values[2] as Value, deletedAt: values[3] as Value, schemaVersion: values[4] as Value, syncState: values[5] as Value, localRevision: values[6] as Value, originDeviceId: values[7] as Value, futureOwnerProfileId: values[8] as Value, lastSyncedAt: values[9] as Value, remoteRevision: values[10] as Value, 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, createdAt: values[1] as Value, updatedAt: values[2] as Value, deletedAt: values[3] as Value, schemaVersion: values[4] as Value, syncState: values[5] as Value, localRevision: values[6] as Value, originDeviceId: values[7] as Value, futureOwnerProfileId: values[8] as Value, lastSyncedAt: values[9] as Value, remoteRevision: values[10] as Value, 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, createdAt: values[1] as Value, updatedAt: values[2] as Value, deletedAt: values[3] as Value, schemaVersion: values[4] as Value, syncState: values[5] as Value, localRevision: values[6] as Value, originDeviceId: values[7] as Value, futureOwnerProfileId: values[8] as Value, lastSyncedAt: values[9] as Value, remoteRevision: values[10] as Value, 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, createdAt: values[1] as Value, updatedAt: values[2] as Value, deletedAt: values[3] as Value, schemaVersion: values[4] as Value, syncState: values[5] as Value, localRevision: values[6] as Value, originDeviceId: values[7] as Value, futureOwnerProfileId: values[8] as Value, lastSyncedAt: values[9] as Value, remoteRevision: values[10] as Value, 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, createdAt: values[1] as Value, updatedAt: values[2] as Value, deletedAt: values[3] as Value, schemaVersion: values[4] as Value, syncState: values[5] as Value, localRevision: values[6] as Value, originDeviceId: values[7] as Value, futureOwnerProfileId: values[8] as Value, lastSyncedAt: values[9] as Value, remoteRevision: values[10] as Value, 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 _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(_utcOrNull(restState.pausedAt)), Variable(restState.accumulatedPausedMs), Variable(restState.metadata.id), ], ); } Future _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(metadata.id), Variable(metadata.createdAt.toUtc()), Variable(metadata.updatedAt.toUtc()), Variable(_utcOrNull(metadata.deletedAt)), Variable(metadata.schemaVersion), Variable(_syncStateToDb(metadata.syncState)), Variable(metadata.localRevision), Variable(metadata.originDeviceId), Variable(metadata.futureOwnerProfileId), Variable(_utcOrNull(metadata.lastSyncedAt)), Variable(metadata.remoteRevision), Variable(state.activeWorkoutSessionId), Variable(state.programIndex), Variable(state.exerciseIndex), Variable(state.setIndex), Variable(_setTimerStatusToDb(state.status)), Variable(_utcOrNull(state.startedAt)), Variable(state.accumulatedMs), Variable(_utcOrNull(state.stoppedAt)), Variable(_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, createdAt: values[1] as Value, updatedAt: values[2] as Value, deletedAt: values[3] as Value, schemaVersion: values[4] as Value, syncState: values[5] as Value, localRevision: values[6] as Value, originDeviceId: values[7] as Value, futureOwnerProfileId: values[8] as Value, lastSyncedAt: values[9] as Value, remoteRevision: values[10] as Value, 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, createdAt: values[1] as Value, updatedAt: values[2] as Value, deletedAt: values[3] as Value, schemaVersion: values[4] as Value, syncState: values[5] as Value, localRevision: values[6] as Value, originDeviceId: values[7] as Value, futureOwnerProfileId: values[8] as Value, lastSyncedAt: values[9] as Value, remoteRevision: values[10] as Value, 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(_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, createdAt: values[1] as Value, updatedAt: values[2] as Value, deletedAt: values[3] as Value, schemaVersion: values[4] as Value, syncState: values[5] as Value, localRevision: values[6] as Value, originDeviceId: values[7] as Value, futureOwnerProfileId: values[8] as Value, lastSyncedAt: values[9] as Value, remoteRevision: values[10] as Value, 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( result.scoreInputModeSnapshot == null ? null : _scoreInputModeToDb(result.scoreInputModeSnapshot!), ), scoreLabelSnapshot: Value(result.scoreLabelSnapshot), scoreUnitSnapshot: Value(result.scoreUnitSnapshot), targetScoreSnapshot: Value(result.targetScoreSnapshot), targetScoreTimeMsSnapshot: Value(result.targetScoreTimeMsSnapshot), status: Value(_setResultStatusToDb(result.status)), 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), note: Value(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, createdAt: values[1] as Value, updatedAt: values[2] as Value, deletedAt: values[3] as Value, schemaVersion: values[4] as Value, syncState: values[5] as Value, localRevision: values[6] as Value, originDeviceId: values[7] as Value, futureOwnerProfileId: values[8] as Value, lastSyncedAt: values[9] as Value, remoteRevision: values[10] as Value, 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, createdAt: values[1] as Value, updatedAt: values[2] as Value, deletedAt: values[3] as Value, schemaVersion: values[4] as Value, syncState: values[5] as Value, localRevision: values[6] as Value, originDeviceId: values[7] as Value, futureOwnerProfileId: values[8] as Value, lastSyncedAt: values[9] as Value, remoteRevision: values[10] as Value, 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, createdAt: values[1] as Value, updatedAt: values[2] as Value, deletedAt: values[3] as Value, schemaVersion: values[4] as Value, syncState: values[5] as Value, localRevision: values[6] as Value, originDeviceId: values[7] as Value, futureOwnerProfileId: values[8] as Value, lastSyncedAt: values[9] as Value, remoteRevision: values[10] as Value, 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( result.scoreInputModeSnapshot == null ? null : _scoreInputModeToDb(result.scoreInputModeSnapshot!), ), scoreLabelSnapshot: Value(result.scoreLabelSnapshot), scoreUnitSnapshot: Value(result.scoreUnitSnapshot), targetScoreSnapshot: Value(result.targetScoreSnapshot), targetScoreTimeMsSnapshot: Value(result.targetScoreTimeMsSnapshot), status: Value(_setResultStatusToDb(result.status)), 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), note: Value(result.note), ); } domain.WorkoutHistory _workoutHistoryFromRow( db.WorkoutHistory row, List results, List 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 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 payload; } Map _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 _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, 'category': exercise.category.name, 'isExample': exercise.isExample, 'steps': exercise.steps.map((step) => step.toSnapshotJson()).toList(), 'archivedAt': exercise.archivedAt?.toUtc().toIso8601String(), }; Map _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 _programPayload(domain.Program program) => { 'metadata': _metadataPayload(program.metadata), 'id': program.metadata.id, 'name': program.name, 'defaultRestSeconds': program.defaultRestSeconds, 'isExample': program.isExample, 'exercises': program.exercises .map((exercise) => exercise.toSnapshotJson()) .toList(), }; Map _workoutTemplatePayload(domain.WorkoutTemplate template) => { 'metadata': _metadataPayload(template.metadata), 'id': template.metadata.id, 'name': template.name, 'lastStartedAt': template.lastStartedAt?.toUtc().toIso8601String(), 'isExample': template.isExample, '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 _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, category: _exerciseCategoryFromDb( payload['category'] as String? ?? 'uncategorized', ), isExample: payload['isExample'] as bool? ?? false, 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, isExample: payload['isExample'] as bool? ?? false, 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']), isExample: payload['isExample'] as bool? ?? false, 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.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 _programExercisesFromPayload( Object? value, domain.EntityMetadata parentMetadata, ) { if (value is! List) { return const []; } return value .whereType() .map((entry) { final map = Map.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 _workoutTemplateProgramsFromPayload( Object? value, domain.EntityMetadata parentMetadata, ) { if (value is! List) { return const []; } return value .whereType() .map((entry) { final map = Map.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 _workoutTemplateOverridesFromPayload( Object? value, domain.EntityMetadata parentMetadata, ) { if (value is! List) { return const []; } return value .whereType() .map((entry) { final map = Map.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 payload, String id, domain.EntityMetadata parentMetadata, ) { final metadata = payload['metadata']; final map = metadata is Map ? Map.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 payload, String key, String fallback, ) { final value = payload[key]; return value is String && value.trim().isNotEmpty ? value : fallback; } List _stringListFromPayload(Object? value) { if (value is! List) { return const []; } return value.whereType().toList(growable: false); } List _stepsFromPayload(Object? value) { if (value is! List) { return const []; } return value .map((raw) { final json = Map.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 _exerciseCategoryToDb(domain.ExerciseCategory category) => switch (category) { domain.ExerciseCategory.shoot => 'shoot', domain.ExerciseCategory.freeThrows => 'freeThrows', domain.ExerciseCategory.dribble => 'dribble', domain.ExerciseCategory.finishing => 'finishing', domain.ExerciseCategory.conditioning => 'conditioning', domain.ExerciseCategory.defense => 'defense', domain.ExerciseCategory.mobility => 'mobility', domain.ExerciseCategory.uncategorized => 'uncategorized', }; domain.ExerciseCategory _exerciseCategoryFromDb(String value) => switch (value) { 'shoot' => domain.ExerciseCategory.shoot, 'freeThrows' => domain.ExerciseCategory.freeThrows, 'dribble' => domain.ExerciseCategory.dribble, 'finishing' => domain.ExerciseCategory.finishing, 'conditioning' => domain.ExerciseCategory.conditioning, 'defense' => domain.ExerciseCategory.defense, 'mobility' => domain.ExerciseCategory.mobility, 'uncategorized' => domain.ExerciseCategory.uncategorized, _ => throw domain.DomainException('Unknown exercise category: $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 imageMediaIds) { return imageMediaIds.isEmpty ? null : jsonEncode(imageMediaIds); } List _decodeImageMediaIdsSnapshot( String? encoded, String? fallbackImageMediaId, ) { if (encoded == null || encoded.trim().isEmpty) { return [?fallbackImageMediaId]; } final decoded = jsonDecode(encoded); if (decoded is! List) { throw const domain.DomainException( 'Invalid exercise image media ids snapshot.', ); } return decoded .whereType() .where((id) => id.trim().isNotEmpty) .toList(growable: false); } String? _encodeExerciseStepsSnapshot(List steps) { return steps.isEmpty ? null : jsonEncode(steps.map((step) => step.toSnapshotJson()).toList()); } List _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.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 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 json, String key) { final value = json[key]; if (value == null) { return null; } return value is String ? value : null; } int _requiredInt(Map json, String key) { final value = json[key]; if (value is int) { return value; } throw domain.DomainException('$key must be an integer.'); } int? _optionalInt(Map json, String key) { final value = json[key]; return value is int ? value : null; } double? _optionalDouble(Map json, String key) { final value = json[key]; return (value as num?)?.toDouble(); } bool _requiredBool(Map 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'), };