Finalise #183 : modele/persistance des types metier d'exercice, mapping vers la strategie Health Services, propagation du payload dans le contrat watch bridge et consommation cote Kotlin montre. Perimetre complet QA vert. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
6676 lines
230 KiB
Dart
6676 lines
230 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, business_types_json '
|
|
'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',
|
|
),
|
|
businessTypes: _decodeBusinessExerciseTypesJson(
|
|
row.readNullable<String>('business_types_json'),
|
|
),
|
|
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 _writeProgramExerciseHealthServicesStrategySnapshot(
|
|
database,
|
|
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.transaction(() async {
|
|
await database
|
|
.into(database.exercises)
|
|
.insertOnConflictUpdate(_exerciseCompanion(exercise));
|
|
await _writeExerciseStarterMetadata(database, exercise);
|
|
await _replaceRemoteExerciseImages(exercise);
|
|
await _replaceRemoteExerciseSteps(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));
|
|
await _writeProgramExerciseHealthServicesStrategySnapshot(
|
|
database,
|
|
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:
|
|
await _replaceRemoteWorkoutHistory(
|
|
_workoutHistoryFromLocalBackupPayload(item),
|
|
item.clientUpdatedAt,
|
|
);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
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,
|
|
_localWorkoutHistoryPayload(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},
|
|
);
|
|
}
|
|
|
|
Future<void> _replaceRemoteWorkoutHistory(
|
|
domain.WorkoutHistory history,
|
|
DateTime deletedAt,
|
|
) async {
|
|
await database.transaction(() async {
|
|
await database
|
|
.into(database.workoutHistories)
|
|
.insertOnConflictUpdate(_workoutHistoryCompanion(history));
|
|
for (final result in history.results) {
|
|
await database
|
|
.into(database.workoutHistorySetResults)
|
|
.insertOnConflictUpdate(_workoutHistorySetResultCompanion(result));
|
|
}
|
|
for (final result in history.stepResults) {
|
|
await database
|
|
.into(database.workoutHistoryStepResults)
|
|
.insertOnConflictUpdate(_workoutHistoryStepResultCompanion(result));
|
|
}
|
|
|
|
final activeResultIds = history.results
|
|
.map((result) => result.metadata.id)
|
|
.toSet();
|
|
await _softDeleteRemoteWorkoutHistoryChildren(
|
|
tableName: 'workout_history_set_results',
|
|
historyId: history.metadata.id,
|
|
keepIds: activeResultIds,
|
|
deletedAt: deletedAt,
|
|
);
|
|
final activeStepResultIds = history.stepResults
|
|
.map((result) => result.metadata.id)
|
|
.toSet();
|
|
await _softDeleteRemoteWorkoutHistoryChildren(
|
|
tableName: 'workout_history_step_results',
|
|
historyId: history.metadata.id,
|
|
keepIds: activeStepResultIds,
|
|
deletedAt: deletedAt,
|
|
);
|
|
});
|
|
}
|
|
|
|
Future<void> _softDeleteRemoteWorkoutHistoryChildren({
|
|
required String tableName,
|
|
required String historyId,
|
|
required Set<String> keepIds,
|
|
required DateTime deletedAt,
|
|
}) async {
|
|
final keepPredicate = keepIds.isEmpty
|
|
? ''
|
|
: 'AND id NOT IN (${List.filled(keepIds.length, '?').join(', ')})';
|
|
await database.customUpdate(
|
|
'''
|
|
UPDATE $tableName
|
|
SET deleted_at = ?, updated_at = ?, sync_state = 'deleted'
|
|
WHERE workout_history_id = ?
|
|
AND deleted_at IS NULL
|
|
$keepPredicate
|
|
''',
|
|
variables: [
|
|
Variable<DateTime>(deletedAt.toUtc()),
|
|
Variable<DateTime>(deletedAt.toUtc()),
|
|
Variable<String>(historyId),
|
|
for (final id in keepIds) Variable<String>(id),
|
|
],
|
|
);
|
|
}
|
|
|
|
Future<void> _replaceRemoteExerciseImages(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 removedIds = activeRowsByMediaId.values
|
|
.where((row) => !desiredIds.contains(row.mediaAssetId))
|
|
.map((row) => row.id)
|
|
.toList();
|
|
await _softDeleteRemoteRows(
|
|
tableName: 'exercise_images',
|
|
ids: removedIds,
|
|
deletedAt: 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,
|
|
syncState: exercise.metadata.syncState,
|
|
originDeviceId: exercise.metadata.originDeviceId,
|
|
)
|
|
: _metadataFromRow(existing).touch(exercise.metadata.updatedAt);
|
|
await 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> _replaceRemoteExerciseSteps(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 removedIds = activeRowsByStepId.values
|
|
.where((row) => !desiredIds.contains(row.id))
|
|
.map((row) => row.id)
|
|
.toList();
|
|
await _softDeleteRemoteRows(
|
|
tableName: 'exercise_steps',
|
|
ids: removedIds,
|
|
deletedAt: 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,
|
|
syncState: exercise.metadata.syncState,
|
|
originDeviceId: exercise.metadata.originDeviceId,
|
|
)
|
|
: _metadataFromRow(existing).touch(exercise.metadata.updatedAt);
|
|
await 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),
|
|
linkedToSeriesScore: Value(step.linkedToSeriesScore),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<void> _softDeleteRemoteRows({
|
|
required String tableName,
|
|
required List<String> ids,
|
|
required DateTime deletedAt,
|
|
}) async {
|
|
if (ids.isEmpty) {
|
|
return;
|
|
}
|
|
await database.customUpdate(
|
|
'''
|
|
UPDATE $tableName
|
|
SET deleted_at = ?, updated_at = ?, sync_state = 'deleted'
|
|
WHERE id IN (${List.filled(ids.length, '?').join(', ')})
|
|
''',
|
|
variables: [
|
|
Variable<DateTime>(deletedAt.toUtc()),
|
|
Variable<DateTime>(deletedAt.toUtc()),
|
|
for (final id in ids) Variable<String>(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,
|
|
await _programExercisesFromRows(database, exercises),
|
|
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,
|
|
await _programExercisesFromRows(database, exercises),
|
|
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: () async {
|
|
await database
|
|
.into(database.programExercises)
|
|
.insertOnConflictUpdate(_programExerciseCompanion(exercise));
|
|
await _writeProgramExerciseHealthServicesStrategySnapshot(
|
|
database,
|
|
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 _saveTemplate(template);
|
|
});
|
|
}
|
|
|
|
@override
|
|
Future<void> saveAll(List<domain.WorkoutTemplate> templates) async {
|
|
await database.transaction(() async {
|
|
for (final template in templates) {
|
|
await _saveTemplate(template);
|
|
}
|
|
});
|
|
}
|
|
|
|
Future<void> _saveTemplate(domain.WorkoutTemplate template) 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 database.transaction(() async {
|
|
final existing = await _findActiveScoreStopwatchStateRowByPosition(
|
|
database,
|
|
state.activeWorkoutSessionId,
|
|
state.programIndex,
|
|
state.exerciseIndex,
|
|
state.setIndex,
|
|
);
|
|
final effectiveState = existing == null
|
|
? state
|
|
: state.copyWith(
|
|
metadata: _metadataWithStoredIdentity(
|
|
state.metadata,
|
|
_metadataFromRow(existing),
|
|
),
|
|
);
|
|
final operation = existing == null
|
|
? 'insert'
|
|
: _operationForExistingMutation(
|
|
existingDeletedAt: existing.deletedAt,
|
|
deletedAt: effectiveState.metadata.deletedAt,
|
|
);
|
|
await _upsertActiveScoreStopwatchState(database, effectiveState);
|
|
await _writeChangeLog(
|
|
database: database,
|
|
entityType: 'ActiveScoreStopwatchState',
|
|
entityId: effectiveState.metadata.id,
|
|
operation: operation,
|
|
localRevision: effectiveState.metadata.localRevision,
|
|
originDeviceId: effectiveState.metadata.originDeviceId,
|
|
createdAt: effectiveState.metadata.updatedAt,
|
|
);
|
|
});
|
|
}
|
|
|
|
@override
|
|
Future<void> saveManualScoreState(domain.ActiveManualScoreState state) async {
|
|
await database.transaction(() async {
|
|
final existing = await _findActiveManualScoreStateRowByPosition(
|
|
database,
|
|
state.activeWorkoutSessionId,
|
|
state.programIndex,
|
|
state.exerciseIndex,
|
|
state.setIndex,
|
|
);
|
|
final effectiveState = existing == null
|
|
? state
|
|
: state.copyWith(
|
|
metadata: _metadataWithStoredIdentity(
|
|
state.metadata,
|
|
_metadataFromRow(existing),
|
|
),
|
|
);
|
|
final operation = existing == null
|
|
? 'insert'
|
|
: _operationForExistingMutation(
|
|
existingDeletedAt: existing.deletedAt,
|
|
deletedAt: effectiveState.metadata.deletedAt,
|
|
);
|
|
await _upsertActiveManualScoreState(database, effectiveState);
|
|
await _writeChangeLog(
|
|
database: database,
|
|
entityType: 'ActiveManualScoreState',
|
|
entityId: effectiveState.metadata.id,
|
|
operation: operation,
|
|
localRevision: effectiveState.metadata.localRevision,
|
|
originDeviceId: effectiveState.metadata.originDeviceId,
|
|
createdAt: effectiveState.metadata.updatedAt,
|
|
);
|
|
});
|
|
}
|
|
|
|
@override
|
|
Future<void> saveExerciseStepProgressState(
|
|
domain.ActiveExerciseStepProgressState state,
|
|
) async {
|
|
await database.transaction(() async {
|
|
final existing = await _findActiveExerciseStepProgressStateRowByPosition(
|
|
database,
|
|
state.activeWorkoutSessionId,
|
|
state.programIndex,
|
|
state.exerciseIndex,
|
|
state.setIndex,
|
|
);
|
|
final effectiveState = existing == null
|
|
? state
|
|
: state.copyWith(
|
|
metadata: _metadataWithStoredIdentity(
|
|
state.metadata,
|
|
_metadataFromRow(existing),
|
|
),
|
|
);
|
|
final operation = existing == null
|
|
? 'insert'
|
|
: _operationForExistingMutation(
|
|
existingDeletedAt: existing.deletedAt,
|
|
deletedAt: effectiveState.metadata.deletedAt,
|
|
);
|
|
await _upsertActiveExerciseStepProgressState(database, effectiveState);
|
|
await _writeChangeLog(
|
|
database: database,
|
|
entityType: 'ActiveExerciseStepProgressState',
|
|
entityId: effectiveState.metadata.id,
|
|
operation: operation,
|
|
localRevision: effectiveState.metadata.localRevision,
|
|
originDeviceId: effectiveState.metadata.originDeviceId,
|
|
createdAt: effectiveState.metadata.updatedAt,
|
|
);
|
|
});
|
|
}
|
|
|
|
@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.update(
|
|
database.activeScoreStopwatchStates,
|
|
)..where((table) => table.id.equals(row.id))).write(
|
|
db.ActiveScoreStopwatchStatesCompanion(
|
|
deletedAt: Value(deletedAt.toUtc()),
|
|
updatedAt: Value(deletedAt.toUtc()),
|
|
localRevision: Value(revision),
|
|
syncState: const Value('deleted'),
|
|
),
|
|
);
|
|
await _writeChangeLog(
|
|
database: database,
|
|
entityType: 'ActiveScoreStopwatchState',
|
|
entityId: row.id,
|
|
operation: 'softDelete',
|
|
localRevision: revision,
|
|
originDeviceId: row.originDeviceId,
|
|
createdAt: deletedAt,
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<void> deleteManualScoreState({
|
|
required String sessionId,
|
|
required int programIndex,
|
|
required int exerciseIndex,
|
|
required int setIndex,
|
|
required DateTime deletedAt,
|
|
}) async {
|
|
final row =
|
|
await (database.select(database.activeManualScoreStates)..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.update(
|
|
database.activeManualScoreStates,
|
|
)..where((table) => table.id.equals(row.id))).write(
|
|
db.ActiveManualScoreStatesCompanion(
|
|
deletedAt: Value(deletedAt.toUtc()),
|
|
updatedAt: Value(deletedAt.toUtc()),
|
|
localRevision: Value(revision),
|
|
syncState: const Value('deleted'),
|
|
),
|
|
);
|
|
await _writeChangeLog(
|
|
database: database,
|
|
entityType: 'ActiveManualScoreState',
|
|
entityId: row.id,
|
|
operation: 'softDelete',
|
|
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.ActiveManualScoreState?> findManualScoreState({
|
|
required String sessionId,
|
|
required int programIndex,
|
|
required int exerciseIndex,
|
|
required int setIndex,
|
|
}) async {
|
|
final row =
|
|
await (database.select(database.activeManualScoreStates)..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 : _activeManualScoreStateFromRow(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();
|
|
}
|
|
|
|
@override
|
|
Future<List<domain.ActiveManualScoreState>> listManualScoreStates(
|
|
String sessionId,
|
|
) async {
|
|
final rows =
|
|
await (database.select(database.activeManualScoreStates)
|
|
..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(_activeManualScoreStateFromRow).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> patchHeartRateSummary({
|
|
required String historyId,
|
|
int? minHeartRateBpm,
|
|
required double averageHeartRateBpm,
|
|
required int maxHeartRateBpm,
|
|
double? totalDistanceMeters,
|
|
double? totalCaloriesKcal,
|
|
required DateTime patchedAt,
|
|
}) async {
|
|
if ((minHeartRateBpm != null && minHeartRateBpm <= 0) ||
|
|
averageHeartRateBpm <= 0 ||
|
|
maxHeartRateBpm <= 0 ||
|
|
(totalDistanceMeters != null && totalDistanceMeters < 0) ||
|
|
(totalCaloriesKcal != null && totalCaloriesKcal < 0)) {
|
|
return;
|
|
}
|
|
final row =
|
|
await (database.select(database.workoutHistories)..where(
|
|
(table) =>
|
|
table.id.equals(historyId) &
|
|
table.deletedAt.isNull() &
|
|
table.minHeartRateBpm.isNull() &
|
|
table.averageHeartRateBpm.isNull() &
|
|
table.maxHeartRateBpm.isNull() &
|
|
table.totalDistanceMeters.isNull() &
|
|
table.totalCaloriesKcal.isNull(),
|
|
))
|
|
.getSingleOrNull();
|
|
if (row == null) {
|
|
return;
|
|
}
|
|
final revision = row.localRevision + 1;
|
|
await (database.update(
|
|
database.workoutHistories,
|
|
)..where((table) => table.id.equals(historyId))).write(
|
|
db.WorkoutHistoriesCompanion(
|
|
updatedAt: Value(patchedAt.toUtc()),
|
|
syncState: const Value('dirty'),
|
|
localRevision: Value(revision),
|
|
minHeartRateBpm: Value(minHeartRateBpm),
|
|
averageHeartRateBpm: Value(averageHeartRateBpm),
|
|
maxHeartRateBpm: Value(maxHeartRateBpm),
|
|
totalDistanceMeters: Value(totalDistanceMeters),
|
|
totalCaloriesKcal: Value(totalCaloriesKcal),
|
|
),
|
|
);
|
|
await _writeChangeLog(
|
|
database: database,
|
|
entityType: 'WorkoutHistory',
|
|
entityId: historyId,
|
|
operation: 'update',
|
|
localRevision: revision,
|
|
originDeviceId: row.originDeviceId,
|
|
createdAt: patchedAt,
|
|
);
|
|
}
|
|
|
|
@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,
|
|
);
|
|
});
|
|
}
|
|
}
|
|
|
|
final class DriftWorkoutTelemetryRepository
|
|
implements WorkoutTelemetryRepository {
|
|
const DriftWorkoutTelemetryRepository(this.database);
|
|
|
|
final db.AppDatabase database;
|
|
|
|
@override
|
|
Future<bool> saveSample(domain.WorkoutTelemetrySample sample) async {
|
|
final existing = await (database.select(
|
|
database.workoutTelemetrySamples,
|
|
)..where((table) => table.id.equals(sample.id))).getSingleOrNull();
|
|
if (existing != null && !sample.capturedAt.isAfter(existing.capturedAt)) {
|
|
return false;
|
|
}
|
|
await database
|
|
.into(database.workoutTelemetrySamples)
|
|
.insertOnConflictUpdate(_workoutTelemetrySampleCompanion(sample));
|
|
return true;
|
|
}
|
|
|
|
@override
|
|
Future<List<domain.WorkoutTelemetrySample>> listSamples(
|
|
String sessionId,
|
|
) async {
|
|
final rows =
|
|
await (database.select(database.workoutTelemetrySamples)
|
|
..where((table) => table.sessionId.equals(sessionId))
|
|
..orderBy([(table) => OrderingTerm.asc(table.capturedAt)]))
|
|
.get();
|
|
return rows.map(_workoutTelemetrySampleFromRow).toList();
|
|
}
|
|
|
|
@override
|
|
Future<List<domain.WorkoutTelemetrySample>> listSamplesForScope({
|
|
required String sessionId,
|
|
required domain.WorkoutTelemetryAggregateScope scope,
|
|
int? programIndex,
|
|
int? exerciseIndex,
|
|
int? setIndex,
|
|
int? passageIndex,
|
|
int? stepIndex,
|
|
}) async {
|
|
final rows =
|
|
await (database.select(database.workoutTelemetrySamples)
|
|
..where((table) => table.sessionId.equals(sessionId))
|
|
..orderBy([(table) => OrderingTerm.asc(table.capturedAt)]))
|
|
.get();
|
|
return rows
|
|
.map(_workoutTelemetrySampleFromRow)
|
|
.where(
|
|
(sample) => _telemetrySampleMatchesScope(
|
|
sample,
|
|
scope: scope,
|
|
programIndex: programIndex,
|
|
exerciseIndex: exerciseIndex,
|
|
setIndex: setIndex,
|
|
passageIndex: passageIndex,
|
|
stepIndex: stepIndex,
|
|
),
|
|
)
|
|
.toList(growable: false);
|
|
}
|
|
|
|
@override
|
|
Future<void> replaceAggregatesForSession({
|
|
required String sessionId,
|
|
required List<domain.WorkoutTelemetryAggregate> aggregates,
|
|
}) async {
|
|
await database.transaction(() async {
|
|
await (database.delete(
|
|
database.workoutTelemetryAggregates,
|
|
)..where((table) => table.sessionId.equals(sessionId))).go();
|
|
if (aggregates.isEmpty) {
|
|
return;
|
|
}
|
|
await database.batch((batch) {
|
|
batch.insertAll(
|
|
database.workoutTelemetryAggregates,
|
|
aggregates.map(_workoutTelemetryAggregateCompanion).toList(),
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
@override
|
|
Future<List<domain.WorkoutTelemetryAggregate>> listAggregates(
|
|
String sessionId,
|
|
) async {
|
|
final rows = await (database.select(
|
|
database.workoutTelemetryAggregates,
|
|
)..where((table) => table.sessionId.equals(sessionId))).get();
|
|
return rows.map(_workoutTelemetryAggregateFromRow).toList();
|
|
}
|
|
|
|
@override
|
|
Future<domain.WorkoutTelemetryAggregate?> findAggregate({
|
|
required String sessionId,
|
|
required domain.WorkoutTelemetryAggregateScope scope,
|
|
int? programIndex,
|
|
int? exerciseIndex,
|
|
int? setIndex,
|
|
int? passageIndex,
|
|
int? stepIndex,
|
|
}) async {
|
|
final rows =
|
|
await (database.select(database.workoutTelemetryAggregates)..where(
|
|
(table) =>
|
|
table.sessionId.equals(sessionId) &
|
|
table.scope.equals(scope.wireName),
|
|
))
|
|
.get();
|
|
for (final row in rows) {
|
|
final aggregate = _workoutTelemetryAggregateFromRow(row);
|
|
if (aggregate.programIndex == programIndex &&
|
|
aggregate.exerciseIndex == exerciseIndex &&
|
|
aggregate.setIndex == setIndex &&
|
|
aggregate.passageIndex == passageIndex &&
|
|
aggregate.stepIndex == stepIndex) {
|
|
return aggregate;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
final class DriftLocalDataBackupRepository
|
|
implements LocalDataBackupRepository {
|
|
const DriftLocalDataBackupRepository(this.database);
|
|
|
|
final db.AppDatabase database;
|
|
|
|
@override
|
|
Future<LocalDataExportSnapshot> readExportSnapshot(
|
|
DateTime exportedAt,
|
|
) async {
|
|
final mediaAssets = await DriftMediaAssetRepository(database).listActive();
|
|
final exercises = await DriftExerciseRepository(database).listActive();
|
|
final programs = await DriftProgramRepository(database).listActive();
|
|
final templates = await DriftWorkoutTemplateRepository(
|
|
database,
|
|
).listActive();
|
|
final histories = await DriftWorkoutHistoryRepository(
|
|
database,
|
|
).listActive();
|
|
return LocalDataExportSnapshot(
|
|
exportedAt: exportedAt.toUtc(),
|
|
appSchemaVersion: database.schemaVersion,
|
|
originDeviceId: 'local-device',
|
|
mediaAssets: [
|
|
for (final asset in mediaAssets)
|
|
_backupResource(asset.metadata, _mediaAssetPayload(asset)),
|
|
],
|
|
exercises: [
|
|
for (final exercise in exercises)
|
|
_backupResource(exercise.metadata, _exercisePayload(exercise)),
|
|
],
|
|
programs: [
|
|
for (final program in programs)
|
|
_backupResource(program.metadata, _programPayload(program)),
|
|
],
|
|
workoutTemplates: [
|
|
for (final template in templates)
|
|
_backupResource(template.metadata, _workoutTemplatePayload(template)),
|
|
],
|
|
workoutHistories: [
|
|
for (final history in histories)
|
|
_backupResource(
|
|
history.metadata,
|
|
_localWorkoutHistoryPayload(history),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<bool> hasOpenActiveWorkoutSession() async {
|
|
return DriftActiveSessionRepository(
|
|
database,
|
|
).findOpen().then((session) => session != null);
|
|
}
|
|
|
|
@override
|
|
Future<bool> hasAnyUserData() async {
|
|
final count = await database.customSelect('''
|
|
SELECT
|
|
(SELECT COUNT(*) FROM media_assets WHERE deleted_at IS NULL) +
|
|
(SELECT COUNT(*) FROM exercises WHERE deleted_at IS NULL AND archived_at IS NULL) +
|
|
(SELECT COUNT(*) FROM programs WHERE deleted_at IS NULL) +
|
|
(SELECT COUNT(*) FROM workout_templates WHERE deleted_at IS NULL) +
|
|
(SELECT COUNT(*) FROM workout_history WHERE deleted_at IS NULL) AS total
|
|
''').getSingle();
|
|
return count.read<int>('total') > 0;
|
|
}
|
|
|
|
@override
|
|
Future<LocalBackupImportResult> applyImportSnapshot({
|
|
required LocalDataExportSnapshot snapshot,
|
|
required LocalBackupImportMode mode,
|
|
required DateTime importedAt,
|
|
}) async {
|
|
if (await hasOpenActiveWorkoutSession()) {
|
|
throw const LocalBackupException(
|
|
LocalBackupValidationError.activeWorkoutInProgress,
|
|
);
|
|
}
|
|
var inserted = 0;
|
|
var updated = 0;
|
|
var ignored = 0;
|
|
var deleted = 0;
|
|
await database.transaction(() async {
|
|
if (mode == LocalBackupImportMode.replaceAll) {
|
|
deleted += await _softDeleteResourcesAbsentFromBackup(
|
|
snapshot,
|
|
importedAt,
|
|
);
|
|
}
|
|
final force = mode == LocalBackupImportMode.replaceAll;
|
|
final mediaResult = await _applyBackupResources(
|
|
resources: snapshot.mediaAssets,
|
|
resourceType: SyncResourceType.mediaAsset,
|
|
tableName: 'media_assets',
|
|
importedAt: importedAt,
|
|
force: force,
|
|
);
|
|
inserted += mediaResult.insertedCount;
|
|
updated += mediaResult.updatedCount;
|
|
ignored += mediaResult.ignoredOlderCount;
|
|
final exerciseResult = await _applyBackupResources(
|
|
resources: snapshot.exercises,
|
|
resourceType: SyncResourceType.exercise,
|
|
tableName: 'exercises',
|
|
importedAt: importedAt,
|
|
force: force,
|
|
);
|
|
inserted += exerciseResult.insertedCount;
|
|
updated += exerciseResult.updatedCount;
|
|
ignored += exerciseResult.ignoredOlderCount;
|
|
final programResult = await _applyBackupResources(
|
|
resources: snapshot.programs,
|
|
resourceType: SyncResourceType.program,
|
|
tableName: 'programs',
|
|
importedAt: importedAt,
|
|
force: force,
|
|
);
|
|
inserted += programResult.insertedCount;
|
|
updated += programResult.updatedCount;
|
|
ignored += programResult.ignoredOlderCount;
|
|
final templateResult = await _applyBackupResources(
|
|
resources: snapshot.workoutTemplates,
|
|
resourceType: SyncResourceType.workoutTemplate,
|
|
tableName: 'workout_templates',
|
|
importedAt: importedAt,
|
|
force: force,
|
|
);
|
|
inserted += templateResult.insertedCount;
|
|
updated += templateResult.updatedCount;
|
|
ignored += templateResult.ignoredOlderCount;
|
|
final historyResult = await _applyBackupResources(
|
|
resources: snapshot.workoutHistories,
|
|
resourceType: SyncResourceType.workoutHistory,
|
|
tableName: 'workout_history',
|
|
importedAt: importedAt,
|
|
force: force,
|
|
);
|
|
inserted += historyResult.insertedCount;
|
|
updated += historyResult.updatedCount;
|
|
ignored += historyResult.ignoredOlderCount;
|
|
});
|
|
return LocalBackupImportResult(
|
|
insertedCount: inserted,
|
|
updatedCount: updated,
|
|
ignoredOlderCount: ignored,
|
|
deletedByReplaceCount: deleted,
|
|
missingMediaCount: 0,
|
|
);
|
|
}
|
|
|
|
Future<LocalBackupImportResult> _applyBackupResources({
|
|
required List<LocalBackupResource> resources,
|
|
required SyncResourceType resourceType,
|
|
required String tableName,
|
|
required DateTime importedAt,
|
|
required bool force,
|
|
}) async {
|
|
var inserted = 0;
|
|
var updated = 0;
|
|
var ignored = 0;
|
|
for (final resource in resources) {
|
|
final local = await _localSyncRow(tableName, resource.id);
|
|
if (!force &&
|
|
local != null &&
|
|
!resource.updatedAt.isAfter(local.updatedAt)) {
|
|
ignored += 1;
|
|
continue;
|
|
}
|
|
final revision = (local?.localRevision ?? 0) + 1;
|
|
final payload = _payloadWithImportedMetadata(
|
|
resource.payload,
|
|
importedAt,
|
|
revision,
|
|
snapshotOriginDeviceId: resource.payload['originDeviceId'] as String?,
|
|
);
|
|
await _saveBackupResource(
|
|
resourceType: resourceType,
|
|
resource: resource.copyWith(payload: payload),
|
|
importedAt: importedAt,
|
|
);
|
|
if (local == null || local.deletedAt != null) {
|
|
inserted += 1;
|
|
} else {
|
|
updated += 1;
|
|
}
|
|
}
|
|
return LocalBackupImportResult(
|
|
insertedCount: inserted,
|
|
updatedCount: updated,
|
|
ignoredOlderCount: ignored,
|
|
deletedByReplaceCount: 0,
|
|
missingMediaCount: 0,
|
|
);
|
|
}
|
|
|
|
Future<void> _saveBackupResource({
|
|
required SyncResourceType resourceType,
|
|
required LocalBackupResource resource,
|
|
required DateTime importedAt,
|
|
}) async {
|
|
final remoteItem = _remoteItemFromBackupResource(resourceType, resource);
|
|
switch (resourceType) {
|
|
case SyncResourceType.mediaAsset:
|
|
await DriftMediaAssetRepository(
|
|
database,
|
|
).save(_mediaAssetFromPayload(remoteItem));
|
|
case SyncResourceType.exercise:
|
|
await DriftExerciseRepository(
|
|
database,
|
|
).save(_exerciseFromPayload(remoteItem));
|
|
case SyncResourceType.program:
|
|
await DriftProgramRepository(
|
|
database,
|
|
).replaceExercises(_programFromPayload(remoteItem), importedAt);
|
|
case SyncResourceType.workoutTemplate:
|
|
await DriftWorkoutTemplateRepository(database).replaceComposition(
|
|
_workoutTemplateFromPayload(remoteItem),
|
|
importedAt,
|
|
);
|
|
case SyncResourceType.workoutHistory:
|
|
await _replaceWorkoutHistory(
|
|
_workoutHistoryFromLocalBackupPayload(remoteItem),
|
|
importedAt,
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<void> _replaceWorkoutHistory(
|
|
domain.WorkoutHistory history,
|
|
DateTime deletedAt,
|
|
) async {
|
|
await DriftWorkoutHistoryRepository(database).save(history);
|
|
final activeResultIds = history.results
|
|
.map((result) => result.metadata.id)
|
|
.toSet();
|
|
final resultRows =
|
|
await (database.select(database.workoutHistorySetResults)..where(
|
|
(table) =>
|
|
table.workoutHistoryId.equals(history.metadata.id) &
|
|
table.deletedAt.isNull() &
|
|
(activeResultIds.isEmpty
|
|
? const Constant<bool>(true)
|
|
: table.id.isNotIn(activeResultIds)),
|
|
))
|
|
.get();
|
|
await _softDeleteWorkoutHistorySetResultRows(
|
|
database,
|
|
resultRows,
|
|
deletedAt,
|
|
);
|
|
final activeStepResultIds = history.stepResults
|
|
.map((result) => result.metadata.id)
|
|
.toSet();
|
|
final stepRows =
|
|
await (database.select(database.workoutHistoryStepResults)..where(
|
|
(table) =>
|
|
table.workoutHistoryId.equals(history.metadata.id) &
|
|
table.deletedAt.isNull() &
|
|
(activeStepResultIds.isEmpty
|
|
? const Constant<bool>(true)
|
|
: table.id.isNotIn(activeStepResultIds)),
|
|
))
|
|
.get();
|
|
await _softDeleteWorkoutHistoryStepResultRows(
|
|
database,
|
|
stepRows,
|
|
deletedAt,
|
|
);
|
|
}
|
|
|
|
Future<int> _softDeleteResourcesAbsentFromBackup(
|
|
LocalDataExportSnapshot snapshot,
|
|
DateTime deletedAt,
|
|
) async {
|
|
var deleted = 0;
|
|
deleted += await _softDeleteMainRowsAbsent(
|
|
tableName: 'media_assets',
|
|
entityType: 'MediaAsset',
|
|
keepIds: snapshot.mediaAssets.map((resource) => resource.id).toSet(),
|
|
deletedAt: deletedAt,
|
|
);
|
|
deleted += await _softDeleteMainRowsAbsent(
|
|
tableName: 'exercises',
|
|
entityType: 'Exercise',
|
|
keepIds: snapshot.exercises.map((resource) => resource.id).toSet(),
|
|
deletedAt: deletedAt,
|
|
);
|
|
deleted += await _softDeleteMainRowsAbsent(
|
|
tableName: 'programs',
|
|
entityType: 'Program',
|
|
keepIds: snapshot.programs.map((resource) => resource.id).toSet(),
|
|
deletedAt: deletedAt,
|
|
);
|
|
deleted += await _softDeleteMainRowsAbsent(
|
|
tableName: 'workout_templates',
|
|
entityType: 'WorkoutTemplate',
|
|
keepIds: snapshot.workoutTemplates.map((resource) => resource.id).toSet(),
|
|
deletedAt: deletedAt,
|
|
);
|
|
deleted += await _softDeleteMainRowsAbsent(
|
|
tableName: 'workout_history',
|
|
entityType: 'WorkoutHistory',
|
|
keepIds: snapshot.workoutHistories.map((resource) => resource.id).toSet(),
|
|
deletedAt: deletedAt,
|
|
);
|
|
await _softDeleteAllActiveSessionRows(deletedAt);
|
|
return deleted;
|
|
}
|
|
|
|
Future<int> _softDeleteMainRowsAbsent({
|
|
required String tableName,
|
|
required String entityType,
|
|
required Set<String> keepIds,
|
|
required DateTime deletedAt,
|
|
}) async {
|
|
final where = keepIds.isEmpty
|
|
? 'deleted_at IS NULL'
|
|
: 'deleted_at IS NULL AND id NOT IN (${List.filled(keepIds.length, '?').join(', ')})';
|
|
final rows = await database
|
|
.customSelect(
|
|
'SELECT id, local_revision, origin_device_id FROM $tableName WHERE $where',
|
|
variables: [for (final id in keepIds) Variable<String>(id)],
|
|
)
|
|
.get();
|
|
for (final row in rows) {
|
|
final id = row.read<String>('id');
|
|
final revision = row.read<int>('local_revision') + 1;
|
|
await database.customUpdate(
|
|
'UPDATE $tableName SET deleted_at = ?, updated_at = ?, '
|
|
'local_revision = ?, sync_state = ? WHERE id = ?',
|
|
variables: [
|
|
Variable<DateTime>(deletedAt.toUtc()),
|
|
Variable<DateTime>(deletedAt.toUtc()),
|
|
Variable<int>(revision),
|
|
const Variable<String>('deleted'),
|
|
Variable<String>(id),
|
|
],
|
|
);
|
|
await _writeChangeLog(
|
|
database: database,
|
|
entityType: entityType,
|
|
entityId: id,
|
|
operation: 'softDelete',
|
|
localRevision: revision,
|
|
originDeviceId: row.read<String>('origin_device_id'),
|
|
createdAt: deletedAt,
|
|
);
|
|
}
|
|
return rows.length;
|
|
}
|
|
|
|
Future<void> _softDeleteAllActiveSessionRows(DateTime deletedAt) async {
|
|
await database.customUpdate(
|
|
'UPDATE active_workout_sessions SET deleted_at = ?, updated_at = ? '
|
|
'WHERE deleted_at IS NULL',
|
|
variables: [
|
|
Variable<DateTime>(deletedAt.toUtc()),
|
|
Variable<DateTime>(deletedAt.toUtc()),
|
|
],
|
|
updates: {database.activeWorkoutSessions},
|
|
);
|
|
}
|
|
|
|
Future<_BackupLocalRow?> _localSyncRow(String tableName, String id) async {
|
|
final row = await database
|
|
.customSelect(
|
|
'SELECT updated_at, deleted_at, local_revision FROM $tableName '
|
|
'WHERE id = ? LIMIT 1',
|
|
variables: [Variable<String>(id)],
|
|
)
|
|
.getSingleOrNull();
|
|
if (row == null) {
|
|
return null;
|
|
}
|
|
return _BackupLocalRow(
|
|
updatedAt: _dateTimeFromData(row.data, 'updated_at'),
|
|
deletedAt: _dateTimeOrNullFromData(row.data, 'deleted_at'),
|
|
localRevision: row.read<int>('local_revision'),
|
|
);
|
|
}
|
|
}
|
|
|
|
final class DriftExercisePerformanceReferenceRepository
|
|
implements ExercisePerformanceReferenceRepository {
|
|
const DriftExercisePerformanceReferenceRepository(this.database);
|
|
|
|
final db.AppDatabase database;
|
|
|
|
@override
|
|
Future<bool> hasAnyCompletedHistoryForExercise(String exerciseId) async {
|
|
final row = await database
|
|
.customSelect(
|
|
'SELECT 1 FROM workout_history_set_results AS result '
|
|
'INNER JOIN workout_history AS history '
|
|
'ON history.id = result.workout_history_id '
|
|
'WHERE result.deleted_at IS NULL '
|
|
'AND history.deleted_at IS NULL '
|
|
'AND history.completed = 1 '
|
|
'AND result.status = ? '
|
|
'AND result.source_exercise_id_snapshot = ? '
|
|
'LIMIT 1',
|
|
variables: [
|
|
const Variable<String>('completed'),
|
|
Variable<String>(exerciseId),
|
|
],
|
|
)
|
|
.getSingleOrNull();
|
|
return row != null;
|
|
}
|
|
|
|
@override
|
|
Future<WorkoutHistorySetPerformance?> findLatestSetPerformance({
|
|
required String exerciseId,
|
|
required ActivePerformanceMeasures activeMeasures,
|
|
required int currentSetIndex,
|
|
}) async {
|
|
final valuePredicate = _activeMeasuresPredicate(activeMeasures);
|
|
if (valuePredicate == null) {
|
|
return null;
|
|
}
|
|
final row = await database
|
|
.customSelect(
|
|
'''
|
|
WITH candidates AS (
|
|
SELECT
|
|
result.workout_history_id,
|
|
history.started_at AS history_started_at,
|
|
result.set_index,
|
|
result.exercise_name_snapshot,
|
|
result.score_input_mode_snapshot,
|
|
result.actual_time_ms,
|
|
result.actual_reps,
|
|
result.actual_score,
|
|
result.actual_score_time_ms,
|
|
result.completed_at
|
|
FROM workout_history_set_results AS result
|
|
INNER JOIN workout_history AS history
|
|
ON history.id = result.workout_history_id
|
|
WHERE result.deleted_at IS NULL
|
|
AND history.deleted_at IS NULL
|
|
AND history.completed = 1
|
|
AND result.status = ?
|
|
AND result.source_exercise_id_snapshot = ?
|
|
AND ($valuePredicate)
|
|
),
|
|
latest_history AS (
|
|
SELECT workout_history_id
|
|
FROM candidates
|
|
ORDER BY history_started_at DESC
|
|
LIMIT 1
|
|
)
|
|
SELECT *
|
|
FROM candidates
|
|
WHERE workout_history_id = (SELECT workout_history_id FROM latest_history)
|
|
ORDER BY
|
|
CASE WHEN set_index = ? THEN 0 ELSE 1 END,
|
|
set_index DESC
|
|
LIMIT 1
|
|
''',
|
|
variables: [
|
|
const Variable<String>('completed'),
|
|
Variable<String>(exerciseId),
|
|
Variable<int>(currentSetIndex),
|
|
],
|
|
)
|
|
.getSingleOrNull();
|
|
return row == null ? null : _setPerformanceFromRow(row);
|
|
}
|
|
|
|
@override
|
|
Future<WorkoutHistoryMetricPerformance?> findBestMetricPerformance({
|
|
required String exerciseId,
|
|
required PerformanceMetric metric,
|
|
required domain.ScoreInputMode scoreInputMode,
|
|
}) async {
|
|
final metricSql = _metricSql(metric, scoreInputMode);
|
|
final row = await database
|
|
.customSelect(
|
|
'''
|
|
SELECT
|
|
result.workout_history_id,
|
|
history.started_at AS history_started_at,
|
|
result.set_index,
|
|
result.exercise_name_snapshot,
|
|
result.score_input_mode_snapshot,
|
|
result.actual_time_ms,
|
|
result.actual_reps,
|
|
result.actual_score,
|
|
result.actual_score_time_ms,
|
|
result.completed_at
|
|
FROM workout_history_set_results AS result
|
|
INNER JOIN workout_history AS history
|
|
ON history.id = result.workout_history_id
|
|
WHERE result.deleted_at IS NULL
|
|
AND history.deleted_at IS NULL
|
|
AND history.completed = 1
|
|
AND result.status = ?
|
|
AND result.source_exercise_id_snapshot = ?
|
|
AND ${metricSql.predicate}
|
|
ORDER BY ${metricSql.ordering},
|
|
history.started_at DESC,
|
|
result.completed_at DESC
|
|
LIMIT 1
|
|
''',
|
|
variables: [
|
|
const Variable<String>('completed'),
|
|
Variable<String>(exerciseId),
|
|
],
|
|
)
|
|
.getSingleOrNull();
|
|
return row == null ? null : _metricPerformanceFromRow(row, metric: metric);
|
|
}
|
|
}
|
|
|
|
final class DriftProgressionStatsRepository
|
|
implements ProgressionStatsRepository {
|
|
const DriftProgressionStatsRepository(this.database);
|
|
|
|
final db.AppDatabase database;
|
|
|
|
@override
|
|
Future<ProgressionGlobalStatsData> readGlobalStats(
|
|
ProgressionDateRange range,
|
|
) async {
|
|
final variables = _rangeVariables(range);
|
|
final row = await database.customSelect('''
|
|
SELECT
|
|
COUNT(*) AS completed_count,
|
|
COALESCE(SUM(total_active_ms), 0) AS total_active_ms,
|
|
MIN(started_at) AS first_started_at
|
|
FROM workout_history
|
|
WHERE deleted_at IS NULL
|
|
AND completed = 1
|
|
AND started_at <= ?
|
|
${range.startedAt == null ? '' : 'AND started_at >= ?'}
|
|
''', variables: variables).getSingle();
|
|
final anyRow = await database.customSelect('''
|
|
SELECT MIN(started_at) AS first_started_at
|
|
FROM workout_history
|
|
WHERE deleted_at IS NULL
|
|
AND completed = 1
|
|
''').getSingle();
|
|
final weekRows = await database.customSelect('''
|
|
SELECT started_at
|
|
FROM workout_history
|
|
WHERE deleted_at IS NULL
|
|
AND completed = 1
|
|
AND started_at <= ?
|
|
${range.startedAt == null ? '' : 'AND started_at >= ?'}
|
|
''', variables: variables).get();
|
|
final weekStarts = {
|
|
for (final row in weekRows)
|
|
_startOfLocalWeekFromUtc(_dateTimeFromData(row.data, 'started_at')),
|
|
}.toList()..sort();
|
|
return ProgressionGlobalStatsData(
|
|
completedSessionCount: row.data['completed_count'] as int,
|
|
totalActiveMs: row.data['total_active_ms'] as int,
|
|
activeWeekStarts: weekStarts,
|
|
hasAnyCompletedHistory: anyRow.data['first_started_at'] != null,
|
|
firstCompletedSessionAt: _dateTimeOrNullFromData(
|
|
anyRow.data,
|
|
'first_started_at',
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<List<ProgressionExerciseOptionData>> listExerciseOptions(
|
|
ProgressionDateRange range,
|
|
) async {
|
|
final rows = await database.customSelect('''
|
|
WITH result_projection AS (
|
|
${_progressionResultProjection(range)}
|
|
),
|
|
ranked AS (
|
|
SELECT
|
|
exercise_key,
|
|
exercise_name_snapshot,
|
|
source_exercise_id_snapshot,
|
|
started_at,
|
|
ROW_NUMBER() OVER (
|
|
PARTITION BY exercise_key
|
|
ORDER BY started_at DESC
|
|
) AS name_rank
|
|
FROM result_projection
|
|
WHERE has_graphable_measure = 1
|
|
)
|
|
SELECT
|
|
ranked.exercise_key,
|
|
MAX(CASE WHEN ranked.name_rank = 1 THEN ranked.exercise_name_snapshot END)
|
|
AS exercise_name_snapshot,
|
|
MAX(ranked.started_at) AS last_performed_at,
|
|
CASE
|
|
WHEN exercises.id IS NOT NULL
|
|
AND exercises.deleted_at IS NULL
|
|
AND exercises.archived_at IS NULL
|
|
THEN 0
|
|
ELSE 1
|
|
END AS is_archived
|
|
FROM ranked
|
|
LEFT JOIN exercises
|
|
ON exercises.id = ranked.source_exercise_id_snapshot
|
|
GROUP BY ranked.exercise_key
|
|
ORDER BY is_archived ASC,
|
|
last_performed_at DESC,
|
|
exercise_name_snapshot COLLATE NOCASE ASC,
|
|
ranked.exercise_key ASC
|
|
''', variables: _projectionVariables(range)).get();
|
|
return rows
|
|
.map(
|
|
(row) => ProgressionExerciseOptionData(
|
|
exerciseKey: row.data['exercise_key'] as String,
|
|
nameSnapshot: row.data['exercise_name_snapshot'] as String,
|
|
isArchived: (row.data['is_archived'] as int) == 1,
|
|
lastPerformedAt: _dateTimeFromData(row.data, 'last_performed_at'),
|
|
),
|
|
)
|
|
.toList();
|
|
}
|
|
|
|
@override
|
|
Future<List<ProgressionMeasureOptionData>> listMeasureOptions({
|
|
required ProgressionDateRange range,
|
|
required String exerciseKey,
|
|
}) async {
|
|
final rows = await database
|
|
.customSelect(
|
|
'''
|
|
WITH result_projection AS (
|
|
${_progressionResultProjection(range)}
|
|
),
|
|
measure_rows AS (
|
|
SELECT 0 AS sort_order, 'manualScore' AS measure, score_label_snapshot,
|
|
score_unit_snapshot, started_at
|
|
FROM result_projection
|
|
WHERE exercise_key = ?
|
|
AND score_input_mode_snapshot = 'manual'
|
|
AND actual_score IS NOT NULL
|
|
UNION ALL
|
|
SELECT 1, 'stopwatchScore', NULL, NULL, started_at
|
|
FROM result_projection
|
|
WHERE exercise_key = ?
|
|
AND score_input_mode_snapshot = 'stopwatch'
|
|
AND actual_score_time_ms IS NOT NULL
|
|
UNION ALL
|
|
SELECT 2, 'reps', NULL, NULL, started_at
|
|
FROM result_projection
|
|
WHERE exercise_key = ?
|
|
AND actual_reps IS NOT NULL
|
|
UNION ALL
|
|
SELECT 3, 'time', NULL, NULL, started_at
|
|
FROM result_projection
|
|
WHERE exercise_key = ?
|
|
AND actual_time_ms IS NOT NULL
|
|
),
|
|
ranked AS (
|
|
SELECT *,
|
|
ROW_NUMBER() OVER (PARTITION BY measure ORDER BY started_at DESC)
|
|
AS value_rank
|
|
FROM measure_rows
|
|
)
|
|
SELECT
|
|
measure,
|
|
MAX(CASE WHEN value_rank = 1 THEN score_label_snapshot END) AS score_label,
|
|
MAX(CASE WHEN value_rank = 1 THEN score_unit_snapshot END) AS score_unit,
|
|
MIN(sort_order) AS sort_order
|
|
FROM ranked
|
|
GROUP BY measure
|
|
ORDER BY sort_order
|
|
''',
|
|
variables: [
|
|
..._projectionVariables(range),
|
|
for (var i = 0; i < 4; i++) Variable<String>(exerciseKey),
|
|
],
|
|
)
|
|
.get();
|
|
return rows.map(_progressionMeasureOptionFromRow).toList();
|
|
}
|
|
|
|
@override
|
|
Future<ProgressionExerciseSeriesData> readExerciseSeries({
|
|
required ProgressionDateRange range,
|
|
required String exerciseKey,
|
|
required ProgressionMeasure measure,
|
|
}) async {
|
|
final measureOption =
|
|
(await listMeasureOptions(
|
|
range: range,
|
|
exerciseKey: exerciseKey,
|
|
)).where((option) => option.measure == measure).firstOrNull ??
|
|
_defaultProgressionMeasureOption(measure);
|
|
final points = await _readExercisePoints(
|
|
range: range,
|
|
exerciseKey: exerciseKey,
|
|
measure: measure,
|
|
);
|
|
final allTimePoints = range.startedAt == null
|
|
? points
|
|
: await _readExercisePoints(
|
|
range: ProgressionDateRange(
|
|
startedAt: null,
|
|
endedAt: range.endedAt,
|
|
),
|
|
exerciseKey: exerciseKey,
|
|
measure: measure,
|
|
);
|
|
final exerciseName = points.isNotEmpty
|
|
? points.last.exerciseNameSnapshot
|
|
: allTimePoints.isNotEmpty
|
|
? allTimePoints.last.exerciseNameSnapshot
|
|
: exerciseKey;
|
|
final hasAnyCompletedExerciseResult = await _hasAnyCompletedExerciseResult(
|
|
range: range,
|
|
exerciseKey: exerciseKey,
|
|
);
|
|
return ProgressionExerciseSeriesData(
|
|
exerciseKey: exerciseKey,
|
|
exerciseNameSnapshot: exerciseName,
|
|
measure: measureOption,
|
|
points: [
|
|
for (final point in points)
|
|
ProgressionPointData(
|
|
workoutHistoryId: point.workoutHistoryId,
|
|
workoutNameSnapshot: point.workoutNameSnapshot,
|
|
startedAt: point.startedAt,
|
|
value: point.value,
|
|
rawValue: point.rawValue,
|
|
),
|
|
],
|
|
hasAnyAllTimeData: allTimePoints.isNotEmpty,
|
|
hasAnyCompletedExerciseResult: hasAnyCompletedExerciseResult,
|
|
);
|
|
}
|
|
|
|
Future<List<_ProgressionPointRow>> _readExercisePoints({
|
|
required ProgressionDateRange range,
|
|
required String exerciseKey,
|
|
required ProgressionMeasure measure,
|
|
}) async {
|
|
final aggregation = _progressionAggregationSql(measure);
|
|
final rows = await database
|
|
.customSelect(
|
|
'''
|
|
WITH result_projection AS (
|
|
${_progressionResultProjection(range)}
|
|
)
|
|
SELECT
|
|
workout_history_id,
|
|
workout_name_snapshot,
|
|
MAX(started_at) AS started_at,
|
|
MAX(exercise_name_snapshot) AS exercise_name_snapshot,
|
|
${aggregation.expression} AS raw_value
|
|
FROM result_projection
|
|
WHERE exercise_key = ?
|
|
AND ${aggregation.predicate}
|
|
GROUP BY workout_history_id, workout_name_snapshot
|
|
ORDER BY started_at ASC
|
|
''',
|
|
variables: [
|
|
..._projectionVariables(range),
|
|
Variable<String>(exerciseKey),
|
|
],
|
|
)
|
|
.get();
|
|
return rows.map((row) => _progressionPointRow(row, measure)).toList();
|
|
}
|
|
|
|
Future<bool> _hasAnyCompletedExerciseResult({
|
|
required ProgressionDateRange range,
|
|
required String exerciseKey,
|
|
}) async {
|
|
final row = await database
|
|
.customSelect(
|
|
'''
|
|
WITH result_projection AS (
|
|
${_progressionResultProjection(range)}
|
|
)
|
|
SELECT 1
|
|
FROM result_projection
|
|
WHERE exercise_key = ?
|
|
LIMIT 1
|
|
''',
|
|
variables: [
|
|
..._projectionVariables(range),
|
|
Variable<String>(exerciseKey),
|
|
],
|
|
)
|
|
.getSingleOrNull();
|
|
return row != null;
|
|
}
|
|
}
|
|
|
|
List<Variable<Object>> _rangeVariables(ProgressionDateRange range) {
|
|
return [
|
|
Variable<DateTime>(range.endedAt.toUtc()),
|
|
if (range.startedAt != null) Variable<DateTime>(range.startedAt!.toUtc()),
|
|
];
|
|
}
|
|
|
|
List<Variable<Object>> _projectionVariables(ProgressionDateRange range) {
|
|
final variables = _rangeVariables(range);
|
|
return [...variables, ...variables];
|
|
}
|
|
|
|
String _progressionResultProjection(ProgressionDateRange range) {
|
|
final rangePredicate =
|
|
'history.started_at <= ? '
|
|
"${range.startedAt == null ? '' : 'AND history.started_at >= ?'}";
|
|
final sharedWhere =
|
|
'history.deleted_at IS NULL '
|
|
'AND history.completed = 1 '
|
|
'AND $rangePredicate';
|
|
return '''
|
|
SELECT
|
|
history.id AS workout_history_id,
|
|
history.name_snapshot AS workout_name_snapshot,
|
|
history.started_at,
|
|
COALESCE(result.source_exercise_id_snapshot, result.exercise_snapshot_id)
|
|
AS exercise_key,
|
|
result.source_exercise_id_snapshot,
|
|
result.exercise_snapshot_id,
|
|
result.exercise_name_snapshot,
|
|
result.score_input_mode_snapshot,
|
|
result.score_label_snapshot,
|
|
result.score_unit_snapshot,
|
|
result.actual_time_ms,
|
|
result.actual_reps,
|
|
result.actual_score,
|
|
result.actual_score_time_ms,
|
|
CASE
|
|
WHEN result.actual_time_ms IS NOT NULL
|
|
OR result.actual_reps IS NOT NULL
|
|
OR result.actual_score IS NOT NULL
|
|
OR result.actual_score_time_ms IS NOT NULL
|
|
THEN 1 ELSE 0
|
|
END AS has_graphable_measure
|
|
FROM workout_history_set_results AS result
|
|
INNER JOIN workout_history AS history
|
|
ON history.id = result.workout_history_id
|
|
WHERE result.deleted_at IS NULL
|
|
AND result.status = 'completed'
|
|
AND $sharedWhere
|
|
UNION ALL
|
|
SELECT
|
|
history.id AS workout_history_id,
|
|
history.name_snapshot AS workout_name_snapshot,
|
|
history.started_at,
|
|
COALESCE(step.source_exercise_id_snapshot, step.exercise_snapshot_id)
|
|
AS exercise_key,
|
|
step.source_exercise_id_snapshot,
|
|
step.exercise_snapshot_id,
|
|
COALESCE(parent.exercise_name_snapshot, step.step_name_snapshot)
|
|
AS exercise_name_snapshot,
|
|
step.score_input_mode_snapshot,
|
|
step.score_label_snapshot,
|
|
step.score_unit_snapshot,
|
|
step.actual_time_ms,
|
|
step.actual_reps,
|
|
step.actual_score,
|
|
step.actual_score_time_ms,
|
|
CASE
|
|
WHEN step.actual_time_ms IS NOT NULL
|
|
OR step.actual_reps IS NOT NULL
|
|
OR step.actual_score IS NOT NULL
|
|
OR step.actual_score_time_ms IS NOT NULL
|
|
THEN 1 ELSE 0
|
|
END AS has_graphable_measure
|
|
FROM workout_history_step_results AS step
|
|
INNER JOIN workout_history AS history
|
|
ON history.id = step.workout_history_id
|
|
LEFT JOIN workout_history_set_results AS parent
|
|
ON parent.workout_history_id = step.workout_history_id
|
|
AND parent.program_index = step.program_index
|
|
AND parent.exercise_index = step.exercise_index
|
|
AND parent.set_index = step.set_index
|
|
AND parent.deleted_at IS NULL
|
|
WHERE step.deleted_at IS NULL
|
|
AND step.status = 'completed'
|
|
AND $sharedWhere
|
|
''';
|
|
}
|
|
|
|
ProgressionMeasureOptionData _progressionMeasureOptionFromRow(QueryRow row) {
|
|
final measure = _progressionMeasureFromDb(row.data['measure'] as String);
|
|
final defaults = _defaultProgressionMeasureOption(measure);
|
|
return ProgressionMeasureOptionData(
|
|
measure: measure,
|
|
label: defaults.label,
|
|
scoreLabel: row.data['score_label'] as String?,
|
|
scoreUnit: row.data['score_unit'] as String?,
|
|
lowerIsBetter: defaults.lowerIsBetter,
|
|
);
|
|
}
|
|
|
|
ProgressionMeasure _progressionMeasureFromDb(String value) {
|
|
switch (value) {
|
|
case 'manualScore':
|
|
return ProgressionMeasure.manualScore;
|
|
case 'stopwatchScore':
|
|
return ProgressionMeasure.stopwatchScore;
|
|
case 'reps':
|
|
return ProgressionMeasure.reps;
|
|
case 'time':
|
|
return ProgressionMeasure.time;
|
|
}
|
|
throw domain.DomainException('Unknown progression measure: $value');
|
|
}
|
|
|
|
ProgressionMeasureOptionData _defaultProgressionMeasureOption(
|
|
ProgressionMeasure measure,
|
|
) {
|
|
switch (measure) {
|
|
case ProgressionMeasure.manualScore:
|
|
return const ProgressionMeasureOptionData(
|
|
measure: ProgressionMeasure.manualScore,
|
|
label: 'Score',
|
|
lowerIsBetter: false,
|
|
);
|
|
case ProgressionMeasure.stopwatchScore:
|
|
return const ProgressionMeasureOptionData(
|
|
measure: ProgressionMeasure.stopwatchScore,
|
|
label: 'Temps réalisé',
|
|
lowerIsBetter: true,
|
|
);
|
|
case ProgressionMeasure.reps:
|
|
return const ProgressionMeasureOptionData(
|
|
measure: ProgressionMeasure.reps,
|
|
label: 'Répétitions',
|
|
lowerIsBetter: false,
|
|
);
|
|
case ProgressionMeasure.time:
|
|
return const ProgressionMeasureOptionData(
|
|
measure: ProgressionMeasure.time,
|
|
label: 'Temps',
|
|
lowerIsBetter: false,
|
|
);
|
|
}
|
|
}
|
|
|
|
_ProgressionAggregationSql _progressionAggregationSql(
|
|
ProgressionMeasure measure,
|
|
) {
|
|
switch (measure) {
|
|
case ProgressionMeasure.manualScore:
|
|
return const _ProgressionAggregationSql(
|
|
expression: 'MAX(actual_score)',
|
|
predicate:
|
|
"score_input_mode_snapshot = 'manual' AND actual_score IS NOT NULL",
|
|
);
|
|
case ProgressionMeasure.stopwatchScore:
|
|
return const _ProgressionAggregationSql(
|
|
expression: 'MIN(actual_score_time_ms)',
|
|
predicate:
|
|
"score_input_mode_snapshot = 'stopwatch' "
|
|
'AND actual_score_time_ms IS NOT NULL',
|
|
);
|
|
case ProgressionMeasure.reps:
|
|
return const _ProgressionAggregationSql(
|
|
expression: 'SUM(actual_reps)',
|
|
predicate: 'actual_reps IS NOT NULL',
|
|
);
|
|
case ProgressionMeasure.time:
|
|
return const _ProgressionAggregationSql(
|
|
expression: 'SUM(actual_time_ms)',
|
|
predicate: 'actual_time_ms IS NOT NULL',
|
|
);
|
|
}
|
|
}
|
|
|
|
_ProgressionPointRow _progressionPointRow(
|
|
QueryRow row,
|
|
ProgressionMeasure measure,
|
|
) {
|
|
final raw = row.data['raw_value'] as num;
|
|
final rawValue = measure == ProgressionMeasure.manualScore
|
|
? raw.toDouble()
|
|
: raw.toInt();
|
|
return _ProgressionPointRow(
|
|
workoutHistoryId: row.data['workout_history_id'] as String,
|
|
workoutNameSnapshot: row.data['workout_name_snapshot'] as String,
|
|
exerciseNameSnapshot: row.data['exercise_name_snapshot'] as String,
|
|
startedAt: _dateTimeFromData(row.data, 'started_at'),
|
|
value: raw.toDouble(),
|
|
rawValue: rawValue,
|
|
);
|
|
}
|
|
|
|
DateTime _startOfLocalWeekFromUtc(DateTime value) {
|
|
final local = value.toLocal();
|
|
final localDay = DateTime(local.year, local.month, local.day);
|
|
return localDay.subtract(Duration(days: localDay.weekday - DateTime.monday));
|
|
}
|
|
|
|
final class _ProgressionAggregationSql {
|
|
const _ProgressionAggregationSql({
|
|
required this.expression,
|
|
required this.predicate,
|
|
});
|
|
|
|
final String expression;
|
|
final String predicate;
|
|
}
|
|
|
|
final class _ProgressionPointRow {
|
|
const _ProgressionPointRow({
|
|
required this.workoutHistoryId,
|
|
required this.workoutNameSnapshot,
|
|
required this.exerciseNameSnapshot,
|
|
required this.startedAt,
|
|
required this.value,
|
|
required this.rawValue,
|
|
});
|
|
|
|
final String workoutHistoryId;
|
|
final String workoutNameSnapshot;
|
|
final String exerciseNameSnapshot;
|
|
final DateTime startedAt;
|
|
final double value;
|
|
final Object rawValue;
|
|
}
|
|
|
|
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),
|
|
linkedToSeriesScore: Value(step.linkedToSeriesScore),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
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.businessTypes = const [],
|
|
this.isExample = false,
|
|
});
|
|
|
|
final domain.ExerciseCategory category;
|
|
final List<domain.BusinessExerciseType> businessTypes;
|
|
final bool isExample;
|
|
}
|
|
|
|
Future<void> _writeExerciseStarterMetadata(
|
|
db.AppDatabase database,
|
|
domain.Exercise exercise,
|
|
) async {
|
|
await database.customStatement(
|
|
'UPDATE exercises SET category = ?, is_example = ?, '
|
|
'business_types_json = ? WHERE id = ?',
|
|
[
|
|
_exerciseCategoryToDb(exercise.category),
|
|
exercise.isExample ? 1 : 0,
|
|
_encodeBusinessExerciseTypes(exercise.businessTypes),
|
|
exercise.metadata.id,
|
|
],
|
|
);
|
|
}
|
|
|
|
Future<void> _writeProgramExerciseHealthServicesStrategySnapshot(
|
|
db.AppDatabase database,
|
|
domain.ProgramExercise exercise,
|
|
) async {
|
|
await database.customStatement(
|
|
'UPDATE program_exercises SET '
|
|
'health_services_exercise_type_strategy_snapshot_json = ? '
|
|
'WHERE id = ?',
|
|
[
|
|
_encodeHealthServicesExerciseTypes(
|
|
exercise.healthServicesExerciseTypeStrategySnapshot,
|
|
),
|
|
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) {
|
|
if (value.abs() < 100000000000) {
|
|
return DateTime.fromMillisecondsSinceEpoch(value * 1000, isUtc: true);
|
|
}
|
|
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) {
|
|
if (value.abs() < 100000000000) {
|
|
return DateTime.fromMillisecondsSinceEpoch(value * 1000, isUtc: true);
|
|
}
|
|
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),
|
|
tagsJson: Value(_encodeTags(exercise.tags)),
|
|
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,
|
|
businessTypes: starterMetadata.businessTypes,
|
|
isExample: starterMetadata.isExample,
|
|
tags: _decodeTags(row.tagsJson),
|
|
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,
|
|
linkedToSeriesScore: row.linkedToSeriesScore,
|
|
);
|
|
}
|
|
|
|
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),
|
|
tagsJson: Value(_encodeTags(program.tags)),
|
|
);
|
|
}
|
|
|
|
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,
|
|
tags: _decodeTags(row.tagsJson),
|
|
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),
|
|
);
|
|
}
|
|
|
|
Future<List<domain.ProgramExercise>> _programExercisesFromRows(
|
|
db.AppDatabase database,
|
|
List<db.ProgramExercise> rows,
|
|
) async {
|
|
final output = <domain.ProgramExercise>[];
|
|
for (final row in rows) {
|
|
output.add(await _programExerciseFromRow(database, row));
|
|
}
|
|
return output;
|
|
}
|
|
|
|
Future<domain.ProgramExercise> _programExerciseFromRow(
|
|
db.AppDatabase database,
|
|
db.ProgramExercise row,
|
|
) async {
|
|
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,
|
|
healthServicesExerciseTypeStrategySnapshot:
|
|
await _programExerciseHealthServicesStrategySnapshot(database, row.id),
|
|
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,
|
|
);
|
|
}
|
|
|
|
Future<List<domain.HealthServicesExerciseType>>
|
|
_programExerciseHealthServicesStrategySnapshot(
|
|
db.AppDatabase database,
|
|
String id,
|
|
) async {
|
|
final row = await database
|
|
.customSelect(
|
|
'SELECT health_services_exercise_type_strategy_snapshot_json '
|
|
'FROM program_exercises WHERE id = ? LIMIT 1',
|
|
variables: [Variable<String>(id)],
|
|
)
|
|
.getSingleOrNull();
|
|
return _decodeHealthServicesExerciseTypesJson(
|
|
row?.readNullable<String>(
|
|
'health_services_exercise_type_strategy_snapshot_json',
|
|
),
|
|
);
|
|
}
|
|
|
|
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)),
|
|
tagsJson: Value(_encodeTags(template.tags)),
|
|
);
|
|
}
|
|
|
|
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,
|
|
tags: _decodeTags(row.tagsJson),
|
|
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<db.ActiveScoreStopwatchState?>
|
|
_findActiveScoreStopwatchStateRowByPosition(
|
|
db.AppDatabase database,
|
|
String sessionId,
|
|
int programIndex,
|
|
int exerciseIndex,
|
|
int setIndex,
|
|
) {
|
|
return (database.select(database.activeScoreStopwatchStates)..where(
|
|
(table) =>
|
|
table.activeWorkoutSessionId.equals(sessionId) &
|
|
table.programIndex.equals(programIndex) &
|
|
table.exerciseIndex.equals(exerciseIndex) &
|
|
table.setIndex.equals(setIndex),
|
|
))
|
|
.getSingleOrNull();
|
|
}
|
|
|
|
Future<db.ActiveManualScoreState?> _findActiveManualScoreStateRowByPosition(
|
|
db.AppDatabase database,
|
|
String sessionId,
|
|
int programIndex,
|
|
int exerciseIndex,
|
|
int setIndex,
|
|
) {
|
|
return (database.select(database.activeManualScoreStates)..where(
|
|
(table) =>
|
|
table.activeWorkoutSessionId.equals(sessionId) &
|
|
table.programIndex.equals(programIndex) &
|
|
table.exerciseIndex.equals(exerciseIndex) &
|
|
table.setIndex.equals(setIndex),
|
|
))
|
|
.getSingleOrNull();
|
|
}
|
|
|
|
Future<db.ActiveExerciseStepProgressState?>
|
|
_findActiveExerciseStepProgressStateRowByPosition(
|
|
db.AppDatabase database,
|
|
String sessionId,
|
|
int programIndex,
|
|
int exerciseIndex,
|
|
int setIndex,
|
|
) {
|
|
return (database.select(database.activeExerciseStepProgressStates)..where(
|
|
(table) =>
|
|
table.activeWorkoutSessionId.equals(sessionId) &
|
|
table.programIndex.equals(programIndex) &
|
|
table.exerciseIndex.equals(exerciseIndex) &
|
|
table.setIndex.equals(setIndex),
|
|
))
|
|
.getSingleOrNull();
|
|
}
|
|
|
|
domain.EntityMetadata _metadataWithStoredIdentity(
|
|
domain.EntityMetadata metadata,
|
|
domain.EntityMetadata stored,
|
|
) {
|
|
return domain.EntityMetadata(
|
|
id: stored.id,
|
|
createdAt: stored.createdAt,
|
|
updatedAt: metadata.updatedAt,
|
|
deletedAt: metadata.deletedAt,
|
|
schemaVersion: metadata.schemaVersion,
|
|
syncState: metadata.syncState,
|
|
localRevision: metadata.localRevision,
|
|
originDeviceId: metadata.originDeviceId,
|
|
futureOwnerProfileId: metadata.futureOwnerProfileId,
|
|
lastSyncedAt: metadata.lastSyncedAt,
|
|
remoteRevision: metadata.remoteRevision,
|
|
);
|
|
}
|
|
|
|
String _operationForExistingMutation({
|
|
required DateTime? existingDeletedAt,
|
|
required DateTime? deletedAt,
|
|
}) {
|
|
if (deletedAt != null) {
|
|
return 'softDelete';
|
|
}
|
|
if (existingDeletedAt != null) {
|
|
return 'restore';
|
|
}
|
|
return 'update';
|
|
}
|
|
|
|
Future<void> _upsertActiveScoreStopwatchState(
|
|
db.AppDatabase database,
|
|
domain.ActiveScoreStopwatchState state,
|
|
) async {
|
|
final metadata = state.metadata;
|
|
await database.customInsert(
|
|
'INSERT INTO active_score_stopwatch_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'
|
|
') VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) '
|
|
'ON CONFLICT(active_workout_session_id, program_index, exercise_index, '
|
|
'set_index) DO UPDATE SET '
|
|
'id = active_score_stopwatch_states.id, '
|
|
'created_at = active_score_stopwatch_states.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',
|
|
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>(_scoreStopwatchStatusToDb(state.status)),
|
|
Variable<DateTime>(state.startedAt.toUtc()),
|
|
Variable<int>(state.accumulatedMs),
|
|
Variable<DateTime>(_utcOrNull(state.stoppedAt)),
|
|
],
|
|
);
|
|
}
|
|
|
|
Future<void> _upsertActiveManualScoreState(
|
|
db.AppDatabase database,
|
|
domain.ActiveManualScoreState state,
|
|
) async {
|
|
final metadata = state.metadata;
|
|
await database.customInsert(
|
|
'INSERT INTO active_manual_score_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, value, score_updated_at'
|
|
') VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) '
|
|
'ON CONFLICT(active_workout_session_id, program_index, exercise_index, '
|
|
'set_index) DO UPDATE SET '
|
|
'id = active_manual_score_states.id, '
|
|
'created_at = active_manual_score_states.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, '
|
|
'value = excluded.value, '
|
|
'score_updated_at = excluded.score_updated_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<double>(state.value),
|
|
Variable<DateTime>(state.updatedAt.toUtc()),
|
|
],
|
|
);
|
|
}
|
|
|
|
Future<void> _upsertActiveExerciseStepProgressState(
|
|
db.AppDatabase database,
|
|
domain.ActiveExerciseStepProgressState state,
|
|
) async {
|
|
final metadata = state.metadata;
|
|
await database.customInsert(
|
|
'INSERT INTO active_exercise_step_progress_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, current_passage_index, '
|
|
'current_step_index, current_step_snapshot_id, status, started_at, '
|
|
'accumulated_ms, last_transition_at'
|
|
') VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) '
|
|
'ON CONFLICT(active_workout_session_id, program_index, exercise_index, '
|
|
'set_index) DO UPDATE SET '
|
|
'id = active_exercise_step_progress_states.id, '
|
|
'created_at = active_exercise_step_progress_states.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, '
|
|
'current_passage_index = excluded.current_passage_index, '
|
|
'current_step_index = excluded.current_step_index, '
|
|
'current_step_snapshot_id = excluded.current_step_snapshot_id, '
|
|
'status = excluded.status, '
|
|
'started_at = excluded.started_at, '
|
|
'accumulated_ms = excluded.accumulated_ms, '
|
|
'last_transition_at = excluded.last_transition_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<int>(state.currentPassageIndex),
|
|
Variable<int>(state.currentStepIndex),
|
|
Variable<String>(state.currentStepSnapshotId),
|
|
Variable<String>(_exerciseStepProgressStatusToDb(state.status)),
|
|
Variable<DateTime>(_utcOrNull(state.startedAt)),
|
|
Variable<int>(state.accumulatedMs),
|
|
Variable<DateTime>(state.lastTransitionAt.toUtc()),
|
|
],
|
|
);
|
|
}
|
|
|
|
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,
|
|
);
|
|
}
|
|
|
|
WorkoutHistorySetPerformance _setPerformanceFromRow(QueryRow row) {
|
|
final data = row.data;
|
|
return WorkoutHistorySetPerformance(
|
|
workoutHistoryId: data['workout_history_id'] as String,
|
|
startedAt: _dateTimeFromData(data, 'history_started_at'),
|
|
setIndex: data['set_index'] as int,
|
|
exerciseNameSnapshot: data['exercise_name_snapshot'] as String,
|
|
scoreInputModeSnapshot: _scoreInputModeFromDb(
|
|
data['score_input_mode_snapshot'] as String,
|
|
),
|
|
actualTimeMs: data['actual_time_ms'] as int?,
|
|
actualReps: data['actual_reps'] as int?,
|
|
actualScore: (data['actual_score'] as num?)?.toDouble(),
|
|
actualScoreTimeMs: data['actual_score_time_ms'] as int?,
|
|
completedAt: _dateTimeOrNullFromData(data, 'completed_at'),
|
|
);
|
|
}
|
|
|
|
WorkoutHistoryMetricPerformance _metricPerformanceFromRow(
|
|
QueryRow row, {
|
|
required PerformanceMetric metric,
|
|
}) {
|
|
final set = _setPerformanceFromRow(row);
|
|
return WorkoutHistoryMetricPerformance(
|
|
workoutHistoryId: set.workoutHistoryId,
|
|
startedAt: set.startedAt,
|
|
setIndex: set.setIndex,
|
|
exerciseNameSnapshot: set.exerciseNameSnapshot,
|
|
metric: metric,
|
|
scoreInputModeSnapshot: set.scoreInputModeSnapshot,
|
|
actualTimeMs: set.actualTimeMs,
|
|
actualReps: set.actualReps,
|
|
actualScore: set.actualScore,
|
|
actualScoreTimeMs: set.actualScoreTimeMs,
|
|
completedAt: set.completedAt,
|
|
);
|
|
}
|
|
|
|
String? _activeMeasuresPredicate(ActivePerformanceMeasures measures) {
|
|
final predicates = <String>[];
|
|
if (measures.scoreEnabled) {
|
|
predicates.add(
|
|
measures.scoreInputMode == domain.ScoreInputMode.stopwatch
|
|
? "(result.score_input_mode_snapshot = 'stopwatch' "
|
|
'AND result.actual_score_time_ms IS NOT NULL)'
|
|
: "(result.score_input_mode_snapshot = 'manual' "
|
|
'AND result.actual_score IS NOT NULL)',
|
|
);
|
|
}
|
|
if (measures.repsEnabled) {
|
|
predicates.add('result.actual_reps IS NOT NULL');
|
|
}
|
|
if (measures.timeEnabled) {
|
|
predicates.add('result.actual_time_ms IS NOT NULL');
|
|
}
|
|
return predicates.isEmpty ? null : predicates.join(' OR ');
|
|
}
|
|
|
|
({String predicate, String ordering}) _metricSql(
|
|
PerformanceMetric metric,
|
|
domain.ScoreInputMode scoreInputMode,
|
|
) {
|
|
return switch (metric) {
|
|
PerformanceMetric.score =>
|
|
scoreInputMode == domain.ScoreInputMode.stopwatch
|
|
? (
|
|
predicate:
|
|
"result.score_input_mode_snapshot = 'stopwatch' "
|
|
'AND result.actual_score_time_ms IS NOT NULL',
|
|
ordering: 'result.actual_score_time_ms ASC',
|
|
)
|
|
: (
|
|
predicate:
|
|
"result.score_input_mode_snapshot = 'manual' "
|
|
'AND result.actual_score IS NOT NULL',
|
|
ordering: 'result.actual_score DESC',
|
|
),
|
|
PerformanceMetric.reps => (
|
|
predicate: 'result.actual_reps IS NOT NULL',
|
|
ordering: 'result.actual_reps DESC',
|
|
),
|
|
PerformanceMetric.time => (
|
|
predicate: 'result.actual_time_ms IS NOT NULL',
|
|
ordering: 'result.actual_time_ms DESC',
|
|
),
|
|
};
|
|
}
|
|
|
|
domain.ActiveManualScoreState _activeManualScoreStateFromRow(
|
|
db.ActiveManualScoreState row,
|
|
) {
|
|
return domain.ActiveManualScoreState(
|
|
metadata: _metadataFromRow(row),
|
|
activeWorkoutSessionId: row.activeWorkoutSessionId,
|
|
programIndex: row.programIndex,
|
|
exerciseIndex: row.exerciseIndex,
|
|
setIndex: row.setIndex,
|
|
value: row.value,
|
|
updatedAt: _utc(row.scoreUpdatedAt),
|
|
);
|
|
}
|
|
|
|
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),
|
|
);
|
|
}
|
|
|
|
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),
|
|
minHeartRateBpm: Value(history.minHeartRateBpm),
|
|
averageHeartRateBpm: Value(history.averageHeartRateBpm),
|
|
maxHeartRateBpm: Value(history.maxHeartRateBpm),
|
|
totalDistanceMeters: Value(history.totalDistanceMeters),
|
|
totalCaloriesKcal: Value(history.totalCaloriesKcal),
|
|
);
|
|
}
|
|
|
|
db.WorkoutTelemetrySamplesCompanion _workoutTelemetrySampleCompanion(
|
|
domain.WorkoutTelemetrySample sample,
|
|
) {
|
|
return db.WorkoutTelemetrySamplesCompanion.insert(
|
|
id: sample.id,
|
|
sessionId: sample.sessionId,
|
|
capturedAt: sample.capturedAt.toUtc(),
|
|
programIndex: Value(sample.programIndex),
|
|
exerciseIndex: Value(sample.exerciseIndex),
|
|
setIndex: Value(sample.setIndex),
|
|
passageIndex: Value(sample.passageIndex),
|
|
stepIndex: Value(sample.stepIndex),
|
|
programSnapshotId: Value(sample.programSnapshotId),
|
|
exerciseSnapshotId: Value(sample.exerciseSnapshotId),
|
|
stepSnapshotId: Value(sample.stepSnapshotId),
|
|
heartRateBpm: Value(sample.heartRateBpm),
|
|
distanceMeters: Value(sample.distanceMeters),
|
|
caloriesKcal: Value(sample.caloriesKcal),
|
|
);
|
|
}
|
|
|
|
db.WorkoutTelemetryAggregatesCompanion _workoutTelemetryAggregateCompanion(
|
|
domain.WorkoutTelemetryAggregate aggregate,
|
|
) {
|
|
return db.WorkoutTelemetryAggregatesCompanion.insert(
|
|
sessionId: aggregate.sessionId,
|
|
scope: aggregate.scope.wireName,
|
|
programIndex: Value(aggregate.programIndex),
|
|
exerciseIndex: Value(aggregate.exerciseIndex),
|
|
setIndex: Value(aggregate.setIndex),
|
|
passageIndex: Value(aggregate.passageIndex),
|
|
stepIndex: Value(aggregate.stepIndex),
|
|
sampleCount: aggregate.sampleCount,
|
|
minHeartRateBpm: Value(aggregate.minHeartRateBpm),
|
|
averageHeartRateBpm: Value(aggregate.averageHeartRateBpm),
|
|
maxHeartRateBpm: Value(aggregate.maxHeartRateBpm),
|
|
totalDistanceMeters: Value(aggregate.totalDistanceMeters),
|
|
totalCaloriesKcal: Value(aggregate.totalCaloriesKcal),
|
|
);
|
|
}
|
|
|
|
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),
|
|
sourceExerciseIdSnapshot: Value(result.sourceExerciseIdSnapshot),
|
|
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),
|
|
sourceExerciseIdSnapshot: Value<String?>(result.sourceExerciseIdSnapshot),
|
|
);
|
|
}
|
|
|
|
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,
|
|
minHeartRateBpm: row.minHeartRateBpm,
|
|
averageHeartRateBpm: row.averageHeartRateBpm,
|
|
maxHeartRateBpm: row.maxHeartRateBpm,
|
|
totalDistanceMeters: row.totalDistanceMeters,
|
|
totalCaloriesKcal: row.totalCaloriesKcal,
|
|
results: results,
|
|
stepResults: stepResults,
|
|
);
|
|
}
|
|
|
|
domain.WorkoutTelemetrySample _workoutTelemetrySampleFromRow(
|
|
db.WorkoutTelemetrySample row,
|
|
) {
|
|
return domain.WorkoutTelemetrySample(
|
|
id: row.id,
|
|
sessionId: row.sessionId,
|
|
capturedAt: _utc(row.capturedAt),
|
|
programIndex: row.programIndex,
|
|
exerciseIndex: row.exerciseIndex,
|
|
setIndex: row.setIndex,
|
|
passageIndex: row.passageIndex,
|
|
stepIndex: row.stepIndex,
|
|
programSnapshotId: row.programSnapshotId,
|
|
exerciseSnapshotId: row.exerciseSnapshotId,
|
|
stepSnapshotId: row.stepSnapshotId,
|
|
heartRateBpm: row.heartRateBpm,
|
|
distanceMeters: row.distanceMeters,
|
|
caloriesKcal: row.caloriesKcal,
|
|
);
|
|
}
|
|
|
|
bool _telemetrySampleMatchesScope(
|
|
domain.WorkoutTelemetrySample sample, {
|
|
required domain.WorkoutTelemetryAggregateScope scope,
|
|
int? programIndex,
|
|
int? exerciseIndex,
|
|
int? setIndex,
|
|
int? passageIndex,
|
|
int? stepIndex,
|
|
}) {
|
|
return switch (scope) {
|
|
domain.WorkoutTelemetryAggregateScope.session => true,
|
|
domain.WorkoutTelemetryAggregateScope.exercise =>
|
|
sample.programIndex == programIndex &&
|
|
sample.exerciseIndex == exerciseIndex,
|
|
domain.WorkoutTelemetryAggregateScope.set =>
|
|
sample.programIndex == programIndex &&
|
|
sample.exerciseIndex == exerciseIndex &&
|
|
sample.setIndex == setIndex,
|
|
domain.WorkoutTelemetryAggregateScope.step =>
|
|
sample.programIndex == programIndex &&
|
|
sample.exerciseIndex == exerciseIndex &&
|
|
sample.setIndex == setIndex &&
|
|
(passageIndex == null || sample.passageIndex == passageIndex) &&
|
|
sample.stepIndex == stepIndex,
|
|
};
|
|
}
|
|
|
|
domain.WorkoutTelemetryAggregate _workoutTelemetryAggregateFromRow(
|
|
db.WorkoutTelemetryAggregate row,
|
|
) {
|
|
return domain.WorkoutTelemetryAggregate(
|
|
sessionId: row.sessionId,
|
|
scope: domain.WorkoutTelemetryAggregateScope.parse(row.scope),
|
|
programIndex: row.programIndex,
|
|
exerciseIndex: row.exerciseIndex,
|
|
setIndex: row.setIndex,
|
|
passageIndex: row.passageIndex,
|
|
stepIndex: row.stepIndex,
|
|
sampleCount: row.sampleCount,
|
|
minHeartRateBpm: row.minHeartRateBpm,
|
|
averageHeartRateBpm: row.averageHeartRateBpm,
|
|
maxHeartRateBpm: row.maxHeartRateBpm,
|
|
totalDistanceMeters: row.totalDistanceMeters,
|
|
totalCaloriesKcal: row.totalCaloriesKcal,
|
|
);
|
|
}
|
|
|
|
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,
|
|
sourceExerciseIdSnapshot: row.sourceExerciseIdSnapshot,
|
|
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,
|
|
sourceExerciseIdSnapshot: row.sourceExerciseIdSnapshot,
|
|
);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
final class _BackupLocalRow {
|
|
const _BackupLocalRow({
|
|
required this.updatedAt,
|
|
required this.deletedAt,
|
|
required this.localRevision,
|
|
});
|
|
|
|
final DateTime updatedAt;
|
|
final DateTime? deletedAt;
|
|
final int localRevision;
|
|
}
|
|
|
|
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,
|
|
'businessTypes': exercise.businessTypes
|
|
.map(_businessExerciseTypeToDb)
|
|
.toList(),
|
|
'effectiveBusinessTypes': exercise.effectiveBusinessTypes
|
|
.map(_businessExerciseTypeToDb)
|
|
.toList(),
|
|
'isExample': exercise.isExample,
|
|
'tags': exercise.tags,
|
|
'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,
|
|
'tags': program.tags,
|
|
'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,
|
|
'tags': template.tags,
|
|
'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,
|
|
'minHeartRateBpm': history.minHeartRateBpm,
|
|
'averageHeartRateBpm': history.averageHeartRateBpm,
|
|
'maxHeartRateBpm': history.maxHeartRateBpm,
|
|
'totalDistanceMeters': history.totalDistanceMeters,
|
|
'totalCaloriesKcal': history.totalCaloriesKcal,
|
|
};
|
|
|
|
Map<String, Object?> _localWorkoutHistoryPayload(
|
|
domain.WorkoutHistory history,
|
|
) => {
|
|
..._workoutHistoryPayload(history),
|
|
'results': history.results.map(_workoutHistorySetResultPayload).toList(),
|
|
'stepResults': history.stepResults
|
|
.map(_workoutHistoryStepResultPayload)
|
|
.toList(),
|
|
};
|
|
|
|
Map<String, Object?> _workoutHistorySetResultPayload(
|
|
domain.WorkoutHistorySetResult result,
|
|
) => {
|
|
'metadata': _metadataPayload(result.metadata),
|
|
'id': result.metadata.id,
|
|
'workoutHistoryId': result.workoutHistoryId,
|
|
'programSnapshotId': result.programSnapshotId,
|
|
'exerciseSnapshotId': result.exerciseSnapshotId,
|
|
'programIndex': result.programIndex,
|
|
'exerciseIndex': result.exerciseIndex,
|
|
'setIndex': result.setIndex,
|
|
'programNameSnapshot': result.programNameSnapshot,
|
|
'exerciseNameSnapshot': result.exerciseNameSnapshot,
|
|
'timeEnabledSnapshot': result.timeEnabledSnapshot,
|
|
'repsEnabledSnapshot': result.repsEnabledSnapshot,
|
|
'scoreEnabledSnapshot': result.scoreEnabledSnapshot,
|
|
'scoreInputModeSnapshot': result.scoreInputModeSnapshot.name,
|
|
'targetTimeSecondsSnapshot': result.targetTimeSecondsSnapshot,
|
|
'targetRepsSnapshot': result.targetRepsSnapshot,
|
|
'targetScoreSnapshot': result.targetScoreSnapshot,
|
|
'targetScoreTimeMsSnapshot': result.targetScoreTimeMsSnapshot,
|
|
'actualTimeMs': result.actualTimeMs,
|
|
'actualReps': result.actualReps,
|
|
'actualScore': result.actualScore,
|
|
'actualScoreTimeMs': result.actualScoreTimeMs,
|
|
'scoreLabelSnapshot': result.scoreLabelSnapshot,
|
|
'scoreUnitSnapshot': result.scoreUnitSnapshot,
|
|
'sourceExerciseIdSnapshot': result.sourceExerciseIdSnapshot,
|
|
'startedAt': result.startedAt?.toUtc().toIso8601String(),
|
|
'completedAt': result.completedAt?.toUtc().toIso8601String(),
|
|
'status': result.status.name,
|
|
};
|
|
|
|
Map<String, Object?> _workoutHistoryStepResultPayload(
|
|
domain.WorkoutHistoryStepResult result,
|
|
) => {
|
|
'metadata': _metadataPayload(result.metadata),
|
|
'id': result.metadata.id,
|
|
'workoutHistoryId': result.workoutHistoryId,
|
|
'programSnapshotId': result.programSnapshotId,
|
|
'exerciseSnapshotId': result.exerciseSnapshotId,
|
|
'programIndex': result.programIndex,
|
|
'exerciseIndex': result.exerciseIndex,
|
|
'setIndex': result.setIndex,
|
|
'passageIndex': result.passageIndex,
|
|
'stepIndex': result.stepIndex,
|
|
'stepSnapshotId': result.stepSnapshotId,
|
|
'stepNameSnapshot': result.stepNameSnapshot,
|
|
'stepTypeSnapshot': result.stepTypeSnapshot.name,
|
|
'targetValueSnapshot': result.targetValueSnapshot,
|
|
'hasScoreSnapshot': result.hasScoreSnapshot,
|
|
'scoreInputModeSnapshot': result.scoreInputModeSnapshot?.name,
|
|
'scoreLabelSnapshot': result.scoreLabelSnapshot,
|
|
'scoreUnitSnapshot': result.scoreUnitSnapshot,
|
|
'targetScoreSnapshot': result.targetScoreSnapshot,
|
|
'targetScoreTimeMsSnapshot': result.targetScoreTimeMsSnapshot,
|
|
'status': result.status.name,
|
|
'startedAt': result.startedAt?.toUtc().toIso8601String(),
|
|
'completedAt': result.completedAt?.toUtc().toIso8601String(),
|
|
'actualTimeMs': result.actualTimeMs,
|
|
'actualReps': result.actualReps,
|
|
'actualScore': result.actualScore,
|
|
'actualScoreTimeMs': result.actualScoreTimeMs,
|
|
'note': result.note,
|
|
'sourceExerciseIdSnapshot': result.sourceExerciseIdSnapshot,
|
|
};
|
|
|
|
LocalBackupResource _backupResource(
|
|
domain.EntityMetadata metadata,
|
|
Map<String, Object?> payload,
|
|
) {
|
|
return LocalBackupResource(
|
|
id: metadata.id,
|
|
updatedAt: metadata.updatedAt.toUtc(),
|
|
payload: payload,
|
|
);
|
|
}
|
|
|
|
RemoteSyncedItem _remoteItemFromBackupResource(
|
|
SyncResourceType resourceType,
|
|
LocalBackupResource resource,
|
|
) {
|
|
final metadata = resource.payload['metadata'];
|
|
final metadataMap = metadata is Map
|
|
? Map<String, Object?>.from(metadata)
|
|
: const <String, Object?>{};
|
|
return RemoteSyncedItem(
|
|
resourceType: resourceType,
|
|
clientId: resource.id,
|
|
serverId: '',
|
|
schemaVersion: metadataMap['schemaVersion'] as int? ?? 1,
|
|
clientUpdatedAt: resource.updatedAt.toUtc(),
|
|
serverUpdatedAt: resource.updatedAt.toUtc(),
|
|
deletedAt: _dateTimeFromPayload(metadataMap['deletedAt']),
|
|
payload: resource.payload,
|
|
);
|
|
}
|
|
|
|
Map<String, Object?> _payloadWithImportedMetadata(
|
|
Map<String, Object?> payload,
|
|
DateTime importedAt,
|
|
int localRevision, {
|
|
String? snapshotOriginDeviceId,
|
|
}) {
|
|
final metadata = payload['metadata'];
|
|
final metadataMap = metadata is Map
|
|
? Map<String, Object?>.from(metadata)
|
|
: <String, Object?>{};
|
|
return {
|
|
...payload,
|
|
'metadata': {
|
|
...metadataMap,
|
|
'updatedAt': importedAt.toUtc().toIso8601String(),
|
|
'deletedAt': null,
|
|
'syncState': domain.SyncState.dirty.name,
|
|
'localRevision': localRevision,
|
|
'originDeviceId':
|
|
metadataMap['originDeviceId'] as String? ??
|
|
snapshotOriginDeviceId ??
|
|
'local-device',
|
|
},
|
|
};
|
|
}
|
|
|
|
domain.WorkoutHistory _workoutHistoryFromLocalBackupPayload(
|
|
RemoteSyncedItem item,
|
|
) {
|
|
final payload = item.payload;
|
|
final metadata = _metadataFromPayload(item);
|
|
return domain.WorkoutHistory(
|
|
metadata: metadata,
|
|
sourceWorkoutTemplateId: payload['sourceWorkoutTemplateId'] as String?,
|
|
sourceActiveWorkoutSessionId:
|
|
payload['sourceActiveWorkoutSessionId'] as String?,
|
|
nameSnapshot: _stringFromPayload(payload, 'nameSnapshot', item.clientId),
|
|
startedAt:
|
|
_dateTimeFromPayload(payload['startedAt']) ?? item.clientUpdatedAt,
|
|
endedAt: _dateTimeFromPayload(payload['endedAt']) ?? item.clientUpdatedAt,
|
|
totalActiveMs: payload['totalActiveMs'] as int? ?? 0,
|
|
completed: payload['completed'] as bool? ?? false,
|
|
historySnapshotJson:
|
|
payload['historySnapshotJson'] as String? ?? '{"programs":[]}',
|
|
minHeartRateBpm: payload['minHeartRateBpm'] as int?,
|
|
averageHeartRateBpm: (payload['averageHeartRateBpm'] as num?)?.toDouble(),
|
|
maxHeartRateBpm: payload['maxHeartRateBpm'] as int?,
|
|
totalDistanceMeters: (payload['totalDistanceMeters'] as num?)?.toDouble(),
|
|
totalCaloriesKcal: (payload['totalCaloriesKcal'] as num?)?.toDouble(),
|
|
results: _workoutHistorySetResultsFromPayload(payload['results'], metadata),
|
|
stepResults: _workoutHistoryStepResultsFromPayload(
|
|
payload['stepResults'],
|
|
metadata,
|
|
),
|
|
);
|
|
}
|
|
|
|
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',
|
|
),
|
|
businessTypes: _businessExerciseTypesFromPayload(payload['businessTypes']),
|
|
isExample: payload['isExample'] as bool? ?? false,
|
|
tags: _stringListFromPayload(payload['tags']),
|
|
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,
|
|
tags: _stringListFromPayload(payload['tags']),
|
|
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,
|
|
tags: _stringListFromPayload(payload['tags']),
|
|
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: map?['syncState'] is String
|
|
? _syncStateFromDb(map!['syncState'] as String)
|
|
: 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: map['sourceExerciseId'] as String?,
|
|
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?,
|
|
healthServicesExerciseTypeStrategySnapshot:
|
|
_healthServicesExerciseTypesFromPayload(
|
|
map['healthServicesExerciseTypeStrategy'],
|
|
),
|
|
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: map['sourceProgramId'] as String?,
|
|
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);
|
|
}
|
|
|
|
List<domain.WorkoutHistorySetResult> _workoutHistorySetResultsFromPayload(
|
|
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', 'history-set-result');
|
|
return domain.WorkoutHistorySetResult(
|
|
metadata: _childMetadataFromPayload(map, id, parentMetadata),
|
|
workoutHistoryId: parentMetadata.id,
|
|
programSnapshotId: _stringFromPayload(map, 'programSnapshotId', ''),
|
|
exerciseSnapshotId: _stringFromPayload(map, 'exerciseSnapshotId', ''),
|
|
programIndex: map['programIndex'] as int? ?? 0,
|
|
exerciseIndex: map['exerciseIndex'] as int? ?? 0,
|
|
setIndex: map['setIndex'] as int? ?? 0,
|
|
programNameSnapshot: _stringFromPayload(
|
|
map,
|
|
'programNameSnapshot',
|
|
'',
|
|
),
|
|
exerciseNameSnapshot: _stringFromPayload(
|
|
map,
|
|
'exerciseNameSnapshot',
|
|
'',
|
|
),
|
|
timeEnabledSnapshot: map['timeEnabledSnapshot'] == true,
|
|
repsEnabledSnapshot: map['repsEnabledSnapshot'] == true,
|
|
scoreEnabledSnapshot: map['scoreEnabledSnapshot'] == true,
|
|
scoreInputModeSnapshot: _scoreInputModeFromDb(
|
|
map['scoreInputModeSnapshot'] as String? ?? 'manual',
|
|
),
|
|
targetTimeSecondsSnapshot: map['targetTimeSecondsSnapshot'] as int?,
|
|
targetRepsSnapshot: map['targetRepsSnapshot'] as int?,
|
|
targetScoreSnapshot: (map['targetScoreSnapshot'] as num?)?.toDouble(),
|
|
targetScoreTimeMsSnapshot: map['targetScoreTimeMsSnapshot'] as int?,
|
|
actualTimeMs: map['actualTimeMs'] as int?,
|
|
actualReps: map['actualReps'] as int?,
|
|
actualScore: (map['actualScore'] as num?)?.toDouble(),
|
|
actualScoreTimeMs: map['actualScoreTimeMs'] as int?,
|
|
scoreLabelSnapshot: map['scoreLabelSnapshot'] as String?,
|
|
scoreUnitSnapshot: map['scoreUnitSnapshot'] as String?,
|
|
sourceExerciseIdSnapshot: map['sourceExerciseIdSnapshot'] as String?,
|
|
startedAt: _dateTimeFromPayload(map['startedAt']),
|
|
completedAt: _dateTimeFromPayload(map['completedAt']),
|
|
status: _setResultStatusFromDb(
|
|
map['status'] as String? ?? 'completed',
|
|
),
|
|
);
|
|
})
|
|
.toList(growable: false);
|
|
}
|
|
|
|
List<domain.WorkoutHistoryStepResult> _workoutHistoryStepResultsFromPayload(
|
|
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', 'history-step-result');
|
|
return domain.WorkoutHistoryStepResult(
|
|
metadata: _childMetadataFromPayload(map, id, parentMetadata),
|
|
workoutHistoryId: parentMetadata.id,
|
|
programSnapshotId: _stringFromPayload(map, 'programSnapshotId', ''),
|
|
exerciseSnapshotId: _stringFromPayload(map, 'exerciseSnapshotId', ''),
|
|
programIndex: map['programIndex'] as int? ?? 0,
|
|
exerciseIndex: map['exerciseIndex'] as int? ?? 0,
|
|
setIndex: map['setIndex'] as int? ?? 0,
|
|
passageIndex: map['passageIndex'] as int? ?? 0,
|
|
stepIndex: map['stepIndex'] as int? ?? 0,
|
|
stepSnapshotId: _stringFromPayload(map, 'stepSnapshotId', ''),
|
|
stepNameSnapshot: _stringFromPayload(map, 'stepNameSnapshot', ''),
|
|
stepTypeSnapshot: _exerciseStepTypeFromDb(
|
|
map['stepTypeSnapshot'] as String? ?? 'reps',
|
|
),
|
|
targetValueSnapshot: map['targetValueSnapshot'] as int? ?? 0,
|
|
hasScoreSnapshot: map['hasScoreSnapshot'] == true,
|
|
scoreInputModeSnapshot: map['scoreInputModeSnapshot'] == null
|
|
? null
|
|
: _scoreInputModeFromDb(map['scoreInputModeSnapshot'] as String),
|
|
scoreLabelSnapshot: map['scoreLabelSnapshot'] as String?,
|
|
scoreUnitSnapshot: map['scoreUnitSnapshot'] as String?,
|
|
targetScoreSnapshot: (map['targetScoreSnapshot'] as num?)?.toDouble(),
|
|
targetScoreTimeMsSnapshot: map['targetScoreTimeMsSnapshot'] as int?,
|
|
status: _setResultStatusFromDb(
|
|
map['status'] as String? ?? 'completed',
|
|
),
|
|
startedAt: _dateTimeFromPayload(map['startedAt']),
|
|
completedAt: _dateTimeFromPayload(map['completedAt']),
|
|
actualTimeMs: map['actualTimeMs'] as int?,
|
|
actualReps: map['actualReps'] as int?,
|
|
actualScore: (map['actualScore'] as num?)?.toDouble(),
|
|
actualScoreTimeMs: map['actualScoreTimeMs'] as int?,
|
|
note: map['note'] as String?,
|
|
sourceExerciseIdSnapshot: map['sourceExerciseIdSnapshot'] as String?,
|
|
);
|
|
})
|
|
.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);
|
|
}
|
|
|
|
String _encodeTags(List<String> tags) => jsonEncode(domain.normalizeTags(tags));
|
|
|
|
List<String> _decodeTags(String? encoded) {
|
|
if (encoded == null || encoded.trim().isEmpty) {
|
|
return const [];
|
|
}
|
|
try {
|
|
final decoded = jsonDecode(encoded);
|
|
if (decoded is! List) {
|
|
return const [];
|
|
}
|
|
return domain.normalizeTags(decoded.whereType<String>());
|
|
} on Object {
|
|
return const [];
|
|
}
|
|
}
|
|
|
|
String _encodeBusinessExerciseTypes(List<domain.BusinessExerciseType> types) {
|
|
return jsonEncode(types.map(_businessExerciseTypeToDb).toList());
|
|
}
|
|
|
|
String _encodeHealthServicesExerciseTypes(
|
|
List<domain.HealthServicesExerciseType> types,
|
|
) {
|
|
return jsonEncode(types.map((type) => type.wireName).toList());
|
|
}
|
|
|
|
List<domain.BusinessExerciseType> _decodeBusinessExerciseTypesJson(
|
|
String? encoded,
|
|
) {
|
|
if (encoded == null || encoded.trim().isEmpty) {
|
|
return const [];
|
|
}
|
|
try {
|
|
final decoded = jsonDecode(encoded);
|
|
return _businessExerciseTypesFromPayload(decoded);
|
|
} on Object {
|
|
return const [];
|
|
}
|
|
}
|
|
|
|
List<domain.HealthServicesExerciseType> _decodeHealthServicesExerciseTypesJson(
|
|
String? encoded,
|
|
) {
|
|
if (encoded == null || encoded.trim().isEmpty) {
|
|
return domain.legacyHealthServicesExerciseTypeStrategy;
|
|
}
|
|
try {
|
|
final decoded = jsonDecode(encoded);
|
|
return _healthServicesExerciseTypesFromPayload(decoded);
|
|
} on Object {
|
|
return domain.legacyHealthServicesExerciseTypeStrategy;
|
|
}
|
|
}
|
|
|
|
List<domain.BusinessExerciseType> _businessExerciseTypesFromPayload(
|
|
Object? value,
|
|
) {
|
|
if (value is! List) {
|
|
return const [];
|
|
}
|
|
final output = <domain.BusinessExerciseType>[];
|
|
for (final raw in value.whereType<String>()) {
|
|
final type = _businessExerciseTypeFromDbOrNull(raw);
|
|
if (type != null && !output.contains(type)) {
|
|
output.add(type);
|
|
}
|
|
}
|
|
return List.unmodifiable(output);
|
|
}
|
|
|
|
List<domain.HealthServicesExerciseType> _healthServicesExerciseTypesFromPayload(
|
|
Object? value,
|
|
) {
|
|
if (value is! List) {
|
|
return domain.legacyHealthServicesExerciseTypeStrategy;
|
|
}
|
|
final output = <domain.HealthServicesExerciseType>[];
|
|
for (final raw in value.whereType<String>()) {
|
|
final type = _healthServicesExerciseTypeFromDbOrNull(raw);
|
|
if (type != null && !output.contains(type)) {
|
|
output.add(type);
|
|
}
|
|
}
|
|
return output.isEmpty
|
|
? domain.legacyHealthServicesExerciseTypeStrategy
|
|
: List.unmodifiable(output);
|
|
}
|
|
|
|
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',
|
|
),
|
|
linkedToSeriesScore: json['linkedToSeriesScore'] == true,
|
|
);
|
|
})
|
|
.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.pack => 'pack',
|
|
};
|
|
|
|
domain.ShareResourceType _shareResourceTypeFromDb(String value) =>
|
|
switch (value) {
|
|
'program' => domain.ShareResourceType.program,
|
|
'workoutTemplate' => domain.ShareResourceType.workoutTemplate,
|
|
'pack' => domain.ShareResourceType.pack,
|
|
_ => 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 _businessExerciseTypeToDb(domain.BusinessExerciseType type) =>
|
|
switch (type) {
|
|
domain.BusinessExerciseType.shoot => 'shoot',
|
|
domain.BusinessExerciseType.freeThrows => 'freeThrows',
|
|
domain.BusinessExerciseType.dribble => 'dribble',
|
|
domain.BusinessExerciseType.finishing => 'finishing',
|
|
domain.BusinessExerciseType.conditioning => 'conditioning',
|
|
domain.BusinessExerciseType.defense => 'defense',
|
|
domain.BusinessExerciseType.mobility => 'mobility',
|
|
domain.BusinessExerciseType.highIntensity => 'highIntensity',
|
|
domain.BusinessExerciseType.recovery => 'recovery',
|
|
};
|
|
|
|
domain.BusinessExerciseType? _businessExerciseTypeFromDbOrNull(String value) =>
|
|
switch (value) {
|
|
'shoot' => domain.BusinessExerciseType.shoot,
|
|
'freeThrows' => domain.BusinessExerciseType.freeThrows,
|
|
'dribble' => domain.BusinessExerciseType.dribble,
|
|
'finishing' => domain.BusinessExerciseType.finishing,
|
|
'conditioning' => domain.BusinessExerciseType.conditioning,
|
|
'defense' => domain.BusinessExerciseType.defense,
|
|
'mobility' => domain.BusinessExerciseType.mobility,
|
|
'highIntensity' => domain.BusinessExerciseType.highIntensity,
|
|
'recovery' => domain.BusinessExerciseType.recovery,
|
|
_ => null,
|
|
};
|
|
|
|
domain.HealthServicesExerciseType? _healthServicesExerciseTypeFromDbOrNull(
|
|
String value,
|
|
) => switch (value) {
|
|
'RUNNING' => domain.HealthServicesExerciseType.running,
|
|
'WALKING' => domain.HealthServicesExerciseType.walking,
|
|
'HIGH_INTENSITY_INTERVAL_TRAINING' =>
|
|
domain.HealthServicesExerciseType.highIntensityIntervalTraining,
|
|
'WORKOUT' => domain.HealthServicesExerciseType.workout,
|
|
_ => null,
|
|
};
|
|
|
|
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',
|
|
),
|
|
linkedToSeriesScore: json['linkedToSeriesScore'] == true,
|
|
);
|
|
})
|
|
.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'),
|
|
};
|