refactor: enhance TimerPage layout and functionality; update theme and tokens for improved design consistency

- Refactored TimerPage component to improve layout and user experience.
- Updated theme configuration to utilize design tokens for better maintainability.
- Introduced new tokens for colors and typography to align with design principles.
- Simplified state management and improved timer functionality.
- Enhanced accessibility and responsiveness of UI elements.
This commit is contained in:
timka123 committed 2026-10-18 23:57:36 +03:00
1 parent 17a0a3c4c9
commit 77b26757bf
84 files changed
+4501 -2682

No files matched your search

+292 -159
View File
@@ -1,12 +1,13 @@
// What the blocker is doing right now, and what it will do.
// The focus guard: what is blocked, when, and what it caught.
//
// The rules themselves are server-side (edit them in the web UI or below); this
// page is about *enforcement on this machine* — the helper, the hosts file and
// the process watcher.
// Lists accept masks. A hosts file has no wildcards of its own, so `*.x.com` is
// expanded server-side into real hostnames; this page shows that count and says
// plainly when a pattern cannot be expressed that way.
import QtQuick
import QtQuick.Controls.Material
import QtQuick.Controls
import QtQuick.Layouts
import "."
import "components"
ScrollView {
@@ -15,176 +16,308 @@ ScrollView {
clip: true
ColumnLayout {
width: page.availableWidth
spacing: 16
width: page.availableWidth - 56
x: 28
y: 26
spacing: 22
Banner {
Layout.fillWidth: true
visible: !controller.helperInstalled
text: "The privileged helper is not installed, so websites cannot be blocked. "
+ "Run: sudo desktop/helper/install.sh"
actionText: ""
PageHeader {
eyebrow: controller.blockerReason
title: controller.blockerActive ? "Blocking is on" : "Blocking is off"
caption: "Rules are shared with the web app; this machine enforces them."
Switch {
text: "Enabled"
checked: controller.settings.blocker ? controller.settings.blocker.enabled : false
onToggled: controller.patchSettings({ blocker: { enabled: checked } })
}
}
Banner {
Layout.fillWidth: true
visible: !controller.helperInstalled
text: "The privileged helper is not installed, so websites cannot be blocked. "
+ "Run: sudo desktop/helper/install.sh"
}
Banner {
visible: controller.blockerError !== ""
text: controller.blockerError
}
ColumnLayout {
Layout.fillWidth: true
Layout.margins: 20
spacing: 16
Card {
title: controller.blockerActive ? "🛡 Blocking is ON" : "Blocking is off"
subtitle: controller.blockerReason
RowLayout {
Layout.fillWidth: true
spacing: 20
StatTile {
label: "Domains"
value: controller.blockedDomains.length
caption: "redirected to 127.0.0.1"
}
StatTile {
label: "Apps"
value: controller.blockedApps.length
caption: "closed on sight"
}
StatTile {
label: "Attempts today"
value: controller.stats.today ? controller.stats.today.blocked_attempts : 0
caption: "and counting"
}
}
RowLayout {
Layout.fillWidth: true
Switch {
text: "Enforce on this machine"
checked: controller.config.enforce_blocking
onToggled: controller.setConfig("enforce_blocking", checked)
}
Switch {
text: "Websites"
checked: controller.config.block_hosts_file
onToggled: controller.setConfig("block_hosts_file", checked)
}
Switch {
text: "Apps"
checked: controller.config.block_apps
onToggled: controller.setConfig("block_apps", checked)
}
}
}
Card {
title: "Blocked websites"
subtitle: "One hostname per line. `www.` and `m.` are added automatically."
TextArea {
id: domainsArea
Layout.fillWidth: true
Layout.minimumHeight: 140
wrapMode: TextArea.NoWrap
font.family: "Roboto Mono, monospace"
text: (controller.settings.blocker ? controller.settings.blocker.blocked_domains : []).join("\n")
placeholderText: "youtube.com\nreddit.com\ntwitter.com"
}
RowLayout {
PrimaryButton {
text: "Save websites"
onClicked: {
var list = domainsArea.text.split("\n").map(function (s) { return s.trim() })
.filter(function (s) { return s.length > 0 })
controller.patchSettings({ blocker: { blocked_domains: list } })
}
}
Item { Layout.fillWidth: true }
}
}
Card {
title: "Blocked applications"
subtitle: "Process names, matched loosely: `steam` also catches `steamwebhelper`."
TextArea {
id: appsArea
Layout.fillWidth: true
Layout.minimumHeight: 100
wrapMode: TextArea.NoWrap
font.family: "Roboto Mono, monospace"
text: (controller.settings.blocker ? controller.settings.blocker.blocked_apps : []).join("\n")
placeholderText: "steam\ndiscord\ntelegram-desktop"
}
RowLayout {
PrimaryButton {
text: "Save applications"
onClicked: {
var list = appsArea.text.split("\n").map(function (s) { return s.trim() })
.filter(function (s) { return s.length > 0 })
controller.patchSettings({ blocker: { blocked_apps: list } })
}
}
Item { Layout.fillWidth: true }
}
}
Card {
title: "When to block"
subtitle: "Server-side, so every device agrees."
RowLayout {
Layout.fillWidth: true
Switch {
text: "Blocking enabled"
checked: controller.settings.blocker ? controller.settings.blocker.enabled : false
onToggled: controller.patchSettings({ blocker: { enabled: checked } })
}
ComboBox {
Layout.preferredWidth: 220
model: ["During focus intervals", "Always", "On a schedule"]
currentIndex: {
var mode = controller.settings.blocker ? controller.settings.blocker.mode : "during_work"
return mode === "always" ? 1 : (mode === "scheduled" ? 2 : 0)
}
onActivated: function (index) {
var modes = ["during_work", "always", "scheduled"]
controller.patchSettings({ blocker: { mode: modes[index] } })
}
}
Switch {
text: "Keep blocking during breaks"
checked: controller.settings.blocker ? controller.settings.blocker.block_during_breaks : false
onToggled: controller.patchSettings({ blocker: { block_during_breaks: checked } })
}
}
}
Card {
title: "Today's temptations"
subtitle: "Every attempt the blocker turned away — this is what the daily report jokes about."
Panel {
padding: 0
RowLayout {
Layout.fillWidth: true
spacing: 0
Repeater {
model: controller.stats.today ? controller.stats.today.blocked_top : []
model: [
{ label: "Hostnames", value: "" + controller.blockedDomains.length, caption: "after expansion" },
{ label: "Apps", value: "" + controller.blockedApps.length, caption: "closed on sight" },
{ label: "Attempts today",
value: controller.stats.today ? "" + controller.stats.today.blocked_attempts : "0",
caption: "turned away" },
{ label: "Enforcing",
value: controller.config.enforce_blocking ? "Yes" : "No",
caption: "on this computer" }
]
delegate: RowLayout {
required property int index
required property var modelData
Layout.fillWidth: true
Label { text: modelData.target; Layout.fillWidth: true }
Label { text: "× " + modelData.count; font.weight: Font.DemiBold }
spacing: 0
Rectangle {
visible: index > 0
width: 1
Layout.preferredHeight: 70
color: Theme.rule
}
Stat {
Layout.fillWidth: true
Layout.margins: 18
label: modelData.label
value: modelData.value
caption: modelData.caption
}
}
}
Label {
visible: !controller.stats.today || controller.stats.today.blocked_top.length === 0
text: "Nothing yet today. Either the blocker is off, or you are behaving."
opacity: 0.6
font.pixelSize: 12
}
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 12
SectionTitle { text: "When" }
RowLayout {
Layout.fillWidth: true
spacing: 16
ComboBox {
Layout.preferredWidth: 240
font.pixelSize: 13
model: ["While a focus interval runs", "Always", "On a schedule"]
currentIndex: {
var mode = controller.settings.blocker ? controller.settings.blocker.mode : "during_work"
return mode === "always" ? 1 : (mode === "scheduled" ? 2 : 0)
}
onActivated: function (index) {
var modes = ["during_work", "always", "scheduled"]
controller.patchSettings({ blocker: { mode: modes[index] } })
}
}
Switch {
text: "Through breaks"
checked: controller.settings.blocker ? controller.settings.blocker.block_during_breaks : false
onToggled: controller.patchSettings({ blocker: { block_during_breaks: checked } })
}
Switch {
text: "Close blocked apps"
checked: controller.settings.blocker ? controller.settings.blocker.kill_blocked_apps : true
onToggled: controller.patchSettings({ blocker: { kill_blocked_apps: checked } })
}
Item { Layout.fillWidth: true }
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 12
SectionTitle { text: "Masks" }
Panel {
spacing: 0
Repeater {
model: [
{ pattern: "youtube.com", meaning: "the site itself, plus www. and m." },
{ pattern: "*.google.com", meaning: "every subdomain in the expansion list" },
{ pattern: "*gram*", meaning: "matches apps; a hosts file cannot express it" }
]
delegate: RowLayout {
required property var modelData
Layout.fillWidth: true
spacing: 16
Label {
text: modelData.pattern
color: Theme.ink
font.pixelSize: 12
font.family: Theme.monoFamily
Layout.preferredWidth: 140
}
Label {
text: modelData.meaning
color: Theme.muted
font.pixelSize: 12
font.family: Theme.sansFamily
Layout.fillWidth: true
}
}
}
}
}
GridLayout {
Layout.fillWidth: true
columns: page.width > 900 ? 3 : 1
columnSpacing: 18
rowSpacing: 18
ColumnLayout {
Layout.fillWidth: true
spacing: 8
SectionTitle { text: "Blocked websites" }
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 160
radius: 10
color: Theme.surface
border.width: 1
border.color: domainsArea.activeFocus ? Theme.ember : Theme.rule
ScrollView {
anchors.fill: parent
anchors.margins: 8
clip: true
TextArea {
id: domainsArea
background: null
wrapMode: TextArea.NoWrap
font.family: Theme.monoFamily
font.pixelSize: 12
color: Theme.ink
placeholderText: "youtube.com\n*.reddit.com"
text: (controller.settings.blocker ? controller.settings.blocker.blocked_domains : []).join("
")
}
}
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 8
SectionTitle { text: "Exceptions" }
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 160
radius: 10
color: Theme.surface
border.width: 1
border.color: allowArea.activeFocus ? Theme.ember : Theme.rule
ScrollView {
anchors.fill: parent
anchors.margins: 8
clip: true
TextArea {
id: allowArea
background: null
wrapMode: TextArea.NoWrap
font.family: Theme.monoFamily
font.pixelSize: 12
color: Theme.ink
placeholderText: "music.youtube.com\n*.docs.google.com"
text: (controller.settings.blocker ? controller.settings.blocker.allowed_domains : []).join("
")
}
}
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 8
SectionTitle { text: "Blocked applications" }
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 160
radius: 10
color: Theme.surface
border.width: 1
border.color: appsArea.activeFocus ? Theme.ember : Theme.rule
ScrollView {
anchors.fill: parent
anchors.margins: 8
clip: true
TextArea {
id: appsArea
background: null
wrapMode: TextArea.NoWrap
font.family: Theme.monoFamily
font.pixelSize: 12
color: Theme.ink
placeholderText: "steam*\n*discord*"
text: (controller.settings.blocker ? controller.settings.blocker.blocked_apps : []).join("
")
}
}
}
}
}
RowLayout {
Layout.fillWidth: true
spacing: 12
TextField {
id: subdomainField
Layout.preferredWidth: 420
placeholderText: "*.site expands to: www, m, app…"
text: (controller.settings.blocker && controller.settings.blocker.subdomain_expansions
? controller.settings.blocker.subdomain_expansions : []).join(", ")
}
Item { Layout.fillWidth: true }
PrimaryButton {
text: "Save lists"
onClicked: {
function split(text, sep) {
return text.split(sep).map(function (s) { return s.trim() })
.filter(function (s) { return s.length > 0 })
}
controller.patchSettings({
blocker: {
blocked_domains: split(domainsArea.text, "\n"),
allowed_domains: split(allowArea.text, "\n"),
blocked_apps: split(appsArea.text, "\n"),
subdomain_expansions: split(subdomainField.text, ",")
}
})
}
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 8
SectionTitle { text: "Today's temptations" }
Repeater {
model: controller.stats.today ? controller.stats.today.blocked_top : []
delegate: RowLayout {
required property var modelData
Layout.fillWidth: true
Label {
text: modelData.target
color: Theme.ink
font.pixelSize: 12
font.family: Theme.monoFamily
Layout.fillWidth: true
}
Label {
text: "× " + modelData.count
color: Theme.muted
font.pixelSize: 12
font.family: Theme.sansFamily
}
}
}
Label {
visible: !controller.stats.today || controller.stats.today.blocked_top.length === 0
text: "Nothing yet — either the blocker is off, or you are behaving."
color: Theme.faint
font.pixelSize: 12
font.family: Theme.sansFamily
}
}
Item { Layout.preferredHeight: 10 }
}
Component.onCompleted: controller.loadSettings()