/
DemienMedich
/
BodyweightBase
Обзор
Документация
Войти
/
DemienMedich
/
BodyweightBase
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
develop
core/src/workout_session_runner.cpp
850 строк
28 KB
DemienMedich
feat: continue quick workouts into full sessions
23 июл 2026, 16:34
23 июл 2026, 16:34
a2ccab1
Код
Авторство
О чём код?
#include "bodyweight/workout_session_runner.h" #include <algorithm> namespace bodyweight { namespace { constexpr int sideSwitchSeconds = 3; bool containsAny(const QString &text, std::initializer_list<const char *> needles) { const QString lower = text.toLower(); for (const char *needle : needles) { if (lower.contains(QString::fromUtf8(needle))) return true; } return false; } } // namespace WorkoutSessionRunner::WorkoutSessionRunner(QVector<ExerciseDefinition> exercises) { for (ExerciseDefinition &exercise : exercises) { exercises_.insert(exercise.id, std::move(exercise)); } } bool WorkoutSessionRunner::start(const WorkoutPlan &plan, QString *error) { if (!validatePlan(plan, error)) { return false; } abort(); plan_ = plan; plan_->roundCount = std::clamp(plan_->roundCount, 1, 6); plan_->steps = composeWorkoutSteps(*plan_); moveToStep(0); return true; } bool WorkoutSessionRunner::startWithQuickCheckpoint( const WorkoutPlan &plan, int quickWorkStepCount, QString *error) { if (!validatePlan(plan, error)) { return false; } abort(); plan_ = plan; WorkoutPlan compositionPlan = *plan_; compositionPlan.roundCount = 1; plan_->steps = composeWorkoutSteps(compositionPlan); int warmupStepCount = 0; while (warmupStepCount < plan_->steps.size() && plan_->steps.at(warmupStepCount).isWarmup) { ++warmupStepCount; } quickCheckpointStepIndex_ = warmupStepCount + std::max(1, quickWorkStepCount); if (quickCheckpointStepIndex_ <= warmupStepCount || quickCheckpointStepIndex_ >= plan_->steps.size()) { if (error) *error = QStringLiteral("Quick checkpoint is outside the workout plan."); abort(); return false; } moveToStep(0); return true; } void WorkoutSessionRunner::abort() { plan_.reset(); stepIndex_ = 0; currentSet_ = 1; setCount_ = 1; phase_ = SessionPhase::idle; phaseBeforePause_ = SessionPhase::idle; phaseDurationSeconds_ = 0; phaseElapsedSeconds_ = 0; currentValue_ = 0; totalRestSeconds_ = 0; currentLoadNote_.clear(); currentExternalLoadKg_ = 0.0; currentEffortRating_ = 0; currentDiscomfortRating_ = 0; completedSteps_.clear(); finishedSession_.reset(); quickCheckpointStepIndex_ = -1; } void WorkoutSessionRunner::advance(int seconds) { int remainingAdvance = std::max(0, seconds); while (remainingAdvance > 0 && (isTimerPhase() || phase_ == SessionPhase::repetitionExercise)) { const int remainingPhase = std::max(0, phaseDurationSeconds_ - phaseElapsedSeconds_); const int consumed = std::min(remainingAdvance, remainingPhase); phaseElapsedSeconds_ += consumed; remainingAdvance -= consumed; if (phase_ == SessionPhase::repetitionExercise) { const int target = targetAt(stepIndex_); currentValue_ = std::clamp( (phaseElapsedSeconds_ + defaultSecondsPerRepetition - 1) / defaultSecondsPerRepetition, 0, target); } if (phaseElapsedSeconds_ < phaseDurationSeconds_) { break; } switch (phase_) { case SessionPhase::preparation: beginExercise(); break; case SessionPhase::timedExercise: finishTimedExercise(); break; case SessionPhase::repetitionExercise: currentValue_ = targetAt(stepIndex_); finishRepetitionExercise(); break; case SessionPhase::rest: totalRestSeconds_ += phaseDurationSeconds_; advanceAfterRest(); break; default: return; } } } void WorkoutSessionRunner::pause() { if (phase_ != SessionPhase::preparation && phase_ != SessionPhase::repetitionExercise && phase_ != SessionPhase::timedExercise && phase_ != SessionPhase::rest) { return; } phaseBeforePause_ = phase_; phase_ = SessionPhase::paused; } void WorkoutSessionRunner::resume() { if (phase_ != SessionPhase::paused) { return; } phase_ = phaseBeforePause_; phaseBeforePause_ = SessionPhase::idle; } void WorkoutSessionRunner::addRep() { if (phase_ == SessionPhase::repetitionExercise) { ++currentValue_; } } void WorkoutSessionRunner::removeRep() { if (phase_ == SessionPhase::repetitionExercise) { currentValue_ = std::max(0, currentValue_ - 1); } } void WorkoutSessionRunner::setRepValue(int value) { if (phase_ == SessionPhase::repetitionExercise) { currentValue_ = std::max(0, value); } } void WorkoutSessionRunner::setCurrentLoadNote(const QString &loadNote) { currentLoadNote_ = loadNote.trimmed(); } void WorkoutSessionRunner::setCurrentSetLog( double externalLoadKg, int effortRating, int discomfortRating, const QString &loadNote) { currentExternalLoadKg_ = std::clamp(externalLoadKg, 0.0, 500.0); currentEffortRating_ = std::clamp(effortRating, 0, 10); currentDiscomfortRating_ = std::clamp(discomfortRating, 0, 10); currentLoadNote_ = loadNote.trimmed(); } std::optional<CompletedExercise> WorkoutSessionRunner::lastCompletedExercise() const { if (completedSteps_.isEmpty()) return std::nullopt; return completedSteps_.constLast(); } bool WorkoutSessionRunner::updateLastCompletedSetLog( double externalLoadKg, int effortRating, int discomfortRating, const QString &loadNote) { if (completedSteps_.isEmpty()) return false; CompletedExercise &step = completedSteps_.last(); step.externalLoadKg = std::clamp(externalLoadKg, 0.0, 500.0); step.effortRating = std::clamp(effortRating, 0, 10); step.discomfortRating = std::clamp(discomfortRating, 0, 10); step.loadNote = loadNote.trimmed(); return true; } void WorkoutSessionRunner::completeRepExercise() { if (!plan_ || phase_ != SessionPhase::repetitionExercise) { return; } finishRepetitionExercise(); } void WorkoutSessionRunner::finishRepetitionExercise() { const ExerciseDefinition &exercise = exerciseAt(stepIndex_); completedSteps_.append({ exercise.id, exercise.name, exercise.metric, targetAt(stepIndex_), currentValue_, std::max(1, phaseElapsedSeconds_), restAt(stepIndex_), currentValue_ >= targetAt(stepIndex_), currentLoadNote_, currentExternalLoadKg_, currentEffortRating_, currentDiscomfortRating_ }); beginRestOrFinish(); } void WorkoutSessionRunner::skipCurrentExercise() { if (!plan_ || phase_ == SessionPhase::idle || phase_ == SessionPhase::completed || phase_ == SessionPhase::rest || phase_ == SessionPhase::paused) { return; } const ExerciseDefinition &exercise = exerciseAt(stepIndex_); const bool activeExercise = phase_ == SessionPhase::repetitionExercise || phase_ == SessionPhase::timedExercise; completedSteps_.append({ exercise.id, exercise.name, exercise.metric, targetAt(stepIndex_), phase_ == SessionPhase::repetitionExercise ? currentValue_ : (phase_ == SessionPhase::timedExercise ? phaseElapsedSeconds_ : 0), activeExercise ? std::max(1, phaseElapsedSeconds_) : 0, restAt(stepIndex_), false, currentLoadNote_, currentExternalLoadKg_, currentEffortRating_, currentDiscomfortRating_ }); beginRestOrFinish(); } void WorkoutSessionRunner::finishEarly() { if (!plan_ || phase_ == SessionPhase::idle || phase_ == SessionPhase::completed) { return; } const SessionPhase effectivePhase = phase_ == SessionPhase::paused ? phaseBeforePause_ : phase_; const bool repetitionInProgress = effectivePhase == SessionPhase::repetitionExercise && currentValue_ > 0; const bool timerInProgress = effectivePhase == SessionPhase::timedExercise && phaseElapsedSeconds_ > 0; if (repetitionInProgress || timerInProgress) { const ExerciseDefinition &exercise = exerciseAt(stepIndex_); completedSteps_.append({ exercise.id, exercise.name, exercise.metric, targetAt(stepIndex_), repetitionInProgress ? currentValue_ : phaseElapsedSeconds_, std::max(1, phaseElapsedSeconds_), restAt(stepIndex_), repetitionInProgress && currentValue_ >= targetAt(stepIndex_), currentLoadNote_, currentExternalLoadKg_, currentEffortRating_, currentDiscomfortRating_ }); } finish(); } bool WorkoutSessionRunner::replaceCurrentExercise(const QString &exerciseId, QString *error) { const SessionPhase effectivePhase = phase_ == SessionPhase::paused ? phaseBeforePause_ : phase_; if (!plan_ || stepIndex_ < 0 || stepIndex_ >= plan_->steps.size() || (effectivePhase != SessionPhase::preparation && effectivePhase != SessionPhase::repetitionExercise && effectivePhase != SessionPhase::timedExercise)) { if (error) *error = QStringLiteral("Exercise cannot be replaced in the current phase."); return false; } auto replacement = exercises_.constFind(exerciseId.trimmed()); if (replacement == exercises_.constEnd()) { if (error) *error = QStringLiteral("Unknown replacement exercise: %1").arg(exerciseId); return false; } const ExerciseDefinition ¤t = exerciseAt(stepIndex_); if (replacement->metric != current.metric) { if (error) *error = QStringLiteral("Replacement exercise uses a different metric."); return false; } WorkoutStep &step = plan_->steps[stepIndex_]; const int baseSets = std::max(1, step.sets); const int logicalSet = current.alternatingSides ? std::clamp((currentSet_ + 1) / 2, 1, baseSets) : std::clamp(currentSet_, 1, baseSets); step.exerciseId = replacement->id; step.coachNote = replacement->primaryCue; setCount_ = replacement->alternatingSides ? baseSets * 2 : baseSets; currentSet_ = replacement->alternatingSides ? (logicalSet - 1) * 2 + 1 : logicalSet; phaseBeforePause_ = SessionPhase::idle; beginPreparation(stepIndex_, false); return true; } void WorkoutSessionRunner::skipCountdown() { if (phase_ == SessionPhase::preparation) { beginExercise(); } else if (phase_ == SessionPhase::rest) { totalRestSeconds_ += phaseElapsedSeconds_; advanceAfterRest(); } } bool WorkoutSessionRunner::continueAfterQuickCheckpoint() { if (!plan_ || phase_ != SessionPhase::quickDecision) return false; quickCheckpointStepIndex_ = -1; plan_->variantId = QStringLiteral("quick-upgraded"); beginPreparation(stepIndex_); return true; } bool WorkoutSessionRunner::finishQuickAtCheckpoint() { if (!plan_ || phase_ != SessionPhase::quickDecision) return false; int cooldownIndex = stepIndex_; while (cooldownIndex < plan_->steps.size() && !plan_->steps.at(cooldownIndex).isCooldown) { ++cooldownIndex; } if (cooldownIndex < plan_->steps.size()) { const WorkoutStep cooldown = plan_->steps.at(cooldownIndex); plan_->steps.erase(plan_->steps.begin() + stepIndex_, plan_->steps.end()); plan_->steps.append(cooldown); } else { plan_->steps.erase(plan_->steps.begin() + stepIndex_, plan_->steps.end()); } quickCheckpointStepIndex_ = -1; moveToStep(stepIndex_); return true; } SessionSnapshot WorkoutSessionRunner::snapshot() const { return { phase_, phaseBeforePause_, stepIndex_, phaseDurationSeconds_, phaseElapsedSeconds_, isTimerPhase() || phase_ == SessionPhase::repetitionExercise ? std::max(0, phaseDurationSeconds_ - phaseElapsedSeconds_) : 0, currentValue_, static_cast<int>(completedSteps_.size()), currentSet_, setCount_ }; } const WorkoutPlan *WorkoutSessionRunner::activePlan() const { return plan_ ? &*plan_ : nullptr; } std::optional<SessionDraft> WorkoutSessionRunner::createDraft() const { if (!plan_ || phase_ == SessionPhase::idle || phase_ == SessionPhase::completed) { return std::nullopt; } return SessionDraft{ *plan_, stepIndex_, currentSet_, setCount_, phase_, phaseBeforePause_, phaseDurationSeconds_, phaseElapsedSeconds_, currentValue_, totalRestSeconds_, currentLoadNote_, completedSteps_, currentExternalLoadKg_, currentEffortRating_, currentDiscomfortRating_, quickCheckpointStepIndex_ }; } bool WorkoutSessionRunner::restore(const SessionDraft &draft, QString *error) { if (!validatePlan(draft.plan, error)) { return false; } const bool activePhase = draft.phase == SessionPhase::preparation || draft.phase == SessionPhase::repetitionExercise || draft.phase == SessionPhase::timedExercise || draft.phase == SessionPhase::rest || draft.phase == SessionPhase::quickDecision; const bool validPausedPhase = draft.phase == SessionPhase::paused && (draft.phaseBeforePause == SessionPhase::preparation || draft.phaseBeforePause == SessionPhase::repetitionExercise || draft.phaseBeforePause == SessionPhase::timedExercise || draft.phaseBeforePause == SessionPhase::rest); if (!activePhase && !validPausedPhase) { if (error) { *error = QStringLiteral("Draft has no restorable active phase."); } return false; } if (draft.stepIndex < 0 || draft.stepIndex >= draft.plan.steps.size()) { if (error) { *error = QStringLiteral("Draft step index is outside the plan."); } return false; } if (draft.phase == SessionPhase::quickDecision && draft.quickCheckpointStepIndex != draft.stepIndex) { if (error) *error = QStringLiteral("Draft contains an invalid quick checkpoint."); return false; } const bool repetitionPhase = draft.phase == SessionPhase::repetitionExercise || (draft.phase == SessionPhase::paused && draft.phaseBeforePause == SessionPhase::repetitionExercise); int maxCompletedStepsAtDraftPoint = 0; for (int index = 0; index < draft.stepIndex; ++index) { const WorkoutStep &step = draft.plan.steps.at(index); const auto it = exercises_.constFind(step.exerciseId); const int baseSets = std::max(1, step.sets); maxCompletedStepsAtDraftPoint += (it != exercises_.constEnd() && it->alternatingSides) ? baseSets * 2 : baseSets; } maxCompletedStepsAtDraftPoint += std::max(0, draft.currentSet - 1); if (draft.phaseDurationSeconds < 0 || draft.phaseElapsedSeconds < 0 || (!repetitionPhase && draft.phaseElapsedSeconds > draft.phaseDurationSeconds) || draft.currentValue < 0 || draft.totalRestSeconds < 0 || draft.currentSet < 1 || draft.setCount < 1 || draft.currentSet > draft.setCount || draft.completedSteps.size() > maxCompletedStepsAtDraftPoint) { if (error) { *error = QStringLiteral("Draft contains invalid counters."); } return false; } abort(); plan_ = draft.plan; stepIndex_ = draft.stepIndex; currentSet_ = draft.currentSet; setCount_ = draft.setCount; phase_ = draft.phase; phaseBeforePause_ = draft.phaseBeforePause; phaseDurationSeconds_ = draft.phaseDurationSeconds; phaseElapsedSeconds_ = draft.phaseElapsedSeconds; currentValue_ = draft.currentValue; totalRestSeconds_ = draft.totalRestSeconds; currentLoadNote_ = draft.currentLoadNote.trimmed(); currentExternalLoadKg_ = std::clamp(draft.currentExternalLoadKg, 0.0, 500.0); currentEffortRating_ = std::clamp(draft.currentEffortRating, 0, 10); currentDiscomfortRating_ = std::clamp(draft.currentDiscomfortRating, 0, 10); completedSteps_ = draft.completedSteps; quickCheckpointStepIndex_ = draft.quickCheckpointStepIndex; return true; } std::optional<FinishedSession> WorkoutSessionRunner::takeFinishedSession() { std::optional<FinishedSession> result = std::move(finishedSession_); finishedSession_.reset(); return result; } bool WorkoutSessionRunner::isTimerPhase() const { return phase_ == SessionPhase::preparation || phase_ == SessionPhase::timedExercise || phase_ == SessionPhase::rest; } const ExerciseDefinition &WorkoutSessionRunner::exerciseAt(int stepIndex) const { const QString &id = plan_->steps.at(stepIndex).exerciseId; auto it = exercises_.constFind(id); if (it != exercises_.constEnd()) return *it; static const ExerciseDefinition empty; return empty; } int WorkoutSessionRunner::targetAt(int stepIndex) const { const WorkoutStep &step = plan_->steps.at(stepIndex); return step.targetOverride.value_or(exerciseAt(stepIndex).defaultTarget); } int WorkoutSessionRunner::restAt(int stepIndex) const { const WorkoutStep &step = plan_->steps.at(stepIndex); return step.restSecondsOverride.value_or(exerciseAt(stepIndex).defaultRestSeconds); } void WorkoutSessionRunner::moveToStep(int stepIndex) { stepIndex_ = stepIndex; if (!plan_) { phase_ = SessionPhase::idle; } else if (stepIndex_ >= plan_->steps.size()) { finish(); } else { beginPreparation(stepIndex_); } } void WorkoutSessionRunner::beginPreparation(int stepIndex, bool resetSets) { stepIndex_ = stepIndex; if (resetSets) { initializeSetCounters(stepIndex); } phase_ = SessionPhase::preparation; phaseDurationSeconds_ = defaultPreparationSeconds; phaseElapsedSeconds_ = 0; currentValue_ = exerciseAt(stepIndex_).metric == ExerciseMetric::repetitions ? targetAt(stepIndex_) : 0; currentLoadNote_.clear(); currentExternalLoadKg_ = 0.0; currentEffortRating_ = 0; currentDiscomfortRating_ = 0; } void WorkoutSessionRunner::initializeSetCounters(int stepIndex) { const int baseSets = std::max(1, plan_->steps.at(stepIndex).sets); const auto exercise = exercises_.constFind(plan_->steps.at(stepIndex).exerciseId); setCount_ = exercise != exercises_.constEnd() && exercise->alternatingSides ? baseSets * 2 : baseSets; currentSet_ = 1; } void WorkoutSessionRunner::beginExercise() { const ExerciseDefinition &exercise = exerciseAt(stepIndex_); phaseElapsedSeconds_ = 0; if (exercise.metric == ExerciseMetric::seconds) { phase_ = SessionPhase::timedExercise; phaseDurationSeconds_ = targetAt(stepIndex_); currentValue_ = 0; } else { phase_ = SessionPhase::repetitionExercise; phaseDurationSeconds_ = std::max(1, targetAt(stepIndex_)) * defaultSecondsPerRepetition; currentValue_ = 0; } } void WorkoutSessionRunner::finishTimedExercise() { const ExerciseDefinition &exercise = exerciseAt(stepIndex_); const int target = targetAt(stepIndex_); completedSteps_.append({ exercise.id, exercise.name, exercise.metric, target, target, target, restAt(stepIndex_), true, currentLoadNote_, currentExternalLoadKg_, currentEffortRating_, currentDiscomfortRating_ }); beginRestOrFinish(); } void WorkoutSessionRunner::beginRestOrFinish() { const int rest = restAfterCurrentSet(); if (currentSet_ < setCount_) { ++currentSet_; phase_ = SessionPhase::rest; phaseDurationSeconds_ = rest > 0 ? rest : 0; phaseElapsedSeconds_ = 0; if (rest <= 0) { beginPreparation(stepIndex_, false); } return; } ++stepIndex_; if (enterQuickCheckpointIfNeeded()) { return; } if (stepIndex_ >= plan_->steps.size() || rest <= 0) { moveToStep(stepIndex_); return; } initializeSetCounters(stepIndex_); phase_ = SessionPhase::rest; phaseDurationSeconds_ = rest; phaseElapsedSeconds_ = 0; } bool WorkoutSessionRunner::enterQuickCheckpointIfNeeded() { if (!plan_ || quickCheckpointStepIndex_ < 0 || stepIndex_ != quickCheckpointStepIndex_) { return false; } phase_ = SessionPhase::quickDecision; phaseBeforePause_ = SessionPhase::idle; phaseDurationSeconds_ = 0; phaseElapsedSeconds_ = 0; currentValue_ = 0; return true; } void WorkoutSessionRunner::advanceAfterRest() { if (currentSet_ <= setCount_ && stepIndex_ < plan_->steps.size()) { beginPreparation(stepIndex_, false); } else { moveToStep(stepIndex_); } } void WorkoutSessionRunner::finish() { phase_ = SessionPhase::completed; FinishedSession session; session.planId = plan_->id; session.planName = plan_->name; session.sessionMode = plan_->variantId == QStringLiteral("quick-start") ? QStringLiteral("quick") : QStringLiteral("full"); session.totalRestSeconds = totalRestSeconds_; session.steps = completedSteps_; int expectedCompletedSteps = 0; for (const WorkoutStep &step : plan_->steps) { const auto it = exercises_.constFind(step.exerciseId); const int baseSets = std::max(1, step.sets); expectedCompletedSteps += (it != exercises_.constEnd() && it->alternatingSides) ? baseSets * 2 : baseSets; } session.expectedStepCount = expectedCompletedSteps; session.completedAll = completedSteps_.size() == expectedCompletedSteps && std::all_of(completedSteps_.cbegin(), completedSteps_.cend(), [](const CompletedExercise &step) { return step.completed; }); for (const CompletedExercise &step : completedSteps_) { session.totalWorkSeconds += step.workSeconds; } finishedSession_ = std::move(session); } bool WorkoutSessionRunner::validatePlan(const WorkoutPlan &plan, QString *error) const { if (plan.steps.isEmpty()) { if (error) { *error = QStringLiteral("Workout plan has no steps."); } return false; } for (const WorkoutStep &step : plan.steps) { if (!exercises_.contains(step.exerciseId)) { if (error) { *error = QStringLiteral("Unknown exercise: %1").arg(step.exerciseId); } return false; } if ((step.targetOverride.has_value() && *step.targetOverride <= 0) || (step.restSecondsOverride.has_value() && *step.restSecondsOverride < 0)) { if (error) { *error = QStringLiteral("Exercise '%1' has invalid target or rest.").arg(step.exerciseId); } return false; } } return true; } QVector<WorkoutStep> WorkoutSessionRunner::composeWorkoutSteps(const WorkoutPlan &plan) const { QVector<WorkoutStep> allSteps; if (plan.warmupIncluded && plan.warmupStepCount > 0) { const QStringList ids = warmupIdsForPlan(plan); for (int i = 0; i < plan.warmupStepCount && i < ids.size(); ++i) { if (exercises_.contains(ids[i])) { allSteps.append({ids[i], {}, {}, {}, 1, true, false}); } } } const int rounds = std::clamp(plan.roundCount, 1, 6); for (int round = 0; round < rounds; ++round) { for (const WorkoutStep &step : plan.steps) { if (step.isWarmup || step.isCooldown) continue; allSteps.append(step); } } if (plan.cooldownIncluded && plan.cooldownStepCount > 0) { const QStringList ids = cooldownIdsForPlan(plan); for (int i = 0; i < plan.cooldownStepCount && i < ids.size(); ++i) { if (exercises_.contains(ids[i])) { allSteps.append({ids[i], {}, {}, {}, 1, false, true}); } } } return allSteps; } QStringList WorkoutSessionRunner::warmupIdsForPlan(const WorkoutPlan &plan) const { QStringList ids; auto add = [&ids, this](const QString &id) { if (!ids.contains(id) && exercises_.contains(id)) ids.append(id); }; for (const WorkoutStep &step : plan.steps) { const auto it = exercises_.constFind(step.exerciseId); if (it == exercises_.constEnd()) continue; const ExerciseDefinition &exercise = *it; const QString category = exercise.category + QLatin1Char(' ') + exercise.name; if (containsAny(category, {"цигун"})) { add(QStringLiteral("warmup_breath_reset")); add(QStringLiteral("warmup_shoulder_circle")); add(QStringLiteral("warmup_march_place")); } if (containsAny(category, {"вин-чун"})) { add(QStringLiteral("warmup_wrist_roll")); add(QStringLiteral("warmup_arm_swing")); add(QStringLiteral("warmup_march_place")); } if (containsAny(category, {"плеч", "дельт", "груд", "трицеп", "спин", "широч", "бицеп"})) { add(QStringLiteral("warmup_arm_swing")); add(QStringLiteral("warmup_shoulder_circle")); add(QStringLiteral("warmup_scap_pushup")); } if (containsAny(category, {"кист", "хват", "предплеч"})) { add(QStringLiteral("warmup_wrist_roll")); } if (containsAny(category, {"ног", "ягод", "квадриц", "икр", "бедр", "таз", "задняя цеп"})) { add(QStringLiteral("warmup_march_place")); add(QStringLiteral("warmup_squat_to_stand")); add(QStringLiteral("warmup_hip_hinge_reach")); } if (containsAny(category, {"кор", "живот", "косые", "стабилиз"})) { add(QStringLiteral("warmup_breath_reset")); add(QStringLiteral("warmup_march_place")); } } add(QStringLiteral("warmup_march_place")); add(QStringLiteral("warmup_arm_swing")); add(QStringLiteral("warmup_breath_reset")); return ids; } QStringList WorkoutSessionRunner::cooldownIdsForPlan(const WorkoutPlan &plan) const { QStringList ids; auto add = [&ids, this](const QString &id) { if (!ids.contains(id) && exercises_.contains(id)) ids.append(id); }; for (const WorkoutStep &step : plan.steps) { const auto it = exercises_.constFind(step.exerciseId); if (it == exercises_.constEnd()) continue; const QString category = it->category + QLatin1Char(' ') + it->name; if (containsAny(category, {"цигун"})) { add(QStringLiteral("warmup_breath_reset")); add(QStringLiteral("thoracic_rotation")); } if (containsAny(category, {"вин-чун"})) { add(QStringLiteral("thoracic_rotation")); add(QStringLiteral("warmup_breath_reset")); } if (containsAny(category, {"ног", "ягод", "квадриц", "икр", "бедр", "таз", "задняя цеп"})) { add(QStringLiteral("switch_90_90")); add(QStringLiteral("hip_flexor_stretch")); add(QStringLiteral("world_greatest_stretch")); } if (containsAny(category, {"плеч", "дельт", "груд", "спин", "широч"})) { add(QStringLiteral("thoracic_rotation")); add(QStringLiteral("world_greatest_stretch")); } if (containsAny(category, {"кор", "живот", "косые", "стабилиз"})) { add(QStringLiteral("thoracic_rotation")); add(QStringLiteral("switch_90_90")); } } add(QStringLiteral("world_greatest_stretch")); add(QStringLiteral("thoracic_rotation")); add(QStringLiteral("switch_90_90")); return ids; } bool WorkoutSessionRunner::currentExerciseAlternatesSides() const { if (!plan_ || stepIndex_ < 0 || stepIndex_ >= plan_->steps.size()) return false; return exerciseAt(stepIndex_).alternatingSides; } int WorkoutSessionRunner::restAfterCurrentSet() const { if (currentExerciseAlternatesSides() && currentSet_ % 2 == 1 && currentSet_ < setCount_) { return sideSwitchSeconds; } return restAt(stepIndex_); } } // namespace bodyweight