/
DemienMedich
/
BodyweightBase
Обзор
Документация
Войти
/
DemienMedich
/
BodyweightBase
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
develop
app/MobileMain.qml
2 068 строк
133 KB
DemienMedich
feat: add profile weekly goals
03 авг 2026, 01:15
03 авг 2026, 01:15
a8f4bdb
Код
Авторство
О чём код?
import QtQuick import QtQuick.Controls import QtQuick.Layouts ApplicationWindow { id: app width: 412 height: 915 minimumWidth: 320 minimumHeight: 560 visible: true title: "BodyweightBase" color: bg property int currentTab: 0 property int workoutFrameIndex: 0 property int workoutFrameDirection: 1 property string workoutFrameKey: "" property bool draftDialogDismissed: false property string wearableChartMetric: "steps" property bool cycleWeeksExpanded: false property int baseMode: 0 property int recoveryEnergy: 3 property int recoverySleep: 3 property int recoveryJoints: 3 property int workoutRoundCount: Math.max(1, Math.min(6, sessionController.selectedPlanOptions.roundCount || 1)) property bool exitApproved: false onClosing: function(close) { if (app.exitApproved) { close.accepted = true return } if (sessionController.needsSave) { close.accepted = false if (!saveDialog.opened) saveDialog.open() return } if (sessionController.active) { close.accepted = false if (!exitWorkoutDialog.opened) exitWorkoutDialog.open() return } sessionController.prepareForExit() close.accepted = true } function workoutFrameDuration(index) { const durations = sessionController.currentExerciseFrameDurationsMs if (index >= 0 && index < durations.length) return Math.max(350, Number(durations[index]) || 1100) return Math.max(350, Number(sessionController.currentExerciseFrameDurationMs) || 1100) } function nextWorkoutFrameIndex() { const count = sessionController.currentExerciseFrameUrls.length if (count < 2 || sessionController.currentExerciseFramePlayback === "static") return 0 if (sessionController.currentExerciseFramePlayback === "pingPong") { const candidate = app.workoutFrameIndex + app.workoutFrameDirection if (candidate >= count) return Math.max(0, count - 2) if (candidate < 0) return Math.min(count - 1, 1) return candidate } return (app.workoutFrameIndex + 1) % count } function advanceWorkoutFrame() { const count = sessionController.currentExerciseFrameUrls.length if (count < 2 || sessionController.currentExerciseFramePlayback === "static") return if (sessionController.currentExerciseFramePlayback === "pingPong") { let candidate = app.workoutFrameIndex + app.workoutFrameDirection if (candidate >= count) { app.workoutFrameDirection = -1 candidate = Math.max(0, count - 2) } else if (candidate < 0) { app.workoutFrameDirection = 1 candidate = Math.min(count - 1, 1) } app.workoutFrameIndex = candidate return } app.workoutFrameIndex = (app.workoutFrameIndex + 1) % count } readonly property color bg: "#08111D" readonly property color panel: "#101C2B" readonly property color surface: "#0C1724" readonly property color selectedSurface: "#19334A" readonly property color border: "#26384D" readonly property color textMain: "#F1F5F9" readonly property color textMuted: "#9FB0C3" readonly property color blue: "#38BDF8" readonly property color accent: "#22C55E" readonly property color warning: "#F59E0B" readonly property color error: "#FCA5A5" palette.window: bg palette.windowText: textMain palette.base: surface palette.alternateBase: panel palette.text: textMain palette.button: surface palette.buttonText: textMain palette.highlight: selectedSurface palette.highlightedText: textMain palette.placeholderText: textMuted palette.mid: border function recentWearableDays() { const days = sessionController.wearableDays return days.length > 10 ? days.slice(days.length - 10) : days } function wearableMaximum(key) { const days = recentWearableDays() let maximum = 1 for (let index = 0; index < days.length; ++index) maximum = Math.max(maximum, Number(days[index][key] || 0)) return maximum } function wearableMetricTitle() { if (wearableChartMetric === "activeCaloriesKcal") return "Активные калории" if (wearableChartMetric === "sleepMinutes") return "Сон" if (wearableChartMetric === "restingHeartRateAvg") return "Пульс покоя" if (wearableChartMetric === "heartRateAvg") return "Средний пульс" return "Шаги" } function wearableMetricColor() { if (wearableChartMetric === "activeCaloriesKcal") return warning if (wearableChartMetric === "sleepMinutes") return "#A78BFA" if (wearableChartMetric === "restingHeartRateAvg") return "#F97316" if (wearableChartMetric === "heartRateAvg") return "#FB7185" return blue } function wearableMetricValue(day) { return Number(day[wearableChartMetric] || 0) } function wearableMetricText(value) { if (!value) return "—" if (wearableChartMetric === "activeCaloriesKcal") return Math.round(value) + " ккал" if (wearableChartMetric === "sleepMinutes") return Math.floor(value / 60) + "ч " + Math.round(value % 60) + "м" if (wearableChartMetric === "heartRateAvg" || wearableChartMetric === "restingHeartRateAvg") return Math.round(value) + " уд/мин" return Math.round(value).toLocaleString(Qt.locale("ru_RU"), "f", 0) } function visibleCycleWeeks() { const weeks = sessionController.trainingCycleWeeks if (cycleWeeksExpanded || weeks.length <= 6) return weeks const current = Math.max(0, Number(sessionController.trainingCycle.currentWeek || 1) - 1) const start = Math.max(0, Math.min(current - 1, weeks.length - 6)) return weeks.slice(start, start + 6) } function progressMonthTitle() { const months = ["Январь", "Февраль", "Март", "Апрель", "Май", "Июнь", "Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь"] const now = new Date() return months[now.getMonth()] + " " + now.getFullYear() } function progressCalendarCells() { const days = sessionController.workoutCalendar const now = new Date() const mondayOffset = (new Date(now.getFullYear(), now.getMonth(), 1).getDay() + 6) % 7 let cells = [] for (let index = 0; index < mondayOffset; ++index) cells.push({ empty: true }) for (let day = 0; day < days.length; ++day) cells.push(days[day]) while (cells.length % 7 !== 0) cells.push({ empty: true }) return cells } function timeLabels(count) { let values = [] for (let value = 0; value < count; ++value) values.push(value < 10 ? "0" + value : String(value)) return values } component SurfaceCard: Rectangle { color: app.panel radius: 18 border.color: app.border } component WorkoutStatusChip: Rectangle { required property string value property color chipColor: app.blue visible: value.length > 0 implicitWidth: Math.min(app.width - 24, chipLabel.implicitWidth + 20) implicitHeight: 32 radius: 11 color: app.surface border.width: 1 border.color: chipColor Label { id: chipLabel anchors.fill: parent anchors.leftMargin: 10 anchors.rightMargin: 10 text: parent.value color: parent.chipColor font.pixelSize: 11 font.bold: true horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter elide: Text.ElideRight } } component PrimaryButton: Button { id: primary implicitHeight: 54 font.pixelSize: 16 font.bold: true scale: down ? 0.96 : 1 Behavior on scale { NumberAnimation { duration: 140; easing.type: Easing.OutCubic } } background: Rectangle { radius: 14 color: primary.down ? "#16803D" : app.accent opacity: primary.enabled ? 1 : 0.45 } contentItem: Text { text: primary.text color: "#04120A" font: primary.font horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } } component DangerButton: Button { id: danger implicitHeight: 48 font.pixelSize: 14 font.bold: true scale: down ? 0.96 : 1 Behavior on scale { NumberAnimation { duration: 140; easing.type: Easing.OutCubic } } background: Rectangle { radius: 14 color: danger.down ? "#991B1B" : "#DC2626" } contentItem: Text { text: danger.text color: app.textMain font: danger.font horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } } component MobileNavButton: Button { id: nav required property int tabIndex property string navLabel: "" property string mark: "" Layout.fillWidth: true implicitHeight: 58 onClicked: app.currentTab = tabIndex background: Rectangle { radius: 14; color: app.currentTab === nav.tabIndex ? app.selectedSurface : "transparent" } contentItem: Column { spacing: 2 Text { anchors.horizontalCenter: parent.horizontalCenter; text: nav.mark; color: app.currentTab === nav.tabIndex ? app.blue : app.textMuted; font.pixelSize: 18; font.bold: true } Text { anchors.horizontalCenter: parent.horizontalCenter; text: nav.navLabel; color: app.currentTab === nav.tabIndex ? app.textMain : app.textMuted; font.pixelSize: 9 } } } Timer { interval: 1000 repeat: true running: sessionController.active && !sessionController.paused onTriggered: sessionController.advanceOneSecond() } Timer { interval: app.workoutFrameDuration(app.workoutFrameIndex) repeat: true running: sessionController.active && !sessionController.paused && sessionController.currentExerciseFrameUrls.length > 1 && sessionController.currentExerciseFramePlayback !== "static" onTriggered: app.advanceWorkoutFrame() } Image { width: 1 height: 1 x: -2 y: -2 z: -1 opacity: 0 asynchronous: true cache: true source: sessionController.active && sessionController.currentExerciseFrameUrls.length > 1 && sessionController.currentExerciseFramePlayback !== "static" ? sessionController.currentExerciseFrameUrls[app.nextWorkoutFrameIndex()] : "" } Connections { target: sessionController function onChanged() { const nextFrameKey = sessionController.currentStepIndex + ":" + (sessionController.currentExerciseFrameUrls.length > 0 ? sessionController.currentExerciseFrameUrls[0] : "") if (app.workoutFrameKey !== nextFrameKey) { app.workoutFrameKey = nextFrameKey app.workoutFrameIndex = 0 app.workoutFrameDirection = 1 } if (app.workoutFrameIndex >= sessionController.currentExerciseFrameUrls.length) { app.workoutFrameIndex = 0 app.workoutFrameDirection = 1 } if (sessionController.needsSave && !saveDialog.opened) saveDialog.open() if (sessionController.quickDecisionPending && !quickContinueDialog.opened) quickContinueDialog.open() if (sessionController.hasRecoverableDraft && !sessionController.active && !app.draftDialogDismissed && !draftDialog.opened && !saveDialog.opened) draftDialog.open() } } Dialog { id: quickContinueDialog anchors.centerIn: parent width: Math.min(app.width - 32, 420) modal: true closePolicy: Popup.NoAutoClose padding: 18 background: Rectangle { color: app.panel radius: 20 border.width: 1 border.color: app.blue } ColumnLayout { width: parent.width spacing: 12 Label { Layout.fillWidth: true text: "Пять минут сделаны" color: app.textMain font.pixelSize: 20 font.bold: true wrapMode: Text.WordWrap } Label { Layout.fillWidth: true text: "Самое противное уже позади. Можно закончить короткую тренировку с заминкой или использовать разгон и пройти весь план." color: app.textMuted font.pixelSize: 12 wrapMode: Text.WordWrap } SurfaceCard { Layout.fillWidth: true implicitHeight: 64 color: app.selectedSurface ColumnLayout { anchors.fill: parent anchors.margins: 11 spacing: 2 Label { text: "Уже засчитано"; color: app.blue; font.pixelSize: 10; font.bold: true } Label { Layout.fillWidth: true text: sessionController.completedStepText color: app.textMain font.pixelSize: 13 font.bold: true wrapMode: Text.WordWrap } } } PrimaryButton { Layout.fillWidth: true implicitHeight: 54 text: "Раз уж начал — продолжить" onClicked: { quickContinueDialog.close() sessionController.continueQuickWorkout() } } Button { Layout.fillWidth: true implicitHeight: 50 scale: down ? 0.96 : 1 Behavior on scale { NumberAnimation { duration: 110; easing.type: Easing.OutCubic } } text: "Хватит на сегодня • перейти к заминке" onClicked: { quickContinueDialog.close() sessionController.finishQuickWorkout() } } } } Dialog { id: saveDialog anchors.centerIn: parent width: Math.min(app.width - 32, 420) modal: true closePolicy: Popup.NoAutoClose title: "Тренировка завершена" property var details: sessionController.pendingResultDetails() ColumnLayout { width: parent.width spacing: 10 SurfaceCard { Layout.fillWidth: true; implicitHeight: 82 ColumnLayout { anchors.fill: parent; anchors.margins: 12; spacing: 3 Label { Layout.fillWidth: true; text: saveDialog.details.resultTitle || "Тренировка завершена"; color: saveDialog.details.resultStatus === "complete" ? app.accent : app.warning; font.pixelSize: 16; font.bold: true; wrapMode: Text.WordWrap } Label { Layout.fillWidth: true; text: saveDialog.details.planName || ""; color: app.textMuted; font.pixelSize: 11; elide: Text.ElideRight } } } RowLayout { Layout.fillWidth: true; spacing: 8 SurfaceCard { Layout.fillWidth: true; implicitHeight: 64 ColumnLayout { anchors.fill: parent; anchors.margins: 10; spacing: 1 Label { text: "Итого"; color: app.textMuted; font.pixelSize: 9 } Label { text: (saveDialog.details.totalMinutes || 0) + " мин"; color: app.textMain; font.pixelSize: 15; font.bold: true } } } SurfaceCard { Layout.fillWidth: true; implicitHeight: 64 ColumnLayout { anchors.fill: parent; anchors.margins: 10; spacing: 1 Label { text: "Закрыто"; color: app.textMuted; font.pixelSize: 9 } Label { text: (saveDialog.details.completionRate || 0) + "%"; color: saveDialog.details.resultStatus === "complete" ? app.accent : app.warning; font.pixelSize: 15; font.bold: true } } } } Label { Layout.fillWidth: true; text: (saveDialog.details.completedText || "") + " • " + (saveDialog.details.workRestText || ""); color: app.textMain; font.pixelSize: 12; font.bold: true; wrapMode: Text.WordWrap } Label { Layout.fillWidth: true; text: saveDialog.details.volumeText || ""; color: app.textMuted; font.pixelSize: 11; wrapMode: Text.WordWrap } SurfaceCard { Layout.fillWidth: true; implicitHeight: 96 ColumnLayout { anchors.fill: parent; anchors.margins: 12; spacing: 3 Label { Layout.fillWidth: true; text: saveDialog.details.recommendationTitle || "Рекомендация"; color: app.textMain; font.pixelSize: 12; font.bold: true } Label { Layout.fillWidth: true; text: saveDialog.details.recommendationText || ""; color: app.textMuted; font.pixelSize: 11; wrapMode: Text.WordWrap } } } ComboBox { id: feedback; Layout.fillWidth: true; model: ["Нормально", "Легко", "Тяжело"] } Label { Layout.fillWidth: true; text: sessionController.adaptationPreview(feedback.currentText); color: app.blue; font.pixelSize: 11; wrapMode: Text.WordWrap } Button { Layout.fillWidth: true visible: sessionController.lastCompletedSetLog.available === true text: sessionController.lastCompletedSetLog.summaryText || "Оценить последний подход" onClicked: { loadLogDialog.editCompleted = true; loadLogDialog.open() } } PrimaryButton { Layout.fillWidth: true; text: "Сохранить"; onClicked: { sessionController.saveFinishedSession(feedback.currentText, true); saveDialog.close() } } Button { Layout.fillWidth: true; text: "Не сохранять"; onClicked: { saveDialog.close(); discardResultDialog.open() } } } onOpened: details = sessionController.pendingResultDetails() } Dialog { id: loadLogDialog property bool editCompleted: false anchors.centerIn: parent width: Math.min(app.width - 32, 420) modal: true title: editCompleted ? "Итог подхода" : "Нагрузка подхода" ColumnLayout { width: parent.width spacing: 10 Label { Layout.fillWidth: true; text: loadLogDialog.editCompleted ? sessionController.lastCompletedSetLog.exerciseName : sessionController.exerciseName; color: app.textMain; font.pixelSize: 14; font.bold: true; wrapMode: Text.WordWrap } Label { Layout.fillWidth: true; text: "Вес снаряда или суммарная внешняя нагрузка"; color: app.textMuted; font.pixelSize: 10; wrapMode: Text.WordWrap } TextField { id: setLoadKg Layout.fillWidth: true implicitHeight: 48 placeholderText: "Вес, кг (необязательно)" inputMethodHints: Qt.ImhFormattedNumbersOnly } RowLayout { Layout.fillWidth: true; spacing: 12 ColumnLayout { Layout.fillWidth: true; spacing: 3 Label { text: "Усилие RPE"; color: app.textMuted; font.pixelSize: 10 } SpinBox { id: setEffort; Layout.fillWidth: true; from: 0; to: 10; value: 0; editable: false } } ColumnLayout { Layout.fillWidth: true; spacing: 3 Label { text: "Дискомфорт"; color: app.textMuted; font.pixelSize: 10 } SpinBox { id: setDiscomfort; Layout.fillWidth: true; from: 0; to: 10; value: 0; editable: false } } } TextField { id: setLoadNote; Layout.fillWidth: true; implicitHeight: 48; placeholderText: "Темп, техника, самочувствие"; maximumLength: 160 } Label { Layout.fillWidth: true; text: "0 означает «не указывать». Боль — не показатель доблести, как бы фитнес-блоги ни старались."; color: app.textMuted; font.pixelSize: 9; wrapMode: Text.WordWrap } PrimaryButton { Layout.fillWidth: true text: "Сохранить для подхода" onClicked: { let saved = false if (loadLogDialog.editCompleted) saved = sessionController.updateLastCompletedSetLog(setLoadKg.text, setEffort.value, setDiscomfort.value, setLoadNote.text) else saved = sessionController.updateCurrentSetLog(setLoadKg.text, setEffort.value, setDiscomfort.value, setLoadNote.text) if (saved) loadLogDialog.close() } } Button { Layout.fillWidth: true; text: "Отмена"; onClicked: loadLogDialog.close() } } onOpened: { const entry = loadLogDialog.editCompleted ? sessionController.lastCompletedSetLog : sessionController.currentSetLog setLoadKg.text = entry.loadKgText || "" setEffort.value = entry.effortRating || 0 setDiscomfort.value = entry.discomfortRating || 0 setLoadNote.text = entry.note || "" } } Dialog { id: replacementDialog anchors.centerIn: parent width: Math.min(app.width - 32, 420) modal: true title: "Заменить упражнение" ColumnLayout { width: parent.width; spacing: 12 Label { Layout.fillWidth: true text: "Цель и отдых сохранятся. Незавершённый текущий подход будет сброшен." color: app.textMuted; font.pixelSize: 11; wrapMode: Text.WordWrap } ComboBox { id: replacementBox Layout.fillWidth: true; implicitHeight: 48 model: sessionController.workoutReplacementExercises textRole: "name"; valueRole: "id" } Label { Layout.fillWidth: true text: replacementBox.currentIndex >= 0 ? ((replacementBox.model[replacementBox.currentIndex].category || "") + " • " + (replacementBox.model[replacementBox.currentIndex].equipment || "")) : "" color: app.blue; font.pixelSize: 10; elide: Text.ElideRight } PrimaryButton { Layout.fillWidth: true; implicitHeight: 48; text: "Заменить" enabled: replacementBox.currentIndex >= 0 onClicked: { sessionController.replaceCurrentExercise(replacementBox.currentValue) replacementDialog.close() } } Button { Layout.fillWidth: true; implicitHeight: 46; text: "Оставить текущее"; onClicked: replacementDialog.close() } } onOpened: replacementBox.currentIndex = 0 } Dialog { id: finishWorkoutDialog anchors.centerIn: parent width: Math.min(app.width - 32, 420) modal: true title: "Закончить тренировку?" ColumnLayout { width: parent.width spacing: 12 Label { Layout.fillWidth: true text: "Закрытые подходы останутся в результате, а тренировка будет отмечена как частичная." color: app.textMuted wrapMode: Text.WordWrap } PrimaryButton { Layout.fillWidth: true text: "Закончить и перейти к результату" onClicked: { finishWorkoutDialog.close() sessionController.finishActiveWorkout() } } Button { Layout.fillWidth: true implicitHeight: 46 text: "Продолжить тренировку" onClicked: finishWorkoutDialog.close() } } } Dialog { id: exitWorkoutDialog anchors.centerIn: parent width: Math.min(app.width - 32, 420) modal: true closePolicy: Popup.NoAutoClose title: "Выйти из тренировки?" ColumnLayout { width: parent.width spacing: 12 Label { Layout.fillWidth: true text: "Текущий этап и выполненные подходы будут сохранены в черновик." color: app.textMuted wrapMode: Text.WordWrap } PrimaryButton { Layout.fillWidth: true text: "Сохранить черновик и выйти" onClicked: { exitWorkoutDialog.close() sessionController.prepareForExit() app.exitApproved = true app.close() } } Button { Layout.fillWidth: true implicitHeight: 46 text: "Остаться в тренировке" onClicked: exitWorkoutDialog.close() } } } Dialog { id: draftDialog anchors.centerIn: parent width: Math.min(app.width - 32, 420) modal: true closePolicy: Popup.NoAutoClose title: "Незавершённая тренировка" ColumnLayout { width: parent.width spacing: 12 Label { Layout.fillWidth: true; text: sessionController.recoverableDraftSummary; color: app.textMain; wrapMode: Text.WordWrap } Label { Layout.fillWidth: true; text: "Черновик блокирует другую тренировку, пока вы не продолжите или не удалите его."; color: app.textMuted; font.pixelSize: 11; wrapMode: Text.WordWrap } PrimaryButton { Layout.fillWidth: true; text: "Продолжить"; onClicked: { sessionController.restoreDraft(); draftDialog.close() } } Button { Layout.fillWidth: true; implicitHeight: 46; text: "Оставить на потом"; onClicked: { app.draftDialogDismissed = true; draftDialog.close() } } Button { Layout.fillWidth: true; implicitHeight: 46; text: "Удалить черновик"; onClicked: { draftDialog.close(); discardDraftDialog.open() } } } } Dialog { id: discardDraftDialog anchors.centerIn: parent width: Math.min(app.width - 32, 420) modal: true closePolicy: Popup.NoAutoClose title: "Удалить черновик?" ColumnLayout { width: parent.width spacing: 12 Label { Layout.fillWidth: true; text: "Прогресс этой тренировки будет удалён без восстановления."; color: app.textMuted; wrapMode: Text.WordWrap } DangerButton { Layout.fillWidth: true; text: "Удалить без восстановления"; onClicked: { sessionController.discardDraft(); discardDraftDialog.close() } } Button { Layout.fillWidth: true; implicitHeight: 46; text: "Отмена"; onClicked: { discardDraftDialog.close(); draftDialog.open() } } } } Dialog { id: discardResultDialog anchors.centerIn: parent width: Math.min(app.width - 32, 420) modal: true closePolicy: Popup.NoAutoClose title: "Не сохранять тренировку?" ColumnLayout { width: parent.width spacing: 12 Label { Layout.fillWidth: true; text: "Завершённая тренировка не попадёт в прогресс и статистику."; color: app.textMuted; wrapMode: Text.WordWrap } DangerButton { Layout.fillWidth: true; text: "Удалить результат"; onClicked: { sessionController.discardFinishedSession(); discardResultDialog.close() } } Button { Layout.fillWidth: true; implicitHeight: 46; text: "Вернуться к сохранению"; onClicked: { discardResultDialog.close(); saveDialog.open() } } } } Timer { interval: 350 running: true repeat: false onTriggered: { if (sessionController.hasRecoverableDraft && !sessionController.active) draftDialog.open() } } ColumnLayout { anchors.fill: parent anchors.leftMargin: 12 anchors.rightMargin: 12 anchors.bottomMargin: 12 // Android draws Qt content edge-to-edge on recent HyperOS builds. // Keep the profile header below the system status bar. anchors.topMargin: Qt.platform.os === "android" ? 28 : 12 spacing: 10 RowLayout { Layout.fillWidth: true Layout.preferredHeight: 54 ColumnLayout { Layout.fillWidth: true spacing: 0 Label { text: "BODYWEIGHT BASE"; color: app.blue; font.pixelSize: 11; font.bold: true } Label { text: sessionController.selectedProfileName; color: app.textMain; font.pixelSize: 20; font.bold: true } } Rectangle { Layout.preferredWidth: 10; Layout.preferredHeight: 10; radius: 5 color: sessionController.ready ? app.accent : app.warning } } StackLayout { Layout.fillWidth: true Layout.fillHeight: true currentIndex: app.currentTab // Today / active workout Flickable { clip: true contentWidth: width contentHeight: homeColumn.implicitHeight ColumnLayout { id: homeColumn width: parent.width spacing: 12 Label { text: sessionController.active ? "Тренировка" : "Сегодня"; color: app.textMuted; font.pixelSize: 13 } Label { Layout.fillWidth: true text: sessionController.active ? sessionController.exerciseName : sessionController.hasScheduledPlanToday ? sessionController.planName : "День отдыха" color: app.textMain; font.pixelSize: 27; font.bold: true; wrapMode: Text.WordWrap } Label { Layout.fillWidth: true text: sessionController.active ? sessionController.phaseTitle : sessionController.hasScheduledPlanToday ? sessionController.planGoal : "План не назначен. При желании можно выбрать другую тренировку." color: sessionController.active ? sessionController.phaseColor : app.blue font.pixelSize: 13; font.bold: sessionController.active; wrapMode: Text.WordWrap } Label { Layout.fillWidth: true visible: sessionController.active && sessionController.currentStepSummary.length > 0 text: sessionController.currentStepSummary color: app.textMuted font.pixelSize: 12 wrapMode: Text.WordWrap } Flow { Layout.fillWidth: true Layout.preferredHeight: childrenRect.height visible: sessionController.active spacing: 6 WorkoutStatusChip { value: sessionController.currentWorkoutPosition.sectionText || "" chipColor: app.accent } WorkoutStatusChip { value: sessionController.currentWorkoutPosition.roundText || "" chipColor: app.blue } WorkoutStatusChip { value: sessionController.currentWorkoutPosition.setText || "" chipColor: app.blue } WorkoutStatusChip { value: sessionController.currentWorkoutPosition.sideText || "" chipColor: sessionController.currentWorkoutPosition.sideSwitchActive ? app.warning : app.accent } } Rectangle { Layout.fillWidth: true implicitHeight: 64 visible: sessionController.active && sessionController.currentWorkoutPosition.sideSwitchActive radius: 16 color: app.panel border.width: 2 border.color: app.warning RowLayout { anchors.fill: parent anchors.leftMargin: 14 anchors.rightMargin: 14 spacing: 10 ColumnLayout { Layout.fillWidth: true spacing: 1 Label { text: "СМЕНА СТОРОНЫ"; color: app.warning; font.pixelSize: 10; font.bold: true } Label { Layout.fillWidth: true text: sessionController.currentWorkoutPosition.sideSwitchText || "Перейдите на другую сторону" color: app.textMain font.pixelSize: 14 font.bold: true elide: Text.ElideRight } } Label { text: sessionController.counterText + " с" color: app.warning font.pixelSize: 26 font.bold: true } } } SurfaceCard { Layout.fillWidth: true implicitHeight: sessionController.active ? Math.min(620, Math.max(470, app.height * 0.62)) : 112 border.color: sessionController.active ? sessionController.phaseColor : app.border Item { anchors.fill: parent; anchors.margins: 14 // Separate layouts prevent hidden active-workout controls from // reserving vertical space in the compact pre-workout card. ColumnLayout { anchors.fill: parent visible: sessionController.active spacing: 8 Rectangle { Layout.fillWidth: true; Layout.fillHeight: true radius: 12; color: app.surface border.width: 1; border.color: Qt.rgba(1, 1, 1, 0.10) clip: true Image { anchors.fill: parent; anchors.margins: 2 visible: sessionController.currentExerciseFrameUrls.length > 0 source: visible ? sessionController.currentExerciseFrameUrls[app.workoutFrameIndex] : "" fillMode: Image.PreserveAspectFit; asynchronous: true; cache: true sourceSize.width: Math.min(720, width * Screen.devicePixelRatio) sourceSize.height: Math.min(720, height * Screen.devicePixelRatio) } Label { anchors.centerIn: parent visible: sessionController.currentExerciseFrameUrls.length === 0 text: "Изображение недоступно" color: app.textMuted font.pixelSize: 12 } Rectangle { anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom anchors.bottomMargin: 14 width: Math.max(112, counterLabel.implicitWidth + 36) height: 82 radius: 18 color: Qt.rgba(0.03, 0.07, 0.12, 0.88) border.width: 2 border.color: sessionController.phaseColor Label { id: counterLabel anchors.centerIn: parent text: sessionController.counterText color: app.textMain font.pixelSize: 58 font.bold: true horizontalAlignment: Text.AlignHCenter } } } ProgressBar { id: workoutPhaseProgress Layout.fillWidth: true implicitHeight: 8 from: 0 to: 1 value: sessionController.phaseProgress background: Rectangle { radius: 4; color: app.surface } contentItem: Item { Rectangle { width: parent.width * workoutPhaseProgress.visualPosition height: parent.height radius: 4 color: sessionController.phaseColor } } } } ColumnLayout { anchors.fill: parent visible: !sessionController.active spacing: 8 Label { Layout.fillWidth: true text: sessionController.hasScheduledPlanToday ? (sessionController.preWorkoutRecommendation.headline || "План готов") : "Восстановление" color: app.textMain; font.pixelSize: 18; font.bold: true; wrapMode: Text.WordWrap } Label { Layout.fillWidth: true text: sessionController.hasScheduledPlanToday ? (sessionController.preWorkoutRecommendation.detail || "Начните в комфортном темпе") : "Выбранный запасной план: " + sessionController.planName color: app.textMuted; font.pixelSize: 12; wrapMode: Text.WordWrap } Item { Layout.fillWidth: true; Layout.fillHeight: true } } } } SurfaceCard { Layout.fillWidth: true visible: sessionController.active && ((sessionController.currentExerciseTechnique.primaryCue || "").length > 0 || (sessionController.currentExerciseTechnique.secondaryCue || "").length > 0 || sessionController.currentCoachNote.length > 0) implicitHeight: techniqueColumn.implicitHeight + 24 ColumnLayout { id: techniqueColumn anchors.fill: parent anchors.margins: 12 spacing: 4 Label { text: "ТЕХНИКА"; color: app.blue; font.pixelSize: 10; font.bold: true } Label { Layout.fillWidth: true visible: text.length > 0 text: sessionController.currentExerciseTechnique.primaryCue || "" color: app.textMain font.pixelSize: 13 font.bold: true wrapMode: Text.WordWrap } Label { Layout.fillWidth: true visible: text.length > 0 text: sessionController.currentExerciseTechnique.secondaryCue || "" color: app.textMuted font.pixelSize: 12 wrapMode: Text.WordWrap } Label { Layout.fillWidth: true visible: text.length > 0 text: sessionController.currentCoachNote color: app.warning font.pixelSize: 11 wrapMode: Text.WordWrap } } } Button { Layout.fillWidth: true visible: sessionController.active && sessionController.currentWorkoutPosition.isRest !== true implicitHeight: 48 text: sessionController.currentSetLog.summaryText || "Записать нагрузку" onClicked: { loadLogDialog.editCompleted = false; loadLogDialog.open() } } Button { Layout.fillWidth: true visible: sessionController.active && sessionController.currentWorkoutPosition.isRest === true && sessionController.lastCompletedSetLog.available === true implicitHeight: 48 text: sessionController.lastCompletedSetLog.summaryText || "Оценить завершённый подход" onClicked: { loadLogDialog.editCompleted = true; loadLogDialog.open() } } Button { Layout.fillWidth: true visible: !sessionController.active && sessionController.ready implicitHeight: 54 scale: down ? 0.96 : 1 Behavior on scale { NumberAnimation { duration: 110; easing.type: Easing.OutCubic } } onClicked: sessionController.startQuickWorkout(app.workoutRoundCount) background: Rectangle { radius: 16 color: app.surface border.width: 1 border.color: app.blue } contentItem: Column { spacing: 1 Text { width: parent.width text: "Быстрый старт • около 5 минут" color: app.blue font.pixelSize: 14 font.bold: true horizontalAlignment: Text.AlignHCenter } Text { width: parent.width text: "Разминка, короткий блок и выбор: закончить или продолжить" color: app.textMuted font.pixelSize: 9 horizontalAlignment: Text.AlignHCenter } } } PrimaryButton { Layout.fillWidth: true text: sessionController.active ? "Завершить этап" : sessionController.hasScheduledPlanToday ? "Начать тренировку" : "Начать выбранный план" enabled: sessionController.ready || sessionController.active onClicked: sessionController.active ? sessionController.completeCurrent() : sessionController.startWithRoundCount(app.workoutRoundCount) } RowLayout { Layout.fillWidth: true visible: !sessionController.active && sessionController.ready spacing: 8 Label { Layout.fillWidth: true text: sessionController.trainingContinuity.returnMode ? "Круги • возвращение: до " + sessionController.trainingContinuity.recommendedMaxRounds : "Круги тренировки" color: sessionController.trainingContinuity.returnMode ? app.warning : app.textMuted font.pixelSize: 12 } SpinBox { from: 1 to: Math.max(1, sessionController.trainingContinuity.recommendedMaxRounds || 6) value: app.workoutRoundCount editable: false onValueModified: app.workoutRoundCount = value } } RowLayout { Layout.fillWidth: true; spacing: 8; visible: sessionController.active Button { Layout.fillWidth: true; implicitHeight: 48; text: sessionController.paused ? "Продолжить" : "Пауза"; onClicked: sessionController.togglePause() } Button { Layout.fillWidth: true; implicitHeight: 48; text: "Заменить" enabled: sessionController.workoutReplacementExercises.length > 0 onClicked: replacementDialog.open() } } RowLayout { Layout.fillWidth: true; spacing: 8; visible: sessionController.active Button { Layout.fillWidth: true; implicitHeight: 48; text: "Пропустить"; onClicked: sessionController.skipCurrentExercise() } Button { Layout.fillWidth: true; implicitHeight: 48; text: "Закончить"; onClicked: finishWorkoutDialog.open() } } RowLayout { Layout.fillWidth: true; spacing: 8; visible: sessionController.active Button { Layout.fillWidth: true; implicitHeight: 48; text: "− повтор"; onClicked: sessionController.removeRep() } Label { text: sessionController.currentWorkoutPosition.setText || "Подход" color: app.blue font.pixelSize: 11 font.bold: true horizontalAlignment: Text.AlignHCenter } Button { Layout.fillWidth: true; implicitHeight: 48; text: "+ повтор"; onClicked: sessionController.addRep() } } Label { Layout.fillWidth: true; visible: sessionController.active; text: "Далее: " + (sessionController.nextExerciseName || "завершение"); color: app.textMuted; font.pixelSize: 12; wrapMode: Text.WordWrap } Label { text: sessionController.hasScheduledPlanToday || sessionController.active ? (sessionController.active ? "Очередь тренировки" : "Упражнения") : "Выбранная тренировка" color: app.textMain; font.pixelSize: 18; font.bold: true } ListView { id: activeWorkoutQueue Layout.fillWidth: true Layout.preferredHeight: 94 visible: sessionController.active orientation: ListView.Horizontal spacing: 8 clip: true boundsBehavior: Flickable.StopAtBounds model: sessionController.workoutQueueSteps currentIndex: Math.max(0, sessionController.currentStepIndex - 1) onCurrentIndexChanged: positionViewAtIndex(currentIndex, ListView.Center) delegate: SurfaceCard { required property var modelData property bool isCurrent: modelData.index === sessionController.currentStepIndex property bool isPast: modelData.index < sessionController.currentStepIndex width: Math.min(238, activeWorkoutQueue.width * 0.72) height: 90 color: isCurrent ? app.selectedSurface : app.panel border.color: isCurrent ? sessionController.phaseColor : app.border border.width: isCurrent ? 2 : 1 opacity: isPast ? 0.58 : 1 scale: isCurrent ? 1 : 0.98 Behavior on scale { NumberAnimation { duration: 140; easing.type: Easing.OutCubic } } Behavior on opacity { NumberAnimation { duration: 140; easing.type: Easing.OutCubic } } RowLayout { anchors.fill: parent anchors.margins: 11 spacing: 9 Label { text: modelData.index color: isCurrent ? sessionController.phaseColor : app.blue font.pixelSize: 18 font.bold: true } ColumnLayout { Layout.fillWidth: true spacing: 2 Label { Layout.fillWidth: true text: modelData.exerciseName color: app.textMain font.pixelSize: 13 font.bold: true elide: Text.ElideRight } Label { Layout.fillWidth: true text: modelData.target + " " + modelData.metricText + (modelData.sets > 1 ? " ×" + modelData.sets : "") color: app.blue font.pixelSize: 10 elide: Text.ElideRight } Label { Layout.fillWidth: true text: modelData.isWarmup ? "разминка" : modelData.isCooldown ? "заминка" : "отдых " + modelData.restSeconds + "с" color: app.textMuted font.pixelSize: 10 elide: Text.ElideRight } } } } } ColumnLayout { Layout.fillWidth: true visible: !sessionController.active spacing: 8 Repeater { model: sessionController.selectedPlanSteps delegate: SurfaceCard { required property var modelData Layout.fillWidth: true; implicitHeight: 68 RowLayout { anchors.fill: parent; anchors.margins: 12; spacing: 10 Label { text: modelData.index; color: app.blue; font.pixelSize: 18; font.bold: true } ColumnLayout { Layout.fillWidth: true; spacing: 2 Label { Layout.fillWidth: true; text: modelData.exerciseName; color: app.textMain; font.pixelSize: 14; font.bold: true; elide: Text.ElideRight } Label { text: modelData.target + " " + modelData.metricText + " • отдых " + modelData.restSeconds + "с"; color: app.textMuted; font.pixelSize: 11 } } } } } } } } // Plans and long-term cycle Flickable { clip: true contentWidth: width contentHeight: plansColumn.implicitHeight ColumnLayout { id: plansColumn width: parent.width spacing: 10 Label { text: "Планы"; color: app.textMain; font.pixelSize: 27; font.bold: true } Label { text: "Месяц, полугодие и тренировки недели"; color: app.textMuted; font.pixelSize: 12 } RowLayout { Layout.fillWidth: true; spacing: 8 Button { Layout.fillWidth: true; implicitHeight: 46; text: "4 недели" enabled: !sessionController.active onClicked: { app.cycleWeeksExpanded = false; sessionController.configureTrainingCycle(4) } } Button { Layout.fillWidth: true; implicitHeight: 46; text: "26 недель" enabled: !sessionController.active onClicked: { app.cycleWeeksExpanded = false; sessionController.configureTrainingCycle(26) } } } SurfaceCard { Layout.fillWidth: true implicitHeight: sessionController.trainingCycle.extended ? 210 : 182 visible: sessionController.trainingCycle.active border.color: app.blue ColumnLayout { anchors.fill: parent; anchors.margins: 14; spacing: 7 RowLayout { Layout.fillWidth: true ColumnLayout { Layout.fillWidth: true; spacing: 1 Label { text: (sessionController.trainingCycle.upcoming ? "Стартовая" : sessionController.trainingCycle.completed ? "Финальная" : "Текущая") + " неделя " + sessionController.trainingCycle.currentWeek + "/" + sessionController.trainingCycle.durationWeeks color: app.blue; font.pixelSize: 11; font.bold: true } Label { text: sessionController.trainingCycle.phase || ""; color: app.textMain; font.pixelSize: 20; font.bold: true } } Label { text: (sessionController.trainingCycle.startDate || "") + "\n" + (sessionController.trainingCycle.extended ? (sessionController.trainingCycle.originalEndDate || "") + " → " + (sessionController.trainingCycle.endDate || "") : (sessionController.trainingCycle.endDate || "")) color: app.textMuted; font.pixelSize: 9; horizontalAlignment: Text.AlignRight } } Label { Layout.fillWidth: true; text: sessionController.trainingCycle.focus || ""; color: app.textMuted; font.pixelSize: 11; wrapMode: Text.WordWrap } Label { Layout.fillWidth: true; text: sessionController.trainingCycle.adjustmentText || ""; color: app.accent; font.pixelSize: 11; font.bold: true; wrapMode: Text.WordWrap } Label { Layout.fillWidth: true visible: sessionController.trainingCycle.extended === true text: "Пропущено: " + sessionController.trainingCycle.missedCount + " • цикл продлён на " + sessionController.trainingCycle.extensionDays + " дн." color: app.warning; font.pixelSize: 10; font.bold: true; wrapMode: Text.WordWrap } ProgressBar { id: cycleProgress Layout.fillWidth: true; implicitHeight: 8 from: 0; to: 1; value: sessionController.trainingCycle.progress || 0 background: Rectangle { radius: 4; color: app.surface } contentItem: Item { Rectangle { width: parent.width * cycleProgress.visualPosition; height: parent.height; radius: 4; color: app.blue } } } } } SurfaceCard { Layout.fillWidth: true implicitHeight: sessionController.cycleReview.due ? 224 : 116 visible: sessionController.cycleReview.active border.color: sessionController.cycleReview.due ? app.warning : app.border ColumnLayout { anchors.fill: parent; anchors.margins: 13; spacing: 7 RowLayout { Layout.fillWidth: true Label { Layout.fillWidth: true; text: "Пересмотр цикла"; color: app.textMain; font.pixelSize: 16; font.bold: true } Label { text: sessionController.cycleReview.due ? "Неделя " + sessionController.cycleReview.reviewWeek : sessionController.cycleReview.nextReviewWeek > 0 ? "Далее: " + sessionController.cycleReview.nextReviewWeek : "Цикл проверен" color: sessionController.cycleReview.due ? app.warning : app.textMuted; font.pixelSize: 10; font.bold: true } } Label { visible: sessionController.cycleReview.due text: sessionController.cycleReview.sessionCount + " полных" + (sessionController.cycleReview.quickSessionCount > 0 ? " + " + sessionController.cycleReview.quickSessionCount + " коротких" : "") + " • " + sessionController.cycleReview.workMinutes + " мин • завершение " + sessionController.cycleReview.completionRate + "%" color: app.blue; font.pixelSize: 11 } Label { Layout.fillWidth: true text: sessionController.cycleReview.due ? sessionController.cycleReview.recommendation : sessionController.cycleReview.completed ? sessionController.cycleReview.lastDecisionText + " • " + sessionController.cycleReview.completedAt : "Контрольная точка появится на четвёртой неделе." color: app.textMuted; font.pixelSize: 10; wrapMode: Text.WordWrap } RowLayout { Layout.fillWidth: true; spacing: 6; visible: sessionController.cycleReview.due Button { Layout.fillWidth: true; implicitHeight: 44; text: "Оставить"; onClicked: sessionController.completeCycleReview("keep") } Button { Layout.fillWidth: true; implicitHeight: 44; text: "+ нагрузка"; onClicked: sessionController.completeCycleReview("progress") } Button { Layout.fillWidth: true; implicitHeight: 44; text: "Облегчить"; onClicked: sessionController.completeCycleReview("reduce") } } Label { Layout.fillWidth: true; visible: sessionController.cycleReview.due; text: "Изменение применяется ко всем недельным планам со следующей тренировки."; color: app.textMuted; font.pixelSize: 8; wrapMode: Text.WordWrap } } } SurfaceCard { Layout.fillWidth: true; implicitHeight: 92 visible: !sessionController.trainingCycle.active ColumnLayout { anchors.centerIn: parent; width: parent.width - 28; spacing: 5 Label { Layout.alignment: Qt.AlignHCenter; text: "Цикл ещё не создан"; color: app.textMain; font.pixelSize: 16; font.bold: true } Label { Layout.fillWidth: true; text: "4 недели — быстрый блок. 26 недель — фазы роста и регулярные разгрузки."; color: app.textMuted; font.pixelSize: 10; wrapMode: Text.WordWrap; horizontalAlignment: Text.AlignHCenter } } } RowLayout { Layout.fillWidth: true; visible: sessionController.trainingCycle.active Label { Layout.fillWidth: true; text: "Недели цикла"; color: app.textMain; font.pixelSize: 16; font.bold: true } Button { implicitHeight: 42 text: app.cycleWeeksExpanded ? "Свернуть" : "Все " + sessionController.trainingCycle.durationWeeks visible: sessionController.trainingCycle.durationWeeks > 6 onClicked: app.cycleWeeksExpanded = !app.cycleWeeksExpanded } } Repeater { model: sessionController.trainingCycle.active ? app.visibleCycleWeeks() : [] delegate: SurfaceCard { required property var modelData Layout.fillWidth: true; implicitHeight: 92 color: modelData.current ? app.selectedSurface : app.panel border.color: modelData.current ? app.blue : modelData.phase === "Разгрузка" ? app.warning : app.border border.width: modelData.current ? 2 : 1 RowLayout { anchors.fill: parent; anchors.margins: 11; spacing: 10 Rectangle { Layout.preferredWidth: 52; Layout.preferredHeight: 52; radius: 12 color: app.surface; border.color: modelData.current ? app.blue : app.border Column { anchors.centerIn: parent; spacing: 0 Label { anchors.horizontalCenter: parent.horizontalCenter; text: modelData.week; color: app.textMain; font.pixelSize: 20; font.bold: true } Label { anchors.horizontalCenter: parent.horizontalCenter; text: "неделя"; color: app.textMuted; font.pixelSize: 8 } } } ColumnLayout { Layout.fillWidth: true; spacing: 2 RowLayout { Layout.fillWidth: true Label { Layout.fillWidth: true; text: modelData.phase; color: app.textMain; font.pixelSize: 13; font.bold: true } Label { text: modelData.dateRange; color: app.textMuted; font.pixelSize: 9 } } Label { Layout.fillWidth: true; text: modelData.focus; color: app.textMuted; font.pixelSize: 9; elide: Text.ElideRight } Label { Layout.fillWidth: true; text: modelData.adjustmentText; color: modelData.phase === "Разгрузка" ? app.warning : app.blue; font.pixelSize: 9; elide: Text.ElideRight } } ColumnLayout { Layout.preferredWidth: 62; spacing: 2 Label { Layout.alignment: Qt.AlignRight; text: modelData.status; color: modelData.current ? app.accent : app.textMuted; font.pixelSize: 9; font.bold: modelData.current } Label { Layout.alignment: Qt.AlignRight text: modelData.completedSessions + " полн." + (modelData.quickSessions > 0 ? " + " + modelData.quickSessions + " коротк." : "") color: app.textMuted; font.pixelSize: 8 } } } } } Button { Layout.fillWidth: true; implicitHeight: 44 visible: sessionController.trainingCycle.active enabled: !sessionController.active text: "Отключить цикл" onClicked: sessionController.clearTrainingCycle() } Label { text: "Тренировки недели"; color: app.textMain; font.pixelSize: 18; font.bold: true } Label { text: "Выберите другой день — план откроется на главном экране"; color: app.textMuted; font.pixelSize: 10; wrapMode: Text.WordWrap; Layout.fillWidth: true } Repeater { model: sessionController.plans delegate: Button { required property var modelData Layout.fillWidth: true; implicitHeight: 82 scale: down ? 0.96 : 1 Behavior on scale { NumberAnimation { duration: 120; easing.type: Easing.OutCubic } } onClicked: { sessionController.selectPlan(modelData.id); app.currentTab = 0 } background: Rectangle { radius: 16; color: modelData.selected ? app.selectedSurface : app.panel; border.color: modelData.selected ? app.blue : app.border; border.width: modelData.selected ? 2 : 1 } contentItem: Column { leftPadding: 14; rightPadding: 14; spacing: 4 Text { width: parent.width - 28; text: modelData.name; color: app.textMain; font.pixelSize: 15; font.bold: true; elide: Text.ElideRight } Text { width: parent.width - 28; text: modelData.goal; color: app.textMuted; font.pixelSize: 11; elide: Text.ElideRight } Text { text: modelData.stepCount + " упражнений"; color: app.blue; font.pixelSize: 10 } } } } Item { Layout.fillWidth: true; implicitHeight: 12 } } } // Progress Flickable { clip: true; contentWidth: width; contentHeight: progressColumn.implicitHeight ColumnLayout { id: progressColumn; width: parent.width; spacing: 10 Label { text: "Прогресс"; color: app.textMain; font.pixelSize: 27; font.bold: true } RowLayout { Layout.fillWidth: true; spacing: 8 Repeater { model: [ { title: "Тренировки", value: sessionController.totalSessions }, { title: "Минуты", value: sessionController.totalWorkMinutes }, { title: "Завершение", value: sessionController.completionRateText } ]; delegate: SurfaceCard { required property var modelData; Layout.fillWidth: true; implicitHeight: 92 ColumnLayout { anchors.fill: parent; anchors.margins: 12; spacing: 3 Label { text: modelData.title; color: app.textMuted; font.pixelSize: 10 } Label { text: modelData.value; color: app.textMain; font.pixelSize: 20; font.bold: true } } } } } SurfaceCard { Layout.fillWidth: true; implicitHeight: (sessionController.weeklySummary.heartRateLoadAvailable ? 280 : 258) + (sessionController.weeklySummary.undertrainedAreasActive ? 22 : 0) ColumnLayout { anchors.fill: parent; anchors.margins: 12; spacing: 6 RowLayout { Layout.fillWidth: true; spacing: 8 Label { Layout.fillWidth: true; text: "Неделя " + sessionController.weeklySummary.weekStart + " — " + sessionController.weeklySummary.weekEnd; color: app.textMain; font.pixelSize: 14; font.bold: true } WorkoutStatusChip { value: sessionController.weeklySummary.rescueGoalActive ? "Возврат • " + sessionController.weeklySummary.effectiveWeeklyGoalMin : "Цель • " + sessionController.weeklySummary.weeklyGoalText chipColor: sessionController.weeklySummary.rescueGoalActive ? app.warning : sessionController.weeklySummary.weeklyGoalAchieved ? app.accent : app.blue } } RowLayout { Layout.fillWidth: true; spacing: 8 Label { Layout.fillWidth: true text: "Полные " + sessionController.weeklySummary.fullSessions + "/" + sessionController.weeklySummary.effectiveWeeklyGoalMin + " • короткие " + sessionController.weeklySummary.quickSessions color: app.textMain; font.pixelSize: 12; font.bold: true } Label { text: sessionController.weeklySummary.weeklyGoalProgress + "%" color: sessionController.weeklySummary.rescueGoalActive ? app.warning : sessionController.weeklySummary.weeklyGoalAchieved ? app.accent : app.blue font.pixelSize: 13; font.bold: true } } Rectangle { Layout.fillWidth: true; implicitHeight: 8; radius: 4; color: app.surface Rectangle { width: parent.width * Math.max(0, Math.min(100, sessionController.weeklySummary.weeklyGoalProgress)) / 100 height: parent.height; radius: 4 color: sessionController.weeklySummary.rescueGoalActive ? app.warning : sessionController.weeklySummary.weeklyGoalAchieved ? app.accent : app.blue } } Label { Layout.fillWidth: true; text: sessionController.weeklySummary.weeklyGoalStatusText color: sessionController.weeklySummary.rescueGoalActive ? app.warning : app.textMuted font.pixelSize: 10; wrapMode: Text.WordWrap } RowLayout { Layout.fillWidth: true; spacing: 10 ColumnLayout { Layout.fillWidth: true; spacing: 1 Label { text: "Последние 7 дней"; color: app.textMuted; font.pixelSize: 9 } Label { text: sessionController.weeklySummary.fullSessions + " полн. • " + sessionController.weeklySummary.quickSessions + " коротк. • " + sessionController.weeklySummary.totalWorkMinutes + " мин" color: app.blue; font.pixelSize: 14; font.bold: true } } ColumnLayout { Layout.fillWidth: true; spacing: 1 Label { text: "Предыдущие 7 дней"; color: app.textMuted; font.pixelSize: 9 } Label { text: sessionController.weeklySummary.previousSessions + " тр. • " + sessionController.weeklySummary.previousWorkMinutes + " мин"; color: app.textMain; font.pixelSize: 14; font.bold: true } } } Label { visible: sessionController.weeklySummary.comparisonAvailable text: "Разница: " + sessionController.weeklySummary.sessionsDeltaText + " тр. • " + sessionController.weeklySummary.workMinutesDeltaText + " мин" color: app.accent; font.pixelSize: 11; font.bold: true } Label { text: "Завершение полных: " + sessionController.weeklySummary.completionRate + "% • подходов: " + sessionController.weeklySummary.completedSteps + "/" + sessionController.weeklySummary.totalSteps; color: app.textMuted; font.pixelSize: 10 } Label { visible: sessionController.weeklySummary.heartRateLoadAvailable Layout.fillWidth: true text: "HR-нагрузка: " + sessionController.weeklySummary.heartRateLoad + " / " + sessionController.weeklySummary.previousHeartRateLoad + (sessionController.weeklySummary.comparisonAvailable ? " • " + sessionController.weeklySummary.heartRateLoadDeltaText : "") color: sessionController.weeklySummary.loadStatusId === "sharp-hr-increase" ? app.warning : app.error font.pixelSize: 10; font.bold: sessionController.weeklySummary.loadStatusId === "sharp-hr-increase"; wrapMode: Text.WordWrap } Label { Layout.fillWidth: true; text: sessionController.weeklySummary.loadStatusText color: (sessionController.weeklySummary.loadStatusId === "sharp-increase" || sessionController.weeklySummary.loadStatusId === "sharp-hr-increase") ? app.warning : app.textMuted font.pixelSize: 10; wrapMode: Text.WordWrap } Label { visible: sessionController.weeklySummary.undertrainedAreasActive Layout.fillWidth: true text: sessionController.weeklySummary.undertrainedAreasText color: app.warning; font.pixelSize: 10; font.bold: true; wrapMode: Text.WordWrap } } } SurfaceCard { Layout.fillWidth: true; implicitHeight: 238 ColumnLayout { anchors.fill: parent; anchors.margins: 12; spacing: 7 RowLayout { Layout.fillWidth: true ColumnLayout { Layout.fillWidth: true; spacing: 1 Label { text: "Вес и V-силуэт"; color: app.textMain; font.pixelSize: 15; font.bold: true } Label { text: sessionController.nutritionSummary.ratioText; color: app.blue; font.pixelSize: 10 } } Label { text: sessionController.nutritionSummary.currentWeightText; color: app.accent; font.pixelSize: 18; font.bold: true } } Item { Layout.fillWidth: true; Layout.fillHeight: true Canvas { id: mobileWeightChart anchors.fill: parent property var chartData: sessionController.weightChartData onChartDataChanged: requestPaint() onWidthChanged: requestPaint() onHeightChanged: requestPaint() onPaint: { const ctx = getContext("2d") ctx.reset() if (!chartData || chartData.length === 0) return const padX = 24 const padTop = 12 const padBottom = 24 const plotWidth = width - padX * 2 const plotHeight = height - padTop - padBottom let minimum = Number(chartData[0].weight) let maximum = minimum for (let i = 1; i < chartData.length; ++i) { minimum = Math.min(minimum, Number(chartData[i].weight)) maximum = Math.max(maximum, Number(chartData[i].weight)) } if (maximum - minimum < 1) { minimum -= 0.5; maximum += 0.5 } const range = maximum - minimum ctx.strokeStyle = Qt.rgba(0.39, 0.56, 0.72, 0.22) ctx.lineWidth = 1 for (let grid = 0; grid < 3; ++grid) { const y = padTop + plotHeight * grid / 2 ctx.beginPath(); ctx.moveTo(padX, y); ctx.lineTo(width - padX, y); ctx.stroke() } ctx.strokeStyle = app.accent ctx.lineWidth = 3 ctx.beginPath() for (let point = 0; point < chartData.length; ++point) { const x = chartData.length === 1 ? width / 2 : padX + point * plotWidth / (chartData.length - 1) const y = padTop + plotHeight - (Number(chartData[point].weight) - minimum) * plotHeight / range if (point === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y) } ctx.stroke() for (let dot = 0; dot < chartData.length; ++dot) { const x = chartData.length === 1 ? width / 2 : padX + dot * plotWidth / (chartData.length - 1) const y = padTop + plotHeight - (Number(chartData[dot].weight) - minimum) * plotHeight / range ctx.fillStyle = dot === chartData.length - 1 ? app.blue : app.accent ctx.beginPath(); ctx.arc(x, y, dot === chartData.length - 1 ? 5 : 3.5, 0, Math.PI * 2); ctx.fill() } } } Label { anchors.centerIn: parent; visible: sessionController.weightChartData.length === 0; text: "История веса появится после синхронизации"; color: app.textMuted; font.pixelSize: 10 } Label { anchors.left: parent.left; anchors.bottom: parent.bottom; text: sessionController.weightChartData.length ? sessionController.weightChartData[0].date : ""; color: app.textMuted; font.pixelSize: 8 } Label { anchors.right: parent.right; anchors.bottom: parent.bottom; text: sessionController.weightChartData.length ? sessionController.weightChartData[sessionController.weightChartData.length - 1].date : ""; color: app.textMuted; font.pixelSize: 8 } } } } SurfaceCard { Layout.fillWidth: true; implicitHeight: 292 ColumnLayout { anchors.fill: parent; anchors.margins: 12; spacing: 6 RowLayout { Layout.fillWidth: true Label { Layout.fillWidth: true; text: "Календарь тренировок"; color: app.textMain; font.pixelSize: 15; font.bold: true } Label { text: app.progressMonthTitle(); color: app.blue; font.pixelSize: 10 } } GridLayout { Layout.fillWidth: true; columns: 7; rowSpacing: 4; columnSpacing: 4 Repeater { model: ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"]; delegate: Label { required property string modelData; Layout.fillWidth: true; text: modelData; color: app.textMuted; font.pixelSize: 8; horizontalAlignment: Text.AlignHCenter } } Repeater { model: app.progressCalendarCells() delegate: Rectangle { required property var modelData Layout.fillWidth: true; implicitHeight: 36; radius: 9 color: modelData.empty ? "transparent" : modelData.hasWorkout ? app.selectedSurface : app.surface border.color: modelData.empty ? "transparent" : modelData.isToday ? app.blue : modelData.completed ? app.accent : app.border border.width: modelData.isToday ? 2 : 1 Label { anchors.centerIn: parent; visible: modelData.empty !== true; text: modelData.day || ""; color: modelData.isFuture === true ? app.textMuted : app.textMain; opacity: modelData.isFuture === true ? 0.45 : 1; font.pixelSize: 10; font.bold: modelData.hasWorkout === true || modelData.isToday === true } Rectangle { visible: !modelData.empty && modelData.hasWorkout width: 5; height: 5; radius: 3 color: modelData.completed ? app.accent : modelData.quickSessionCount > 0 ? app.blue : app.warning anchors.horizontalCenter: parent.horizontalCenter; anchors.bottom: parent.bottom; anchors.bottomMargin: 3 } } } } Label { Layout.fillWidth: true; text: "Точка — тренировка, зелёная рамка — выполнена полностью."; color: app.textMuted; font.pixelSize: 9; wrapMode: Text.WordWrap } } } Label { Layout.fillWidth: true; text: sessionController.preWorkoutRecommendation.title + ": " + sessionController.preWorkoutRecommendation.detail; color: sessionController.preWorkoutRecommendation.color || app.blue; font.pixelSize: 12; wrapMode: Text.WordWrap } SurfaceCard { Layout.fillWidth: true visible: sessionController.progressExerciseBuckets.length > 0 implicitHeight: 50 + Math.min(3, sessionController.progressExerciseBuckets.length) * 42 ColumnLayout { anchors.fill: parent; anchors.margins: 12; spacing: 4 Label { text: "Личные результаты"; color: app.textMain; font.pixelSize: 15; font.bold: true } Repeater { model: sessionController.progressExerciseBuckets.slice(0, 3) delegate: RowLayout { required property var modelData Layout.fillWidth: true; spacing: 8 Label { Layout.fillWidth: true; text: modelData.exerciseName; color: app.textMain; font.pixelSize: 11; font.bold: true; elide: Text.ElideRight } Label { text: "Лучшее " + modelData.bestActual + " " + modelData.metricText; color: app.blue; font.pixelSize: 10; font.bold: true } Label { text: modelData.bestDateText; color: app.textMuted; font.pixelSize: 9 } } } } } SurfaceCard { Layout.fillWidth: true visible: sessionController.loadJournalEntries.length > 0 implicitHeight: 50 + Math.min(6, sessionController.loadJournalEntries.length) * 58 ColumnLayout { anchors.fill: parent; anchors.margins: 12; spacing: 6 Label { text: "Журнал нагрузки"; color: app.textMain; font.pixelSize: 15; font.bold: true } Repeater { model: sessionController.loadJournalEntries.slice(0, 6) delegate: ColumnLayout { required property var modelData Layout.fillWidth: true; spacing: 1 RowLayout { Layout.fillWidth: true; spacing: 8 Label { Layout.fillWidth: true; text: modelData.exerciseName; color: app.textMain; font.pixelSize: 11; font.bold: true; elide: Text.ElideRight } Label { text: modelData.dateText; color: app.textMuted; font.pixelSize: 9 } } Label { Layout.fillWidth: true; text: modelData.detailsText + (modelData.note ? (modelData.detailsText ? " • " : "") + modelData.note : ""); color: modelData.discomfortRating >= 5 ? app.warning : app.blue; font.pixelSize: 10; elide: Text.ElideRight } } } } } Label { text: "Редко выполнялись"; color: app.textMain; font.pixelSize: 16; font.bold: true } Repeater { model: sessionController.undertrainedExercises.slice(0, 3); delegate: Label { required property var modelData; Layout.fillWidth: true; text: "• " + modelData.exerciseName + " — " + modelData.lastDoneText; color: app.textMuted; font.pixelSize: 11; elide: Text.ElideRight } } Label { text: "Последние тренировки"; color: app.textMain; font.pixelSize: 17; font.bold: true } Repeater { model: sessionController.recentSessions; delegate: SurfaceCard { required property var modelData; Layout.fillWidth: true; implicitHeight: modelData.heartRateLoadAvailable ? 102 : (modelData.heartRateAvailable ? 88 : 74) ColumnLayout { anchors.fill: parent; anchors.margins: 12; spacing: 2 Label { Layout.fillWidth: true; text: modelData.planName; color: app.textMain; font.pixelSize: 14; font.bold: true; elide: Text.ElideRight } Label { text: modelData.endedAt + " • " + modelData.workMinutes + " мин • " + modelData.modeText color: modelData.sessionMode === "quick" ? app.blue : app.textMuted font.pixelSize: 11 } Label { visible: modelData.heartRateAvailable; text: modelData.heartRateText; color: app.error; font.pixelSize: 10 } Label { visible: modelData.heartRateLoadAvailable; text: modelData.heartRateLoadText; color: app.textMuted; font.pixelSize: 10 } } } } Item { Layout.fillWidth: true; implicitHeight: 12 } } } // Wearable Flickable { clip: true; contentWidth: width; contentHeight: watchColumn.implicitHeight ColumnLayout { id: watchColumn; width: parent.width; spacing: 10 Label { text: "Часы"; color: app.textMain; font.pixelSize: 27; font.bold: true } Label { text: sessionController.wearableStatistics.dateRangeText || "Импортируйте данные Health Connect"; color: app.blue; font.pixelSize: 12 } SurfaceCard { Layout.fillWidth: true implicitHeight: 82 RowLayout { anchors.fill: parent; anchors.margins: 12; spacing: 10 ColumnLayout { Layout.fillWidth: true; spacing: 2 Label { text: "Данные этого телефона"; color: app.textMain; font.pixelSize: 13; font.bold: true } Label { text: "Часы, вес и активность попадут только этому профилю"; color: app.textMuted; font.pixelSize: 9 } } ComboBox { Layout.preferredWidth: 132; implicitHeight: 44 textRole: "name"; valueRole: "id"; model: sessionController.profiles currentIndex: indexOfValue(sessionController.healthConnectOwnerProfileId) onActivated: sessionController.setHealthConnectOwnerProfile(currentValue) } } } PrimaryButton { Layout.fillWidth: true text: sessionController.healthSyncInProgress ? "Синхронизация…" : "Синхронизировать Health Connect" enabled: !sessionController.healthSyncInProgress onClicked: sessionController.syncHealthConnect() } Label { Layout.fillWidth: true text: sessionController.healthSyncStatusText + "\n" + sessionController.healthSyncLastSuccessText + "\nАвтосинхронизация при запуске • текущий день включён" color: app.textMuted font.pixelSize: 11 wrapMode: Text.WordWrap } Label { Layout.fillWidth: true; text: "Источники Health Connect"; color: app.textMain; font.pixelSize: 14; font.bold: true } RowLayout { Layout.fillWidth: true; spacing: 8 ComboBox { id: activitySourceBox Layout.fillWidth: true textRole: "label"; valueRole: "value" model: [ { label: "Активность: Xiaomi", value: "com.xiaomi.wearable" }, { label: "Активность: Google Fit", value: "com.google.android.apps.fitness" }, { label: "Активность: все", value: "auto" } ] Component.onCompleted: currentIndex = indexOfValue(sessionController.healthActivitySource) onActivated: sessionController.setHealthSourcePreferences(currentValue, weightSourceBox.currentValue) } ComboBox { id: weightSourceBox Layout.fillWidth: true textRole: "label"; valueRole: "value" model: [ { label: "Вес: Zepp/Google", value: "com.google.android.apps.fitness" }, { label: "Вес: Xiaomi", value: "com.xiaomi.wearable" }, { label: "Вес: любой", value: "auto" } ] Component.onCompleted: currentIndex = indexOfValue(sessionController.healthWeightSource) onActivated: sessionController.setHealthSourcePreferences(activitySourceBox.currentValue, currentValue) } } Label { Layout.fillWidth: true; text: "Рекомендуется: Xiaomi Wearable для активности, Zepp Life через Google Fit для веса."; color: app.blue; font.pixelSize: 10; wrapMode: Text.WordWrap } Repeater { model: [ { title: "Шаги", value: sessionController.wearableStatistics.stepsText || "—", detail: sessionController.wearableStatistics.averageStepsText || "нет данных" }, { title: "Активные калории", value: sessionController.wearableStatistics.caloriesText || "—", detail: "данные Health Connect" }, { title: "Сон", value: sessionController.wearableStatistics.sleepText || "—", detail: sessionController.wearableStatistics.averageSleepText || "нет данных" }, { title: "Пульс", value: sessionController.wearableStatistics.heartRateText || "—", detail: sessionController.wearableStatistics.oxygenText || "SpO₂ —" }, { title: "Пульс покоя", value: sessionController.wearableStatistics.restingHeartRateText || "—", detail: sessionController.wearableStatistics.restingHeartRateTrendText || "нужны данные" }, { title: "Вес", value: sessionController.wearableStatistics.weightText || sessionController.nutritionSummary.currentWeightText, detail: sessionController.wearableStatistics.weightSourceText || "ручная запись" } ]; delegate: SurfaceCard { required property var modelData; Layout.fillWidth: true; implicitHeight: 92 ColumnLayout { anchors.fill: parent; anchors.margins: 13; spacing: 3 Label { text: modelData.title; color: app.textMuted; font.pixelSize: 11 } Label { Layout.fillWidth: true; text: modelData.value; color: app.textMain; font.pixelSize: 18; font.bold: true; elide: Text.ElideRight } Label { Layout.fillWidth: true; text: modelData.detail; color: app.blue; font.pixelSize: 10; elide: Text.ElideRight } } } } SurfaceCard { Layout.fillWidth: true; implicitHeight: 274 ColumnLayout { anchors.fill: parent; anchors.margins: 13; spacing: 8 RowLayout { Layout.fillWidth: true Label { Layout.fillWidth: true; text: app.wearableMetricTitle() + " • 10 дней"; color: app.textMain; font.pixelSize: 14; font.bold: true } Label { text: { const days = app.recentWearableDays() return days.length ? app.wearableMetricText(app.wearableMetricValue(days[days.length - 1])) : "—" } color: app.wearableMetricColor(); font.pixelSize: 11; font.bold: true } } RowLayout { Layout.fillWidth: true; spacing: 5 Repeater { model: [ { label: "Шаги", key: "steps" }, { label: "Ккал", key: "activeCaloriesKcal" }, { label: "Сон", key: "sleepMinutes" }, { label: "Покой", key: "restingHeartRateAvg" }, { label: "Пульс", key: "heartRateAvg" } ] delegate: Button { required property var modelData Layout.fillWidth: true implicitHeight: 42 text: modelData.label font.pixelSize: 10 font.bold: app.wearableChartMetric === modelData.key scale: down ? 0.96 : 1 Behavior on scale { NumberAnimation { duration: 120; easing.type: Easing.OutCubic } } onClicked: app.wearableChartMetric = modelData.key background: Rectangle { radius: 11 color: app.wearableChartMetric === modelData.key ? app.selectedSurface : app.surface border.color: app.wearableChartMetric === modelData.key ? app.wearableMetricColor() : app.border } } } } RowLayout { Layout.fillWidth: true; Layout.fillHeight: true; spacing: 4 Repeater { model: app.recentWearableDays(); delegate: ColumnLayout { required property var modelData; Layout.fillWidth: true; Layout.fillHeight: true; spacing: 3 Item { Layout.fillWidth: true; Layout.fillHeight: true Rectangle { anchors.left: parent.left; anchors.right: parent.right; anchors.bottom: parent.bottom height: Math.max(2, parent.height * app.wearableMetricValue(modelData) / app.wearableMaximum(app.wearableChartMetric)) radius: 3; color: app.wearableMetricColor(); opacity: app.wearableMetricValue(modelData) > 0 ? 0.9 : 0.18 Behavior on height { NumberAnimation { duration: 180; easing.type: Easing.OutCubic } } } } Label { Layout.alignment: Qt.AlignHCenter; text: modelData.dateText; color: app.textMuted; font.pixelSize: 8 } } } } } } } } // Profile, nutrition and exercise library ColumnLayout { spacing: 9 Label { text: "Профиль и база"; color: app.textMain; font.pixelSize: 27; font.bold: true } RowLayout { Layout.fillWidth: true; spacing: 6 Repeater { model: [{ label: "Профиль", mode: 0 }, { label: "Упражнения", mode: 1 }] delegate: Button { required property var modelData Layout.fillWidth: true; implicitHeight: 44 text: modelData.label font.bold: app.baseMode === modelData.mode scale: down ? 0.96 : 1 Behavior on scale { NumberAnimation { duration: 120; easing.type: Easing.OutCubic } } onClicked: app.baseMode = modelData.mode background: Rectangle { radius: 12 color: app.baseMode === modelData.mode ? app.selectedSurface : app.surface border.color: app.baseMode === modelData.mode ? app.blue : app.border } } } } StackLayout { Layout.fillWidth: true; Layout.fillHeight: true currentIndex: app.baseMode Flickable { clip: true; contentWidth: width; contentHeight: profileColumn.implicitHeight ColumnLayout { id: profileColumn width: parent.width; spacing: 10 SurfaceCard { Layout.fillWidth: true; implicitHeight: 92 RowLayout { anchors.fill: parent; anchors.margins: 13; spacing: 10 ColumnLayout { Layout.fillWidth: true; spacing: 2 Label { text: "Профиль"; color: app.textMuted; font.pixelSize: 10 } ComboBox { Layout.fillWidth: true; implicitHeight: 44 textRole: "name"; valueRole: "id"; model: sessionController.profiles currentIndex: indexOfValue(sessionController.selectedProfileId) enabled: !sessionController.active && !sessionController.hasRecoverableDraft && !sessionController.needsSave onActivated: sessionController.selectProfile(currentValue) contentItem: Text { leftPadding: 12; rightPadding: 30 text: parent.displayText color: app.textMain font: parent.font verticalAlignment: Text.AlignVCenter elide: Text.ElideRight } background: Rectangle { radius: 10 color: app.selectedSurface border.color: app.blue border.width: 1 } } } ColumnLayout { spacing: 2 Label { text: "Вес"; color: app.textMuted; font.pixelSize: 10 } Label { text: sessionController.nutritionSummary.currentWeightText; color: app.blue; font.pixelSize: 18; font.bold: true } Label { text: "Health Connect приоритетен"; color: app.textMuted; font.pixelSize: 8 } } } } PrimaryButton { Layout.fillWidth: true implicitHeight: 46 text: "Создать курс Насти (3 дня)" visible: !sessionController.profiles.some(function(profile) { return profile.name === "Настя" }) enabled: !sessionController.active && !sessionController.hasRecoverableDraft && !sessionController.needsSave onClicked: sessionController.createNastyaStarterCourse() } GridLayout { Layout.fillWidth: true; columns: 2; rowSpacing: 8; columnSpacing: 8 Repeater { model: [ { title: "Тренировочный день", value: sessionController.nutritionSummary.trainingCaloriesText + " ккал" }, { title: "День отдыха", value: sessionController.nutritionSummary.recoveryCaloriesText + " ккал" }, { title: "Белок", value: sessionController.nutritionSummary.proteinTargetText }, { title: "Питание", value: sessionController.nutritionSummary.adherenceText }, { title: "V-индекс", value: sessionController.nutritionMeasurementEntries.length > 0 ? sessionController.nutritionSummary.ratioText : "Нет данных" }, { title: "Замеры", value: sessionController.nutritionSummary.measurementEntryCount + " записей" } ] delegate: SurfaceCard { required property var modelData; Layout.fillWidth: true; implicitHeight: 86 ColumnLayout { anchors.fill: parent; anchors.margins: 11; spacing: 3 Label { Layout.fillWidth: true; text: modelData.title; color: app.textMuted; font.pixelSize: 9; elide: Text.ElideRight } Label { Layout.fillWidth: true; text: modelData.value; color: app.textMain; font.pixelSize: 13; font.bold: true; maximumLineCount: 2; wrapMode: Text.WordWrap; elide: Text.ElideRight } } } } } SurfaceCard { Layout.fillWidth: true; implicitHeight: 292 ColumnLayout { anchors.fill: parent; anchors.margins: 13; spacing: 8 RowLayout { Layout.fillWidth: true ColumnLayout { Layout.fillWidth: true; spacing: 1 Label { text: "Замеры V-силуэта"; color: app.textMain; font.pixelSize: 16; font.bold: true } Label { text: "Сантиметры • плечи ÷ талия"; color: app.textMuted; font.pixelSize: 9 } } Label { text: sessionController.nutritionMeasurementEntries.length > 0 ? sessionController.nutritionSummary.ratioText : "После первого замера"; color: app.blue; font.pixelSize: 11; font.bold: true } } GridLayout { Layout.fillWidth: true; columns: 2; rowSpacing: 7; columnSpacing: 7 TextField { id: waistInput; Layout.fillWidth: true; implicitHeight: 44; placeholderText: "Талия, см"; maximumLength: 6; inputMethodHints: Qt.ImhFormattedNumbersOnly } TextField { id: shouldersInput; Layout.fillWidth: true; implicitHeight: 44; placeholderText: "Плечи, см"; maximumLength: 6; inputMethodHints: Qt.ImhFormattedNumbersOnly } TextField { id: chestInput; Layout.fillWidth: true; implicitHeight: 44; placeholderText: "Грудь, см"; maximumLength: 6; inputMethodHints: Qt.ImhFormattedNumbersOnly } TextField { id: armInput; Layout.fillWidth: true; implicitHeight: 44; placeholderText: "Рука, см"; maximumLength: 6; inputMethodHints: Qt.ImhFormattedNumbersOnly } } Label { Layout.fillWidth: true; text: "Измеряйте в одинаковых условиях, без тренировки перед замером."; color: app.textMuted; font.pixelSize: 9; wrapMode: Text.WordWrap } PrimaryButton { Layout.fillWidth: true; implicitHeight: 46; text: "Сохранить замеры" enabled: waistInput.text.length > 0 && shouldersInput.text.length > 0 && chestInput.text.length > 0 && armInput.text.length > 0 onClicked: sessionController.logBodyMeasurements(waistInput.text, shouldersInput.text, chestInput.text, armInput.text) } } } Label { visible: sessionController.nutritionMeasurementEntries.length > 0; text: "Последние замеры"; color: app.textMain; font.pixelSize: 14; font.bold: true } Repeater { model: sessionController.nutritionMeasurementEntries.slice(0, 3) delegate: SurfaceCard { required property var modelData; Layout.fillWidth: true; implicitHeight: 82 ColumnLayout { anchors.fill: parent; anchors.margins: 11; spacing: 3 RowLayout { Layout.fillWidth: true Label { Layout.fillWidth: true; text: modelData.loggedAt; color: app.textMuted; font.pixelSize: 9 } Label { text: "V " + modelData.ratioText; color: app.blue; font.pixelSize: 10; font.bold: true } } Label { Layout.fillWidth: true; text: "Талия " + modelData.waistText + " • плечи " + modelData.shouldersText; color: app.textMain; font.pixelSize: 11; elide: Text.ElideRight } Label { Layout.fillWidth: true; text: "Грудь " + modelData.chestText + " • рука " + modelData.armText; color: app.textMuted; font.pixelSize: 10; elide: Text.ElideRight } } } } Label { text: "Рацион"; color: app.textMain; font.pixelSize: 16; font.bold: true } ComboBox { id: nutritionPresetBox Layout.fillWidth: true; implicitHeight: 44 textRole: "label"; valueRole: "value" model: [ { label: "Сбалансированный", value: "balanced" }, { label: "Бюджетный", value: "cheap" }, { label: "При слабом аппетите", value: "low-appetite" } ] currentIndex: indexOfValue(sessionController.nutritionSummary.presetText) onActivated: sessionController.selectNutritionPreset(currentValue) } Label { Layout.fillWidth: true; text: "Как прошёл рацион сегодня?"; color: app.textMuted; font.pixelSize: 11 } RowLayout { Layout.fillWidth: true; spacing: 6 Button { Layout.fillWidth: true; implicitHeight: 44; text: "Выполнен"; onClicked: sessionController.markNutritionAdherence("good") } Button { Layout.fillWidth: true; implicitHeight: 44; text: "Частично"; onClicked: sessionController.markNutritionAdherence("partial") } Button { Layout.fillWidth: true; implicitHeight: 44; text: "Срыв"; onClicked: sessionController.markNutritionAdherence("off") } } SurfaceCard { Layout.fillWidth: true; implicitHeight: 300 ColumnLayout { anchors.fill: parent; anchors.margins: 13; spacing: 8 Label { text: "Восстановление"; color: app.textMain; font.pixelSize: 16; font.bold: true } Label { Layout.fillWidth: true; text: sessionController.nutritionSummary.recoveryText; color: app.textMuted; font.pixelSize: 10; wrapMode: Text.WordWrap } Repeater { model: [ { label: "Энергия", key: "energy" }, { label: "Сон", key: "sleep" }, { label: "Суставы", key: "joints" } ] delegate: RowLayout { required property var modelData Layout.fillWidth: true; spacing: 4 Label { Layout.preferredWidth: 64; text: modelData.label; color: app.textMuted; font.pixelSize: 10 } Repeater { model: 5 delegate: Button { required property int index property int selectedValue: modelData.key === "energy" ? app.recoveryEnergy : modelData.key === "sleep" ? app.recoverySleep : app.recoveryJoints Layout.fillWidth: true; implicitHeight: 40; text: index + 1 font.bold: selectedValue === index + 1 onClicked: { if (modelData.key === "energy") app.recoveryEnergy = index + 1 else if (modelData.key === "sleep") app.recoverySleep = index + 1 else app.recoveryJoints = index + 1 } background: Rectangle { radius: 10; color: parent.selectedValue === index + 1 ? app.selectedSurface : app.surface; border.color: parent.selectedValue === index + 1 ? app.blue : app.border } } } } } TextField { id: recoveryNote; Layout.fillWidth: true; implicitHeight: 42; placeholderText: "Заметка — необязательно"; maximumLength: 240 } PrimaryButton { Layout.fillWidth: true; implicitHeight: 46; text: "Сохранить восстановление" onClicked: { sessionController.logRecoveryCheckIn(app.recoveryEnergy, app.recoverySleep, app.recoveryJoints, recoveryNote.text) recoveryNote.clear() } } } } Label { text: "Пример рациона"; color: app.textMain; font.pixelSize: 16; font.bold: true } Repeater { model: sessionController.nutritionMeals; delegate: SurfaceCard { required property var modelData; Layout.fillWidth: true; implicitHeight: 94 ColumnLayout { anchors.fill: parent; anchors.margins: 11; spacing: 3 Label { Layout.fillWidth: true; text: modelData.title; color: app.textMain; font.pixelSize: 12; font.bold: true } Label { Layout.fillWidth: true; text: modelData.description; color: app.textMuted; font.pixelSize: 9; maximumLineCount: 2; wrapMode: Text.WordWrap; elide: Text.ElideRight } Label { Layout.fillWidth: true; text: modelData.macroText; color: app.blue; font.pixelSize: 9 } } } } Label { text: "Настройки приложения"; color: app.textMain; font.pixelSize: 16; font.bold: true } SurfaceCard { Layout.fillWidth: true implicitHeight: 88 RowLayout { anchors.fill: parent; anchors.margins: 13; spacing: 11 Rectangle { Layout.preferredWidth: 46; Layout.preferredHeight: 46 radius: 13 color: sessionController.soundsEnabled ? app.selectedSurface : app.surface border.color: sessionController.soundsEnabled ? app.blue : app.border Behavior on color { ColorAnimation { duration: 160 } } Label { anchors.centerIn: parent text: sessionController.soundsEnabled ? "♪" : "×" color: sessionController.soundsEnabled ? app.blue : app.textMuted font.pixelSize: 23; font.bold: true } } ColumnLayout { Layout.fillWidth: true; spacing: 2 Label { text: "Звуки и вибрация"; color: app.textMain; font.pixelSize: 14; font.bold: true } Label { Layout.fillWidth: true text: sessionController.soundsEnabled ? "Сигналы старта, отдыха и завершения включены" : "Включите сигналы для мобильной тренировки" color: app.textMuted; font.pixelSize: 9; wrapMode: Text.WordWrap } } Switch { id: workoutSoundSwitch Layout.preferredWidth: 56; Layout.preferredHeight: 48 checked: sessionController.soundsEnabled onToggled: sessionController.soundsEnabled = checked contentItem: Item { } indicator: Rectangle { x: (workoutSoundSwitch.width - width) / 2 y: (workoutSoundSwitch.height - height) / 2 width: 52; height: 30; radius: 15 scale: workoutSoundSwitch.down ? 0.96 : 1 color: workoutSoundSwitch.checked ? app.blue : app.surface border.color: workoutSoundSwitch.checked ? app.blue : app.border Behavior on scale { NumberAnimation { duration: 140; easing.type: Easing.OutCubic } } Behavior on color { ColorAnimation { duration: 160 } } Rectangle { width: 22; height: 22; radius: 11 y: 4 x: workoutSoundSwitch.checked ? parent.width - width - 4 : 4 color: workoutSoundSwitch.checked ? app.textMain : app.textMuted Behavior on x { NumberAnimation { duration: 160; easing.type: Easing.OutCubic } } } } } } } SurfaceCard { Layout.fillWidth: true implicitHeight: 184 ColumnLayout { anchors.fill: parent; anchors.margins: 13; spacing: 9 RowLayout { Layout.fillWidth: true ColumnLayout { Layout.fillWidth: true; spacing: 2 Label { text: "Напоминание"; color: app.textMain; font.pixelSize: 16; font.bold: true } Label { text: "Мотивация по пропускам + один повтор через 90 минут"; color: app.textMuted; font.pixelSize: 9 } } Switch { id: workoutReminderSwitch implicitWidth: 52; implicitHeight: 44 checked: sessionController.workoutReminderEnabled } } RowLayout { Layout.fillWidth: true; spacing: 8 Label { text: "Время"; color: app.textMuted; font.pixelSize: 11 } Item { Layout.fillWidth: true } ComboBox { id: reminderHourBox Layout.preferredWidth: 82; implicitHeight: 44 model: app.timeLabels(24) currentIndex: Math.max(0, Number(sessionController.workoutReminderTime.substring(0, 2))) } Label { text: ":"; color: app.textMain; font.pixelSize: 18; font.bold: true } ComboBox { id: reminderMinuteBox Layout.preferredWidth: 82; implicitHeight: 44 model: app.timeLabels(60) currentIndex: Math.max(0, Number(sessionController.workoutReminderTime.substring(3, 5))) } } PrimaryButton { Layout.fillWidth: true; implicitHeight: 44; text: "Сохранить напоминание" onClicked: sessionController.setWorkoutReminderSettings( workoutReminderSwitch.checked, reminderHourBox.currentText + ":" + reminderMinuteBox.currentText) } } } Label { text: "Данные"; color: app.textMain; font.pixelSize: 16; font.bold: true } RowLayout { Layout.fillWidth: true; spacing: 8 Button { Layout.fillWidth: true; implicitHeight: 44; text: "Создать копию"; onClicked: sessionController.createLocalBackup() } Button { Layout.fillWidth: true; implicitHeight: 44; text: "Восстановить"; onClicked: sessionController.restoreLatestBackup() } } Button { Layout.fillWidth: true; implicitHeight: 46; text: "Перенос JSON: Android ↔ Windows"; onClicked: sessionController.openAndroidDataTransfer() } Item { Layout.fillWidth: true; implicitHeight: 12 } } } ColumnLayout { spacing: 8 TextField { Layout.fillWidth: true placeholderText: "Поиск упражнения" onTextChanged: sessionController.exerciseSearch = text } RowLayout { Layout.fillWidth: true; spacing: 8 ComboBox { id: metricFilter Layout.fillWidth: true textRole: "label"; valueRole: "value" model: [ { label: "Все упражнения", value: "all" }, { label: "Повторы", value: "repetitions" }, { label: "На время", value: "seconds" } ] onActivated: sessionController.exerciseMetricFilter = currentValue } Label { text: sessionController.exerciseLibrary.length; color: app.textMuted; font.pixelSize: 12 } } ListView { Layout.fillWidth: true; Layout.fillHeight: true spacing: 8; clip: true model: sessionController.exerciseLibrary delegate: SurfaceCard { required property var modelData width: ListView.view.width; height: 104 RowLayout { anchors.fill: parent; anchors.margins: 10; spacing: 10 Rectangle { Layout.preferredWidth: 78; Layout.fillHeight: true radius: 12; color: app.surface; clip: true Image { anchors.fill: parent; anchors.margins: 4 source: modelData.frameUrls.length > 0 ? modelData.frameUrls[0] : "" fillMode: Image.PreserveAspectFit; asynchronous: true } } ColumnLayout { Layout.fillWidth: true; spacing: 3 Label { Layout.fillWidth: true; text: modelData.name; color: app.textMain; font.pixelSize: 14; font.bold: true; elide: Text.ElideRight } Label { Layout.fillWidth: true; text: modelData.category + " • " + modelData.equipment; color: app.blue; font.pixelSize: 10; elide: Text.ElideRight } Label { Layout.fillWidth: true; text: modelData.description; color: app.textMuted; font.pixelSize: 10; maximumLineCount: 2; wrapMode: Text.WordWrap; elide: Text.ElideRight } Label { text: modelData.defaultTarget + " " + modelData.metricText + " • отдых " + modelData.defaultRestSeconds + "с"; color: app.textMuted; font.pixelSize: 9 } } } } } } } Label { Layout.fillWidth: true; text: sessionController.status; color: app.textMuted; font.pixelSize: 10; elide: Text.ElideRight } } } Rectangle { Layout.fillWidth: true Layout.preferredHeight: 68 color: app.panel radius: 18 border.color: app.border RowLayout { anchors.fill: parent; anchors.margins: 5; spacing: 2 MobileNavButton { tabIndex: 0; mark: "○"; navLabel: "Сегодня" } MobileNavButton { tabIndex: 1; mark: "≡"; navLabel: "Планы" } MobileNavButton { tabIndex: 2; mark: "↗"; navLabel: "Прогресс" } MobileNavButton { tabIndex: 3; mark: "◇"; navLabel: "Часы" } MobileNavButton { tabIndex: 4; mark: "▦"; navLabel: "База" } } } } }