601 lines
25 KiB
QML
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()
|
|
}
|