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