// Settings, split by where they live. // // "This computer" is local to this machine (~/.config/kadence/desktop.json). // Everything else is server-side, shared with the web app and the Telegram bot. import QtQuick import QtQuick.Controls import QtQuick.Layouts import "." import "components" ScrollView { id: page contentWidth: availableWidth clip: true readonly property var pomodoro: controller.settings.pomodoro ? controller.settings.pomodoro : null readonly property var telegram: controller.settings.notifications ? controller.settings.notifications.telegram : null readonly property var report: controller.settings.notifications ? controller.settings.notifications.report : null function patchPomodoro(key, value) { var patch = {}; patch[key] = value controller.patchSettings({ pomodoro: patch }) } function patchTelegram(key, value) { var patch = {}; patch[key] = value controller.patchSettings({ notifications: { telegram: patch } }) } // The Focus/Short break/Long break fields always show and edit the live // settings. When they still match a saved preset, the edit also rewrites // that preset's own numbers, so picking it again later gets you back to // where you left off rather than to some older saved value. function patchDuration(key, value) { if (!page.pomodoro) return var patch = {}; patch[key] = value var idx = controller.activePresetIndex if (idx >= 0 && page.pomodoro.presets) { var presets = page.pomodoro.presets.map(function (p) { return Object.assign({}, p) }) presets[idx] = Object.assign(presets[idx], patch) controller.patchSettings({ pomodoro: Object.assign({ presets: presets }, patch) }) } else { controller.patchSettings({ pomodoro: patch }) } } component Field: ColumnLayout { property string label: "" default property alias content: holder.data spacing: 4 Label { text: parent.label color: Theme.muted font.pixelSize: 11 font.family: Theme.sansFamily } ColumnLayout { id: holder; spacing: 0 } } ColumnLayout { width: page.availableWidth - 56 x: 28 y: 26 spacing: 26 PageHeader { eyebrow: "Shared with the web app and the Telegram bot" title: "Settings" caption: "Secrets are masked once stored — leave the dots alone to keep the saved value." GhostButton { text: "Open the web app"; onClicked: controller.openWebUi() } } // ---- backend ---------------------------------------------------------- ColumnLayout { Layout.fillWidth: true spacing: 12 SectionTitle { text: "Backend" trailing: controller.connected ? "connected" : "offline" } RowLayout { Layout.fillWidth: true spacing: 10 TextField { id: serverField Layout.fillWidth: true text: controller.config.server_url placeholderText: "http://localhost:8000" } TextField { id: tokenField Layout.preferredWidth: 180 text: controller.config.api_token placeholderText: "API token (optional)" echoMode: TextInput.Password } PrimaryButton { text: "Connect" onClicked: { controller.setConfig("server_url", serverField.text.trim()) controller.setConfig("api_token", tokenField.text) } } } Label { visible: !controller.connected && controller.connectionError !== "" text: controller.connectionError color: Theme.clay font.pixelSize: 12 font.family: Theme.sansFamily Layout.fillWidth: true wrapMode: Text.WordWrap } } // ---- timer ------------------------------------------------------------- ColumnLayout { Layout.fillWidth: true spacing: 12 SectionTitle { text: "Timer" } // The preset switcher: pick one and it applies at once. The fields // below always belong to whichever one is lit — there is nothing // to show for the others until you switch to them. RowLayout { Layout.fillWidth: true spacing: 8 Repeater { model: page.pomodoro && page.pomodoro.presets ? page.pomodoro.presets : [] delegate: PresetChip { required property var modelData required property int index text: modelData.name active: controller.activePresetIndex === index onClicked: controller.applyPreset(index) } } Label { visible: controller.activePresetIndex === -1 text: "Custom" color: Theme.muted font.pixelSize: 12 font.family: Theme.sansFamily leftPadding: 6 } Item { Layout.fillWidth: true } } Flow { Layout.fillWidth: true spacing: 20 Field { label: "Focus (min)" SpinBox { from: 1; to: 240 value: page.pomodoro ? page.pomodoro.work_minutes : 25 onValueModified: page.patchDuration("work_minutes", value) } } Field { label: "Short break" SpinBox { from: 1; to: 120 value: page.pomodoro ? page.pomodoro.short_break_minutes : 5 onValueModified: page.patchDuration("short_break_minutes", value) } } Field { label: "Long break" SpinBox { from: 1; to: 240 value: page.pomodoro ? page.pomodoro.long_break_minutes : 15 onValueModified: page.patchDuration("long_break_minutes", value) } } Field { label: "Long break every" SpinBox { from: 1; to: 12 value: page.pomodoro ? page.pomodoro.cycles_before_long_break : 4 onValueModified: page.patchPomodoro("cycles_before_long_break", value) } } Field { label: "Daily goal" SpinBox { from: 1; to: 40 value: page.pomodoro ? page.pomodoro.daily_goal_pomodoros : 8 onValueModified: page.patchPomodoro("daily_goal_pomodoros", value) } } } Flow { Layout.fillWidth: true spacing: 18 Switch { text: "Timer enabled (off = view-only mode)" checked: controller.timerEnabled onToggled: controller.setTimerEnabled(checked) } Switch { text: "Auto-start breaks" checked: page.pomodoro ? page.pomodoro.auto_start_breaks : true onToggled: page.patchPomodoro("auto_start_breaks", checked) } Switch { text: "Auto-start next focus" checked: page.pomodoro ? page.pomodoro.auto_start_work : false onToggled: page.patchPomodoro("auto_start_work", checked) } Switch { text: "Allow pausing" checked: page.pomodoro ? page.pomodoro.allow_pause : true onToggled: page.patchPomodoro("allow_pause", checked) } Switch { text: "Strict mode" checked: page.pomodoro ? page.pomodoro.strict_mode : false onToggled: page.patchPomodoro("strict_mode", checked) ToolTip.visible: hovered ToolTip.text: "Abandoning a focus interval requires a reason." } } } // ---- this computer ------------------------------------------------------ ColumnLayout { Layout.fillWidth: true spacing: 12 SectionTitle { text: "This computer"; trailing: "native desktop features" } Flow { Layout.fillWidth: true spacing: 18 Switch { text: "Start in view-only mode" checked: controller.config.view_only onToggled: controller.setConfig("view_only", checked) } Switch { text: "Tray icon" checked: controller.config.tray_icon onToggled: controller.setConfig("tray_icon", checked) } Switch { text: "Close to tray" checked: controller.config.close_to_tray onToggled: controller.setConfig("close_to_tray", checked) } Switch { text: "Notifications" checked: controller.config.notifications onToggled: controller.setConfig("notifications", checked) } Switch { text: "Notification buttons" checked: controller.config.notification_actions onToggled: controller.setConfig("notification_actions", checked) } Switch { text: "Keep the screen awake" checked: controller.config.inhibit_screensaver onToggled: controller.setConfig("inhibit_screensaver", checked) } Switch { text: "Do Not Disturb while focusing" checked: controller.config.do_not_disturb onToggled: controller.setConfig("do_not_disturb", checked) } Switch { text: "Pause music on focus" checked: controller.config.pause_media onToggled: controller.setConfig("pause_media", checked) ToolTip.visible: hovered ToolTip.text: "Any MPRIS player: Spotify, VLC, mpv, browsers." } Switch { text: "Mini timer" checked: controller.config.mini_timer onToggled: controller.setConfig("mini_timer", checked) ToolTip.visible: hovered ToolTip.text: "A small always-on-top countdown. Ctrl+M, or the tray menu." } Switch { text: "Mini timer follows the window" checked: controller.config.mini_timer_auto onToggled: controller.setConfig("mini_timer_auto", checked) ToolTip.visible: hovered ToolTip.text: "Show it while an interval runs and the main window is hidden." } Switch { text: "Keep the mini timer above everything" enabled: controller.config.keep_above_supported checked: controller.config.mini_timer_keep_above onToggled: controller.setConfig("mini_timer_keep_above", checked) ToolTip.visible: hovered ToolTip.text: controller.config.keep_above_supported ? "Installs a KWin window rule — the only way to float above " + "other apps on Wayland. Switching this off removes it again." : "Needs KWin (KDE). Elsewhere the Qt window flag is used." } Switch { text: "Taskbar progress" checked: controller.config.taskbar_progress onToggled: controller.setConfig("taskbar_progress", checked) } Switch { text: "Pause when the screen locks" checked: controller.config.pause_on_lock onToggled: controller.setConfig("pause_on_lock", checked) } Switch { text: "D-Bus control" checked: controller.config.dbus_service onToggled: controller.setConfig("dbus_service", checked) ToolTip.visible: hovered ToolTip.text: "org.kadence.Desktop — bind it to a global shortcut." } } RowLayout { Layout.fillWidth: true spacing: 20 Field { label: "Mini timer appears" ComboBox { implicitWidth: 210 textRole: "label" valueRole: "key" model: [ { key: "top-left", label: "Top left" }, { key: "top-centre", label: "Top centre" }, { key: "top-right", label: "Top right" }, { key: "bottom-left", label: "Bottom left" }, { key: "bottom-centre", label: "Bottom centre" }, { key: "bottom-right", label: "Bottom right" }, { key: "centre", label: "Centre of the screen" }, { key: "remember", label: "Where I last dragged it" } ] currentIndex: { var corner = controller.config.mini_timer_corner for (var i = 0; i < model.length; i++) if (model[i].key === corner) return i return 5 } onActivated: function (index) { controller.setConfig("mini_timer_corner", model[index].key) } } } Field { label: "Gap from the edges (px)" SpinBox { from: 0; to: 400; stepSize: 4 value: controller.config.mini_timer_margin onValueModified: controller.setConfig("mini_timer_margin", value) } } Item { Layout.fillWidth: true } } RowLayout { Layout.fillWidth: true spacing: 20 Field { label: "Pause after idle (min, 0 = never)" SpinBox { from: 0; to: 120 value: controller.config.pause_on_idle_minutes onValueModified: controller.setConfig("pause_on_idle_minutes", value) } } Field { label: "Poll the backend every (s)" SpinBox { from: 1; to: 60 value: controller.config.poll_seconds onValueModified: controller.setConfig("poll_seconds", value) } } Item { Layout.fillWidth: true } } Flow { Layout.fillWidth: true spacing: 18 Switch { text: "Start with the session" checked: controller.config.autostart onToggled: controller.setConfig("autostart", checked) } Switch { text: "systemd user service" checked: controller.config.service_enabled onToggled: controller.setConfig("service_enabled", checked) ToolTip.visible: hovered ToolTip.text: "Restarts the app if it dies; logs to the journal." } Switch { text: "Start minimised" checked: controller.config.start_minimized onToggled: controller.setConfig("start_minimized", checked) } } } // ---- telegram ------------------------------------------------------------- ColumnLayout { Layout.fillWidth: true spacing: 12 SectionTitle { text: "Telegram"; trailing: "reports, briefings and nudges" } RowLayout { Layout.fillWidth: true spacing: 10 Switch { text: "Enabled" checked: page.telegram ? page.telegram.enabled : false onToggled: page.patchTelegram("enabled", checked) } TextField { Layout.fillWidth: true placeholderText: "Bot token from @BotFather" text: page.telegram ? page.telegram.bot_token : "" onEditingFinished: if (text !== "••••••••") page.patchTelegram("bot_token", text) } TextField { Layout.preferredWidth: 160 placeholderText: "Chat id" text: page.telegram ? page.telegram.chat_id : "" onEditingFinished: page.patchTelegram("chat_id", text) } } Flow { Layout.fillWidth: true spacing: 18 Switch { text: "Daily report" checked: page.telegram ? page.telegram.daily_report : true onToggled: page.patchTelegram("daily_report", checked) } Switch { text: "Morning briefing" checked: page.telegram ? page.telegram.morning_briefing : true onToggled: page.patchTelegram("morning_briefing", checked) } Switch { text: "Weekly review" checked: page.telegram ? page.telegram.weekly_report : true onToggled: page.patchTelegram("weekly_report", checked) } Switch { text: "Nudge me when idle" checked: page.telegram ? page.telegram.nudge_enabled : true onToggled: page.patchTelegram("nudge_enabled", checked) } Switch { text: "Chat commands" checked: page.telegram ? page.telegram.commands_enabled : true onToggled: page.patchTelegram("commands_enabled", checked) } } Flow { Layout.fillWidth: true spacing: 20 Field { label: "Report at (HH:MM)" TextField { implicitWidth: 110 text: page.telegram ? page.telegram.daily_report_time : "21:00" onEditingFinished: page.patchTelegram("daily_report_time", text) } } Field { label: "Briefing at (HH:MM)" TextField { implicitWidth: 110 text: page.telegram ? page.telegram.morning_briefing_time : "09:00" onEditingFinished: page.patchTelegram("morning_briefing_time", text) } } Field { label: "Nudge after (min idle)" SpinBox { from: 10; to: 600 value: page.telegram ? page.telegram.nudge_after_idle_minutes : 90 onValueModified: page.patchTelegram("nudge_after_idle_minutes", value) } } Field { label: "Tone" ComboBox { implicitWidth: 170 model: ["friendly", "coach", "drill_sergeant", "dry"] currentIndex: page.report ? model.indexOf(page.report.tone) : 0 onActivated: function (index) { controller.patchSettings({ notifications: { report: { tone: model[index] } } }) } } } Field { label: "Language" ComboBox { implicitWidth: 110 model: ["en", "ru"] currentIndex: page.report && page.report.language === "ru" ? 1 : 0 onActivated: function (index) { controller.patchSettings({ notifications: { report: { language: model[index] } } }) } } } } } // ---- scripting ------------------------------------------------------------- ColumnLayout { Layout.fillWidth: true spacing: 12 SectionTitle { text: "Shortcuts and scripting" } Panel { Label { Layout.fillWidth: true wrapMode: Text.WordWrap color: Theme.muted font.pixelSize: 11 font.family: Theme.monoFamily text: "qdbus org.kadence.Desktop /Timer org.kadence.Timer.Toggle\n" + "qdbus org.kadence.Desktop /Timer org.kadence.Timer.Status\n" + "kadence-desktop --toggle" } Label { Layout.fillWidth: true wrapMode: Text.WordWrap color: Theme.faint font.pixelSize: 11 font.family: Theme.sansFamily text: "System Settings → Shortcuts → Custom → D-Bus command binds these to a key." } } Label { text: "Kadence " + appVersion color: Theme.faint font.pixelSize: 11 font.family: Theme.sansFamily } } Item { Layout.preferredHeight: 10 } } Component.onCompleted: controller.loadSettings() }