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:
84 files changed
+4501
-2682
No files matched your search
@@ -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()
|
||||
|
||||
Reference in new issue
Block a user