// The focus guard: what is blocked, when, and what it caught. // // Lists accept masks. A hosts file has no wildcards of its own, so `*.x.com` is // expanded server-side into real hostnames, and the patterns that leaves over // are answered by the local DNS stub for the zones it can route. The stats show // what is *really* in place on this computer — the hosts region and the routed // zones — and every banner says plainly what could not be enforced here. import QtQuick import QtQuick.Controls import QtQuick.Layouts import "." import "components" ScrollView { id: page contentWidth: availableWidth clip: true ColumnLayout { width: page.availableWidth - 56 x: 28 y: 26 spacing: 22 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 { 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 } Banner { visible: controller.invalidExpansions.length > 0 text: controller.invalidExpansions.join(", ") + " is not a subdomain label, so it was ignored. Each entry is one " + "label — web, m, mobile. A bare `*` is kept and means every subdomain." } Banner { visible: controller.blockerActive && controller.unroutableDomains.length > 0 text: controller.unroutableDomains.join(", ") + " cannot be blocked on this computer: no DNS zone can be routed for " + "a mask with nothing after its last `*`. The phone still matches it " + "against the URLs you open." } Banner { visible: controller.blockerActive && controller.blockedDomains.length > 0 && controller.appliedDomains.length === 0 text: "Blocking is on, but this machine's hosts file has none of the " + controller.blockedDomains.length + " hostnames in it yet. Either the " + "write has not run, or it was refused — a message above would say which." } Panel { padding: 0 RowLayout { Layout.fillWidth: true spacing: 0 Repeater { model: [ { label: "Hostnames", value: "" + controller.appliedDomains.length, caption: controller.config.enforce_blocking && controller.config.block_hosts_file ? "in /etc/hosts" : "not written (enforcement off)" }, { label: "Zones", value: "" + controller.routedZones.length, caption: "routed to the stub" }, { label: "Apps", value: "" + controller.blockedApps.length, caption: !controller.config.enforce_blocking || !controller.config.block_apps ? "not enforced" : (controller.settings.blocker && controller.settings.blocker.kill_blocked_apps ? "closed on sight" : "warned only") }, { 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 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 } } } } } 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: "the bare domain plus every subdomain in the expansion list" }, { pattern: "*gram*", meaning: "matches apps, and URLs on the phone; no DNS zone can be routed for it" }, { pattern: "* in the list below", meaning: "makes *.site cover every subdomain — answered by the DNS stub" } ] 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, web… (`*` = every subdomain)" 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() }