docker / image (push) Failing after 24s
lint / ruff (push) Failing after 28s
tests / backend (push) Successful in 37s
tests / desktop (push) Successful in 22s
tests / android-core (push) Successful in 49s
tests / web (push) Successful in 1m20s
android / apk (push) Successful in 8m49s
379 lines
14 KiB
QML
379 lines
14 KiB
QML
// The one window with fields in it.
|
|
//
|
|
// Kadence on the desktop is an agent and a countdown: the tray menu drives the
|
|
// timer, the mini timer shows it, and the blocking happens with no window open
|
|
// at all. The rules, the task sources, the report and the lengths are the
|
|
// *server's*, edited in the web app, so a second place to edit them was a second
|
|
// place to be wrong — the five-page window that used to be here has gone.
|
|
//
|
|
// What is left genuinely belongs to this machine, and a fresh install has to be
|
|
// able to set it somewhere: which backend, and how this box behaves. One column,
|
|
// hairline sections, exactly one filled button.
|
|
import QtQuick
|
|
import QtQuick.Controls
|
|
import QtQuick.Layouts
|
|
import QtQuick.Window
|
|
|
|
import "."
|
|
import "components"
|
|
|
|
Window {
|
|
id: dialog
|
|
|
|
width: 460
|
|
height: Math.min(720, content.implicitHeight + 76)
|
|
minimumWidth: 420
|
|
minimumHeight: 420
|
|
title: "Kadence settings"
|
|
color: Theme.paper
|
|
flags: Qt.Dialog
|
|
|
|
// The live config, re-read whenever it changes underneath us (the tray can
|
|
// change view-only mode, and a toggle that fails is reverted in Python).
|
|
readonly property var config: controller.config
|
|
|
|
// Typed into, not bound: a text field that re-binds on every keystroke
|
|
// fights the cursor. These are committed on editing finished, or by Save.
|
|
property string draftUrl: ""
|
|
property string draftToken: ""
|
|
|
|
function reload() {
|
|
draftUrl = config.server_url
|
|
draftToken = config.api_token
|
|
}
|
|
|
|
onVisibleChanged: if (visible) reload()
|
|
Component.onCompleted: reload()
|
|
|
|
function commitBackend() {
|
|
if (draftUrl !== config.server_url) controller.setConfig("server_url", draftUrl)
|
|
if (draftToken !== config.api_token) controller.setConfig("api_token", draftToken)
|
|
}
|
|
|
|
// ---- the furniture -----------------------------------------------------
|
|
|
|
component FieldLabel: Label {
|
|
color: Theme.muted
|
|
font.pixelSize: 11
|
|
font.family: Theme.sansFamily
|
|
}
|
|
|
|
component Field: TextField {
|
|
Layout.fillWidth: true
|
|
color: Theme.ink
|
|
font.pixelSize: 13
|
|
font.family: Theme.sansFamily
|
|
background: Rectangle {
|
|
radius: 8
|
|
color: Theme.surface
|
|
border.width: 1
|
|
border.color: parent.activeFocus ? Theme.ember : Theme.rule
|
|
}
|
|
}
|
|
|
|
// A labelled switch with its own explanation underneath, because every one
|
|
// of these turns a real desktop facility on or off and "Inhibit" alone does
|
|
// not say what it will do to your screen.
|
|
component Toggle: RowLayout {
|
|
id: toggleRow
|
|
|
|
property string label: ""
|
|
property string detail: ""
|
|
property bool on: false
|
|
property bool available: true
|
|
/**
|
|
* A one-way ratchet: while this is set the switch can be turned on but not
|
|
* off, so it greys only when it is *already* on. `available` greys both
|
|
* directions, which is the wrong shape for "you may not switch this off".
|
|
*/
|
|
property bool lockedOn: false
|
|
readonly property bool settable: available && !(lockedOn && on)
|
|
signal toggledTo(bool value)
|
|
|
|
Layout.fillWidth: true
|
|
spacing: 12
|
|
|
|
ColumnLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 1
|
|
Label {
|
|
text: toggleRow.label
|
|
color: toggleRow.settable ? Theme.ink : Theme.faint
|
|
font.pixelSize: 13
|
|
font.family: Theme.sansFamily
|
|
}
|
|
Label {
|
|
visible: toggleRow.detail !== ""
|
|
text: toggleRow.detail
|
|
color: Theme.faint
|
|
font.pixelSize: 11
|
|
font.family: Theme.sansFamily
|
|
wrapMode: Text.WordWrap
|
|
Layout.fillWidth: true
|
|
}
|
|
}
|
|
Switch {
|
|
checked: toggleRow.on
|
|
enabled: toggleRow.settable
|
|
onToggled: toggleRow.toggledTo(checked)
|
|
}
|
|
}
|
|
|
|
ScrollView {
|
|
anchors.fill: parent
|
|
contentWidth: availableWidth
|
|
clip: true
|
|
|
|
ColumnLayout {
|
|
id: content
|
|
width: dialog.width
|
|
spacing: 18
|
|
|
|
Item { Layout.fillWidth: true; implicitHeight: 4 }
|
|
|
|
ColumnLayout {
|
|
Layout.leftMargin: 22
|
|
Layout.rightMargin: 22
|
|
spacing: 2
|
|
Label {
|
|
text: "KADENCE"
|
|
color: Theme.muted
|
|
font.pixelSize: 10
|
|
font.letterSpacing: 1.4
|
|
font.weight: Font.DemiBold
|
|
font.family: Theme.sansFamily
|
|
}
|
|
Label {
|
|
text: "This machine"
|
|
color: Theme.ink
|
|
font.pixelSize: 26
|
|
font.family: Theme.displayFamily
|
|
}
|
|
Label {
|
|
text: "Timer lengths, the block lists, the task sources and the "
|
|
+ "report all live on the server — edit those in the web app."
|
|
color: Theme.muted
|
|
font.pixelSize: 11
|
|
font.family: Theme.sansFamily
|
|
wrapMode: Text.WordWrap
|
|
Layout.fillWidth: true
|
|
Layout.topMargin: 6
|
|
}
|
|
}
|
|
|
|
// ---- backend -------------------------------------------------------
|
|
ColumnLayout {
|
|
Layout.leftMargin: 22
|
|
Layout.rightMargin: 22
|
|
spacing: 10
|
|
|
|
SectionTitle {
|
|
text: "Backend"
|
|
trailing: controller.connected
|
|
? "reachable"
|
|
: (controller.connectionError !== "" ? "not answering" : "…")
|
|
}
|
|
|
|
Banner {
|
|
visible: !controller.connected && controller.connectionError !== ""
|
|
text: controller.connectionError
|
|
}
|
|
|
|
FieldLabel { text: "Address" }
|
|
Field {
|
|
text: dialog.draftUrl
|
|
placeholderText: "http://localhost:8000"
|
|
onTextChanged: dialog.draftUrl = text
|
|
onEditingFinished: dialog.commitBackend()
|
|
}
|
|
|
|
FieldLabel { text: "API token — API_TOKEN from the backend's .env, if it has one" }
|
|
Field {
|
|
text: dialog.draftToken
|
|
echoMode: TextInput.Password
|
|
placeholderText: "none"
|
|
onTextChanged: dialog.draftToken = text
|
|
onEditingFinished: dialog.commitBackend()
|
|
}
|
|
}
|
|
|
|
// ---- the mini timer ------------------------------------------------
|
|
ColumnLayout {
|
|
Layout.leftMargin: 22
|
|
Layout.rightMargin: 22
|
|
spacing: 10
|
|
|
|
SectionTitle { text: "Mini timer" }
|
|
|
|
Toggle {
|
|
label: "Show it while an interval runs"
|
|
detail: "Otherwise it appears only when you ask for it — tray, Ctrl+M, or --mini."
|
|
on: dialog.config.mini_timer_auto
|
|
onToggledTo: (value) => controller.setConfig("mini_timer_auto", value)
|
|
}
|
|
|
|
Toggle {
|
|
label: "Keep it above everything"
|
|
detail: dialog.config.keep_above_supported
|
|
? "A KWin window rule, because Wayland gives an app no way to ask."
|
|
: "Not available on this desktop — your compositor decides."
|
|
on: dialog.config.mini_timer_keep_above
|
|
available: dialog.config.keep_above_supported
|
|
onToggledTo: (value) => controller.setConfig("mini_timer_keep_above", value)
|
|
}
|
|
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 12
|
|
ColumnLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 4
|
|
FieldLabel { text: "Where it appears" }
|
|
ComboBox {
|
|
Layout.fillWidth: true
|
|
model: controller.miniCornerLabels
|
|
currentIndex: controller.miniCornerIndex
|
|
font.family: Theme.sansFamily
|
|
onActivated: controller.setMiniCorner(currentIndex)
|
|
}
|
|
}
|
|
ColumnLayout {
|
|
spacing: 4
|
|
FieldLabel { text: "Edge gap" }
|
|
SpinBox {
|
|
from: 0
|
|
to: 400
|
|
stepSize: 4
|
|
value: dialog.config.mini_timer_margin
|
|
onValueModified: controller.setConfig("mini_timer_margin", value)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- enforcement ---------------------------------------------------
|
|
ColumnLayout {
|
|
Layout.leftMargin: 22
|
|
Layout.rightMargin: 22
|
|
spacing: 10
|
|
|
|
SectionTitle {
|
|
text: "Focus guard"
|
|
trailing: controller.blockerActive ? "enforcing" : controller.blockerReason
|
|
}
|
|
|
|
Banner {
|
|
visible: dialog.config.enforce_blocking && !controller.helperInstalled
|
|
text: "The privileged helper is not installed, so websites cannot be "
|
|
+ "blocked on this machine. Run: sudo ./desktop/helper/install.sh"
|
|
}
|
|
Banner {
|
|
visible: controller.blockerError !== ""
|
|
text: controller.blockerError
|
|
}
|
|
Banner {
|
|
visible: controller.blockerLocked
|
|
text: controller.blockerLockNote
|
|
}
|
|
|
|
Toggle {
|
|
label: "Enforce the server's rules here"
|
|
detail: "Off, this machine is a timer and nothing else."
|
|
on: dialog.config.enforce_blocking
|
|
lockedOn: controller.blockerLocked
|
|
onToggledTo: (value) => controller.setConfig("enforce_blocking", value)
|
|
}
|
|
Toggle {
|
|
label: "Block websites"
|
|
detail: "The managed region of /etc/hosts, plus a local DNS stub for the masks it cannot express."
|
|
on: dialog.config.block_hosts_file
|
|
available: dialog.config.enforce_blocking
|
|
lockedOn: controller.blockerLocked
|
|
onToggledTo: (value) => controller.setConfig("block_hosts_file", value)
|
|
}
|
|
Toggle {
|
|
label: "Close blocked applications"
|
|
detail: "/proc is scanned every few seconds and matches are asked to quit, then made to."
|
|
on: dialog.config.block_apps
|
|
available: dialog.config.enforce_blocking
|
|
lockedOn: controller.blockerLocked
|
|
onToggledTo: (value) => controller.setConfig("block_apps", value)
|
|
}
|
|
}
|
|
|
|
// ---- the session ---------------------------------------------------
|
|
ColumnLayout {
|
|
Layout.leftMargin: 22
|
|
Layout.rightMargin: 22
|
|
spacing: 10
|
|
|
|
SectionTitle { text: "This session" }
|
|
|
|
Toggle {
|
|
label: "Start with the session"
|
|
detail: "An XDG autostart entry, minimised to the tray."
|
|
on: dialog.config.autostart
|
|
onToggledTo: (value) => controller.setConfig("autostart", value)
|
|
}
|
|
Toggle {
|
|
label: "Run as a systemd user service"
|
|
detail: "Restarts on failure and logs to the journal: journalctl --user -u kadence-desktop."
|
|
on: dialog.config.service_enabled
|
|
onToggledTo: (value) => controller.setConfig("service_enabled", value)
|
|
}
|
|
Toggle {
|
|
label: "Pause when the session locks"
|
|
detail: "logind says when you walked away; the interval resumes when you come back."
|
|
on: dialog.config.pause_on_lock
|
|
onToggledTo: (value) => controller.setConfig("pause_on_lock", value)
|
|
}
|
|
Toggle {
|
|
label: "Pause other people's music"
|
|
detail: "Any MPRIS player is paused when focus starts; only the ones we paused resume."
|
|
on: dialog.config.pause_media
|
|
onToggledTo: (value) => controller.setConfig("pause_media", value)
|
|
}
|
|
Toggle {
|
|
label: "Silence other apps' notifications"
|
|
detail: "Do Not Disturb for the length of a focus interval, put back afterwards."
|
|
on: dialog.config.do_not_disturb
|
|
onToggledTo: (value) => controller.setConfig("do_not_disturb", value)
|
|
}
|
|
Toggle {
|
|
label: "Keep the screen awake while focusing"
|
|
on: dialog.config.inhibit_screensaver
|
|
onToggledTo: (value) => controller.setConfig("inhibit_screensaver", value)
|
|
}
|
|
}
|
|
|
|
// ---- out -----------------------------------------------------------
|
|
RowLayout {
|
|
Layout.leftMargin: 22
|
|
Layout.rightMargin: 22
|
|
Layout.bottomMargin: 20
|
|
Layout.topMargin: 4
|
|
spacing: 10
|
|
|
|
GhostButton {
|
|
text: "Open the web app"
|
|
onClicked: controller.openWebUi()
|
|
}
|
|
Item { Layout.fillWidth: true }
|
|
PrimaryButton {
|
|
text: "Done"
|
|
onClicked: {
|
|
dialog.commitBackend()
|
|
dialog.close()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Esc closes it, like any dialog. Enter commits what was typed.
|
|
Shortcut {
|
|
sequence: StandardKey.Cancel
|
|
onActivated: dialog.close()
|
|
}
|
|
}
|