/
DemienMedich
/
BodyweightBase
Обзор
Документация
Войти
/
DemienMedich
/
BodyweightBase
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
core/src/state_store.cpp
379 строк
14 KB
DemienMedich
Replace C# app with C++ Qt version
20 июн 2026, 00:04
20 июн 2026, 00:04
e516ad2
Код
Авторство
О чём код?
#include "bodyweight/state_store.h" #include <algorithm> #include <QDir> #include <QFile> #include <QJsonArray> #include <QJsonDocument> #include <QJsonParseError> #include <QSaveFile> #include <QUuid> namespace bodyweight { namespace { QJsonObject completedExerciseToJson(const CompletedExercise &step) { return { {"exerciseId", step.exerciseId}, {"exerciseName", step.exerciseName}, {"metric", step.metric == ExerciseMetric::seconds ? "seconds" : "repetitions"}, {"targetValue", step.targetValue}, {"actualValue", step.actualValue}, {"workSeconds", step.workSeconds}, {"restSeconds", step.restSeconds}, {"completed", step.completed}, {"loadNote", step.loadNote} }; } QJsonObject stepToJson(const WorkoutStep &step) { QJsonObject result{ {"exerciseId", step.exerciseId}, {"coachNote", step.coachNote} }; result.insert("targetOverride", step.targetOverride ? QJsonValue(*step.targetOverride) : QJsonValue::Null); result.insert("restSecondsOverride", step.restSecondsOverride ? QJsonValue(*step.restSecondsOverride) : QJsonValue::Null); return result; } QJsonObject planToJson(const WorkoutPlan &plan) { QJsonArray steps; for (const WorkoutStep &step : plan.steps) steps.append(stepToJson(step)); return { {"id", plan.id}, {"basePlanId", plan.basePlanId}, {"name", plan.name}, {"goal", plan.goal}, {"description", plan.description}, {"variantId", plan.variantId}, {"roundCount", plan.roundCount}, {"warmupIncluded", plan.warmupIncluded}, {"warmupStepCount", plan.warmupStepCount}, {"roundStepCount", plan.roundStepCount}, {"recommendedRoundsText", plan.recommendedRoundsText}, {"steps", steps} }; } int draftPhaseToJson(SessionPhase phase) { switch (phase) { case SessionPhase::repetitionExercise: return 1; case SessionPhase::timedExercise: return 2; case SessionPhase::preparation: return 3; case SessionPhase::rest: return 4; case SessionPhase::completed: return 5; case SessionPhase::paused: return 6; default: return 0; } } SessionPhase draftPhaseFromJson(int phase) { switch (phase) { case 1: return SessionPhase::repetitionExercise; case 2: return SessionPhase::timedExercise; case 3: return SessionPhase::preparation; case 4: return SessionPhase::rest; case 5: return SessionPhase::completed; case 6: return SessionPhase::paused; default: return SessionPhase::idle; } } WorkoutStep stepFromJson(const QJsonObject &object) { WorkoutStep step; step.exerciseId = object.value("exerciseId").toString(); step.coachNote = object.value("coachNote").toString(); if (object.value("targetOverride").isDouble()) step.targetOverride = object.value("targetOverride").toInt(); if (object.value("restSecondsOverride").isDouble()) step.restSecondsOverride = object.value("restSecondsOverride").toInt(); return step; } WorkoutPlan planFromJson(const QJsonObject &object) { WorkoutPlan plan; plan.id = object.value("id").toString(); plan.basePlanId = object.value("basePlanId").toString(); plan.name = object.value("name").toString(); plan.goal = object.value("goal").toString(); plan.description = object.value("description").toString(); plan.variantId = object.value("variantId").toString("full"); plan.roundCount = object.value("roundCount").toInt(1); plan.warmupIncluded = object.value("warmupIncluded").toBool(); plan.warmupStepCount = object.value("warmupStepCount").toInt(); plan.roundStepCount = object.value("roundStepCount").toInt(); plan.recommendedRoundsText = object.value("recommendedRoundsText").toString(); for (const QJsonValue &value : object.value("steps").toArray()) { if (value.isObject()) plan.steps.append(stepFromJson(value.toObject())); } return plan; } CompletedExercise completedExerciseFromJson(const QJsonObject &object) { return { object.value("exerciseId").toString(), object.value("exerciseName").toString(), object.value("metric").toString() == "seconds" ? ExerciseMetric::seconds : ExerciseMetric::repetitions, object.value("targetValue").toInt(), object.value("actualValue").toInt(), object.value("workSeconds").toInt(), object.value("restSeconds").toInt(), object.value("completed").toBool(), object.value("loadNote").toString() }; } QString localTimestamp(const QDateTime &value) { return value.toString(Qt::ISODateWithMs); } bool writeAtomically(const QString &path, const QByteArray &data, QString *error) { QSaveFile file(path); if (!file.open(QIODevice::WriteOnly)) { if (error) { *error = file.errorString(); } return false; } if (file.write(data) != data.size()) { file.cancelWriting(); if (error) { *error = file.errorString(); } return false; } if (!file.commit()) { if (error) { *error = file.errorString(); } return false; } return true; } } // namespace StateStore::StateStore(QString storageDirectory) : storageDirectory_(std::move(storageDirectory)) { } QString StateStore::stateFilePath() const { return QDir(storageDirectory_).filePath(QStringLiteral("native-state.json")); } QString StateStore::backupFilePath() const { return QDir(storageDirectory_).filePath(QStringLiteral("native-state.backup.json")); } StoreLoadResult StateStore::load() const { StoreLoadResult result; QString primaryError; if (readObject(stateFilePath(), &result.root, &primaryError)) { result.ok = true; return result; } if (QFile::exists(stateFilePath())) { result.preservedCorruptPath = preserveCorruptState(); if (result.preservedCorruptPath.isEmpty()) { result.error = QStringLiteral("Invalid state could not be preserved: %1").arg(primaryError); return result; } } QString backupError; if (readObject(backupFilePath(), &result.root, &backupError)) { result.ok = true; result.recoveredFromBackup = true; QString saveError; if (!save(result.root, &saveError)) { result.ok = false; result.error = QStringLiteral("Backup loaded but could not be restored: %1").arg(saveError); } return result; } result.error = QFile::exists(backupFilePath()) ? QStringLiteral("Neither primary state nor backup is valid. Primary: %1 Backup: %2") .arg(primaryError, backupError) : QStringLiteral("No valid state file is available: %1").arg(primaryError); return result; } bool StateStore::save(QJsonObject root, QString *error) const { QDir directory; if (!directory.mkpath(storageDirectory_)) { if (error) { *error = QStringLiteral("Could not create storage directory."); } return false; } root.insert("schemaVersion", schemaVersion); root.insert("lastSavedAt", localTimestamp(QDateTime::currentDateTime())); if (QFile::exists(stateFilePath())) { QFile current(stateFilePath()); if (!current.open(QIODevice::ReadOnly)) { if (error) { *error = QStringLiteral("Could not read current state for backup: %1").arg(current.errorString()); } return false; } QString backupError; if (!writeAtomically(backupFilePath(), current.readAll(), &backupError)) { if (error) { *error = QStringLiteral("Could not create state backup: %1").arg(backupError); } return false; } } const QByteArray json = QJsonDocument(root).toJson(QJsonDocument::Indented); return writeAtomically(stateFilePath(), json, error); } bool StateStore::appendFinishedSession( QJsonObject &root, const FinishedSession &session, const QDateTime &startedAtLocal, const QDateTime &endedAtLocal, const QString &feedbackLabel, QString *error) { if (session.planId.isEmpty() || session.steps.isEmpty() || !startedAtLocal.isValid() || !endedAtLocal.isValid() || endedAtLocal < startedAtLocal) { if (error) { *error = QStringLiteral("Finished session is incomplete or has invalid timestamps."); } return false; } if (root.contains("sessions") && !root.value("sessions").isArray()) { if (error) { *error = QStringLiteral("sessions is not an array."); } return false; } QJsonArray steps; for (const CompletedExercise &step : session.steps) { steps.append(completedExerciseToJson(step)); } QJsonArray history = root.value("sessions").toArray(); history.append(QJsonObject{ {"id", QUuid::createUuid().toString(QUuid::WithoutBraces)}, {"planId", session.planId}, {"planName", session.planName}, {"startedAt", localTimestamp(startedAtLocal)}, {"endedAt", localTimestamp(endedAtLocal)}, {"totalWorkSeconds", session.totalWorkSeconds}, {"totalRestSeconds", session.totalRestSeconds}, {"completedAll", session.completedAll}, {"feedback", feedbackLabel.trimmed()}, {"steps", steps} }); while (history.size() > StateStore::maxSessionHistoryItems) { history.removeFirst(); } root.insert("sessions", history); root.insert("pendingDraft", QJsonValue::Null); return true; } bool StateStore::setPendingDraft( QJsonObject &root, const SessionDraft &draft, const QDateTime &sessionStartedAtLocal, QString *error) { if (!sessionStartedAtLocal.isValid() || draft.plan.steps.isEmpty() || draft.phase == SessionPhase::idle || draft.phase == SessionPhase::completed) { if (error) *error = QStringLiteral("Session draft is incomplete."); return false; } QJsonArray completed; for (const CompletedExercise &step : draft.completedSteps) completed.append(completedExerciseToJson(step)); root.insert("pendingDraft", QJsonObject{ {"plan", planToJson(draft.plan)}, {"stepIndex", draft.stepIndex}, {"phase", draftPhaseToJson(draft.phase)}, {"phaseBeforePause", draftPhaseToJson(draft.phaseBeforePause)}, {"phaseDurationSeconds", draft.phaseDurationSeconds}, {"phaseElapsedSeconds", draft.phaseElapsedSeconds}, {"currentValue", draft.currentValue}, {"sessionStartedAt", localTimestamp(sessionStartedAtLocal)}, {"completedSteps", completed}, {"totalRestSeconds", draft.totalRestSeconds}, {"currentLoadNote", draft.currentLoadNote}, {"savedAt", localTimestamp(QDateTime::currentDateTime())} }); return true; } std::optional<StoredSessionDraft> StateStore::pendingDraft(const QJsonObject &root, QString *error) { const QJsonValue value = root.value("pendingDraft"); if (value.isNull() || value.isUndefined()) return std::nullopt; if (!value.isObject()) { if (error) *error = QStringLiteral("PendingSessionDraft is not an object."); return std::nullopt; } const QJsonObject object = value.toObject(); if (!object.value("plan").isObject() || !object.value("completedSteps").isArray()) { if (error) *error = QStringLiteral("PendingSessionDraft has invalid plan or completed steps."); return std::nullopt; } StoredSessionDraft stored; stored.draft.plan = planFromJson(object.value("plan").toObject()); stored.draft.stepIndex = object.value("stepIndex").toInt(-1); stored.draft.phase = draftPhaseFromJson(object.value("phase").toInt()); stored.draft.phaseBeforePause = draftPhaseFromJson(object.value("phaseBeforePause").toInt()); stored.draft.phaseDurationSeconds = object.value("phaseDurationSeconds").toInt(-1); stored.draft.phaseElapsedSeconds = object.value("phaseElapsedSeconds").toInt(-1); stored.draft.currentValue = object.value("currentValue").toInt(-1); stored.draft.totalRestSeconds = object.value("totalRestSeconds").toInt(-1); stored.draft.currentLoadNote = object.value("currentLoadNote").toString(); stored.sessionStartedAtLocal = QDateTime::fromString(object.value("sessionStartedAt").toString(), Qt::ISODate); for (const QJsonValue &step : object.value("completedSteps").toArray()) { if (!step.isObject()) { if (error) *error = QStringLiteral("PendingSessionDraft contains invalid completed step."); return std::nullopt; } stored.draft.completedSteps.append(completedExerciseFromJson(step.toObject())); } if (!stored.sessionStartedAtLocal.isValid()) { if (error) *error = QStringLiteral("PendingSessionDraft has invalid start time."); return std::nullopt; } return stored; } void StateStore::clearPendingDraft(QJsonObject &root) { root.insert("pendingDraft", QJsonValue::Null); } QString StateStore::preserveCorruptState() const { const QString name = QStringLiteral("native-state.corrupt.%1.%2.json") .arg(QDateTime::currentDateTime().toString("yyyyMMdd-HHmmss")) .arg(QUuid::createUuid().toString(QUuid::WithoutBraces)); const QString destination = QDir(storageDirectory_).filePath(name); return QFile::rename(stateFilePath(), destination) ? destination : QString(); } bool StateStore::readObject(const QString &path, QJsonObject *root, QString *error) { QFile file(path); if (!file.open(QIODevice::ReadOnly)) { if (error) { *error = file.errorString(); } return false; } QJsonParseError parseError; const QJsonDocument document = QJsonDocument::fromJson(file.readAll(), &parseError); if (parseError.error != QJsonParseError::NoError || !document.isObject()) { if (error) { *error = parseError.error == QJsonParseError::NoError ? QStringLiteral("State root is not an object.") : parseError.errorString(); } return false; } *root = document.object(); return true; } } // namespace bodyweight