/
DemienMedich
/
BodyweightBase
Обзор
Документация
Войти
/
DemienMedich
/
BodyweightBase
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
develop
app/Main.qml
1 669 строк
133 KB
DemienMedich
feat: add profile weekly goals
03 авг 2026, 01:15
03 авг 2026, 01:15
a8f4bdb
Код
Авторство
О чём код?
import QtQuick import QtQuick.Controls import QtQuick.Dialogs import QtQuick.Layouts ApplicationWindow { id: window width: 1180 height: 760 minimumWidth: 900 minimumHeight: 560 visible: true title: "BodyweightBase C++" color: bg readonly property color bg: "#08111D" property int currentPage: 0 property int exerciseFrameIndex: 0 property int exerciseFrameDirection: 1 property string exerciseFrameKey: "" property var selectedExerciseDetails: ({}) property int selectedPlanStepIndex: 0 property var selectedProgressExercise: ({}) property string lastPhase: "" property bool loading: true property bool startupPlanPromptShown: false property bool cycleShowHalfYear: false function exerciseFrameDuration(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 nextExerciseFrameIndex() { const count = sessionController.currentExerciseFrameUrls.length if (count < 2 || sessionController.currentExerciseFramePlayback === "static") return 0 if (sessionController.currentExerciseFramePlayback === "pingPong") { const candidate = window.exerciseFrameIndex + window.exerciseFrameDirection if (candidate >= count) return Math.max(0, count - 2) if (candidate < 0) return Math.min(count - 1, 1) return candidate } return (window.exerciseFrameIndex + 1) % count } function advanceExerciseFrame() { const count = sessionController.currentExerciseFrameUrls.length if (count < 2 || sessionController.currentExerciseFramePlayback === "static") return if (sessionController.currentExerciseFramePlayback === "pingPong") { let candidate = window.exerciseFrameIndex + window.exerciseFrameDirection if (candidate >= count) { window.exerciseFrameDirection = -1 candidate = Math.max(0, count - 2) } else if (candidate < 0) { window.exerciseFrameDirection = 1 candidate = Math.min(count - 1, 1) } window.exerciseFrameIndex = candidate return } window.exerciseFrameIndex = (window.exerciseFrameIndex + 1) % count } readonly property color panel: "#101C2B" readonly property color border: "#26384D" readonly property color textMain: "#F1F5F9" readonly property color textMuted: "#9FB0C3" readonly property color accent: "#22C55E" readonly property color accentHover: "#1EA34F" readonly property color accentPressed: "#16803D" readonly property color blue: "#38BDF8" readonly property color surface: "#0C1724" readonly property color imageSurface: "#07111C" readonly property color selectedSurface: "#19334A" readonly property color selectedStepSurface: "#132B42" readonly property color hoverSurface: "#111F30" readonly property color warmupSurface: "#1A3322" readonly property color cooldownSurface: "#331A22" readonly property color warning: "#F59E0B" readonly property color error: "#FCA5A5" readonly property color accentText: "#04120A" readonly property color imageOutline: Qt.rgba(1, 1, 1, 0.10) 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 palette.light: hoverSurface palette.dark: bg function syncSelectedExerciseDetails() { const library = sessionController.exerciseLibrary if (library.length === 0) { selectedExerciseDetails = ({}); return } for (let i = 0; i < library.length; ++i) { if (selectedExerciseDetails.id && library[i].id === selectedExerciseDetails.id) { selectedExerciseDetails = library[i]; return } } selectedExerciseDetails = library[0] } function exerciseAtComboIndex(index) { const library = sessionController.exerciseLibrary return index >= 0 && index < library.length ? library[index] : ({}) } function shouldReplaceStepNote(currentNote, oldCue, oldDescription) { const note = (currentNote || "").trim() return note.length === 0 || note === (oldCue || "").trim() || note === (oldDescription || "").trim() } function selectedPlanStepDetails() { const steps = sessionController.selectedPlanSteps return selectedPlanStepIndex >= 0 && selectedPlanStepIndex < steps.length ? steps[selectedPlanStepIndex] : ({}) } function syncSelectedPlanStep() { const steps = sessionController.selectedPlanSteps if (steps.length === 0) { selectedPlanStepIndex = 0; return } if (selectedPlanStepIndex < 0) selectedPlanStepIndex = 0 if (selectedPlanStepIndex >= steps.length) selectedPlanStepIndex = steps.length - 1 } function syncSelectedProgressExercise() { const buckets = sessionController.progressExerciseBuckets if (buckets.length === 0) { selectedProgressExercise = ({}); return } for (let i = 0; i < buckets.length; ++i) { if (selectedProgressExercise.exerciseId && buckets[i].exerciseId === selectedProgressExercise.exerciseId) { selectedProgressExercise = buckets[i]; return } } selectedProgressExercise = buckets[0] } function updateSelectedPlanFlowOptions() { sessionController.updateSelectedPlanFlowOptions( warmupSwitch.checked, warmupStepCount.value, cooldownSwitch.checked, cooldownStepCount.value) } function currentDayOfWeek() { const jsDay = new Date().getDay() return jsDay === 0 ? 7 : jsDay } function currentDayName() { return ["", "понедельник", "вторник", "среду", "четверг", "пятницу", "субботу", "воскресенье"][currentDayOfWeek()] } function planAtIndex(index) { const allPlans = sessionController.plans return index >= 0 && index < allPlans.length ? allPlans[index] : ({}) } function scheduledPlanForToday() { const allPlans = sessionController.plans const today = currentDayOfWeek() for (let i = 0; i < allPlans.length; ++i) { if (allPlans[i].dayOfWeek === today) return allPlans[i] } return ({}) } function openStartupWorkoutDialog() { if (startupPlanPromptShown || sessionController.active || sessionController.needsSave || sessionController.hasRecoverableDraft || sessionController.plans.length === 0) return startupPlanPromptShown = true startupWorkoutDialog.open() } function visibleCycleWeeks() { const weeks = sessionController.trainingCycleWeeks if (cycleShowHalfYear || weeks.length <= 4) return weeks const current = Math.max(0, (sessionController.trainingCycle.currentWeek || 1) - 1) const start = Math.max(0, Math.min(current, weeks.length - 4)) return weeks.slice(start, start + 4) } function recentWearableDays() { const days = sessionController.wearableDays return days.length > 14 ? days.slice(days.length - 14) : days } function wearableMaximum(key) { const days = recentWearableDays() let maximum = 1 for (let i = 0; i < days.length; ++i) maximum = Math.max(maximum, Number(days[i][key] || 0)) return maximum } component ActionButton: Button { id: control implicitHeight: 48 scale: control.down ? 0.96 : 1.0 font.pixelSize: 15 font.weight: Font.DemiBold focusPolicy: Qt.StrongFocus Behavior on scale { NumberAnimation { duration: 150; easing.type: Easing.OutCubic } } background: Rectangle { radius: 12 color: control.down ? window.accentPressed : control.hovered ? window.accentHover : window.accent border.color: control.activeFocus ? window.blue : "transparent" border.width: 2 } contentItem: Text { text: control.text color: window.accentText font: control.font horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } } // Splash screen Rectangle { id: splash anchors.fill: parent z: 200 visible: window.loading color: window.bg ColumnLayout { anchors.centerIn: parent spacing: 16 Label { Layout.alignment: Qt.AlignHCenter; text: "BODYWEIGHT"; color: window.blue; font.pixelSize: 16; font.bold: true } Label { Layout.alignment: Qt.AlignHCenter; text: "Base"; color: window.textMain; font.pixelSize: 48; font.bold: true } Label { Layout.alignment: Qt.AlignHCenter; text: "C++ / Qt Quick"; color: window.textMuted; font.pixelSize: 14 } BusyIndicator { Layout.alignment: Qt.AlignHCenter; running: true; implicitWidth: 40; implicitHeight: 40 } } Timer { interval: 1500; running: true; onTriggered: window.loading = false } } Timer { interval: 1000; running: sessionController.active && !sessionController.paused; repeat: true; onTriggered: sessionController.advanceOneSecond() } Timer { interval: window.exerciseFrameDuration(window.exerciseFrameIndex) running: sessionController.active && !sessionController.paused && sessionController.currentExerciseFrameUrls.length > 1 && sessionController.currentExerciseFramePlayback !== "static" repeat: true onTriggered: window.advanceExerciseFrame() } 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[window.nextExerciseFrameIndex()] : "" } Connections { target: sessionController function onChanged() { const nextFrameKey = sessionController.currentStepIndex + ":" + (sessionController.currentExerciseFrameUrls.length > 0 ? sessionController.currentExerciseFrameUrls[0] : "") if (window.exerciseFrameKey !== nextFrameKey) { window.exerciseFrameKey = nextFrameKey window.exerciseFrameIndex = 0 window.exerciseFrameDirection = 1 } if (window.exerciseFrameIndex >= sessionController.currentExerciseFrameUrls.length) { window.exerciseFrameIndex = 0 window.exerciseFrameDirection = 1 } if (sessionController.needsSave && !saveDialog.opened) saveDialog.open() if (sessionController.hasRecoverableDraft && !restoreDialog.opened) restoreDialog.open() window.syncSelectedPlanStep() var phase = sessionController.phaseTitle if (phase !== window.lastPhase) { if (phase === "Подготовка" || phase === "Повторения" || phase === "Рабочий таймер") sessionController.playBeep(880, 150) else if (phase === "Отдых") sessionController.playBeep(660, 150) else if (phase === "Тренировка завершена") sessionController.playBeep(1200, 200) window.lastPhase = phase } } } Component.onCompleted: { window.syncSelectedPlanStep() if (sessionController.hasRecoverableDraft) restoreDialog.open() else if (sessionController.totalSessions === 0) welcomeDialog.open() else window.openStartupWorkoutDialog() } // Navigation shortcuts Shortcut { sequence: "1"; onActivated: window.currentPage = 0 } Shortcut { sequence: "2"; onActivated: window.currentPage = 1 } Shortcut { sequence: "3"; onActivated: window.currentPage = 2 } Shortcut { sequence: "4"; onActivated: window.currentPage = 3 } Shortcut { sequence: "5"; onActivated: window.currentPage = 4 } Shortcut { sequence: "6"; onActivated: window.currentPage = 5 } Shortcut { sequence: "7"; onActivated: window.currentPage = 6 } Shortcut { sequence: "F"; enabled: window.currentPage === 0 && (sessionController.ready || sessionController.active) && !focusOverlay.visible; onActivated: focusOverlay.open() } // Focus mode shortcuts Shortcut { sequence: "Space"; enabled: focusOverlay.visible && (sessionController.ready || sessionController.active); onActivated: sessionController.active ? sessionController.completeCurrent() : sessionController.start() } Shortcut { sequence: "P"; enabled: focusOverlay.visible && sessionController.active; onActivated: sessionController.togglePause() } Shortcut { sequence: "S"; enabled: focusOverlay.visible && sessionController.active; onActivated: sessionController.skipCurrentExercise() } Shortcut { sequence: "+"; enabled: focusOverlay.visible && sessionController.active; onActivated: sessionController.addRep() } Shortcut { sequence: "-"; enabled: focusOverlay.visible && sessionController.active; onActivated: sessionController.removeRep() } Shortcut { sequence: "Esc"; enabled: focusOverlay.visible; onActivated: focusOverlay.close() } // Welcome dialog Dialog { id: welcomeDialog anchors.centerIn: parent width: 500 modal: true title: "Добро пожаловать!" closePolicy: Popup.NoAutoClose standardButtons: Dialog.Ok background: Rectangle { color: window.panel; radius: 12; border.color: window.border } contentItem: ColumnLayout { spacing: 12 Label { Layout.fillWidth: true; text: "BodyweightBase — тренировочный ассистент"; color: window.textMain; font.pixelSize: 20; font.bold: true; wrapMode: Text.WordWrap } Label { Layout.fillWidth: true; text: "Быстрый старт:"; color: window.blue; font.pixelSize: 14; font.bold: true } Label { Layout.fillWidth: true; text: "1. Выбери план в разделе «Планы»\n2. Настрой упражнения\n3. Нажми «Начать»\n4. Оцени результат"; color: window.textMain; font.pixelSize: 13; wrapMode: Text.WordWrap } Label { Layout.fillWidth: true; text: "Горячие клавиши: 1-5 навигация, F фокус"; color: window.textMuted; font.pixelSize: 12; wrapMode: Text.WordWrap } } onAccepted: window.openStartupWorkoutDialog() } Dialog { id: startupWorkoutDialog anchors.centerIn: parent width: 520 modal: true title: "Тренировка на сегодня" closePolicy: Popup.CloseOnEscape property var recommendedPlan: ({}) property var chosenPlan: window.planAtIndex(startupPlanCombo.currentIndex) property int chosenRoundCount: Math.max(1, startupRoundCount.value) background: Rectangle { color: window.panel; radius: 14; border.color: window.border } onOpened: { recommendedPlan = window.scheduledPlanForToday() const preferredId = recommendedPlan.id || sessionController.selectedPlanId const preferredIndex = startupPlanCombo.indexOfValue(preferredId) startupPlanCombo.currentIndex = preferredIndex >= 0 ? preferredIndex : 0 startupRoundCount.value = Math.max(1, Math.min(6, startupWorkoutDialog.chosenPlan.roundCount || 1)) } contentItem: ColumnLayout { spacing: 14 Label { Layout.fillWidth: true text: startupWorkoutDialog.recommendedPlan.id ? "На " + window.currentDayName() + " по расписанию:" : "На " + window.currentDayName() + " план не назначен. Выберите тренировку:" color: window.textMuted font.pixelSize: 13 wrapMode: Text.WordWrap } Label { Layout.fillWidth: true visible: !!startupWorkoutDialog.recommendedPlan.id text: startupWorkoutDialog.recommendedPlan.name || "" color: window.textMain font.pixelSize: 22 font.bold: true wrapMode: Text.WordWrap } Label { text: "При желании выберите другую"; color: window.blue; font.pixelSize: 12; font.bold: true } ComboBox { id: startupPlanCombo Layout.fillWidth: true textRole: "name" valueRole: "id" model: sessionController.plans focusPolicy: Qt.StrongFocus onActivated: startupRoundCount.value = Math.max(1, Math.min(6, startupWorkoutDialog.chosenPlan.roundCount || 1)) } Rectangle { Layout.fillWidth: true implicitHeight: 72 radius: 12 color: window.surface border.color: window.border ColumnLayout { anchors.fill: parent anchors.margins: 10 spacing: 3 Label { Layout.fillWidth: true; text: startupWorkoutDialog.chosenPlan.goal || "Без описания цели"; color: window.textMain; font.pixelSize: 13; elide: Text.ElideRight } Label { text: (startupWorkoutDialog.chosenPlan.stepCount || 0) + " упражнений"; color: window.textMuted; font.pixelSize: 12 } } } RowLayout { Layout.fillWidth: true spacing: 10 Label { Layout.fillWidth: true; text: "Круги тренировки"; color: window.textMain; font.pixelSize: 13; font.bold: true } SpinBox { id: startupRoundCount from: 1 to: 6 value: 1 editable: false focusPolicy: Qt.StrongFocus } } RowLayout { Layout.fillWidth: true spacing: 8 Button { Layout.fillWidth: true; text: "Не сейчас"; focusPolicy: Qt.StrongFocus; onClicked: startupWorkoutDialog.close() } ActionButton { Layout.fillWidth: true text: "Начать" enabled: !!startupWorkoutDialog.chosenPlan.id onClicked: { sessionController.selectPlan(startupWorkoutDialog.chosenPlan.id) window.currentPage = 0 startupWorkoutDialog.close() sessionController.startWithRoundCount(startupWorkoutDialog.chosenRoundCount) } } } } } component SidebarIconButton: Button { id: iconControl property string glyph: "" property string description: "" implicitHeight: 44 implicitWidth: 44 scale: iconControl.down ? 0.96 : 1.0 focusPolicy: Qt.StrongFocus Behavior on scale { NumberAnimation { duration: 150; easing.type: Easing.OutCubic } } ToolTip { text: iconControl.description; visible: iconControl.hovered; delay: 500 } background: Rectangle { radius: 10 color: iconControl.down ? window.selectedSurface : iconControl.hovered ? window.hoverSurface : window.surface border.color: iconControl.activeFocus ? window.blue : window.border } contentItem: Text { text: iconControl.glyph color: iconControl.hovered || iconControl.activeFocus ? window.blue : window.textMuted font.family: "Segoe MDL2 Assets" font.pixelSize: 18 horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } } component ThemedProgressBar: ProgressBar { id: progressControl implicitHeight: 8 background: Rectangle { radius: 4; color: window.surface; border.color: window.border } contentItem: Item { implicitHeight: 8 Rectangle { width: parent.width * progressControl.visualPosition height: parent.height radius: 4 color: window.blue } } } // Restore dialog Dialog { id: restoreDialog; anchors.centerIn: parent; width: 420; modal: true; title: "Продолжить тренировку?"; closePolicy: Popup.NoAutoClose; standardButtons: Dialog.Yes | Dialog.Discard Label { width: parent.width; text: "Найден черновик.\n" + sessionController.recoverableDraftSummary; color: window.textMain; wrapMode: Text.WordWrap } onAccepted: sessionController.restoreDraft() onDiscarded: { sessionController.discardDraft(); Qt.callLater(window.openStartupWorkoutDialog) } } Dialog { id: replacementDialog anchors.centerIn: parent; width: 520; modal: true title: "Заменить упражнение" standardButtons: Dialog.Ok | Dialog.Cancel ColumnLayout { width: parent.width; spacing: 12 Label { Layout.fillWidth: true; text: "Цель и отдых сохранятся. Текущий незавершённый подход будет сброшен."; color: window.textMuted; wrapMode: Text.WordWrap } ComboBox { id: replacementBox Layout.fillWidth: true; implicitHeight: 44 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: window.blue; font.pixelSize: 11; elide: Text.ElideRight } } onOpened: replacementBox.currentIndex = 0 onAccepted: sessionController.replaceCurrentExercise(replacementBox.currentValue) } // About dialog Dialog { id: aboutDialog; anchors.centerIn: parent; width: 560; modal: true; title: "О программе"; standardButtons: Dialog.Close ColumnLayout { width: parent.width; spacing: 10 Label { Layout.fillWidth: true; text: sessionController.applicationInfo.applicationName + " " + sessionController.applicationInfo.applicationVersion; color: window.textMain; font.pixelSize: 20; font.bold: true; wrapMode: Text.WordWrap } Label { Layout.fillWidth: true; text: sessionController.applicationInfo.uiStack + " • Qt " + sessionController.applicationInfo.qtVersion; color: window.blue; font.pixelSize: 13; wrapMode: Text.WordWrap } Label { Layout.fillWidth: true; text: "Данные: " + sessionController.applicationInfo.storageDirectory; color: window.textMuted; font.pixelSize: 12; wrapMode: Text.WrapAnywhere } } } // Export/Import dialogs FileDialog { id: exportDataDialog; title: "Экспорт JSON"; fileMode: FileDialog.SaveFile; defaultSuffix: "json"; nameFilters: ["JSON (*.json)"]; onAccepted: sessionController.exportLocalData(selectedFile.toString()) } FileDialog { id: csvExportDialog; title: "Экспорт CSV"; fileMode: FileDialog.SaveFile; defaultSuffix: "csv"; nameFilters: ["CSV (*.csv)"]; onAccepted: sessionController.exportCsv(selectedFile.toString()) } FileDialog { id: exportPlanDialog; title: "Экспорт плана"; fileMode: FileDialog.SaveFile; defaultSuffix: "json"; nameFilters: ["JSON (*.json)"]; onAccepted: sessionController.exportSelectedPlan(selectedFile.toString()) } FileDialog { id: importPlanDialog; title: "Импорт плана"; fileMode: FileDialog.OpenFile; nameFilters: ["JSON (*.json)"]; onAccepted: sessionController.importPlan(selectedFile.toString()) } FileDialog { id: importHealthDialog; title: "Импорт Health Connect"; fileMode: FileDialog.OpenFile; nameFilters: ["Health Connect JSON (*.json)"]; onAccepted: sessionController.importHealthConnectData(selectedFile.toString()) } // Clear data dialog Dialog { id: clearPersonalDataDialog; anchors.centerIn: parent; width: 520; modal: true; title: "Очистить записи?"; standardButtons: Dialog.Yes | Dialog.No Label { width: parent.width; text: "Удалены история, черновик, вес, замеры, питание, recovery, фото. Планы и каталог останутся."; color: window.textMain; wrapMode: Text.WordWrap } onAccepted: sessionController.clearPersonalData() } // Settings dialog Dialog { id: settingsDialog; anchors.centerIn: parent; width: 500; modal: true; title: "Настройки"; standardButtons: Dialog.Close ColumnLayout { width: parent.width; spacing: 16 Label { text: "Хранение данных"; color: window.blue; font.pixelSize: 14; font.bold: true } Rectangle { Layout.fillWidth: true; implicitHeight: 60; radius: 12; color: window.surface; border.color: window.border ColumnLayout { anchors.fill: parent; anchors.margins: 12; spacing: 4 Label { text: "Текущая папка:"; color: window.textMuted; font.pixelSize: 11 } Label { text: sessionController.applicationInfo.storageDirectory; color: window.textMain; font.pixelSize: 13; wrapMode: Text.WrapAnywhere } } } Label { text: "Настройки приложения"; color: window.blue; font.pixelSize: 14; font.bold: true } RowLayout { Layout.fillWidth: true; spacing: 12 Label { Layout.fillWidth: true; text: "Звуки фаз"; color: window.textMain; font.pixelSize: 14 } Switch { checked: sessionController.soundsEnabled; onToggled: sessionController.soundsEnabled = checked } } Label { text: "Данные"; color: window.blue; font.pixelSize: 14; font.bold: true } RowLayout { Layout.fillWidth: true; spacing: 8 Button { Layout.fillWidth: true; text: "Экспорт JSON"; focusPolicy: Qt.StrongFocus; onClicked: { settingsDialog.close(); exportDataDialog.open() } } Button { Layout.fillWidth: true; text: "Экспорт CSV"; focusPolicy: Qt.StrongFocus; onClicked: { settingsDialog.close(); csvExportDialog.open() } } } Button { Layout.fillWidth: true; text: "Очистить данные"; focusPolicy: Qt.StrongFocus; enabled: !sessionController.active; onClicked: { settingsDialog.close(); clearPersonalDataDialog.open() } } } } // Save dialog Dialog { id: saveDialog; anchors.centerIn: parent; width: 620; modal: true; title: "Результат"; closePolicy: Popup.NoAutoClose; standardButtons: Dialog.Save | Dialog.Discard property var details: sessionController.pendingResultDetails() ColumnLayout { width: parent.width; spacing: 10 Rectangle { Layout.fillWidth: true; implicitHeight: 86; radius: 18; color: window.panel; border.color: saveDialog.details.resultStatus === "complete" ? window.accent : window.warning ColumnLayout { anchors.fill: parent; anchors.margins: 14; spacing: 4 Label { Layout.fillWidth: true; text: saveDialog.details.resultTitle || "Тренировка завершена"; color: saveDialog.details.resultStatus === "complete" ? window.accent : window.warning; font.pixelSize: 18; font.bold: true; wrapMode: Text.WordWrap } Label { Layout.fillWidth: true; text: saveDialog.details.planName || ""; color: window.textMain; font.pixelSize: 13; elide: Text.ElideRight } } } RowLayout { Layout.fillWidth: true; spacing: 8 Rectangle { Layout.fillWidth: true; implicitHeight: 68; radius: 12; color: window.surface; border.color: window.border ColumnLayout { anchors.fill: parent; anchors.margins: 8; spacing: 2 Label { text: "Итого"; color: window.textMuted; font.pixelSize: 10 } Label { text: (saveDialog.details.totalMinutes || 0) + " мин"; color: window.textMain; font.pixelSize: 16; font.bold: true } } } Rectangle { Layout.fillWidth: true; implicitHeight: 68; radius: 12; color: window.surface; border.color: window.border ColumnLayout { anchors.fill: parent; anchors.margins: 8; spacing: 2 Label { text: "Работа / Отдых"; color: window.textMuted; font.pixelSize: 10 } Label { text: saveDialog.details.workRestText || "0 / 0 мин"; color: window.textMain; font.pixelSize: 16; font.bold: true } } } Rectangle { Layout.fillWidth: true; implicitHeight: 68; radius: 12; color: window.surface; border.color: window.border ColumnLayout { anchors.fill: parent; anchors.margins: 8; spacing: 2 Label { text: "Завершение"; color: window.textMuted; font.pixelSize: 10 } Label { text: (saveDialog.details.completionRate || 0) + "%"; color: saveDialog.details.resultStatus === "complete" ? window.accent : window.warning; font.pixelSize: 16; font.bold: true } } } } RowLayout { Layout.fillWidth: true; spacing: 8 Rectangle { Layout.fillWidth: true; implicitHeight: 54; radius: 12; color: window.surface; border.color: window.border Label { anchors.fill: parent; anchors.margins: 10; text: saveDialog.details.completedText || ""; color: window.textMain; font.pixelSize: 13; font.bold: true; verticalAlignment: Text.AlignVCenter } } Rectangle { Layout.fillWidth: true; implicitHeight: 54; radius: 12; color: window.surface; border.color: window.border Label { anchors.fill: parent; anchors.margins: 10; text: saveDialog.details.volumeText || ""; color: window.textMuted; font.pixelSize: 12; wrapMode: Text.WordWrap; verticalAlignment: Text.AlignVCenter } } } Rectangle { Layout.fillWidth: true; radius: 14; color: window.surface; border.color: window.border; implicitHeight: exerciseResultList.implicitHeight + 20 ColumnLayout { id: exerciseResultList; anchors.fill: parent; anchors.margins: 10; spacing: 4 Repeater { model: saveDialog.details.exercises || [] delegate: RowLayout { Layout.fillWidth: true; spacing: 8 Rectangle { width: 6; height: 6; radius: 3; color: modelData.completed ? window.accent : "#EF4444" } Label { Layout.fillWidth: true; text: modelData.name; color: window.textMain; font.pixelSize: 12; elide: Text.ElideRight } Label { text: modelData.actual + "/" + modelData.target + " " + modelData.metric; color: modelData.completed ? window.accent : window.textMuted; font.pixelSize: 11 } } } } } ComboBox { id: feedback; Layout.fillWidth: true; model: ["Нормально", "Легко", "Тяжело"] } Rectangle { Layout.fillWidth: true; implicitHeight: 104; radius: 14; color: window.surface; border.color: window.border ColumnLayout { anchors.fill: parent; anchors.margins: 12; spacing: 4 Label { Layout.fillWidth: true; text: saveDialog.details.recommendationTitle || "Рекомендация"; color: window.textMain; font.pixelSize: 13; font.bold: true } Label { Layout.fillWidth: true; text: saveDialog.details.recommendationText || ""; color: window.textMuted; font.pixelSize: 12; wrapMode: Text.WordWrap } Label { Layout.fillWidth: true; text: sessionController.adaptationPreview(feedback.currentText); color: window.blue; font.pixelSize: 12; wrapMode: Text.WordWrap } } } CheckBox { id: applyAdaptation; text: "Применить адаптацию"; checked: false; enabled: feedback.currentText !== "Нормально" } } onOpened: details = sessionController.pendingResultDetails() onAccepted: sessionController.saveFinishedSession(feedback.currentText, applyAdaptation.checked) onDiscarded: sessionController.discardFinishedSession() } // Focus mode - full-screen overlay Rectangle { id: focusOverlay anchors.fill: parent z: 100 visible: false color: window.bg focus: visible function open() { visible = true; forceActiveFocus() } function close() { visible = false } Keys.onPressed: function(event) { if (event.key === Qt.Key_Escape) { focusOverlay.close(); event.accepted = true } else if (event.key === Qt.Key_Space) { sessionController.active ? sessionController.completeCurrent() : sessionController.start(); event.accepted = true } else if (event.key === Qt.Key_P) { sessionController.togglePause(); event.accepted = true } else if (event.key === Qt.Key_S) { sessionController.skipCurrentExercise(); event.accepted = true } else if (event.key === Qt.Key_Plus || event.key === Qt.Key_Equal) { sessionController.addRep(); event.accepted = true } else if (event.key === Qt.Key_Minus) { sessionController.removeRep(); event.accepted = true } } RowLayout { anchors.fill: parent; anchors.margins: 16; spacing: 16 // Exercise image Rectangle { Layout.fillWidth: true; Layout.fillHeight: true; radius: 16; color: window.surface; border.color: window.border visible: sessionController.currentExerciseFrameUrls.length > 0 Image { anchors.fill: parent; anchors.margins: 8 source: sessionController.currentExerciseFrameUrls.length > 0 ? sessionController.currentExerciseFrameUrls[window.exerciseFrameIndex % sessionController.currentExerciseFrameUrls.length] : "" fillMode: Image.PreserveAspectFit; smooth: true; asynchronous: true opacity: 1 Behavior on opacity { NumberAnimation { duration: 400; easing.type: Easing.InOutQuad } } onStatusChanged: { if (status === Image.Ready) { opacity = 0; opacity = 1 } } } Rectangle { anchors.fill: parent; anchors.margins: 8; color: "transparent"; border.color: window.imageOutline; radius: 10; visible: sessionController.currentExerciseFrameUrls.length > 0 } Label { anchors.centerIn: parent; text: "Нет кадра"; color: window.textMuted; font.pixelSize: 16; visible: sessionController.currentExerciseFrameUrls.length === 0 } } // Controls panel Rectangle { Layout.preferredWidth: 340; Layout.fillHeight: true; radius: 16; color: window.surface; border.color: window.border ColumnLayout { anchors.fill: parent; anchors.margins: 16; spacing: 8 Label { text: sessionController.phaseTitle; color: window.blue; font.pixelSize: 16; font.bold: true } Label { Layout.fillWidth: true; text: sessionController.exerciseName; color: window.textMain; font.pixelSize: 22; font.bold: true; wrapMode: Text.WordWrap } Label { Layout.fillWidth: true; visible: sessionController.currentStepSummary.length > 0; text: sessionController.currentStepSummary; color: window.blue; font.pixelSize: 13 } Label { Layout.fillWidth: true; text: sessionController.currentExerciseTechnique.primaryCue || ""; visible: text.length > 0; color: window.textMuted; font.pixelSize: 12; wrapMode: Text.WordWrap } Item { Layout.fillHeight: true } Label { Layout.alignment: Qt.AlignHCenter; text: sessionController.counterText; color: window.textMain; font.pixelSize: 72; font.bold: true } ThemedProgressBar { Layout.fillWidth: true; from: 0; to: 1; value: sessionController.phaseProgress; visible: sessionController.phaseProgress > 0 } Label { Layout.alignment: Qt.AlignHCenter; visible: sessionController.phaseProgressText.length > 0; text: sessionController.phaseProgressText; color: window.textMuted; font.pixelSize: 11 } Label { Layout.alignment: Qt.AlignHCenter; visible: sessionController.recommendedRestText.length > 0; text: sessionController.recommendedRestText; color: window.blue; font.pixelSize: 14 } Label { Layout.alignment: Qt.AlignHCenter; visible: sessionController.nextExerciseName.length > 0; text: "Дальше: " + sessionController.nextExerciseName; color: window.accent; font.pixelSize: 14; font.bold: true } RowLayout { Layout.fillWidth: true; spacing: 8 Button { text: "−"; Layout.preferredWidth: 44; enabled: sessionController.active; onClicked: sessionController.removeRep(); focusPolicy: Qt.StrongFocus } ActionButton { Layout.fillWidth: true; implicitHeight: 48; text: sessionController.active ? "Завершить" : "Начать"; enabled: sessionController.ready || sessionController.active; onClicked: sessionController.active ? sessionController.completeCurrent() : sessionController.start(); focusPolicy: Qt.StrongFocus } Button { text: "+"; Layout.preferredWidth: 44; enabled: sessionController.active; onClicked: sessionController.addRep(); focusPolicy: Qt.StrongFocus } } RowLayout { Layout.fillWidth: true; spacing: 6 Button { Layout.fillWidth: true; text: sessionController.paused ? "Продолжить" : "Пауза"; enabled: sessionController.active; onClicked: sessionController.togglePause(); focusPolicy: Qt.StrongFocus } Button { Layout.fillWidth: true; text: "Заменить"; enabled: sessionController.workoutReplacementExercises.length > 0; onClicked: replacementDialog.open(); focusPolicy: Qt.StrongFocus } } RowLayout { Layout.fillWidth: true; spacing: 6 Button { Layout.fillWidth: true; text: "Пропустить"; enabled: sessionController.active; onClicked: sessionController.skipCurrentExercise(); focusPolicy: Qt.StrongFocus } Button { Layout.fillWidth: true; text: "Закончить"; enabled: sessionController.active; focusPolicy: Qt.StrongFocus; onClicked: { sessionController.finishActiveWorkout(); focusOverlay.close() } } } Button { Layout.fillWidth: true; text: "Закрыть"; focusPolicy: Qt.StrongFocus; onClicked: focusOverlay.close() } } } } } // Main layout RowLayout { anchors.fill: parent; anchors.margins: 24; spacing: 20 // Sidebar Rectangle { Layout.preferredWidth: 250; Layout.fillHeight: true; radius: 20; color: window.panel; border.color: window.border ColumnLayout { anchors.fill: parent; anchors.margins: 18; spacing: 8 Label { text: "BODYWEIGHT"; color: window.blue; font.pixelSize: 12; font.bold: true } Label { text: "Base"; color: window.textMain; font.pixelSize: 28; font.bold: true } Label { text: "C++ / Qt Quick"; color: window.textMuted; font.pixelSize: 13; Layout.bottomMargin: 8 } Rectangle { Layout.fillWidth: true; implicitHeight: 118; radius: 14; color: window.surface; border.color: window.border ColumnLayout { anchors.fill: parent; anchors.margins: 12; spacing: 8 Label { Layout.fillWidth: true; text: "Профиль"; color: window.blue; font.pixelSize: 12; font.bold: true } ComboBox { id: profileCombo; Layout.fillWidth: true; textRole: "name"; valueRole: "id"; model: sessionController.profiles; enabled: !sessionController.active && !sessionController.needsSave && !sessionController.hasRecoverableDraft Component.onCompleted: currentIndex = indexOfValue(sessionController.selectedProfileId) onActivated: sessionController.selectProfile(currentValue) Connections { target: sessionController; function onChanged() { profileCombo.currentIndex = profileCombo.indexOfValue(sessionController.selectedProfileId) } } } RowLayout { Layout.fillWidth: true; spacing: 6 TextField { id: newProfileName; Layout.fillWidth: true; placeholderText: "имя"; selectByMouse: true; enabled: !sessionController.active && !sessionController.needsSave && !sessionController.hasRecoverableDraft } Button { Layout.preferredWidth: 42; text: "+"; enabled: newProfileName.text.trim().length > 0 && !sessionController.active; focusPolicy: Qt.StrongFocus; onClicked: { sessionController.createProfile(newProfileName.text); newProfileName.clear() } } } } } ColumnLayout { Layout.fillWidth: true; spacing: 4 Repeater { model: [ { title: "Тренировка", hint: "таймер и этапы", tip: "Клавиша 1" }, { title: "Планы", hint: "выбор и состав", tip: "Клавиша 2" }, { title: "Библиотека", hint: "упражнения", tip: "Клавиша 3" }, { title: "Прогресс", hint: "история", tip: "Клавиша 4" }, { title: "Цикл", hint: "4–26 недель", tip: "Клавиша 5" }, { title: "Питание", hint: "вес и рацион", tip: "Клавиша 6" }, { title: "Часы", hint: "здоровье и активность", tip: "Клавиша 7" } ]; delegate: Button { required property int index; required property var modelData Layout.fillWidth: true; implicitHeight: 40; focusPolicy: Qt.StrongFocus; onClicked: window.currentPage = index scale: down ? 0.96 : 1.0 Behavior on scale { NumberAnimation { duration: 150; easing.type: Easing.OutCubic } } ToolTip { text: modelData.tip; visible: parent.hovered; delay: 800 } background: Rectangle { radius: 10; color: window.currentPage === index ? window.selectedSurface : parent.hovered ? window.hoverSurface : "transparent"; border.color: parent.activeFocus || window.currentPage === index ? window.blue : "transparent"; border.width: window.currentPage === index ? 2 : 1 } contentItem: Column { leftPadding: 12; spacing: 0 Text { text: modelData.title; color: window.currentPage === index ? window.textMain : window.textMuted; font.pixelSize: 13; font.bold: window.currentPage === index } Text { text: modelData.hint; color: window.textMuted; font.pixelSize: 9 } } } } } Item { Layout.fillHeight: true } Label { Layout.fillWidth: true; text: sessionController.status; color: window.textMuted; elide: Text.ElideRight; font.pixelSize: 11 } Rectangle { Layout.fillWidth: true; implicitHeight: 68; radius: 14; color: window.surface; border.color: window.border RowLayout { anchors.fill: parent; anchors.margins: 12; spacing: 10 ColumnLayout { Layout.fillWidth: true; spacing: 1 Label { text: "Прогресс"; color: window.blue; font.pixelSize: 11; font.bold: true } Label { text: sessionController.totalSessions + " тренировок"; color: window.textMain; font.pixelSize: 16; font.bold: true } } ColumnLayout { spacing: 1 Label { Layout.alignment: Qt.AlignRight; text: sessionController.totalWorkMinutes + " мин"; color: window.textMuted; font.pixelSize: 11 } Label { Layout.alignment: Qt.AlignRight; text: sessionController.completionRateText; color: window.accent; font.pixelSize: 11; font.bold: true } } } } RowLayout { Layout.fillWidth: true; spacing: 8 SidebarIconButton { Layout.fillWidth: true; glyph: "\uE946"; description: "О программе"; onClicked: aboutDialog.open() } SidebarIconButton { Layout.fillWidth: true; glyph: "\uE713"; description: "Настройки"; onClicked: settingsDialog.open() } SidebarIconButton { Layout.fillWidth: true; glyph: "\uE74E"; description: "Экспорт данных"; onClicked: exportDataDialog.open() } } } } // Pages StackLayout { Layout.fillWidth: true; Layout.fillHeight: true; currentIndex: window.currentPage opacity: 1 Behavior on opacity { NumberAnimation { duration: 150 } } onCurrentIndexChanged: { opacity = 0; opacity = 1 } // Page 0: Workout ColumnLayout { spacing: 0 Rectangle { Layout.fillWidth: true; implicitHeight: 60; color: window.panel RowLayout { anchors.fill: parent; anchors.leftMargin: 20; anchors.rightMargin: 20; spacing: 20 Column { Layout.fillWidth: true Label { text: sessionController.planName; color: window.textMain; font.pixelSize: 18; font.bold: true } Label { text: sessionController.phaseTitle + " • " + sessionController.exerciseName; color: window.blue; font.pixelSize: 12 } } Label { text: sessionController.progressText; color: window.blue; font.pixelSize: 22; font.bold: true } Button { text: "Повторить"; visible: !sessionController.active && sessionController.lastSessionPlanId().length > 0 && sessionController.lastSessionPlanId() !== sessionController.selectedPlanId; font.pixelSize: 12; onClicked: sessionController.repeatLastSession(); focusPolicy: Qt.StrongFocus } } } // Workout main area: image left + controls right Rectangle { Layout.fillWidth: true; Layout.fillHeight: true; radius: 24; color: window.panel; border.color: window.border RowLayout { anchors.fill: parent; anchors.margins: 16; spacing: 16 // Image Rectangle { Layout.fillWidth: true; Layout.fillHeight: true; radius: 8; color: window.imageSurface; border.color: window.imageOutline Image { anchors.fill: parent; anchors.margins: 10 source: sessionController.currentExerciseFrameUrls.length > 0 ? sessionController.currentExerciseFrameUrls[window.exerciseFrameIndex % sessionController.currentExerciseFrameUrls.length] : "" fillMode: Image.PreserveAspectFit; smooth: true; asynchronous: true opacity: 1 Behavior on opacity { NumberAnimation { duration: 400; easing.type: Easing.InOutQuad } } onStatusChanged: { if (status === Image.Ready) { opacity = 0; opacity = 1 } } } Rectangle { anchors.fill: parent; anchors.margins: 10; color: "transparent"; border.color: window.imageOutline; radius: 6; visible: sessionController.currentExerciseFrameUrls.length > 0 } Label { anchors.centerIn: parent; text: "Нет кадра"; color: window.textMuted; font.pixelSize: 16; visible: sessionController.currentExerciseFrameUrls.length === 0 } } // Controls Rectangle { Layout.preferredWidth: 340; Layout.fillHeight: true; radius: 8; color: window.surface; border.color: window.border ColumnLayout { anchors.fill: parent; anchors.margins: 16; spacing: 8 Label { text: sessionController.phaseTitle; color: window.blue; font.pixelSize: 16; font.bold: true } Label { Layout.fillWidth: true; text: sessionController.exerciseName; color: window.textMain; font.pixelSize: 22; font.bold: true; wrapMode: Text.WordWrap } Label { Layout.fillWidth: true; visible: sessionController.currentStepSummary.length > 0; text: sessionController.currentStepSummary; color: window.blue; font.pixelSize: 13 } Label { Layout.fillWidth: true; visible: sessionController.currentCoachNote.length > 0; text: sessionController.currentCoachNote; color: window.textMuted; font.pixelSize: 12; wrapMode: Text.WordWrap } Label { Layout.fillWidth: true property string categoryText: sessionController.currentExerciseTechnique.category || "" property string equipmentText: sessionController.currentExerciseTechnique.equipment || "" text: categoryText + (equipmentText.length > 0 ? " • " + equipmentText : "") visible: text.length > 0 color: window.accent; font.pixelSize: 12 } Label { Layout.fillWidth: true; text: sessionController.currentExerciseTechnique.primaryCue || ""; visible: text.length > 0; color: window.textMain; font.pixelSize: 12; wrapMode: Text.WordWrap } Item { Layout.fillHeight: true } Label { Layout.alignment: Qt.AlignHCenter; Layout.preferredWidth: 220; text: sessionController.counterText; color: window.textMain; font.pixelSize: 72; font.bold: true; horizontalAlignment: Text.AlignHCenter } ThemedProgressBar { Layout.fillWidth: true; from: 0; to: 1; value: sessionController.phaseProgress; visible: sessionController.phaseProgress > 0 } Label { Layout.alignment: Qt.AlignHCenter; visible: sessionController.phaseProgressText.length > 0; text: sessionController.phaseProgressText; color: window.textMuted; font.pixelSize: 11 } TextField { Layout.fillWidth: true; text: sessionController.currentLoadNote; placeholderText: "заметка нагрузки"; enabled: sessionController.active; selectByMouse: true; onEditingFinished: sessionController.currentLoadNote = text; implicitHeight: 32; font.pixelSize: 12 } RowLayout { Layout.fillWidth: true; spacing: 8 Button { text: "−"; Layout.preferredWidth: 44; enabled: sessionController.active; onClicked: sessionController.removeRep(); focusPolicy: Qt.StrongFocus } ActionButton { Layout.fillWidth: true; implicitHeight: 48; text: sessionController.active ? "Завершить" : "Начать"; enabled: sessionController.ready; onClicked: sessionController.active ? sessionController.completeCurrent() : sessionController.start(); focusPolicy: Qt.StrongFocus } Button { text: "+"; Layout.preferredWidth: 44; enabled: sessionController.active; onClicked: sessionController.addRep(); focusPolicy: Qt.StrongFocus } } RowLayout { Layout.fillWidth: true; spacing: 6 Button { Layout.fillWidth: true; text: sessionController.paused ? "Продолжить" : "Пауза"; enabled: sessionController.active; onClicked: sessionController.togglePause(); focusPolicy: Qt.StrongFocus } Button { Layout.fillWidth: true; text: "Заменить"; enabled: sessionController.workoutReplacementExercises.length > 0; onClicked: replacementDialog.open(); focusPolicy: Qt.StrongFocus } } RowLayout { Layout.fillWidth: true; spacing: 6 Button { Layout.fillWidth: true; text: "Пропустить"; enabled: sessionController.active; onClicked: sessionController.skipCurrentExercise(); focusPolicy: Qt.StrongFocus } Button { Layout.fillWidth: true; text: "Закончить"; enabled: sessionController.active; onClicked: sessionController.finishActiveWorkout(); focusPolicy: Qt.StrongFocus } } Button { Layout.fillWidth: true; text: "Фокус"; enabled: sessionController.ready || sessionController.active; onClicked: focusOverlay.open(); focusPolicy: Qt.StrongFocus } } } } } // Plan steps bar Rectangle { Layout.fillWidth: true; implicitHeight: 90; color: window.panel; border.color: window.border; border.width: 1 ListView { anchors.fill: parent; anchors.margins: 6; orientation: ListView.Horizontal; spacing: 6; clip: true; model: sessionController.workoutQueueSteps delegate: Rectangle { required property var modelData property bool isCurrent: sessionController.currentStepIndex === modelData.index scale: isCurrent ? 1.02 : 1.0 width: 160; height: 78; radius: 8 color: modelData.isWarmup ? window.warmupSurface : modelData.isCooldown ? window.cooldownSurface : isCurrent ? window.selectedSurface : window.surface border.color: isCurrent ? window.blue : modelData.isWarmup ? window.accent : modelData.isCooldown ? window.warning : window.border border.width: isCurrent ? 2 : 1 Behavior on scale { NumberAnimation { duration: 150; easing.type: Easing.OutCubic } } Behavior on border.color { ColorAnimation { duration: 150; easing.type: Easing.OutCubic } } Row { anchors.fill: parent; anchors.margins: 6; spacing: 6 Rectangle { width: 36; height: 56; radius: 5; color: window.imageSurface; border.color: window.imageOutline Image { anchors.fill: parent; anchors.margins: 1; source: modelData.frameUrls.length > 0 ? modelData.frameUrls[0] : ""; fillMode: Image.PreserveAspectFit; smooth: true; asynchronous: true } } Column { width: parent.width - 42; spacing: 2 Text { width: parent.width; text: modelData.index + ". " + modelData.exerciseName; color: window.textMain; font.pixelSize: 11; font.bold: true; elide: Text.ElideRight } Text { text: modelData.metricText + " • " + modelData.target + (modelData.sets > 1 ? " ×" + modelData.sets : ""); color: window.blue; font.pixelSize: 10 } Text { text: modelData.isWarmup ? "разминка" : modelData.isCooldown ? "заминка" : "отдых " + modelData.restSeconds + "с"; color: window.textMuted; font.pixelSize: 10 } } } } } } } // Page 1: Plans ColumnLayout { spacing: 18 ColumnLayout { Layout.fillWidth: true Label { text: "Планы тренировок"; color: window.textMuted; font.pixelSize: 14 } Label { text: sessionController.planName; color: window.textMain; font.pixelSize: 30; font.bold: true } RowLayout { Layout.fillWidth: true; spacing: 8 Button { text: "Экспорт плана"; focusPolicy: Qt.StrongFocus; onClicked: exportPlanDialog.open() } Button { text: "Импорт плана"; focusPolicy: Qt.StrongFocus; onClicked: importPlanDialog.open() } } TextField { Layout.fillWidth: true; placeholderText: "Поиск упражнений в плане..."; onTextChanged: sessionController.exerciseSearch = text } RowLayout { Layout.fillWidth: true; spacing: 16; visible: sessionController.selectedPlanId.length > 0 Label { text: "Разминка"; color: window.textMain; font.pixelSize: 13 } Switch { id: warmupSwitch; checked: !!sessionController.selectedPlanOptions.warmupIncluded; enabled: !sessionController.active && !sessionController.needsSave && !sessionController.hasRecoverableDraft; onToggled: window.updateSelectedPlanFlowOptions() } SpinBox { id: warmupStepCount; from: 1; to: 8; value: Math.max(1, sessionController.selectedPlanOptions.warmupStepCount || 4); enabled: warmupSwitch.checked && !sessionController.active && !sessionController.needsSave && !sessionController.hasRecoverableDraft; onValueModified: window.updateSelectedPlanFlowOptions() } Label { text: "Заминка"; color: window.textMain; font.pixelSize: 13 } Switch { id: cooldownSwitch; checked: !!sessionController.selectedPlanOptions.cooldownIncluded; enabled: !sessionController.active && !sessionController.needsSave && !sessionController.hasRecoverableDraft; onToggled: window.updateSelectedPlanFlowOptions() } SpinBox { id: cooldownStepCount; from: 1; to: 4; value: Math.max(1, sessionController.selectedPlanOptions.cooldownStepCount || 3); enabled: cooldownSwitch.checked && !sessionController.active && !sessionController.needsSave && !sessionController.hasRecoverableDraft; onValueModified: window.updateSelectedPlanFlowOptions() } Label { text: "День"; color: window.textMain; font.pixelSize: 13 } ComboBox { id: dayOfWeekCombo; model: ["—", "Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"]; currentIndex: sessionController.selectedPlanDayOfWeek; enabled: !sessionController.active && !sessionController.needsSave && !sessionController.hasRecoverableDraft; onActivated: sessionController.setSelectedPlanDayOfWeek(currentIndex) } } } Rectangle { Layout.fillWidth: true; implicitHeight: 184; radius: 18; color: window.panel; border.color: window.border ColumnLayout { anchors.fill: parent; anchors.margins: 12; spacing: 10 ListView { Layout.fillWidth: true; Layout.preferredHeight: 82; orientation: ListView.Horizontal; spacing: 10; clip: true; model: sessionController.plans delegate: Button { required property var modelData; width: 220; height: 82; enabled: !sessionController.active && !sessionController.needsSave && !sessionController.hasRecoverableDraft; onClicked: sessionController.selectPlan(modelData.id); focusPolicy: Qt.StrongFocus scale: down ? 0.96 : 1.0 Behavior on scale { NumberAnimation { duration: 150; easing.type: Easing.OutCubic } } background: Rectangle { radius: 13; color: modelData.selected ? window.selectedSurface : parent.hovered ? window.hoverSurface : window.surface; border.color: parent.activeFocus || modelData.selected ? window.blue : window.border; border.width: modelData.selected ? 2 : 1 } contentItem: Column { spacing: 5 Text { width: parent.width; text: modelData.name; color: window.textMain; font.pixelSize: 14; font.bold: true; elide: Text.ElideRight } Text { width: parent.width; text: modelData.goal; color: window.textMuted; font.pixelSize: 11; elide: Text.ElideRight } RowLayout { spacing: 6 Text { text: modelData.stepCount + " упражнений"; color: window.blue; font.pixelSize: 11 } Text { visible: modelData.dayOfWeek > 0; text: ["", "Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"][modelData.dayOfWeek] || ""; color: window.accent; font.pixelSize: 11; font.bold: true } } } } } RowLayout { Layout.fillWidth: true; spacing: 8 TextField { id: planNameEdit; Layout.preferredWidth: 250; placeholderText: "название"; text: sessionController.planName; selectByMouse: true } TextField { id: planGoalEdit; Layout.fillWidth: true; placeholderText: "цель"; text: sessionController.planGoal; selectByMouse: true } Button { Layout.preferredWidth: 120; text: "Сохранить"; enabled: !sessionController.active && !sessionController.needsSave && !sessionController.hasRecoverableDraft; focusPolicy: Qt.StrongFocus; onClicked: sessionController.renameSelectedPlan(planNameEdit.text, planGoalEdit.text) } Button { Layout.preferredWidth: 100; text: "Копия"; enabled: !sessionController.active && !sessionController.needsSave && !sessionController.hasRecoverableDraft; focusPolicy: Qt.StrongFocus; onClicked: sessionController.duplicateSelectedPlan(planNameEdit.text + " copy") } Button { Layout.preferredWidth: 110; text: "Удалить"; enabled: !sessionController.active && !sessionController.needsSave && !sessionController.hasRecoverableDraft; focusPolicy: Qt.StrongFocus; onClicked: sessionController.deleteSelectedPlan() } } } } Rectangle { Layout.fillWidth: true; Layout.fillHeight: true; radius: 18; color: window.panel; border.color: window.border ColumnLayout { anchors.fill: parent; anchors.margins: 14; spacing: 10 RowLayout { Layout.fillWidth: true Label { Layout.fillWidth: true; text: "Состав плана"; color: window.textMain; font.pixelSize: 18; font.bold: true } Label { text: sessionController.selectedPlanSteps.length + " шагов"; color: window.textMuted; font.pixelSize: 12 } } RowLayout { Layout.fillWidth: true; spacing: 8 ComboBox { id: newStepExercise; Layout.preferredWidth: 240; textRole: "name"; valueRole: "id"; model: sessionController.exerciseLibrary; onActivated: { const ex = exerciseAtComboIndex(currentIndex); if (newStepNote.text.trim().length === 0 && ex.primaryCue && ex.primaryCue.length > 0) newStepNote.text = ex.primaryCue } } TextField { id: newStepNote; Layout.fillWidth: true; placeholderText: "заметка"; selectByMouse: true } Button { Layout.preferredWidth: 120; text: "+ Добавить"; enabled: !sessionController.active && !sessionController.needsSave && !sessionController.hasRecoverableDraft; focusPolicy: Qt.StrongFocus; onClicked: { const ex = exerciseAtComboIndex(newStepExercise.currentIndex); sessionController.appendSelectedPlanStep(newStepExercise.currentValue, newStepNote.text.trim().length > 0 ? newStepNote.text : (ex.primaryCue || "")); newStepNote.clear() } } } ListView { id: planStepsList; Layout.fillWidth: true; Layout.fillHeight: true; spacing: 8; clip: true; model: sessionController.selectedPlanSteps; Component.onCompleted: window.syncSelectedPlanStep(); onCountChanged: window.syncSelectedPlanStep() delegate: Rectangle { id: stepDelegate required property var modelData property bool selected: window.selectedPlanStepIndex === modelData.index - 1 property bool isDragged: false width: ListView.view.width; implicitHeight: 130; radius: 10 color: selected ? window.selectedStepSurface : window.surface border.color: isDragged ? window.accent : selected ? window.blue : window.border border.width: isDragged ? 3 : selected ? 2 : 1 Behavior on border.color { ColorAnimation { duration: 150 } } Behavior on scale { NumberAnimation { duration: 150 } } scale: isDragged ? 1.03 : 1.0 DragHandler { id: dragHandler target: stepDelegate cursorShape: Qt.OpenHandCursor onActiveChanged: { if (active) { stepDelegate.isDragged = true } else { stepDelegate.isDragged = false // Find drop position var fromIndex = modelData.index - 1 var yPos = stepDelegate.y + stepDelegate.height / 2 var toIndex = Math.round(yPos / (stepDelegate.height + 8)) toIndex = Math.max(0, Math.min(sessionController.selectedPlanSteps.length - 1, toIndex)) if (fromIndex !== toIndex) { var delta = toIndex - fromIndex sessionController.moveSelectedPlanStep(fromIndex, delta) } stepDelegate.y = 0 } } } DropArea { anchors.fill: parent onEntered: { if (modelData.index - 1 !== dragHandler.target.modelData.index - 1) { stepDelegate.border.color = window.accent } } onExited: { stepDelegate.border.color = stepDelegate.selected ? window.blue : window.border } } ColumnLayout { anchors.fill: parent; anchors.margins: 12; spacing: 6 RowLayout { Layout.fillWidth: true; spacing: 10 Label { text: "⋮⋮"; color: window.textMuted; font.pixelSize: 16; Layout.preferredWidth: 20 } Label { text: modelData.index; color: window.blue; font.pixelSize: 16; font.bold: true; Layout.preferredWidth: 28 } Label { Layout.fillWidth: true; text: modelData.exerciseName; color: window.textMain; font.pixelSize: 14; font.bold: true; elide: Text.ElideRight } Label { text: modelData.metricText + " • " + modelData.target; color: window.blue; font.pixelSize: 12 } Button { Layout.preferredWidth: 36; text: "↑"; enabled: !sessionController.active && !sessionController.needsSave && !sessionController.hasRecoverableDraft; focusPolicy: Qt.StrongFocus; onClicked: sessionController.moveSelectedPlanStep(modelData.index - 1, -1) } Button { Layout.preferredWidth: 36; text: "↓"; enabled: !sessionController.active && !sessionController.needsSave && !sessionController.hasRecoverableDraft; focusPolicy: Qt.StrongFocus; onClicked: sessionController.moveSelectedPlanStep(modelData.index - 1, 1) } } RowLayout { Layout.fillWidth: true; spacing: 6 ComboBox { id: stepExerciseEdit; Layout.preferredWidth: 190; textRole: "name"; valueRole: "id"; model: sessionController.exerciseLibrary; Component.onCompleted: currentIndex = indexOfValue(modelData.exerciseId); onActivated: { const ex = exerciseAtComboIndex(currentIndex); if (ex.primaryCue && ex.primaryCue.length > 0 && shouldReplaceStepNote(stepNoteEdit.text, modelData.primaryCue, modelData.description)) stepNoteEdit.text = ex.primaryCue } } TextField { id: stepTargetEdit; Layout.preferredWidth: 70; text: String(modelData.target); placeholderText: "цель"; validator: IntValidator { bottom: 1; top: 999 } inputMethodHints: Qt.ImhDigitsOnly; selectByMouse: true } TextField { id: stepRestEdit; Layout.preferredWidth: 80; text: String(modelData.restSeconds); placeholderText: "отдых"; validator: IntValidator { bottom: 0; top: 900 } inputMethodHints: Qt.ImhDigitsOnly; selectByMouse: true } TextField { id: stepSetsEdit; Layout.preferredWidth: 50; text: String(modelData.sets || 1); placeholderText: "×1"; validator: IntValidator { bottom: 1; top: 10 } inputMethodHints: Qt.ImhDigitsOnly; selectByMouse: true } TextField { id: stepNoteEdit; Layout.fillWidth: true; text: modelData.coachNote; placeholderText: "заметка"; selectByMouse: true } Button { Layout.preferredWidth: 80; text: selected ? "Выбран" : "Выбрать"; focusPolicy: Qt.StrongFocus; onClicked: window.selectedPlanStepIndex = modelData.index - 1 } Button { Layout.preferredWidth: 90; text: "OK"; enabled: !sessionController.active && !sessionController.needsSave && !sessionController.hasRecoverableDraft && stepTargetEdit.acceptableInput && stepRestEdit.acceptableInput; focusPolicy: Qt.StrongFocus; onClicked: { window.selectedPlanStepIndex = modelData.index - 1; sessionController.updateSelectedPlanStep(modelData.index - 1, stepExerciseEdit.currentValue, stepTargetEdit.text, stepRestEdit.text, stepNoteEdit.text); if (stepSetsEdit.acceptableInput) sessionController.updateSelectedPlanStepSets(modelData.index - 1, parseInt(stepSetsEdit.text)) } } Button { Layout.preferredWidth: 80; text: "Удалить"; enabled: !sessionController.active && !sessionController.needsSave && !sessionController.hasRecoverableDraft; focusPolicy: Qt.StrongFocus; onClicked: { window.selectedPlanStepIndex = Math.max(0, modelData.index - 2); sessionController.removeSelectedPlanStep(modelData.index - 1) } } } Label { Layout.fillWidth: true; visible: !stepTargetEdit.acceptableInput || !stepRestEdit.acceptableInput; text: "Цель: 1..999, отдых: 0..900"; color: window.error; font.pixelSize: 11 } } } } } } } // Page 2: Library ColumnLayout { spacing: 18 ColumnLayout { Layout.fillWidth: true Label { text: "Библиотека упражнений"; color: window.textMuted; font.pixelSize: 14 } Label { text: "Каталог"; color: window.textMain; font.pixelSize: 30; font.bold: true } } Rectangle { Layout.fillWidth: true; Layout.fillHeight: true; radius: 18; color: window.panel; border.color: window.border ColumnLayout { anchors.fill: parent; anchors.margins: 14; spacing: 10 RowLayout { Layout.fillWidth: true TextField { Layout.fillWidth: true; placeholderText: "Поиск по названию или id"; text: sessionController.exerciseSearch; onTextChanged: { sessionController.exerciseSearch = text; window.syncSelectedExerciseDetails() } } ComboBox { Layout.preferredWidth: 160; model: [{ text: "Все", value: "all" }, { text: "Повторы", value: "repetitions" }, { text: "Секунды", value: "seconds" }]; textRole: "text"; valueRole: "value"; onActivated: { sessionController.exerciseMetricFilter = currentValue; window.syncSelectedExerciseDetails() } } } RowLayout { Layout.fillWidth: true; Layout.fillHeight: true; spacing: 12 GridView { Layout.fillWidth: true; Layout.fillHeight: true; cellWidth: 230; cellHeight: 165; clip: true; model: sessionController.exerciseLibrary; Component.onCompleted: window.syncSelectedExerciseDetails(); onCountChanged: window.syncSelectedExerciseDetails() delegate: Rectangle { required property var modelData; width: 216; height: 152; radius: 10; color: window.selectedExerciseDetails.id === modelData.id ? window.selectedSurface : window.surface; border.color: window.selectedExerciseDetails.id === modelData.id ? window.blue : window.border; border.width: window.selectedExerciseDetails.id === modelData.id ? 2 : 1 scale: exerciseCardPress.pressed ? 0.96 : 1.0 Behavior on scale { NumberAnimation { duration: 150; easing.type: Easing.OutCubic } } MouseArea { id: exerciseCardPress; anchors.fill: parent; onClicked: window.selectedExerciseDetails = modelData } Row { anchors.fill: parent; anchors.margins: 10; spacing: 10 Rectangle { width: 52; height: 88; radius: 5; color: window.imageSurface; border.color: window.imageOutline Image { anchors.fill: parent; anchors.margins: 1; source: modelData.frameUrls.length > 0 ? modelData.frameUrls[0] : ""; fillMode: Image.PreserveAspectFit; smooth: true; asynchronous: true } } Column { width: parent.width - 62; spacing: 3 Text { width: parent.width; text: modelData.name; color: window.textMain; font.pixelSize: 13; font.bold: true; elide: Text.ElideRight } Text { text: modelData.metricText + " • цель " + modelData.defaultTarget; color: window.blue; font.pixelSize: 11 } Text { text: modelData.category + (modelData.equipment.length > 0 ? " • " + modelData.equipment : ""); color: window.accent; font.pixelSize: 10; elide: Text.ElideRight; width: parent.width } Text { text: modelData.primaryCue.length > 0 ? modelData.primaryCue : modelData.description; color: window.textMuted; font.pixelSize: 10; elide: Text.ElideRight; width: parent.width } } } } } Rectangle { Layout.preferredWidth: 320; Layout.fillHeight: true; radius: 16; color: window.surface; border.color: window.border; clip: true ColumnLayout { anchors.fill: parent; anchors.margins: 14; spacing: 10 Label { Layout.fillWidth: true; text: window.selectedExerciseDetails.name || "Выберите упражнение"; color: window.textMain; font.pixelSize: 20; font.bold: true; wrapMode: Text.WordWrap } Label { Layout.fillWidth: true; visible: !!window.selectedExerciseDetails.id; text: window.selectedExerciseDetails.category + " • " + window.selectedExerciseDetails.equipment; color: window.accent; font.pixelSize: 12; wrapMode: Text.WordWrap } Rectangle { Layout.fillWidth: true; Layout.preferredHeight: 200; visible: !!window.selectedExerciseDetails.id; radius: 6; color: window.imageSurface; border.color: window.imageOutline Image { anchors.fill: parent; anchors.margins: 10; source: window.selectedExerciseDetails.frameUrls && window.selectedExerciseDetails.frameUrls.length > 0 ? window.selectedExerciseDetails.frameUrls[0] : ""; fillMode: Image.PreserveAspectFit; smooth: true; asynchronous: true } Rectangle { anchors.fill: parent; anchors.margins: 10; color: "transparent"; border.color: window.imageOutline; radius: 4 } } Label { Layout.fillWidth: true; visible: !!window.selectedExerciseDetails.description; text: window.selectedExerciseDetails.description || ""; color: window.textMuted; font.pixelSize: 12; wrapMode: Text.WordWrap } Label { Layout.fillWidth: true; visible: !!window.selectedExerciseDetails.primaryCue; text: "1. " + window.selectedExerciseDetails.primaryCue; color: window.textMain; font.pixelSize: 13; wrapMode: Text.WordWrap } Label { Layout.fillWidth: true; visible: !!window.selectedExerciseDetails.secondaryCue; text: "2. " + window.selectedExerciseDetails.secondaryCue; color: window.textMain; font.pixelSize: 13; wrapMode: Text.WordWrap } Button { Layout.fillWidth: true; text: "Добавить в план"; visible: !!window.selectedExerciseDetails.id; enabled: !sessionController.active && !sessionController.needsSave && !sessionController.hasRecoverableDraft; focusPolicy: Qt.StrongFocus; onClicked: { sessionController.appendSelectedPlanStep(window.selectedExerciseDetails.id, window.selectedExerciseDetails.primaryCue || "") } } Item { Layout.fillHeight: true } Label { Layout.fillWidth: true; visible: !!window.selectedExerciseDetails.id; text: window.selectedExerciseDetails.id; color: window.textMuted; font.pixelSize: 11 } } } } } } } // Page 3: Progress ColumnLayout { spacing: 18 ColumnLayout { Layout.fillWidth: true Label { text: "Прогресс"; color: window.textMuted; font.pixelSize: 14 } Label { text: "История тренировок"; color: window.textMain; font.pixelSize: 30; font.bold: true } } // Empty state Rectangle { Layout.fillWidth: true; Layout.fillHeight: true; radius: 18; color: window.panel; border.color: window.border; visible: sessionController.totalSessions === 0 ColumnLayout { anchors.centerIn: parent; spacing: 12 Label { Layout.alignment: Qt.AlignHCenter; text: "Пока нет тренировок"; color: window.textMain; font.pixelSize: 20; font.bold: true } Label { Layout.alignment: Qt.AlignHCenter; text: "Начни первую тренировку"; color: window.textMuted; font.pixelSize: 14; wrapMode: Text.WordWrap; horizontalAlignment: Text.AlignHCenter } Button { Layout.alignment: Qt.AlignHCenter; text: "К тренировке"; onClicked: window.currentPage = 0; focusPolicy: Qt.StrongFocus } } } // Weekly summary Rectangle { Layout.fillWidth: true; implicitHeight: (sessionController.weeklySummary.heartRateLoadAvailable ? 242 : 218) + (sessionController.weeklySummary.undertrainedAreasActive ? 20 : 0); radius: 18; color: window.surface; border.color: window.border ColumnLayout { anchors.fill: parent; anchors.margins: 16; spacing: 8 RowLayout { Layout.fillWidth: true; spacing: 12 Label { Layout.fillWidth: true; text: sessionController.weeklySummary.rescueGoalActive ? "Неделя возвращения" : "Недельная цель"; color: window.textMain; font.pixelSize: 14; font.bold: true } Label { text: sessionController.weeklySummary.rescueGoalActive ? "минимум " + sessionController.weeklySummary.effectiveWeeklyGoalMin : sessionController.weeklySummary.weeklyGoalText color: sessionController.weeklySummary.rescueGoalActive ? window.warning : sessionController.weeklySummary.weeklyGoalAchieved ? window.accent : window.blue font.pixelSize: 12; font.bold: true } } RowLayout { Layout.fillWidth: true; spacing: 12 Label { text: "Полные " + sessionController.weeklySummary.fullSessions + "/" + sessionController.weeklySummary.effectiveWeeklyGoalMin; color: window.textMain; font.pixelSize: 12; font.bold: true } Label { text: "Короткие " + sessionController.weeklySummary.quickSessions; color: window.blue; font.pixelSize: 12; font.bold: true } Rectangle { Layout.fillWidth: true; implicitHeight: 8; radius: 4; color: window.panel Rectangle { width: parent.width * Math.max(0, Math.min(100, sessionController.weeklySummary.weeklyGoalProgress)) / 100 height: parent.height; radius: 4 color: sessionController.weeklySummary.rescueGoalActive ? window.warning : sessionController.weeklySummary.weeklyGoalAchieved ? window.accent : window.blue } } Label { text: sessionController.weeklySummary.weeklyGoalProgress + "%"; color: sessionController.weeklySummary.weeklyGoalAchieved ? window.accent : window.blue; font.pixelSize: 12; font.bold: true } } Label { Layout.fillWidth: true; text: sessionController.weeklySummary.weeklyGoalStatusText; color: sessionController.weeklySummary.rescueGoalActive ? window.warning : window.textMuted; font.pixelSize: 11; wrapMode: Text.WordWrap } RowLayout { Layout.fillWidth: true; spacing: 20 ColumnLayout { Layout.fillWidth: true; spacing: 3 Label { text: "Последние 7 дней"; color: window.blue; font.pixelSize: 12; font.bold: true } Label { text: (sessionController.weeklySummary.weekStart || "") + " – " + (sessionController.weeklySummary.weekEnd || ""); color: window.textMuted; font.pixelSize: 11 } } ColumnLayout { Layout.fillWidth: true; spacing: 3 Label { text: "Тренировки"; color: window.textMuted; font.pixelSize: 11 } Label { text: sessionController.weeklySummary.totalSessions + " / " + sessionController.weeklySummary.previousSessions; color: window.textMain; font.pixelSize: 21; font.bold: true } Label { visible: sessionController.weeklySummary.comparisonAvailable; text: sessionController.weeklySummary.sessionsDeltaText + " к прошлой неделе"; color: window.accent; font.pixelSize: 10 } } ColumnLayout { Layout.fillWidth: true; spacing: 3 Label { text: "Работа, минуты"; color: window.textMuted; font.pixelSize: 11 } Label { text: sessionController.weeklySummary.totalWorkMinutes + " / " + sessionController.weeklySummary.previousWorkMinutes; color: window.textMain; font.pixelSize: 21; font.bold: true } Label { visible: sessionController.weeklySummary.comparisonAvailable; text: sessionController.weeklySummary.workMinutesDeltaText + " мин"; color: window.accent; font.pixelSize: 10 } } ColumnLayout { Layout.fillWidth: true; spacing: 3 Label { text: "Полные завершены"; color: window.textMuted; font.pixelSize: 11 } Label { text: (sessionController.weeklySummary.completionRate || 0) + "%"; color: window.accent; font.pixelSize: 21; font.bold: true } } } 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" ? window.warning : window.error font.pixelSize: 11; 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") ? window.warning : window.textMuted font.pixelSize: 11; wrapMode: Text.WordWrap } Label { visible: sessionController.weeklySummary.undertrainedAreasActive Layout.fillWidth: true text: sessionController.weeklySummary.undertrainedAreasText color: window.warning; font.pixelSize: 11; font.bold: true; wrapMode: Text.WordWrap } } } // Stats RowLayout { Layout.fillWidth: true; spacing: 12 Repeater { model: [ { title: "Тренировки", value: sessionController.totalSessions, suffix: "" }, { title: "Работа", value: sessionController.totalWorkMinutes, suffix: " мин" }, { title: "Завершение", value: sessionController.completionRateText, suffix: "" } ]; delegate: Rectangle { required property var modelData; Layout.fillWidth: true; implicitHeight: 100; radius: 18; color: window.panel; border.color: window.border ColumnLayout { anchors.fill: parent; anchors.margins: 16; spacing: 4 Label { text: modelData.title; color: window.textMuted; font.pixelSize: 12 } Label { text: modelData.value + modelData.suffix; color: window.textMain; font.pixelSize: 28; font.bold: true } } } } } // Exercise trend chart Rectangle { Layout.fillWidth: true; implicitHeight: 200; radius: 18; color: window.panel; border.color: window.border visible: sessionController.progressExerciseBuckets.length > 1 ColumnLayout { anchors.fill: parent; anchors.margins: 14; spacing: 8 Label { text: "Тренд по упражнениям"; color: window.textMain; font.pixelSize: 14; font.bold: true } Item { Layout.fillWidth: true; Layout.fillHeight: true Canvas { id: exerciseCanvas anchors.fill: parent property var chartData: sessionController.progressExerciseBuckets onChartDataChanged: requestPaint() onWidthChanged: requestPaint() onHeightChanged: requestPaint() onPaint: { var ctx = getContext("2d") ctx.reset() if (!chartData || chartData.length < 2) return var padding = 40 var w = width - padding * 2 var h = height - padding * 2 var minV = chartData[0].actualTotal || 0 var maxV = chartData[0].actualTotal || 0 for (var i = 1; i < chartData.length; i++) { var v = chartData[i].actualTotal || 0 if (v < minV) minV = v if (v > maxV) maxV = v } var range = maxV - minV if (range < 1) range = 1 minV -= range * 0.1 maxV += range * 0.1 range = maxV - minV // Grid ctx.strokeStyle = Qt.rgba(0.15, 0.2, 0.25, 0.5) ctx.lineWidth = 1 for (var g = 0; g <= 4; g++) { var gy = padding + h * g / 4 ctx.beginPath() ctx.moveTo(padding, gy) ctx.lineTo(width - padding, gy) ctx.stroke() ctx.fillStyle = window.textMuted ctx.font = "10px sans-serif" var val = maxV - range * g / 4 ctx.fillText(Math.round(val), 2, gy - 3) } // Line ctx.strokeStyle = window.blue ctx.lineWidth = 2.5 ctx.beginPath() for (var j = 0; j < chartData.length; j++) { var x = padding + j * w / (chartData.length - 1) var y = padding + h - ((chartData[j].actualTotal || 0) - minV) / range * h if (j === 0) ctx.moveTo(x, y) else ctx.lineTo(x, y) } ctx.stroke() // Dots for (var k = 0; k < chartData.length; k++) { var dx = padding + k * w / (chartData.length - 1) var dy = padding + h - ((chartData[k].actualTotal || 0) - minV) / range * h ctx.fillStyle = (chartData[k].completedCount || 0) > 0 ? window.accent : window.textMuted ctx.beginPath() ctx.arc(dx, dy, 4, 0, Math.PI * 2) ctx.fill() } // Last point highlight if (chartData.length > 0) { var lx = padding + (chartData.length - 1) * w / (chartData.length - 1) var ly = padding + h - ((chartData[chartData.length - 1].actualTotal || 0) - minV) / range * h ctx.fillStyle = window.accent ctx.beginPath() ctx.arc(lx, ly, 6, 0, Math.PI * 2) ctx.fill() } } } // Labels RowLayout { anchors.bottom: parent.bottom; anchors.left: parent.left; anchors.right: parent.right; anchors.leftMargin: 40; anchors.rightMargin: 10 Repeater { model: sessionController.progressExerciseBuckets.length > 0 ? sessionController.progressExerciseBuckets : [] delegate: Text { required property var modelData required property int index visible: index % Math.max(1, Math.floor(sessionController.progressExerciseBuckets.length / 6)) === 0 || index === sessionController.progressExerciseBuckets.length - 1 text: modelData.exerciseName ? modelData.exerciseName.substring(0, 6) : "" color: window.textMuted font.pixelSize: 9 Layout.fillWidth: true horizontalAlignment: Text.AlignHCenter } } } } } } Rectangle { Layout.fillWidth: true; Layout.fillHeight: true; radius: 18; color: window.panel; border.color: window.border ColumnLayout { anchors.fill: parent; anchors.margins: 14; spacing: 10 Label { text: "Активность по дням"; color: window.textMain; font.pixelSize: 18; font.bold: true } ListView { Layout.fillWidth: true; Layout.preferredHeight: 85; orientation: ListView.Horizontal; spacing: 10; clip: true; model: sessionController.progressDayBuckets delegate: Rectangle { required property var modelData; width: 120; height: 78; radius: 10; color: window.surface; border.color: window.border Column { anchors.fill: parent; anchors.margins: 10; spacing: 4 Text { text: modelData.dateText; color: window.textMuted; font.pixelSize: 11 } Text { text: modelData.workMinutes + " мин"; color: window.textMain; font.pixelSize: 18; font.bold: true } Text { text: modelData.completedCount + "/" + modelData.sessionCount; color: window.blue; font.pixelSize: 11 } } } } Label { text: "Последние тренировки"; color: window.textMain; font.pixelSize: 18; font.bold: true } ListView { Layout.fillWidth: true; Layout.fillHeight: true; spacing: 8; clip: true; model: sessionController.recentSessions delegate: Rectangle { required property var modelData; width: ListView.view.width; implicitHeight: modelData.heartRateLoadAvailable ? 82 : 68; radius: 10; color: window.surface; border.color: modelData.completedAll ? window.accent : window.border RowLayout { anchors.fill: parent; anchors.margins: 12; spacing: 12 ColumnLayout { Layout.fillWidth: true; spacing: 2 Label { Layout.fillWidth: true; text: modelData.planName; color: window.textMain; font.pixelSize: 14; font.bold: true; elide: Text.ElideRight } Label { text: modelData.endedAt + " • " + modelData.modeText + " • " + modelData.feedback + (modelData.heartRateAvailable ? " • " + modelData.heartRateText : ""); color: modelData.sessionMode === "quick" ? window.blue : modelData.heartRateAvailable ? window.error : window.textMuted; font.pixelSize: 11 } Label { visible: modelData.heartRateLoadAvailable; text: modelData.heartRateLoadText; color: window.textMuted; font.pixelSize: 10 } } Label { text: modelData.workMinutes + " мин"; color: window.blue; font.pixelSize: 12 } Label { text: modelData.sessionMode === "quick" ? "короткая" : modelData.completedAll ? "закрыта" : "частично"; color: modelData.sessionMode === "quick" ? window.blue : modelData.completedAll ? window.accent : window.textMuted; font.pixelSize: 12 } } } } } } } // Page 4: Training cycle ColumnLayout { spacing: 16 RowLayout { Layout.fillWidth: true; spacing: 12 ColumnLayout { Layout.fillWidth: true; spacing: 3 Label { text: "Тренировочный цикл"; color: window.textMuted; font.pixelSize: 14 } Label { text: sessionController.trainingCycle.active ? "План на " + sessionController.trainingCycle.durationWeeks + " недель" : "Месяц и полугодие"; color: window.textMain; font.pixelSize: 30; font.bold: true } } Button { text: "4 недели"; enabled: !sessionController.active; focusPolicy: Qt.StrongFocus; onClicked: { sessionController.configureTrainingCycle(4); window.cycleShowHalfYear = false } } ActionButton { Layout.preferredWidth: 150; text: "26 недель"; enabled: !sessionController.active; onClicked: { sessionController.configureTrainingCycle(26); window.cycleShowHalfYear = true } } Button { text: "Отключить"; visible: sessionController.trainingCycle.active; enabled: !sessionController.active; focusPolicy: Qt.StrongFocus; onClicked: sessionController.clearTrainingCycle() } } Rectangle { Layout.fillWidth: true; Layout.fillHeight: true; radius: 18; color: window.panel; border.color: window.border; visible: !sessionController.trainingCycle.active ColumnLayout { anchors.centerIn: parent; spacing: 12 Label { Layout.alignment: Qt.AlignHCenter; text: "Цикл ещё не создан"; color: window.textMain; font.pixelSize: 22; font.bold: true } Label { Layout.alignment: Qt.AlignHCenter; Layout.preferredWidth: 500; text: "4 недели — быстрый блок. 26 недель — шесть фаз с регулярной разгрузкой."; color: window.textMuted; font.pixelSize: 13; wrapMode: Text.WordWrap; horizontalAlignment: Text.AlignHCenter } } } Rectangle { Layout.fillWidth: true; implicitHeight: sessionController.trainingCycle.extended ? 174 : 146; radius: 18; color: window.panel; border.color: window.border; visible: sessionController.trainingCycle.active ColumnLayout { anchors.fill: parent; anchors.margins: 16; spacing: 8 RowLayout { Layout.fillWidth: true ColumnLayout { Layout.fillWidth: true; spacing: 2 Label { text: (sessionController.trainingCycle.upcoming ? "Стартовая" : "Текущая") + " неделя " + sessionController.trainingCycle.currentWeek; color: window.blue; font.pixelSize: 13; font.bold: true } Label { text: sessionController.trainingCycle.phase || ""; color: window.textMain; font.pixelSize: 22; font.bold: true } } Label { text: (sessionController.trainingCycle.startDate || "") + " — " + (sessionController.trainingCycle.endDate || ""); color: window.textMuted; font.pixelSize: 12 } } Label { Layout.fillWidth: true; text: sessionController.trainingCycle.focus || ""; color: window.textMuted; font.pixelSize: 13; wrapMode: Text.WordWrap } Label { text: sessionController.trainingCycle.adjustmentText || ""; color: window.accent; font.pixelSize: 13; font.bold: true } Label { visible: sessionController.trainingCycle.extended === true; text: "Пропущено " + sessionController.trainingCycle.missedCount + " • исходный финиш " + sessionController.trainingCycle.originalEndDate + " • продление " + sessionController.trainingCycle.extensionDays + " дн."; color: window.warning; font.pixelSize: 12; font.bold: true } ThemedProgressBar { Layout.fillWidth: true; from: 0; to: 1; value: sessionController.trainingCycle.progress || 0 } } } Rectangle { Layout.fillWidth: true; implicitHeight: sessionController.cycleReview.due ? 166 : 92 radius: 18; color: window.surface border.color: sessionController.cycleReview.due ? window.warning : window.border visible: sessionController.cycleReview.active ColumnLayout { anchors.fill: parent; anchors.margins: 14; spacing: 7 RowLayout { Layout.fillWidth: true Label { Layout.fillWidth: true; text: "Пересмотр цикла"; color: window.textMain; font.pixelSize: 17; font.bold: true } Label { text: sessionController.cycleReview.due ? "Контрольная неделя " + sessionController.cycleReview.reviewWeek : sessionController.cycleReview.nextReviewWeek > 0 ? "Следующая: неделя " + sessionController.cycleReview.nextReviewWeek : "Все точки пройдены" color: sessionController.cycleReview.due ? window.warning : window.textMuted; font.pixelSize: 11; font.bold: true } } Label { Layout.fillWidth: true text: sessionController.cycleReview.due ? sessionController.cycleReview.sessionCount + " тренировок • " + sessionController.cycleReview.workMinutes + " мин • завершение " + sessionController.cycleReview.completionRate + "% — " + sessionController.cycleReview.recommendation : sessionController.cycleReview.completed ? sessionController.cycleReview.lastDecisionText + " • " + sessionController.cycleReview.completedAt : "Контрольная точка появится на четвёртой неделе." color: window.textMuted; font.pixelSize: 11; wrapMode: Text.WordWrap } RowLayout { Layout.fillWidth: true; spacing: 8; visible: sessionController.cycleReview.due Button { Layout.fillWidth: true; implicitHeight: 42; text: "Оставить"; onClicked: sessionController.completeCycleReview("keep"); focusPolicy: Qt.StrongFocus } ActionButton { Layout.fillWidth: true; implicitHeight: 42; text: "Мягко увеличить"; onClicked: sessionController.completeCycleReview("progress") } Button { Layout.fillWidth: true; implicitHeight: 42; text: "Облегчить"; onClicked: sessionController.completeCycleReview("reduce"); focusPolicy: Qt.StrongFocus } } } } RowLayout { Layout.fillWidth: true; visible: sessionController.trainingCycle.active Label { Layout.fillWidth: true; text: "Недели цикла"; color: window.textMain; font.pixelSize: 18; font.bold: true } Button { text: "Текущие 4"; checkable: true; checked: !window.cycleShowHalfYear; onClicked: window.cycleShowHalfYear = false } Button { text: "Все недели"; checkable: true; checked: window.cycleShowHalfYear; enabled: sessionController.trainingCycle.durationWeeks > 4; onClicked: window.cycleShowHalfYear = true } } ListView { Layout.fillWidth: true; Layout.fillHeight: true; spacing: 8; clip: true; visible: sessionController.trainingCycle.active; model: window.visibleCycleWeeks() delegate: Rectangle { required property var modelData width: ListView.view.width; implicitHeight: 88; radius: 13 color: modelData.current ? window.selectedSurface : window.surface border.color: modelData.current ? window.blue : modelData.phase === "Разгрузка" ? window.warning : window.border border.width: modelData.current ? 2 : 1 RowLayout { anchors.fill: parent; anchors.margins: 12; spacing: 14 Rectangle { Layout.preferredWidth: 58; Layout.preferredHeight: 58; radius: 10; color: window.panel; border.color: window.border Column { anchors.centerIn: parent; spacing: 1 Text { anchors.horizontalCenter: parent.horizontalCenter; text: modelData.week; color: window.textMain; font.pixelSize: 22; font.bold: true } Text { anchors.horizontalCenter: parent.horizontalCenter; text: "неделя"; color: window.textMuted; font.pixelSize: 9 } } } ColumnLayout { Layout.fillWidth: true; spacing: 3 RowLayout { Layout.fillWidth: true Label { Layout.fillWidth: true; text: modelData.phase; color: window.textMain; font.pixelSize: 15; font.bold: true } Label { text: modelData.dateRange; color: window.textMuted; font.pixelSize: 11 } } Label { Layout.fillWidth: true; text: modelData.focus; color: window.textMuted; font.pixelSize: 11; elide: Text.ElideRight } Label { text: modelData.adjustmentText; color: modelData.phase === "Разгрузка" ? window.warning : window.blue; font.pixelSize: 11 } } ColumnLayout { Layout.preferredWidth: 100; spacing: 3 Label { Layout.alignment: Qt.AlignRight; text: modelData.status; color: modelData.current ? window.accent : window.textMuted; font.pixelSize: 11; font.bold: modelData.current } Label { Layout.alignment: Qt.AlignRight text: modelData.completedSessions + " полных" + (modelData.quickSessions > 0 ? " + " + modelData.quickSessions + " коротких" : "") color: window.textMuted; font.pixelSize: 10 } } } } } } // Page 5: Nutrition ColumnLayout { spacing: 18 ColumnLayout { Layout.fillWidth: true Label { text: "Питание"; color: window.textMuted; font.pixelSize: 14 } Label { text: "Рацион и восстановление"; color: window.textMain; font.pixelSize: 30; font.bold: true } } RowLayout { Layout.fillWidth: true; spacing: 12 Repeater { model: [ { title: "Тренировочный день", value: sessionController.nutritionSummary.trainingCaloriesText, detail: "ккал" }, { title: "Восстановление", value: sessionController.nutritionSummary.recoveryCaloriesText, detail: "ккал" }, { title: "Белок", value: sessionController.nutritionSummary.proteinTargetText, detail: "ежедневно" }, { title: "Вес", value: sessionController.nutritionSummary.currentWeightText, detail: sessionController.nutritionSummary.weightLastLoggedText } ]; delegate: Rectangle { required property var modelData; Layout.fillWidth: true; implicitHeight: 100; radius: 18; color: window.panel; border.color: window.border ColumnLayout { anchors.fill: parent; anchors.margins: 14; spacing: 4 Label { text: modelData.title; color: window.textMuted; font.pixelSize: 12 } Label { text: modelData.value; color: window.textMain; font.pixelSize: 24; font.bold: true } Label { Layout.fillWidth: true; text: modelData.detail; color: window.blue; font.pixelSize: 11; elide: Text.ElideRight } } } } } RowLayout { Layout.fillWidth: true; spacing: 12 Rectangle { Layout.fillWidth: true; implicitHeight: 80; radius: 18; color: window.panel; border.color: window.border RowLayout { anchors.fill: parent; anchors.margins: 14; spacing: 12 ColumnLayout { Layout.fillWidth: true; spacing: 2 Label { text: "Запись веса"; color: window.textMain; font.pixelSize: 15; font.bold: true } Label { text: "Атомарно"; color: window.textMuted; font.pixelSize: 11 } } TextField { id: bodyWeightInput; Layout.preferredWidth: 100; placeholderText: "76,4"; inputMethodHints: Qt.ImhFormattedNumbersOnly; text: sessionController.nutritionSummary.currentWeightText.replace(" кг", ""); selectByMouse: true; onAccepted: sessionController.logBodyWeight(text) } Button { Layout.preferredWidth: 120; text: "Сохранить"; onClicked: sessionController.logBodyWeight(bodyWeightInput.text) } } } } // Weight trend chart Rectangle { Layout.fillWidth: true; implicitHeight: 180; radius: 18; color: window.panel; border.color: window.border visible: sessionController.weightChartData.length > 1 ColumnLayout { anchors.fill: parent; anchors.margins: 14; spacing: 8 Label { text: "Динамика веса"; color: window.textMain; font.pixelSize: 14; font.bold: true } Item { Layout.fillWidth: true; Layout.fillHeight: true Canvas { id: weightCanvas anchors.fill: parent property var chartData: sessionController.weightChartData onChartDataChanged: requestPaint() onWidthChanged: requestPaint() onHeightChanged: requestPaint() onPaint: { var ctx = getContext("2d") ctx.reset() if (!chartData || chartData.length < 2) return var padding = 40 var w = width - padding * 2 var h = height - padding * 2 var minW = chartData[0].weight var maxW = chartData[0].weight for (var i = 1; i < chartData.length; i++) { if (chartData[i].weight < minW) minW = chartData[i].weight if (chartData[i].weight > maxW) maxW = chartData[i].weight } var range = maxW - minW if (range < 1) range = 1 minW -= range * 0.1 maxW += range * 0.1 range = maxW - minW // Grid lines ctx.strokeStyle = Qt.rgba(0.15, 0.2, 0.25, 0.5) ctx.lineWidth = 1 for (var g = 0; g <= 4; g++) { var gy = padding + h * g / 4 ctx.beginPath() ctx.moveTo(padding, gy) ctx.lineTo(width - padding, gy) ctx.stroke() ctx.fillStyle = window.textMuted ctx.font = "10px sans-serif" var val = maxW - range * g / 4 ctx.fillText(val.toFixed(1), 2, gy - 3) } // Line ctx.strokeStyle = window.accent ctx.lineWidth = 2 ctx.beginPath() for (var j = 0; j < chartData.length; j++) { var x = padding + j * w / (chartData.length - 1) var y = padding + h - (chartData[j].weight - minW) / range * h if (j === 0) ctx.moveTo(x, y) else ctx.lineTo(x, y) } ctx.stroke() // Dots for (var k = 0; k < chartData.length; k++) { var dx = padding + k * w / (chartData.length - 1) var dy = padding + h - (chartData[k].weight - minW) / range * h ctx.fillStyle = window.blue ctx.beginPath() ctx.arc(dx, dy, 3, 0, Math.PI * 2) ctx.fill() } // Last point highlight if (chartData.length > 0) { var lx = padding + (chartData.length - 1) * w / (chartData.length - 1) var ly = padding + h - (chartData[chartData.length - 1].weight - minW) / range * h ctx.fillStyle = window.accent ctx.beginPath() ctx.arc(lx, ly, 5, 0, Math.PI * 2) ctx.fill() } } } // Date labels RowLayout { anchors.bottom: parent.bottom; anchors.left: parent.left; anchors.right: parent.right; anchors.leftMargin: 40; anchors.rightMargin: 10; anchors.bottomMargin: 2 Repeater { model: sessionController.weightChartData.length > 0 ? sessionController.weightChartData : [] delegate: Text { required property var modelData required property int index visible: index % Math.max(1, Math.floor(sessionController.weightChartData.length / 5)) === 0 || index === sessionController.weightChartData.length - 1 text: modelData.date color: window.textMuted font.pixelSize: 9 Layout.fillWidth: true horizontalAlignment: Text.AlignHCenter } } } } } } RowLayout { Layout.fillWidth: true; Layout.fillHeight: true; spacing: 14 Rectangle { Layout.fillWidth: true; Layout.fillHeight: true; radius: 18; color: window.panel; border.color: window.border ColumnLayout { anchors.fill: parent; anchors.margins: 14; spacing: 10 Label { text: "План питания"; color: window.textMain; font.pixelSize: 18; font.bold: true } RowLayout { Layout.fillWidth: true; spacing: 10; Label { text: "Пресет"; color: window.textMuted; font.pixelSize: 12 } ComboBox { id: nutritionPresetBox; Layout.fillWidth: true; textRole: "text"; valueRole: "value"; model: [{ text: "База", value: "balanced" }, { text: "Дешево", value: "cheap" }, { text: "Слабый аппетит", value: "low-appetite" }]; Component.onCompleted: currentIndex = indexOfValue(sessionController.nutritionSummary.presetText); onActivated: sessionController.selectNutritionPreset(currentValue); Connections { target: sessionController; function onChanged() { var idx = nutritionPresetBox.indexOfValue(sessionController.nutritionSummary.presetText); if (idx >= 0 && nutritionPresetBox.currentIndex !== idx) nutritionPresetBox.currentIndex = idx } } } } ListView { Layout.fillWidth: true; Layout.fillHeight: true; spacing: 8; clip: true; model: sessionController.nutritionMeals delegate: Rectangle { required property var modelData; width: ListView.view.width; implicitHeight: 88; radius: 10; color: window.surface; border.color: window.border Column { anchors.fill: parent; anchors.margins: 10; spacing: 4 Text { width: parent.width; text: modelData.title; color: window.textMain; font.pixelSize: 13; font.bold: true; elide: Text.ElideRight } Text { width: parent.width; text: modelData.description; color: window.textMuted; font.pixelSize: 11; wrapMode: Text.WordWrap; maximumLineCount: 2; elide: Text.ElideRight } Text { text: modelData.macroText; color: window.blue; font.pixelSize: 11 } } } } } } Rectangle { Layout.fillWidth: true; Layout.fillHeight: true; radius: 18; color: window.panel; border.color: window.border ColumnLayout { anchors.fill: parent; anchors.margins: 14; spacing: 10 Label { text: "Recovery"; color: window.textMain; font.pixelSize: 18; font.bold: true } Label { Layout.fillWidth: true; text: sessionController.nutritionSummary.recoveryText; color: window.textMuted; font.pixelSize: 12; wrapMode: Text.WordWrap } Label { Layout.fillWidth: true; text: sessionController.nutritionSummary.adherenceText; color: window.accent; font.pixelSize: 12; wrapMode: Text.WordWrap } Rectangle { Layout.fillWidth: true; implicitHeight: 112; radius: 12; color: window.surface; border.color: sessionController.wearableSummary.available ? window.blue : window.border ColumnLayout { anchors.fill: parent; anchors.margins: 10; spacing: 4 RowLayout { Layout.fillWidth: true Label { Layout.fillWidth: true; text: sessionController.wearableSummary.headline; color: window.textMain; font.pixelSize: 13; font.bold: true; elide: Text.ElideRight } Button { text: "Импорт JSON"; focusPolicy: Qt.StrongFocus; onClicked: importHealthDialog.open() } } Label { Layout.fillWidth: true; text: sessionController.wearableSummary.detail; color: window.blue; font.pixelSize: 11; wrapMode: Text.WordWrap } Label { Layout.fillWidth: true; text: sessionController.wearableSummary.recommendation; color: window.textMuted; font.pixelSize: 11; wrapMode: Text.WordWrap } } } RowLayout { Layout.fillWidth: true; spacing: 8 Button { Layout.fillWidth: true; text: "OK"; focusPolicy: Qt.StrongFocus; onClicked: sessionController.markNutritionAdherence("good") } Button { Layout.fillWidth: true; text: "Частично"; focusPolicy: Qt.StrongFocus; onClicked: sessionController.markNutritionAdherence("partial") } Button { Layout.fillWidth: true; text: "Срыв"; focusPolicy: Qt.StrongFocus; onClicked: sessionController.markNutritionAdherence("off") } } RowLayout { Layout.fillWidth: true; spacing: 8 TextField { id: photoPath; Layout.fillWidth: true; placeholderText: "фото"; selectByMouse: true } Button { Layout.preferredWidth: 100; text: "Выбрать"; focusPolicy: Qt.StrongFocus; onClicked: photoDialog.open() } Button { Layout.preferredWidth: 110; text: "Фото"; onClicked: { sessionController.logPhotoProgress(photoPath.text); photoPath.clear() } } } FileDialog { id: photoDialog; title: "Фото"; nameFilters: ["Изображения (*.png *.jpg *.jpeg)"]; onAccepted: { photoPath.text = decodeURIComponent(selectedFile.toString().replace(/^file:\/\/\//, "")) } } RowLayout { Layout.fillWidth: true; spacing: 8 ComboBox { id: recoveryEnergy; Layout.preferredWidth: 90; model: [1, 2, 3, 4, 5]; currentIndex: 2 } ComboBox { id: recoverySleep; Layout.preferredWidth: 90; model: [1, 2, 3, 4, 5]; currentIndex: 2 } ComboBox { id: recoveryJoints; Layout.preferredWidth: 90; model: [1, 2, 3, 4, 5]; currentIndex: 2 } TextField { id: recoveryNote; Layout.fillWidth: true; placeholderText: "заметка"; selectByMouse: true } Button { Layout.preferredWidth: 110; text: "Recovery"; onClicked: sessionController.logRecoveryCheckIn(recoveryEnergy.currentValue, recoverySleep.currentValue, recoveryJoints.currentValue, recoveryNote.text) } } } } } } // Page 6: Wearable statistics ColumnLayout { spacing: 14 RowLayout { Layout.fillWidth: true; spacing: 12 ColumnLayout { Layout.fillWidth: true; spacing: 3 Label { text: "Health Connect"; color: window.textMuted; font.pixelSize: 14 } Label { text: "Статистика с часов"; color: window.textMain; font.pixelSize: 30; font.bold: true } Label { text: sessionController.wearableStatistics.available ? sessionController.wearableStatistics.dateRangeText : "Данные ещё не импортированы"; color: window.blue; font.pixelSize: 12 } } Button { text: "Импортировать JSON"; focusPolicy: Qt.StrongFocus; onClicked: importHealthDialog.open() } } RowLayout { Layout.fillWidth: true; spacing: 10 Label { text: "Данные этого устройства:"; color: window.textMuted; font.pixelSize: 12 } ComboBox { Layout.preferredWidth: 180 textRole: "name"; valueRole: "id"; model: sessionController.profiles currentIndex: indexOfValue(sessionController.healthConnectOwnerProfileId) onActivated: sessionController.setHealthConnectOwnerProfile(currentValue) } Label { Layout.fillWidth: true; text: "Импорт Health Connect не зависит от открытого профиля"; color: window.blue; font.pixelSize: 11 } } RowLayout { Layout.fillWidth: true; spacing: 10 Repeater { model: [ { title: "Шаги", value: sessionController.wearableStatistics.stepsText || "—", detail: sessionController.wearableStatistics.averageStepsText || "нет данных", color: window.blue }, { title: "Активные калории", value: sessionController.wearableStatistics.caloriesText || "—", detail: sessionController.wearableStatistics.totalCaloriesText || "нет данных", color: window.warning }, { title: "Сон", value: sessionController.wearableStatistics.sleepText || "—", detail: sessionController.wearableStatistics.averageSleepText || "нет данных", color: "#A78BFA" }, { title: "Вес", value: sessionController.wearableStatistics.weightText || sessionController.nutritionSummary.currentWeightText, detail: sessionController.wearableStatistics.weightSourceText || "ручная запись", color: window.accent } ]; delegate: Rectangle { required property var modelData Layout.fillWidth: true; implicitHeight: 108; radius: 18; color: window.panel; border.color: window.border ColumnLayout { anchors.fill: parent; anchors.margins: 14; spacing: 4 Label { text: modelData.title; color: window.textMuted; font.pixelSize: 12 } Label { Layout.fillWidth: true; text: modelData.value; color: window.textMain; font.pixelSize: 19; font.bold: true; elide: Text.ElideRight } Label { Layout.fillWidth: true; text: modelData.detail; color: modelData.color; font.pixelSize: 10; elide: Text.ElideRight } } } } } RowLayout { Layout.fillWidth: true; spacing: 10 Repeater { model: [ { title: "Средний пульс", value: sessionController.wearableStatistics.heartRateText || "—", color: window.error }, { title: "Пульс покоя", value: sessionController.wearableStatistics.restingHeartRateText || "—", color: window.warning }, { title: "Насыщение кислородом", value: sessionController.wearableStatistics.oxygenText || "—", color: window.blue }, { title: "Покрытие данных", value: (sessionController.wearableStatistics.dayCount || 0) + " дней", color: window.accent } ]; delegate: Rectangle { required property var modelData Layout.fillWidth: true; implicitHeight: 72; radius: 14; color: window.surface; border.color: window.border RowLayout { anchors.fill: parent; anchors.margins: 12; spacing: 10 Rectangle { Layout.preferredWidth: 5; Layout.fillHeight: true; radius: 3; color: modelData.color } ColumnLayout { Layout.fillWidth: true; spacing: 2 Label { text: modelData.title; color: window.textMuted; font.pixelSize: 11 } Label { Layout.fillWidth: true; text: modelData.value; color: window.textMain; font.pixelSize: 15; font.bold: true; elide: Text.ElideRight } } } } } } Label { Layout.fillWidth: true text: "Пульс покоя: " + (sessionController.wearableStatistics.restingHeartRateTrendText || "для тренда нужны данные за две недели") color: sessionController.wearableStatistics.restingHeartRateTrendId === "higher" ? window.warning : window.textMuted font.pixelSize: 11; wrapMode: Text.WordWrap } RowLayout { Layout.fillWidth: true; Layout.fillHeight: true; spacing: 12 Rectangle { Layout.fillWidth: true; Layout.fillHeight: true; radius: 18; color: window.panel; border.color: window.border ColumnLayout { anchors.fill: parent; anchors.margins: 14; spacing: 10 Label { text: "Шаги • последние 14 дней"; color: window.textMain; font.pixelSize: 15; font.bold: true } RowLayout { Layout.fillWidth: true; Layout.fillHeight: true; spacing: 4 Repeater { model: window.recentWearableDays(); delegate: ColumnLayout { required property var modelData required property int index Layout.fillWidth: true; Layout.fillHeight: true; spacing: 4 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 * Number(modelData.steps || 0) / window.wearableMaximum("steps")); radius: 3; color: window.blue; opacity: modelData.steps > 0 ? 0.9 : 0.18 } } Label { Layout.alignment: Qt.AlignHCenter; text: modelData.dateText; color: window.textMuted; font.pixelSize: 8; visible: index % 3 === 0 || index === window.recentWearableDays().length - 1 } } } } } } Rectangle { Layout.fillWidth: true; Layout.fillHeight: true; radius: 18; color: window.panel; border.color: window.border ColumnLayout { anchors.fill: parent; anchors.margins: 14; spacing: 10 Label { text: "Активные калории • последние 14 дней"; color: window.textMain; font.pixelSize: 15; font.bold: true } RowLayout { Layout.fillWidth: true; Layout.fillHeight: true; spacing: 4 Repeater { model: window.recentWearableDays(); delegate: ColumnLayout { required property var modelData required property int index Layout.fillWidth: true; Layout.fillHeight: true; spacing: 4 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 * Number(modelData.activeCaloriesKcal || 0) / window.wearableMaximum("activeCaloriesKcal")); radius: 3; color: window.warning; opacity: modelData.activeCaloriesKcal > 0 ? 0.9 : 0.18 } } Label { Layout.alignment: Qt.AlignHCenter; text: modelData.dateText; color: window.textMuted; font.pixelSize: 8; visible: index % 3 === 0 || index === window.recentWearableDays().length - 1 } } } } } } } Label { Layout.fillWidth: true; text: "Вес импортируется из записей Weight в Health Connect. Если Zepp Life передаёт вес, в карточке будет показан его package-id как источник."; color: window.textMuted; font.pixelSize: 11; wrapMode: Text.WordWrap } } } } }