Finish the wonderland: System told truthfully, in eight tabs instead of ten
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
@@ -9,6 +9,14 @@
|
||||
// happen, so the Snapshots page shows "before 32 package updates" rather than a
|
||||
// timestamp. That is the thing neither macOS nor Windows does cleanly, and it
|
||||
// is nearly free here.
|
||||
//
|
||||
// A package row can say what it changes. "1 update available" tells you nothing
|
||||
// about whether to install it now or after the meeting; the changelog does, and
|
||||
// it is fetched per package on request rather than for a list nobody read.
|
||||
//
|
||||
// The automatic switches sit in their own card. The one that downloads packages
|
||||
// used to live at the bottom of Firmware, which is where somebody put it once
|
||||
// and nobody could find it since.
|
||||
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
@@ -20,9 +28,16 @@ SettingsPage {
|
||||
|
||||
objectName: "updates"
|
||||
title: "Software Update"
|
||||
lede: "Packages, applications, and firmware, each from the place it actually comes from."
|
||||
lede: root.headline
|
||||
|
||||
property string expandedSource: ""
|
||||
// Rows are shown a screenful at a time. A dnf update is routinely thirty
|
||||
// packages, and a page that opens on thirty rows is a page nobody reads.
|
||||
readonly property int previewCount: 6
|
||||
|
||||
property bool showAllPackages: false
|
||||
|
||||
// "dnf:name" or "flatpak:id" -- the one row whose changelog is open.
|
||||
property string expandedPackage: ""
|
||||
|
||||
Component.onCompleted: {
|
||||
Updates.refresh();
|
||||
@@ -32,6 +47,50 @@ SettingsPage {
|
||||
Updates.check();
|
||||
}
|
||||
|
||||
function joined(parts: var): string {
|
||||
return parts.filter(part => String(part ?? "") !== "").join(" · ");
|
||||
}
|
||||
|
||||
readonly property string headline: {
|
||||
if (Updates.checking)
|
||||
return "Checking every source…";
|
||||
const security = Updates.securityCount > 0
|
||||
? Updates.securityCount + " carrying a security advisory"
|
||||
: (Updates.total > 0 ? "nothing security-critical" : "");
|
||||
return root.joined([
|
||||
Updates.summary(), security,
|
||||
Updates.lastCheckedText().replace("Checked", "checked")
|
||||
]) + ".";
|
||||
}
|
||||
|
||||
readonly property var packages: Updates.dnf?.packages ?? []
|
||||
readonly property var shownPackages: root.showAllPackages
|
||||
? root.packages
|
||||
: root.packages.slice(0, root.previewCount)
|
||||
readonly property var applications: Updates.flatpak?.applications ?? []
|
||||
readonly property var firmwareDevices: Updates.firmware?.devices ?? []
|
||||
|
||||
function toggleChangelog(source: string, name: string): void {
|
||||
const key = source + ":" + name;
|
||||
root.expandedPackage = root.expandedPackage === key ? "" : key;
|
||||
}
|
||||
|
||||
// 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.
|
||||
function changelogText(source: string, name: string): string {
|
||||
const record = Updates.changelogFor(source, name);
|
||||
if (record === null || record === undefined)
|
||||
return "Reading the changelog…";
|
||||
if (String(record.text ?? "") !== "")
|
||||
return String(record.text);
|
||||
return String(record.error ?? "") !== ""
|
||||
? String(record.error)
|
||||
: "No changelog published for this update.";
|
||||
}
|
||||
|
||||
// ── What just happened ───────────────────────────────────────────────────
|
||||
|
||||
TextRow {
|
||||
visible: Updates.lastError !== ""
|
||||
label: "Updates need attention"
|
||||
@@ -54,43 +113,7 @@ SettingsPage {
|
||||
divider: false
|
||||
}
|
||||
|
||||
// ── The headline ─────────────────────────────────────────────────────────
|
||||
|
||||
SettingsCard {
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 10
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: 12
|
||||
|
||||
Text {
|
||||
text: Updates.checking ? "Checking…" : Updates.summary()
|
||||
color: Theme.fg
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeTitle
|
||||
font.weight: Font.DemiBold
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: Updates.securityCount > 0
|
||||
text: Updates.securityCount + " carry a security advisory"
|
||||
color: Theme.warn
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: Updates.lastCheckedText()
|
||||
color: Theme.fgMuted
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
}
|
||||
}
|
||||
|
||||
ActionRow {
|
||||
label: "Check for updates"
|
||||
detail: "Refreshes package metadata, application remotes, and firmware. Takes a few seconds."
|
||||
@@ -113,55 +136,90 @@ SettingsPage {
|
||||
}
|
||||
}
|
||||
|
||||
// ── One card per source ──────────────────────────────────────────────────
|
||||
// ── System packages ──────────────────────────────────────────────────────
|
||||
|
||||
SettingsCard {
|
||||
title: "System packages"
|
||||
subtitle: Updates.dnf?.available === false
|
||||
? "dnf is not available on this machine."
|
||||
: (Number(Updates.dnf?.count ?? 0) === 0
|
||||
: (root.packages.length === 0
|
||||
? "Nothing waiting."
|
||||
: Updates.dnf.count + " package"
|
||||
+ (Updates.dnf.count === 1 ? "" : "s") + " ready to install"
|
||||
+ (Updates.securityCount > 0
|
||||
? ", " + Updates.securityCount + " carrying an advisory" : ""))
|
||||
|
||||
ActionRow {
|
||||
visible: Number(Updates.dnf?.count ?? 0) > 0
|
||||
label: "Install package updates"
|
||||
detail: "Asks for your password, and takes a snapshot first so this can be undone"
|
||||
action: Updates.applying ? "Working…" : "Install"
|
||||
enabled: !Updates.busy
|
||||
onTriggered: Updates.apply("dnf")
|
||||
}
|
||||
|
||||
ActionRow {
|
||||
visible: Number(Updates.dnf?.count ?? 0) > 0
|
||||
label: "What would change"
|
||||
detail: root.expandedSource === "dnf"
|
||||
? "Every package that would be replaced"
|
||||
: Updates.dnf.count + " packages"
|
||||
action: root.expandedSource === "dnf" ? "Hide" : "Show"
|
||||
divider: root.expandedSource === "dnf"
|
||||
onTriggered: root.expandedSource = root.expandedSource === "dnf" ? "" : "dnf"
|
||||
}
|
||||
: root.joined([
|
||||
root.packages.length + " ready to install",
|
||||
Updates.securityCount > 0
|
||||
? Updates.securityCount + " carrying an advisory" : "",
|
||||
Updates.sourceDownloadSize("dnf") === ""
|
||||
? "" : Updates.sourceDownloadSize("dnf") + " to download"
|
||||
]))
|
||||
|
||||
Repeater {
|
||||
model: root.expandedSource === "dnf" ? (Updates.dnf?.packages ?? []) : []
|
||||
id: packageRows
|
||||
model: root.shownPackages
|
||||
|
||||
Column {
|
||||
id: packageRow
|
||||
|
||||
delegate: TextRow {
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
readonly property string name: String(packageRow.modelData.name ?? "")
|
||||
readonly property bool open: root.expandedPackage === "dnf:" + packageRow.name
|
||||
|
||||
width: parent.width
|
||||
label: String(modelData.name ?? "")
|
||||
detail: String(modelData.repository ?? "")
|
||||
value: String(modelData.version ?? "")
|
||||
divider: index < (Updates.dnf?.packages ?? []).length - 1
|
||||
|
||||
ActionRow {
|
||||
width: parent.width
|
||||
label: packageRow.name
|
||||
detail: root.joined([
|
||||
String(packageRow.modelData.version ?? ""),
|
||||
String(packageRow.modelData.repository ?? "") === ""
|
||||
? "" : "from " + packageRow.modelData.repository
|
||||
])
|
||||
action: packageRow.open ? "Hide" : "Changelog"
|
||||
divider: !packageRow.open && packageRow.index < packageRows.count - 1
|
||||
onTriggered: root.toggleChangelog("dnf", packageRow.name)
|
||||
}
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
visible: packageRow.open
|
||||
text: root.changelogText("dnf", packageRow.name)
|
||||
color: Theme.fgDim
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
wrapMode: Text.WordWrap
|
||||
lineHeight: 1.3
|
||||
topPadding: 2
|
||||
bottomPadding: 14
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ActionRow {
|
||||
visible: root.packages.length > root.previewCount
|
||||
label: root.showAllPackages
|
||||
? "Showing all " + root.packages.length + " packages"
|
||||
: (root.packages.length - root.previewCount) + " more waiting"
|
||||
detail: root.showAllPackages ? "" : "Each one can say what it changes"
|
||||
action: root.showAllPackages ? "Show fewer" : "Show all"
|
||||
onTriggered: {
|
||||
root.expandedPackage = "";
|
||||
root.showAllPackages = !root.showAllPackages;
|
||||
}
|
||||
}
|
||||
|
||||
ActionRow {
|
||||
visible: root.packages.length > 0
|
||||
label: "Install package updates"
|
||||
detail: "Asks for your password, and takes a snapshot first — undo lives in System › Snapshots"
|
||||
action: Updates.applying ? "Working…" : "Install updates"
|
||||
enabled: !Updates.busy
|
||||
divider: false
|
||||
onTriggered: Updates.apply("dnf")
|
||||
}
|
||||
|
||||
TextRow {
|
||||
visible: Number(Updates.dnf?.count ?? 0) === 0 && Updates.everChecked
|
||||
visible: root.packages.length === 0 && Updates.everChecked
|
||||
label: "Packages are current"
|
||||
detail: "Nothing from the system repositories is waiting"
|
||||
value: ""
|
||||
@@ -169,36 +227,31 @@ SettingsPage {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Automatic ────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Both switches, in the one card about the same idea. The package one used
|
||||
// to sit at the bottom of Firmware.
|
||||
|
||||
SettingsCard {
|
||||
title: "Applications"
|
||||
subtitle: Updates.flatpak?.available === false
|
||||
? "Flatpak is not installed."
|
||||
: (Number(Updates.flatpak?.count ?? 0) === 0
|
||||
? "Nothing waiting."
|
||||
: Updates.flatpak.count + " application"
|
||||
+ (Updates.flatpak.count === 1 ? "" : "s") + " ready to update")
|
||||
title: "Automatic"
|
||||
|
||||
Repeater {
|
||||
model: Updates.flatpak?.applications ?? []
|
||||
|
||||
delegate: TextRow {
|
||||
required property var modelData
|
||||
required property int index
|
||||
width: parent.width
|
||||
label: String(modelData.id ?? "")
|
||||
detail: "Flatpak"
|
||||
value: String(modelData.version ?? "")
|
||||
divider: true
|
||||
}
|
||||
SwitchRow {
|
||||
visible: Updates.automatic?.dnfAutomaticAvailable === true
|
||||
label: "Download package updates automatically"
|
||||
detail: "Fetches them in the background each morning so installing is quick. It does not install them: a machine that updates packages unattended can reboot into a kernel nobody chose."
|
||||
checked: Updates.automatic?.dnfAutomaticEnabled === true
|
||||
enabled: !Updates.busy
|
||||
onToggled: value => Updates.setAutomaticDnf(value)
|
||||
}
|
||||
|
||||
ActionRow {
|
||||
visible: Number(Updates.flatpak?.count ?? 0) > 0
|
||||
label: "Update applications"
|
||||
detail: "Needs no password: these are installed for your account"
|
||||
action: Updates.applying ? "Working…" : "Update"
|
||||
enabled: !Updates.busy
|
||||
onTriggered: Updates.apply("flatpak")
|
||||
// Offered only when the machinery exists. When it does not, this says
|
||||
// so rather than showing a switch that could not do anything --
|
||||
// installing software is not a settings action.
|
||||
TextRow {
|
||||
visible: Updates.automatic?.dnfAutomaticAvailable === false
|
||||
label: "Download package updates automatically"
|
||||
detail: "Not set up. dnf-automatic is not installed, and Settings does not install software."
|
||||
value: "Off"
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
@@ -211,32 +264,122 @@ SettingsPage {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Applications & firmware ──────────────────────────────────────────────
|
||||
|
||||
SettingsCard {
|
||||
title: "Firmware"
|
||||
subtitle: Updates.firmware?.available === false
|
||||
? "Firmware updating is not available on this machine."
|
||||
: (Number(Updates.firmware?.count ?? 0) === 0
|
||||
? "No firmware updates are offered for this hardware."
|
||||
: Updates.firmware.count + " device"
|
||||
+ (Updates.firmware.count === 1 ? "" : "s") + " have firmware available")
|
||||
title: "Applications & firmware"
|
||||
|
||||
TextRow {
|
||||
visible: Updates.flatpak?.available === false
|
||||
label: "Flatpak"
|
||||
detail: "Flatpak is not installed."
|
||||
value: "Unavailable"
|
||||
}
|
||||
|
||||
TextRow {
|
||||
visible: 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"
|
||||
}
|
||||
|
||||
// Per application, because they update independently and one of them
|
||||
// being 400 MB is a reason to do the other three now and that one
|
||||
// later.
|
||||
Repeater {
|
||||
model: Updates.firmware?.devices ?? []
|
||||
id: applicationRows
|
||||
model: root.applications
|
||||
|
||||
Column {
|
||||
id: applicationRow
|
||||
|
||||
delegate: TextRow {
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
readonly property string appId: String(applicationRow.modelData.id ?? "")
|
||||
readonly property bool open: root.expandedPackage === "flatpak:" + applicationRow.appId
|
||||
|
||||
width: parent.width
|
||||
label: String(modelData.name ?? "")
|
||||
detail: String(modelData.version ?? "") + " → " + String(modelData.target ?? "")
|
||||
+ (modelData.needsReboot ? " · installs on restart" : "")
|
||||
value: ""
|
||||
divider: true
|
||||
|
||||
ActionRow {
|
||||
width: parent.width
|
||||
label: applicationRow.appId
|
||||
detail: root.joined([
|
||||
String(applicationRow.modelData.version ?? ""), "Flatpak",
|
||||
Updates.formatBytes(Number(applicationRow.modelData.downloadBytes ?? 0))
|
||||
])
|
||||
action: Updates.applying ? "Working…" : "Update"
|
||||
enabled: !Updates.busy
|
||||
divider: false
|
||||
onTriggered: Updates.applyFlatpakApp(applicationRow.appId)
|
||||
}
|
||||
|
||||
ActionRow {
|
||||
width: parent.width
|
||||
label: "What changed"
|
||||
detail: applicationRow.open
|
||||
? ""
|
||||
: "Release notes, when the application publishes them"
|
||||
action: applicationRow.open ? "Hide" : "Changelog"
|
||||
divider: !applicationRow.open
|
||||
onTriggered: root.toggleChangelog("flatpak", applicationRow.appId)
|
||||
}
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
visible: applicationRow.open
|
||||
text: root.changelogText("flatpak", applicationRow.appId)
|
||||
color: Theme.fgDim
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
wrapMode: Text.WordWrap
|
||||
lineHeight: 1.3
|
||||
topPadding: 2
|
||||
bottomPadding: 14
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ActionRow {
|
||||
visible: Number(Updates.firmware?.count ?? 0) > 0
|
||||
visible: root.applications.length > 1
|
||||
label: "Update every application"
|
||||
detail: "Needs no password: these are installed for your account"
|
||||
action: Updates.applying ? "Working…" : "Update all"
|
||||
enabled: !Updates.busy
|
||||
onTriggered: Updates.apply("flatpak")
|
||||
}
|
||||
|
||||
TextRow {
|
||||
visible: Updates.firmware?.available === false
|
||||
label: "Firmware"
|
||||
detail: "Firmware updating is not available on this machine."
|
||||
value: "Unavailable"
|
||||
divider: false
|
||||
}
|
||||
|
||||
TextRow {
|
||||
visible: Updates.firmware?.available !== false && root.firmwareDevices.length === 0
|
||||
label: "Firmware"
|
||||
detail: "No firmware updates are offered for this hardware"
|
||||
value: "Current"
|
||||
divider: false
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.firmwareDevices
|
||||
|
||||
TextRow {
|
||||
required property var modelData
|
||||
|
||||
label: String(modelData.name ?? "")
|
||||
detail: String(modelData.version ?? "") + " → " + String(modelData.target ?? "")
|
||||
+ (modelData.needsReboot ? " · installs on restart" : "")
|
||||
value: ""
|
||||
}
|
||||
}
|
||||
|
||||
ActionRow {
|
||||
visible: root.firmwareDevices.length > 0
|
||||
label: "Install firmware"
|
||||
detail: "Some devices only finish updating after a restart"
|
||||
action: Updates.applying ? "Working…" : "Install"
|
||||
@@ -244,27 +387,6 @@ SettingsPage {
|
||||
divider: false
|
||||
onTriggered: Updates.apply("firmware")
|
||||
}
|
||||
|
||||
// Offered only when the machinery exists. When it does not, this says
|
||||
// so rather than showing a switch that could not do anything --
|
||||
// installing software is not a settings action.
|
||||
SwitchRow {
|
||||
visible: Updates.automatic?.dnfAutomaticAvailable === true
|
||||
label: "Download package updates automatically"
|
||||
detail: "Fetches them in the background each morning so installing is quick. It does not install them: a machine that updates packages unattended can reboot into a kernel nobody chose."
|
||||
checked: Updates.automatic?.dnfAutomaticEnabled === true
|
||||
enabled: !Updates.busy
|
||||
divider: false
|
||||
onToggled: value => Updates.setAutomaticDnf(value)
|
||||
}
|
||||
|
||||
TextRow {
|
||||
visible: Updates.automatic?.dnfAutomaticAvailable === false
|
||||
label: "Automatic package updates"
|
||||
detail: "Not set up. dnf-automatic is not installed, and Settings does not install software."
|
||||
value: "Off"
|
||||
divider: false
|
||||
}
|
||||
}
|
||||
|
||||
// Worth showing because automatic updates leave no other trace. Something
|
||||
@@ -288,10 +410,10 @@ SettingsPage {
|
||||
Repeater {
|
||||
model: Updates.history
|
||||
|
||||
delegate: TextRow {
|
||||
TextRow {
|
||||
required property var modelData
|
||||
required property int index
|
||||
width: parent.width
|
||||
|
||||
label: String(modelData.summary ?? "")
|
||||
detail: Updates.agoText(Number(modelData.at ?? 0))
|
||||
value: Updates.describeHistory(modelData)
|
||||
|
||||
Reference in New Issue
Block a user