feat(exécution): refonte des chronos de séance et carte exercice actif (ticket #90)
Réorganise l'écran d'exécution autour d'une carte "Exercice actif" sous le compteur SÉRIE X/Y (nom, médias, temps de série, action "Démarrer l'exercice"). Le timer de série devient un état persistant dédié (plus un DateTime UI volatile), pause-aware, et "Démarrer l'exercice" lance en une action tous les chronos qui doivent démarrer en début de série (série, première étape temps, score chrono si stopwatch). Implémentation restée non commitée depuis sa réalisation ; QA a validé les vérifications statiques exécutables en sandbox (dart analyze, git diff --check). flutter analyze / flutter test restent à relancer dans un environnement avec cache Flutter SDK accessible. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -998,6 +998,17 @@ final class DriftActiveSessionRepository implements ActiveSessionRepository {
|
||||
);
|
||||
}
|
||||
|
||||
@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(
|
||||
@ -1005,18 +1016,24 @@ final class DriftActiveSessionRepository implements ActiveSessionRepository {
|
||||
tableName: 'active_rest_states',
|
||||
entityType: 'ActiveRestState',
|
||||
metadata: restState.metadata,
|
||||
write: () => database
|
||||
.into(database.activeRestStates)
|
||||
.insertOnConflictUpdate(_activeRestStateCompanion(restState)),
|
||||
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.select(
|
||||
database.activeRestStates,
|
||||
)..where((table) => table.id.equals(id))).getSingleOrNull();
|
||||
return row == null ? null : _activeRestStateFromRow(row);
|
||||
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
|
||||
@ -1122,6 +1139,33 @@ final class DriftActiveSessionRepository implements ActiveSessionRepository {
|
||||
return row == null ? null : _activeScoreStopwatchStateFromRow(row);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<domain.ActiveSetTimerState?> findSetTimerState({
|
||||
required String sessionId,
|
||||
required int programIndex,
|
||||
required int exerciseIndex,
|
||||
required int setIndex,
|
||||
}) async {
|
||||
final row = await database
|
||||
.customSelect(
|
||||
'SELECT * FROM active_set_timer_states '
|
||||
'WHERE active_workout_session_id = ? '
|
||||
'AND program_index = ? '
|
||||
'AND exercise_index = ? '
|
||||
'AND set_index = ? '
|
||||
'AND deleted_at IS NULL '
|
||||
'LIMIT 1',
|
||||
variables: [
|
||||
Variable<String>(sessionId),
|
||||
Variable<int>(programIndex),
|
||||
Variable<int>(exerciseIndex),
|
||||
Variable<int>(setIndex),
|
||||
],
|
||||
)
|
||||
.getSingleOrNull();
|
||||
return row == null ? null : _activeSetTimerStateFromCustomRow(row);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<domain.ActiveExerciseStepProgressState?>
|
||||
findExerciseStepProgressState({
|
||||
@ -1158,6 +1202,22 @@ final class DriftActiveSessionRepository implements ActiveSessionRepository {
|
||||
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 {
|
||||
@ -1201,12 +1261,15 @@ final class DriftActiveSessionRepository implements ActiveSessionRepository {
|
||||
|
||||
@override
|
||||
Future<List<domain.ActiveRestState>> listRestStates(String sessionId) async {
|
||||
final rows =
|
||||
await (database.select(database.activeRestStates)
|
||||
..where((table) => table.activeWorkoutSessionId.equals(sessionId))
|
||||
..orderBy([(table) => OrderingTerm.asc(table.startedAt)]))
|
||||
.get();
|
||||
return rows.map(_activeRestStateFromRow).toList();
|
||||
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
|
||||
@ -1839,6 +1902,47 @@ domain.EntityMetadata _metadataFromRow(dynamic row) {
|
||||
);
|
||||
}
|
||||
|
||||
domain.EntityMetadata _metadataFromData(Map<String, dynamic> data) {
|
||||
return domain.EntityMetadata(
|
||||
id: data['id'] as String,
|
||||
createdAt: _dateTimeFromData(data, 'created_at'),
|
||||
updatedAt: _dateTimeFromData(data, 'updated_at'),
|
||||
deletedAt: _dateTimeOrNullFromData(data, 'deleted_at'),
|
||||
schemaVersion: data['schema_version'] as int,
|
||||
syncState: _syncStateFromDb(data['sync_state'] as String),
|
||||
localRevision: data['local_revision'] as int,
|
||||
originDeviceId: data['origin_device_id'] as String,
|
||||
futureOwnerProfileId: data['future_owner_profile_id'] as String?,
|
||||
lastSyncedAt: _dateTimeOrNullFromData(data, 'last_synced_at'),
|
||||
remoteRevision: data['remote_revision'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
DateTime _dateTimeFromData(Map<String, dynamic> data, String key) {
|
||||
final value = data[key];
|
||||
if (value is DateTime) {
|
||||
return value.toUtc();
|
||||
}
|
||||
if (value is int) {
|
||||
return DateTime.fromMillisecondsSinceEpoch(value, isUtc: true);
|
||||
}
|
||||
throw domain.DomainException('Invalid timestamp column: $key');
|
||||
}
|
||||
|
||||
DateTime? _dateTimeOrNullFromData(Map<String, dynamic> data, String key) {
|
||||
final value = data[key];
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value is DateTime) {
|
||||
return value.toUtc();
|
||||
}
|
||||
if (value is int) {
|
||||
return DateTime.fromMillisecondsSinceEpoch(value, isUtc: true);
|
||||
}
|
||||
throw domain.DomainException('Invalid timestamp column: $key');
|
||||
}
|
||||
|
||||
List<dynamic> _metadataValues(domain.EntityMetadata metadata) => [
|
||||
Value(metadata.id),
|
||||
Value(metadata.createdAt.toUtc()),
|
||||
@ -2458,18 +2562,112 @@ db.ActiveRestStatesCompanion _activeRestStateCompanion(
|
||||
);
|
||||
}
|
||||
|
||||
domain.ActiveRestState _activeRestStateFromRow(db.ActiveRestState row) {
|
||||
Future<void> _updateActiveRestPauseFields(
|
||||
db.AppDatabase database,
|
||||
domain.ActiveRestState restState,
|
||||
) async {
|
||||
await database.customUpdate(
|
||||
'UPDATE active_rest_states SET paused_at = ?, accumulated_paused_ms = ? '
|
||||
'WHERE id = ?',
|
||||
variables: [
|
||||
Variable<DateTime>(_utcOrNull(restState.pausedAt)),
|
||||
Variable<int>(restState.accumulatedPausedMs),
|
||||
Variable<String>(restState.metadata.id),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _upsertActiveSetTimerState(
|
||||
db.AppDatabase database,
|
||||
domain.ActiveSetTimerState state,
|
||||
) async {
|
||||
final metadata = state.metadata;
|
||||
await database.customInsert(
|
||||
'INSERT INTO active_set_timer_states ('
|
||||
'id, created_at, updated_at, deleted_at, schema_version, sync_state, '
|
||||
'local_revision, origin_device_id, future_owner_profile_id, '
|
||||
'last_synced_at, remote_revision, active_workout_session_id, '
|
||||
'program_index, exercise_index, set_index, status, started_at, '
|
||||
'accumulated_ms, stopped_at, skipped_at'
|
||||
') VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) '
|
||||
'ON CONFLICT(active_workout_session_id, program_index, exercise_index, '
|
||||
'set_index) DO UPDATE SET '
|
||||
'id = excluded.id, '
|
||||
'created_at = excluded.created_at, '
|
||||
'updated_at = excluded.updated_at, '
|
||||
'deleted_at = excluded.deleted_at, '
|
||||
'schema_version = excluded.schema_version, '
|
||||
'sync_state = excluded.sync_state, '
|
||||
'local_revision = excluded.local_revision, '
|
||||
'origin_device_id = excluded.origin_device_id, '
|
||||
'future_owner_profile_id = excluded.future_owner_profile_id, '
|
||||
'last_synced_at = excluded.last_synced_at, '
|
||||
'remote_revision = excluded.remote_revision, '
|
||||
'active_workout_session_id = excluded.active_workout_session_id, '
|
||||
'program_index = excluded.program_index, '
|
||||
'exercise_index = excluded.exercise_index, '
|
||||
'set_index = excluded.set_index, '
|
||||
'status = excluded.status, '
|
||||
'started_at = excluded.started_at, '
|
||||
'accumulated_ms = excluded.accumulated_ms, '
|
||||
'stopped_at = excluded.stopped_at, '
|
||||
'skipped_at = excluded.skipped_at',
|
||||
variables: [
|
||||
Variable<String>(metadata.id),
|
||||
Variable<DateTime>(metadata.createdAt.toUtc()),
|
||||
Variable<DateTime>(metadata.updatedAt.toUtc()),
|
||||
Variable<DateTime>(_utcOrNull(metadata.deletedAt)),
|
||||
Variable<int>(metadata.schemaVersion),
|
||||
Variable<String>(_syncStateToDb(metadata.syncState)),
|
||||
Variable<int>(metadata.localRevision),
|
||||
Variable<String>(metadata.originDeviceId),
|
||||
Variable<String>(metadata.futureOwnerProfileId),
|
||||
Variable<DateTime>(_utcOrNull(metadata.lastSyncedAt)),
|
||||
Variable<String>(metadata.remoteRevision),
|
||||
Variable<String>(state.activeWorkoutSessionId),
|
||||
Variable<int>(state.programIndex),
|
||||
Variable<int>(state.exerciseIndex),
|
||||
Variable<int>(state.setIndex),
|
||||
Variable<String>(_setTimerStatusToDb(state.status)),
|
||||
Variable<DateTime>(_utcOrNull(state.startedAt)),
|
||||
Variable<int>(state.accumulatedMs),
|
||||
Variable<DateTime>(_utcOrNull(state.stoppedAt)),
|
||||
Variable<DateTime>(_utcOrNull(state.skippedAt)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
domain.ActiveSetTimerState _activeSetTimerStateFromCustomRow(QueryRow row) {
|
||||
final data = row.data;
|
||||
return domain.ActiveSetTimerState(
|
||||
metadata: _metadataFromData(data),
|
||||
activeWorkoutSessionId: data['active_workout_session_id'] as String,
|
||||
programIndex: data['program_index'] as int,
|
||||
exerciseIndex: data['exercise_index'] as int,
|
||||
setIndex: data['set_index'] as int,
|
||||
status: _setTimerStatusFromDb(data['status'] as String),
|
||||
startedAt: _dateTimeOrNullFromData(data, 'started_at'),
|
||||
accumulatedMs: data['accumulated_ms'] as int,
|
||||
stoppedAt: _dateTimeOrNullFromData(data, 'stopped_at'),
|
||||
skippedAt: _dateTimeOrNullFromData(data, 'skipped_at'),
|
||||
);
|
||||
}
|
||||
|
||||
domain.ActiveRestState _activeRestStateFromCustomRow(QueryRow row) {
|
||||
final data = row.data;
|
||||
return domain.ActiveRestState(
|
||||
metadata: _metadataFromRow(row),
|
||||
activeWorkoutSessionId: row.activeWorkoutSessionId,
|
||||
afterProgramIndex: row.afterProgramIndex,
|
||||
afterExerciseIndex: row.afterExerciseIndex,
|
||||
afterSetIndex: row.afterSetIndex,
|
||||
plannedRestSeconds: row.plannedRestSeconds,
|
||||
adjustedRestSeconds: row.adjustedRestSeconds,
|
||||
startedAt: _utc(row.startedAt),
|
||||
endedAt: _utcOrNull(row.endedAt),
|
||||
skippedAt: _utcOrNull(row.skippedAt),
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
@ -3509,18 +3707,39 @@ domain.SetResultStatus _setResultStatusFromDb(String value) => switch (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) {
|
||||
|
||||
Reference in New Issue
Block a user