Tier 0: render what the services already decided, honestly
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
@@ -48,6 +48,48 @@ SettingsPage {
|
||||
property bool importOpen: false
|
||||
property string importPath: ""
|
||||
|
||||
// The proxy dropdown and its address, page-local until they add up to a
|
||||
// whole setting.
|
||||
//
|
||||
// A manual proxy is a host AND a port -- GNOME ignores a proxy whose host
|
||||
// is empty or whose port is 0 -- so engaging "manual" before an address
|
||||
// exists produces a mode that says it is proxying and is not. The dropdown
|
||||
// therefore leads the applied mode: picking Manual reveals the fields, and
|
||||
// the proxy is written on the edit that completes the pair, whichever of
|
||||
// the two that is. Committing on the first field instead would have to
|
||||
// guess at the other one; requiring the other to be stored first, which is
|
||||
// what this page used to do, meant neither could ever be first.
|
||||
//
|
||||
// The three start as bindings and stay bound until the first edit, which is
|
||||
// what carries the helper's opening read into a page that was built before
|
||||
// it answered. After that they are the user's, and nothing re-seeds them:
|
||||
// a reply landing mid-edit must not move the dropdown or empty the field
|
||||
// somebody is typing into.
|
||||
property string proxyChoice: NetworkTools.proxyMode
|
||||
property string draftProxyHost: NetworkTools.proxyHost
|
||||
property string draftProxyPort: NetworkTools.proxyPort
|
||||
|
||||
// "host", "port", "both" or "" -- which half of the pair is still missing.
|
||||
readonly property string proxyMissing: {
|
||||
const host = root.draftProxyHost.trim() === "";
|
||||
const port = root.draftProxyPort.trim() === "";
|
||||
if (host && port)
|
||||
return "both";
|
||||
if (host)
|
||||
return "host";
|
||||
if (port)
|
||||
return "port";
|
||||
return "";
|
||||
}
|
||||
|
||||
// Called after either field is edited. Writes only a whole address, so a
|
||||
// half-filled form leaves the proxy exactly as it was.
|
||||
function commitProxyManual(): void {
|
||||
if (root.proxyMissing !== "")
|
||||
return;
|
||||
NetworkTools.setProxyManual(root.draftProxyHost.trim(), root.draftProxyPort.trim());
|
||||
}
|
||||
|
||||
readonly property string wiredConnection:
|
||||
String(Connectivity.wiredDevice?.network?.name ?? "")
|
||||
|
||||
@@ -410,9 +452,18 @@ SettingsPage {
|
||||
OptionPickerRow {
|
||||
width: parent.width
|
||||
label: "Network proxy"
|
||||
detail: NetworkTools.proxyMode === "none"
|
||||
? "Applications that honour the system proxy use this. Not every application does."
|
||||
: "In use: " + NetworkTools.proxySummary
|
||||
detail: {
|
||||
if (root.proxyChoice === "manual" && NetworkTools.proxyMode !== "manual") {
|
||||
if (root.proxyMissing === "both")
|
||||
return "Not applied yet — fill in the host and the port below.";
|
||||
if (root.proxyMissing !== "")
|
||||
return "Not applied yet — the " + root.proxyMissing + " below is still empty.";
|
||||
return "Not applied yet.";
|
||||
}
|
||||
if (NetworkTools.proxyMode === "none")
|
||||
return "Applications that honour the system proxy use this. Not every application does.";
|
||||
return "In use: " + NetworkTools.proxySummary;
|
||||
}
|
||||
enabled: !NetworkTools.busy
|
||||
options: [
|
||||
{
|
||||
@@ -431,48 +482,60 @@ SettingsPage {
|
||||
detail: "A configuration URL decides, per address"
|
||||
}
|
||||
]
|
||||
current: NetworkTools.proxyMode
|
||||
onPicked: value => NetworkTools.setProxyMode(String(value))
|
||||
current: root.proxyChoice
|
||||
// Off and Automatic mean something the moment they are picked;
|
||||
// Manual does not, so it only opens the fields. See proxyChoice.
|
||||
onPicked: value => {
|
||||
root.proxyChoice = String(value);
|
||||
if (root.proxyChoice === "manual")
|
||||
root.commitProxyManual();
|
||||
else
|
||||
NetworkTools.setProxyMode(root.proxyChoice);
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
visible: NetworkTools.proxyMode === "manual"
|
||||
visible: root.proxyChoice === "manual"
|
||||
|
||||
// Host and port are written together, because the proxy is only
|
||||
// usable as a pair -- so each field commits with whatever the other
|
||||
// one currently holds, and neither writes a half-configuration.
|
||||
// usable as a pair. Each field edits the page's draft; the pair is
|
||||
// written once both halves are there.
|
||||
TextFieldRow {
|
||||
width: parent.width
|
||||
label: "Proxy host"
|
||||
detail: "The machine applications should go through"
|
||||
detail: root.proxyMissing === "host"
|
||||
? "Still empty — the proxy applies once this and the port are both set"
|
||||
: "The machine applications should go through"
|
||||
placeholder: "proxy.example.com"
|
||||
text: NetworkTools.proxyHost
|
||||
text: root.draftProxyHost
|
||||
enabled: !NetworkTools.busy
|
||||
onAccepted: value => {
|
||||
if (value.trim() !== "" && NetworkTools.proxyPort !== "")
|
||||
NetworkTools.setProxyManual(value.trim(), NetworkTools.proxyPort);
|
||||
root.draftProxyHost = value.trim();
|
||||
root.commitProxyManual();
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldRow {
|
||||
width: parent.width
|
||||
label: "Port"
|
||||
detail: "The port that proxy listens on"
|
||||
detail: root.proxyMissing === "port"
|
||||
? "Still empty — the proxy applies once this and the host are both set"
|
||||
: "The port that proxy listens on"
|
||||
placeholder: "8080"
|
||||
text: NetworkTools.proxyPort
|
||||
text: root.draftProxyPort
|
||||
enabled: !NetworkTools.busy
|
||||
divider: false
|
||||
onAccepted: value => {
|
||||
if (value.trim() !== "" && NetworkTools.proxyHost !== "")
|
||||
NetworkTools.setProxyManual(NetworkTools.proxyHost, value.trim());
|
||||
root.draftProxyPort = value.trim();
|
||||
root.commitProxyManual();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldRow {
|
||||
width: parent.width
|
||||
visible: NetworkTools.proxyMode === "auto"
|
||||
visible: root.proxyChoice === "auto"
|
||||
label: "Configuration URL"
|
||||
detail: "The .pac file whoever runs the network published"
|
||||
placeholder: "http://example.com/proxy.pac"
|
||||
|
||||
@@ -78,17 +78,74 @@ SettingsPage {
|
||||
// runs exactly as the hotkey runs it, and the words arrive here. The helper
|
||||
// is addressed through the path the Dictation service already publishes
|
||||
// rather than one spelled again in this file.
|
||||
|
||||
// True only once the helper has confirmed a recording actually started.
|
||||
//
|
||||
// It used to be set on the press, which made it a claim rather than a fact:
|
||||
// `panama-dictate start` refuses with {"ok":false,"error":"already-recording"}
|
||||
// when the hotkey is already holding the microphone, and the field said
|
||||
// "Listening…" over that refusal. Worse, the button then read "Stop and
|
||||
// type it" -- so the next press stopped the hotkey's recording and typed
|
||||
// somebody else's words into this test field.
|
||||
property bool listening: false
|
||||
|
||||
// Why the last press did not do what it said, in this page's words.
|
||||
property string testError: ""
|
||||
|
||||
// The helper answers with a code, not a sentence, so the words live here.
|
||||
// Anything unrecognised is shown as itself rather than swallowed.
|
||||
readonly property var dictateReasons: ({
|
||||
"already-recording": "Something is already listening — the dictation hotkey, most likely. Let that finish first.",
|
||||
"not-recording": "Nothing was listening, so there was nothing to type.",
|
||||
"not-downloaded": "The speech model has not been downloaded yet.",
|
||||
"no-image": "The speech server is not installed yet.",
|
||||
"server-unavailable": "The speech server did not answer.",
|
||||
"transcribe-failed": "That could not be transcribed.",
|
||||
"too-short": "That was too short to transcribe.",
|
||||
"no-speech": "Nothing was said.",
|
||||
"deliver-failed": "The words could not be typed into the field.",
|
||||
"unknown-command": "The dictation helper did not understand that."
|
||||
})
|
||||
|
||||
Process {
|
||||
id: dictateRun
|
||||
|
||||
// Which action this run was, so its reply can be read against it.
|
||||
property string action: ""
|
||||
|
||||
stdout: StdioCollector { onStreamFinished: root.absorbDictate(this.text) }
|
||||
onExited: (exitCode, exitStatus) => Dictation.refresh()
|
||||
}
|
||||
|
||||
// The helper's reply decides what happened. A start that was refused leaves
|
||||
// this page exactly as it was, with the refusal on screen.
|
||||
function absorbDictate(text: string): void {
|
||||
let reply = null;
|
||||
try {
|
||||
reply = JSON.parse(text);
|
||||
} catch (error) {
|
||||
reply = null;
|
||||
}
|
||||
const ok = reply?.ok === true;
|
||||
root.listening = dictateRun.action === "start" && ok;
|
||||
if (ok) {
|
||||
root.testError = "";
|
||||
return;
|
||||
}
|
||||
const code = String(reply?.error ?? "");
|
||||
if (code === "")
|
||||
root.testError = "The dictation helper did not answer.";
|
||||
else
|
||||
root.testError = root.dictateReasons[code] !== undefined
|
||||
? String(root.dictateReasons[code])
|
||||
: code;
|
||||
}
|
||||
|
||||
function runDictate(action: string): void {
|
||||
if (dictateRun.running)
|
||||
return;
|
||||
root.testError = "";
|
||||
dictateRun.action = action;
|
||||
dictateRun.command = [Dictation.helper, action];
|
||||
dictateRun.running = true;
|
||||
}
|
||||
@@ -181,7 +238,11 @@ SettingsPage {
|
||||
|
||||
SettingRow {
|
||||
label: "Try it"
|
||||
detail: "Speak a sentence and it is typed into the field here, rather than into whatever you were working on"
|
||||
// The refusal takes the detail's place while there is one, so a
|
||||
// press that did nothing says so where the press happened.
|
||||
detail: root.testError !== ""
|
||||
? root.testError
|
||||
: "Speak a sentence and it is typed into the field here, rather than into whatever you were working on"
|
||||
controlWidth: 340
|
||||
|
||||
Row {
|
||||
@@ -226,19 +287,27 @@ SettingsPage {
|
||||
|
||||
SettingsButton {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.listening ? "Stop and type it" : "Test dictation"
|
||||
// Four states, because stopping is not instant: the helper
|
||||
// transcribes before it types, and a button still reading
|
||||
// "Stop and type it" through those seconds invites a second
|
||||
// press for a stop that has already been asked for.
|
||||
text: {
|
||||
if (dictateRun.running)
|
||||
return root.listening ? "Transcribing…" : "Starting…";
|
||||
return root.listening ? "Stop and type it" : "Test dictation";
|
||||
}
|
||||
tone: root.listening ? "accent" : "normal"
|
||||
enabled: !dictateRun.running
|
||||
onClicked: {
|
||||
// Focus first, and keep it: the helper types with wtype
|
||||
// into whatever holds keyboard focus when it finishes.
|
||||
heard.forceActiveFocus();
|
||||
if (root.listening) {
|
||||
root.listening = false;
|
||||
root.runDictate("stop");
|
||||
return;
|
||||
}
|
||||
heard.text = "";
|
||||
root.listening = true;
|
||||
// `listening` is set by the reply, not by this press.
|
||||
root.runDictate("start");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,10 +39,25 @@ SettingsPage {
|
||||
// and the stored entry for vrrMode, which it does not report. Reading it
|
||||
// from the service rather than rebuilding it here is what stops the page
|
||||
// from showing one thing while an apply carries another.
|
||||
//
|
||||
// With one exception, and it is the whole reason this is not a one-liner.
|
||||
// While the Keep-or-revert banner is up, nothing has been stored yet --
|
||||
// confirm() is what writes -- so currentLayout() still answers vrrMode from
|
||||
// the PREVIOUS stored record. The variable-refresh picker therefore snapped
|
||||
// back to its old value the instant the change was applied, while the
|
||||
// banner beside it asked whether to keep a change the page had just stopped
|
||||
// showing. For the length of that window the requested layout is what the
|
||||
// pickers must render: it is what was asked for, and it is what keeping
|
||||
// will store.
|
||||
readonly property var record: {
|
||||
const name = root.monitor ? root.monitor.name : "";
|
||||
if (name === "" || Displays.monitors.length === 0)
|
||||
return null;
|
||||
if (Displays.awaitingConfirmation && Displays.pendingRequestedLayout) {
|
||||
const requested = Displays.pendingRequestedLayout.find(entry => entry.name === name);
|
||||
if (requested)
|
||||
return requested;
|
||||
}
|
||||
return Displays.currentLayout().find(entry => entry.name === name) ?? null;
|
||||
}
|
||||
|
||||
|
||||
@@ -236,10 +236,11 @@ SettingsPage {
|
||||
text: "Close it"
|
||||
tone: "danger"
|
||||
enabled: !Firewall.busy
|
||||
// Every range in one call. The label says "the range",
|
||||
// and a range is a tcp rule and a udp rule.
|
||||
onClicked: {
|
||||
root.confirmingRange = false;
|
||||
for (const spec of Firewall.openRanges)
|
||||
Firewall.removePort(String(spec));
|
||||
Firewall.removePorts(Firewall.openRanges);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,41 +8,36 @@ SettingsCard {
|
||||
objectName: "health-summary"
|
||||
implicitHeight: 126
|
||||
|
||||
readonly property int observationCount: Health.summary.warnings + Health.summary.errors
|
||||
readonly property string heroTitle: {
|
||||
if (Health.diagnosticUnavailable)
|
||||
return "Health check unavailable";
|
||||
if (Health.checks.length === 0)
|
||||
return "Checking the desktop";
|
||||
if (Health.status === "error")
|
||||
return "Action required";
|
||||
if (Health.status === "warning")
|
||||
return "Needs attention";
|
||||
return "Healthy";
|
||||
}
|
||||
// The verdict itself is Health's, not this card's — see Health.headlineState
|
||||
// for why it can only be decided in one place. This card renders it and
|
||||
// adds the long-form detail underneath.
|
||||
readonly property int observationCount: Health.observationCount
|
||||
readonly property string heroTitle: Health.headline
|
||||
readonly property string heroDetail: {
|
||||
if (Health.diagnosticUnavailable)
|
||||
switch (Health.headlineState) {
|
||||
case "unavailable":
|
||||
return Health.lastError || "The latest health check could not be completed.";
|
||||
if (Health.checks.length === 0)
|
||||
case "checking":
|
||||
return "Checking the desktop services, tools, and integrations this app owns.";
|
||||
if (Health.status === "error")
|
||||
case "error":
|
||||
return root.observationCount === 1
|
||||
? "One part of the desktop needs action."
|
||||
: `${root.observationCount} parts of the desktop need action.`;
|
||||
if (Health.status === "warning")
|
||||
case "warning":
|
||||
return root.observationCount === 1
|
||||
? "Your desktop is working. One feature needs a decision."
|
||||
: `Your desktop is working. ${root.observationCount} features need a decision.`;
|
||||
return "Desktop services and tools are working normally.";
|
||||
default:
|
||||
return "Desktop services and tools are working normally.";
|
||||
}
|
||||
}
|
||||
readonly property color statusColor: {
|
||||
if (Health.diagnosticUnavailable || Health.status === "error")
|
||||
return Theme.danger;
|
||||
if (Health.status === "warning")
|
||||
return Theme.warn;
|
||||
if (Health.checks.length === 0)
|
||||
return Theme.fgMuted;
|
||||
return Theme.ok;
|
||||
switch (Health.tone) {
|
||||
case "danger": return Theme.danger;
|
||||
case "warn": return Theme.warn;
|
||||
case "muted": return Theme.fgMuted;
|
||||
default: return Theme.ok;
|
||||
}
|
||||
}
|
||||
|
||||
function scanTime(): string {
|
||||
|
||||
@@ -4,10 +4,16 @@
|
||||
// PrivacyLiveTile {
|
||||
// glyph: "\u{F0100}"
|
||||
// label: "Camera"
|
||||
// measured: PrivacyState.initialized
|
||||
// active: PrivacyState.cameraActive
|
||||
// app: PrivacyState.cameraApp
|
||||
// }
|
||||
//
|
||||
// Three states, not two. `active` is false both when nothing is using the
|
||||
// camera and when nobody has looked -- PrivacyState's probe is pw-dump, which
|
||||
// is absent on a machine without PipeWire's tools and leaves every flag at its
|
||||
// default false -- so `measured` says which of the two "Idle" would have meant.
|
||||
//
|
||||
// Deliberately still. An indicator that pulses or fades is reporting the same
|
||||
// fact over and over at sixty frames a second, and this one sits on a settings
|
||||
// page that may be open for a long time -- the border and the warn-toned line
|
||||
@@ -22,14 +28,25 @@ Rectangle {
|
||||
property string glyph: ""
|
||||
property string label: ""
|
||||
property bool active: false
|
||||
// Whether anything has actually been read. False until the first probe
|
||||
// lands, and false forever if the probe cannot run at all.
|
||||
property bool measured: false
|
||||
// The application name PipeWire reported, which is often empty even while
|
||||
// a stream is plainly running.
|
||||
property string app: ""
|
||||
|
||||
// Not `state`: Item already owns that name.
|
||||
readonly property string useLine: root.active
|
||||
? "In use by " + (root.app !== "" ? root.app : "an application")
|
||||
: "Idle"
|
||||
readonly property string useLine: {
|
||||
if (root.active)
|
||||
return "In use by " + (root.app !== "" ? root.app : "an application");
|
||||
return root.measured ? "Idle" : "Not measured";
|
||||
}
|
||||
|
||||
// The neutral third state is neutral in the colour too: warn means in use,
|
||||
// dim means idle, and "not measured" must not read as either.
|
||||
readonly property color toneColor: root.active
|
||||
? Theme.warn
|
||||
: (root.measured ? Theme.fgDim : Theme.fgMuted)
|
||||
|
||||
implicitHeight: 56
|
||||
radius: Theme.cardRadius
|
||||
@@ -46,7 +63,7 @@ Rectangle {
|
||||
anchors.leftMargin: 12
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.glyph
|
||||
color: root.active ? Theme.warn : Theme.fgDim
|
||||
color: root.toneColor
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: 17
|
||||
}
|
||||
@@ -72,7 +89,7 @@ Rectangle {
|
||||
Text {
|
||||
width: parent.width
|
||||
text: root.useLine
|
||||
color: root.active ? Theme.warn : Theme.fgDim
|
||||
color: root.toneColor
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Math.max(9, Theme.fontSizeSmall - 1)
|
||||
elide: Text.ElideRight
|
||||
|
||||
@@ -138,9 +138,11 @@ SettingsPage {
|
||||
|
||||
SettingsCard {
|
||||
title: "Camera, microphone & screen"
|
||||
subtitle: Permissions.available
|
||||
? "What is in use now, and the answers applications gave the desktop portal when they asked."
|
||||
: (Permissions.lastError || "The desktop portal's permission store is not running, so the grants below cannot be read.")
|
||||
subtitle: !Permissions.scanned
|
||||
? "Reading what is in use now, and what the desktop portal has recorded."
|
||||
: (Permissions.available
|
||||
? "What is in use now, and the answers applications gave the desktop portal when they asked."
|
||||
: (Permissions.lastError || "The desktop portal's permission store is not running, so the grants below cannot be read."))
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
@@ -151,6 +153,9 @@ SettingsPage {
|
||||
width: (parent.width - 20) / 3
|
||||
glyph: "\u{F0100}"
|
||||
label: "Camera"
|
||||
// False until pw-dump has answered once, and false forever if
|
||||
// it cannot run: an unmeasured tile must not read as "Idle".
|
||||
measured: PrivacyState.initialized
|
||||
active: PrivacyState.cameraActive
|
||||
app: PrivacyState.cameraApp
|
||||
}
|
||||
@@ -159,6 +164,7 @@ SettingsPage {
|
||||
width: (parent.width - 20) / 3
|
||||
glyph: "\u{F036C}"
|
||||
label: "Microphone"
|
||||
measured: PrivacyState.initialized
|
||||
active: PrivacyState.microphoneActive
|
||||
app: PrivacyState.microphoneApp
|
||||
}
|
||||
@@ -167,6 +173,7 @@ SettingsPage {
|
||||
width: (parent.width - 20) / 3
|
||||
glyph: "\u{F0379}"
|
||||
label: "Screen sharing"
|
||||
measured: PrivacyState.initialized
|
||||
active: PrivacyState.screenSharingActive
|
||||
app: PrivacyState.screenSharingApp
|
||||
}
|
||||
@@ -200,11 +207,22 @@ SettingsPage {
|
||||
count: root.askedCount(section.rows.length)
|
||||
}
|
||||
|
||||
// An empty table means "nothing has asked" only once the store
|
||||
// has been read and answered. Before that, and when the
|
||||
// permission store is not running at all, every table is empty
|
||||
// for the same reason -- and "no app has asked for the camera"
|
||||
// is then a measurement nobody took.
|
||||
TextRow {
|
||||
width: section.width
|
||||
visible: section.rows.length === 0
|
||||
label: "Nothing has asked yet"
|
||||
detail: String(section.modelData.empty)
|
||||
label: !Permissions.scanned
|
||||
? "Not read yet"
|
||||
: (Permissions.available ? "Nothing has asked yet" : "Not measured")
|
||||
detail: !Permissions.scanned
|
||||
? "Reading what the desktop portal has recorded."
|
||||
: (Permissions.available
|
||||
? String(section.modelData.empty)
|
||||
: "The desktop portal's permission store is not answering, so what has asked for this cannot be read.")
|
||||
divider: false
|
||||
}
|
||||
|
||||
@@ -714,10 +732,21 @@ SettingsPage {
|
||||
|
||||
SettingsCard {
|
||||
title: "Device security"
|
||||
subtitle: DeviceSecurity.attentionCount === 0
|
||||
? "Everything below is in its recommended state."
|
||||
: DeviceSecurity.attentionCount + " item"
|
||||
+ (DeviceSecurity.attentionCount === 1 ? "" : "s") + " below may deserve attention."
|
||||
// Three answers, not two. "Everything is in its recommended state" is
|
||||
// computed from an empty fact list exactly as happily as from a clean
|
||||
// one, so a helper that failed to run would be reported as a machine
|
||||
// with nothing wrong with it. No facts means nothing was checked.
|
||||
subtitle: {
|
||||
if (!DeviceSecurity.scanned)
|
||||
return "Reading the firmware, the kernel and the filesystem…";
|
||||
if (DeviceSecurity.facts.length === 0)
|
||||
return "Could not check — " + (DeviceSecurity.lastError
|
||||
|| "the security helper did not answer.");
|
||||
if (DeviceSecurity.attentionCount === 0)
|
||||
return "Everything below is in its recommended state.";
|
||||
return DeviceSecurity.attentionCount + " item"
|
||||
+ (DeviceSecurity.attentionCount === 1 ? "" : "s") + " below may deserve attention.";
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: DeviceSecurity.facts
|
||||
|
||||
@@ -284,26 +284,21 @@ Rectangle {
|
||||
border.width: activeFocus ? 2 : 0
|
||||
border.color: Theme.accent
|
||||
|
||||
// Health owns the verdict; this footer only renders it. It used to
|
||||
// decide for itself, in the opposite order to the Health page's hero,
|
||||
// and the two then disagreed out loud whenever a health check failed
|
||||
// after a successful one -- see Health.headlineState.
|
||||
function footerText(): string {
|
||||
if (Health.checks.length === 0)
|
||||
return Health.diagnosticUnavailable ? "Health check unavailable" : "Checking the desktop";
|
||||
if (Health.status === "error")
|
||||
return "Desktop needs attention";
|
||||
if (Health.status === "warning") {
|
||||
const count = Health.summary.warnings + Health.summary.errors;
|
||||
return count + (count === 1 ? " health observation" : " health observations");
|
||||
}
|
||||
return "Desktop is healthy";
|
||||
return Health.headline;
|
||||
}
|
||||
|
||||
function footerColor(): color {
|
||||
if (Health.checks.length === 0)
|
||||
return Health.diagnosticUnavailable ? Theme.danger : Theme.fgMuted;
|
||||
if (Health.status === "error")
|
||||
return Theme.danger;
|
||||
if (Health.status === "warning")
|
||||
return Theme.warn;
|
||||
return Theme.ok;
|
||||
switch (Health.tone) {
|
||||
case "danger": return Theme.danger;
|
||||
case "warn": return Theme.warn;
|
||||
case "muted": return Theme.fgMuted;
|
||||
default: return Theme.ok;
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
|
||||
@@ -88,4 +88,18 @@ Flow {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A chip that lights and stays silent is the point of this strip; a chip
|
||||
// that lights and stays silent BECAUSE pw-play is not installed is not, and
|
||||
// the two are indistinguishable without this line. Full width, so the Flow
|
||||
// gives it a row of its own beneath the chips.
|
||||
Text {
|
||||
width: root.width
|
||||
visible: SoundTest.lastError !== ""
|
||||
text: SoundTest.lastError
|
||||
color: Theme.warn
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +100,12 @@ SettingsPage {
|
||||
// level meter above says something is arriving, not that it is you.
|
||||
ActionRow {
|
||||
label: "Test your microphone"
|
||||
detail: "Record a few seconds and play it back through the selected output"
|
||||
// A test that fails does it by being silent, which is the same
|
||||
// thing a broken microphone does. The reason takes the detail's
|
||||
// place so the two can be told apart.
|
||||
detail: SoundTest.lastError !== ""
|
||||
? SoundTest.lastError
|
||||
: "Record a few seconds and play it back through the selected output"
|
||||
divider: captureRow.visible
|
||||
action: {
|
||||
if (SoundTest.micTestState === "recording")
|
||||
|
||||
@@ -343,12 +343,23 @@ SettingsPage {
|
||||
|
||||
SettingsCard {
|
||||
title: "Reset"
|
||||
subtitle: "Restores the appearance and theme, dock, clock, focus, and display policy, and clears your Home accessory arrangement. Pinned applications, files, and paired devices are not changed."
|
||||
// Every claim here is checked against PreferenceSchema.defaults(), since
|
||||
// a reset is exactly the schema store going back to it. The dock's
|
||||
// pinned applications ARE a schema key (dockPinned), so the shipped
|
||||
// sixteen come back — this sentence used to promise the opposite. Saved
|
||||
// themes (themeProfiles) are a schema key too, and go with them. Files
|
||||
// and Bluetooth pairings are not in the schema and not in the Home
|
||||
// store, and backups live in their own state directory, so all three
|
||||
// genuinely survive.
|
||||
subtitle: "Restores the appearance and theme, the dock and the applications pinned to it, the clock, focus, and display policy, removes any themes you saved, and clears your Home accessory arrangement. Your files, paired devices, and settings backups are not changed."
|
||||
|
||||
SettingRow {
|
||||
id: resetRow
|
||||
|
||||
readonly property bool armed: root.confirming === "reset"
|
||||
// Stays armed while the safety backup is still being written, so
|
||||
// the confirmation does not collapse into a page that looks
|
||||
// untouched during the one window where nothing has happened yet.
|
||||
readonly property bool armed: root.confirming === "reset" || SystemSettings.resetPending
|
||||
|
||||
label: "Restore defaults"
|
||||
detail: resetRow.armed
|
||||
@@ -364,13 +375,18 @@ SettingsPage {
|
||||
|
||||
SettingsButton {
|
||||
text: resetRow.armed ? "Cancel" : "Reset…"
|
||||
enabled: !SystemSettings.resetPending
|
||||
onClicked: root.arm("reset")
|
||||
}
|
||||
|
||||
SettingsButton {
|
||||
visible: resetRow.armed
|
||||
text: "Restore defaults"
|
||||
// Nothing is wiped until the backup lands, so the button
|
||||
// stays occupied for as long as that takes rather than
|
||||
// looking like it did nothing.
|
||||
text: SystemSettings.resetPending ? "Backing up…" : "Restore defaults"
|
||||
tone: "danger"
|
||||
enabled: !SystemSettings.resetPending && !SettingsBackup.busy
|
||||
onClicked: {
|
||||
root.confirming = "";
|
||||
SystemSettings.restoreDefaults();
|
||||
|
||||
@@ -54,9 +54,16 @@ SettingsPage {
|
||||
readonly property string headline: {
|
||||
if (Updates.checking)
|
||||
return "Checking every source…";
|
||||
// "nothing security-critical" is a measurement, not a default. When the
|
||||
// advisory query did not answer, the page says so instead of saying the
|
||||
// reassuring thing it would otherwise have said.
|
||||
const security = Updates.securityCount > 0
|
||||
? Updates.securityCount + " carrying a security advisory"
|
||||
: (Updates.total > 0 ? "nothing security-critical" : "");
|
||||
: (Updates.total > 0
|
||||
? (Updates.securityKnown
|
||||
? "nothing security-critical"
|
||||
: "security advisories could not be read")
|
||||
: "");
|
||||
return root.joined([
|
||||
Updates.summary(), security,
|
||||
Updates.lastCheckedText().replace("Checked", "checked")
|
||||
@@ -72,12 +79,17 @@ SettingsPage {
|
||||
|
||||
function toggleChangelog(source: string, name: string): void {
|
||||
const key = source + ":" + name;
|
||||
root.expandedPackage = root.expandedPackage === key ? "" : key;
|
||||
const opening = root.expandedPackage !== key;
|
||||
root.expandedPackage = opening ? key : "";
|
||||
// The fetch starts here, on the press, not as a side effect of the
|
||||
// text rendering -- see Updates.changelogFor's comment for the
|
||||
// binding loop that taught us the difference.
|
||||
if (opening)
|
||||
Updates.requestChangelog(source, name);
|
||||
}
|
||||
|
||||
// Asking for a changelog is what starts fetching one -- the page requests
|
||||
// it by rendering it, and Updates keeps the answer for the life of the
|
||||
// shell. Null means the fetch is still out.
|
||||
// Pure read: Updates keeps the answer for the life of the shell, and
|
||||
// null means the fetch is still out.
|
||||
function changelogText(source: string, name: string): string {
|
||||
const record = Updates.changelogFor(source, name);
|
||||
if (record === null || record === undefined)
|
||||
@@ -140,7 +152,12 @@ SettingsPage {
|
||||
|
||||
SettingsCard {
|
||||
title: "System packages"
|
||||
subtitle: Updates.dnf?.available === false
|
||||
// The failed case comes before the empty one: dnf reporting nothing and
|
||||
// dnf being unable to report are the same empty list, and only one of
|
||||
// them means there is nothing waiting.
|
||||
subtitle: Updates.sourceError("dnf") !== ""
|
||||
? Updates.sourceError("dnf")
|
||||
: Updates.dnf?.available === false
|
||||
? "dnf is not available on this machine."
|
||||
: (root.packages.length === 0
|
||||
? "Nothing waiting."
|
||||
@@ -269,15 +286,25 @@ SettingsPage {
|
||||
SettingsCard {
|
||||
title: "Applications & firmware"
|
||||
|
||||
// A check that failed is not a machine that is current. "Current" is
|
||||
// only ever said about a list that was actually read.
|
||||
TextRow {
|
||||
visible: Updates.flatpak?.available === false
|
||||
visible: Updates.sourceError("flatpak") !== ""
|
||||
label: "Flatpak"
|
||||
detail: Updates.sourceError("flatpak")
|
||||
value: "Not checked"
|
||||
}
|
||||
|
||||
TextRow {
|
||||
visible: Updates.sourceError("flatpak") === "" && Updates.flatpak?.available === false
|
||||
label: "Flatpak"
|
||||
detail: "Flatpak is not installed."
|
||||
value: "Unavailable"
|
||||
}
|
||||
|
||||
TextRow {
|
||||
visible: Updates.flatpak?.available !== false && root.applications.length === 0
|
||||
visible: Updates.sourceError("flatpak") === ""
|
||||
&& Updates.flatpak?.available !== false && root.applications.length === 0
|
||||
label: "Flatpak"
|
||||
detail: "Everything current — each application gets its own row when an update appears"
|
||||
value: "Current"
|
||||
@@ -350,7 +377,15 @@ SettingsPage {
|
||||
}
|
||||
|
||||
TextRow {
|
||||
visible: Updates.firmware?.available === false
|
||||
visible: Updates.sourceError("firmware") !== ""
|
||||
label: "Firmware"
|
||||
detail: Updates.sourceError("firmware")
|
||||
value: "Not checked"
|
||||
divider: false
|
||||
}
|
||||
|
||||
TextRow {
|
||||
visible: Updates.sourceError("firmware") === "" && Updates.firmware?.available === false
|
||||
label: "Firmware"
|
||||
detail: "Firmware updating is not available on this machine."
|
||||
value: "Unavailable"
|
||||
@@ -358,7 +393,8 @@ SettingsPage {
|
||||
}
|
||||
|
||||
TextRow {
|
||||
visible: Updates.firmware?.available !== false && root.firmwareDevices.length === 0
|
||||
visible: Updates.sourceError("firmware") === ""
|
||||
&& Updates.firmware?.available !== false && root.firmwareDevices.length === 0
|
||||
label: "Firmware"
|
||||
detail: "No firmware updates are offered for this hardware"
|
||||
value: "Current"
|
||||
|
||||
@@ -637,6 +637,21 @@ SettingsPage {
|
||||
readonly property bool lastAdministrator:
|
||||
otherBlock.modelData.administrator && UserAccounts.administratorCount <= 1
|
||||
|
||||
// The real path, from accountsservice, rather than the home root
|
||||
// joined to the account name. Every sentence below used to
|
||||
// construct that guess, the armed confirmation for deleting the
|
||||
// files among them — so a home that had been moved was named
|
||||
// confidently and wrongly exactly where being wrong costs the
|
||||
// most. When no path is reported, the sentences name the
|
||||
// directory in words rather than inventing one.
|
||||
readonly property string homePath:
|
||||
UserAccounts.homeDirectory(otherBlock.modelData)
|
||||
readonly property bool homeKnown: otherBlock.homePath !== ""
|
||||
readonly property string homePhrase: otherBlock.homeKnown
|
||||
? otherBlock.homePath : "their home directory"
|
||||
readonly property string homeSubject: otherBlock.homeKnown
|
||||
? otherBlock.homePath : "Their home directory"
|
||||
|
||||
SettingRow {
|
||||
width: otherBlock.width
|
||||
label: UserAccounts.displayName(otherBlock.modelData)
|
||||
@@ -706,12 +721,12 @@ SettingsPage {
|
||||
OptionPickerRow {
|
||||
width: otherBlock.width - 14
|
||||
label: "Their files"
|
||||
detail: "What deleting the account does to /home/" + otherBlock.userName
|
||||
detail: "What deleting the account does to " + otherBlock.homePhrase
|
||||
options: [
|
||||
{
|
||||
value: "keep",
|
||||
label: "Keep the files",
|
||||
detail: "/home/" + otherBlock.userName
|
||||
detail: otherBlock.homeSubject
|
||||
+ " stays exactly where it is, and you can hand it to someone later"
|
||||
},
|
||||
{
|
||||
@@ -734,9 +749,9 @@ SettingsPage {
|
||||
if (!otherBlock.confirming)
|
||||
return "The account stops existing. What happens to their files is the choice above.";
|
||||
if (root.removalDisposition === "remove")
|
||||
return "This cannot be undone — /home/" + otherBlock.userName
|
||||
return "This cannot be undone — " + otherBlock.homePhrase
|
||||
+ " and everything in it is deleted along with the account.";
|
||||
return "Their files stay in /home/" + otherBlock.userName
|
||||
return "Their files stay in " + otherBlock.homePhrase
|
||||
+ ", so only the account itself goes.";
|
||||
}
|
||||
controlWidth: 260
|
||||
|
||||
Reference in New Issue
Block a user