Files
kadence/desktop/kadence_desktop/qml/SettingsPage.qml
T
2026-10-19 09:19:47 +03:00

601 lines
25 KiB
QML

// 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 } })
}
// Presets are a saved list on the server (`pomodoro.presets`), each one a
// name plus its own work/short/long minutes — editable here, not baked
// into the UI. `_merge` on the backend replaces list fields wholesale, so
// editing one preset's number means resending the whole list.
function patchPreset(index, key, value) {
if (!page.pomodoro || !page.pomodoro.presets) return
var presets = page.pomodoro.presets.map(function (p) { return Object.assign({}, p) })
presets[index][key] = value
controller.patchSettings({ pomodoro: { presets: presets } })
}
function applyPreset(index) {
if (!page.pomodoro || !page.pomodoro.presets) return
var preset = page.pomodoro.presets[index]
controller.patchSettings({
pomodoro: {
work_minutes: preset.work_minutes,
short_break_minutes: preset.short_break_minutes,
long_break_minutes: preset.long_break_minutes,
cycles_before_long_break: preset.cycles_before_long_break
}
})
}
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" }
// Presets: a saved list of work/short/long combos. "Apply" copies
// one onto the live settings below; the numbers on each row are
// the preset's own and edit it in place.
ColumnLayout {
Layout.fillWidth: true
spacing: 8
Repeater {
model: page.pomodoro && page.pomodoro.presets ? page.pomodoro.presets : []
RowLayout {
id: presetRow
required property var modelData
required property int index
Layout.fillWidth: true
spacing: 16
readonly property bool isActive: page.pomodoro
&& page.pomodoro.work_minutes === modelData.work_minutes
&& page.pomodoro.short_break_minutes === modelData.short_break_minutes
&& page.pomodoro.long_break_minutes === modelData.long_break_minutes
Label {
text: presetRow.modelData.name
color: presetRow.isActive ? Theme.ember : Theme.ink
font.pixelSize: 13
font.weight: presetRow.isActive ? Font.DemiBold : Font.Medium
font.family: Theme.sansFamily
Layout.preferredWidth: 70
}
Field {
label: "Focus"
SpinBox {
from: 1; to: 240
value: presetRow.modelData.work_minutes
onValueModified: page.patchPreset(presetRow.index, "work_minutes", value)
}
}
Field {
label: "Short break"
SpinBox {
from: 1; to: 120
value: presetRow.modelData.short_break_minutes
onValueModified: page.patchPreset(presetRow.index, "short_break_minutes", value)
}
}
Field {
label: "Long break"
SpinBox {
from: 1; to: 240
value: presetRow.modelData.long_break_minutes
onValueModified: page.patchPreset(presetRow.index, "long_break_minutes", value)
}
}
GhostButton {
text: presetRow.isActive ? "Active" : "Apply"
enabled: !presetRow.isActive
onClicked: page.applyPreset(presetRow.index)
}
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.patchPomodoro("work_minutes", value)
}
}
Field {
label: "Short break"
SpinBox {
from: 1; to: 120
value: page.pomodoro ? page.pomodoro.short_break_minutes : 5
onValueModified: page.patchPomodoro("short_break_minutes", value)
}
}
Field {
label: "Long break"
SpinBox {
from: 1; to: 240
value: page.pomodoro ? page.pomodoro.long_break_minutes : 15
onValueModified: page.patchPomodoro("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()
}