The page had a row for the case where dnf-automatic is absent and nothing for the case where it is present, so installing it made the setting disappear rather than appear. The switch enables downloading only, which is the shipped default and the right one to keep: a machine that installs packages unattended can reboot into a kernel nobody chose. Downloading ahead of time is what makes the install quick when someone does choose it, and the row says exactly that rather than implying updates apply themselves. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
270 lines
10 KiB
QML
270 lines
10 KiB
QML
// Software updates.
|
|
//
|
|
// Three sources that fail independently -- packages, applications, firmware --
|
|
// so each is counted and applied on its own. Blending them into one number
|
|
// would hide the case that matters: a flatpak mirror being down says nothing
|
|
// about whether a security fix is waiting.
|
|
//
|
|
// Applying packages takes a snapshot first, named after what is about to
|
|
// 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.
|
|
|
|
import Quickshell
|
|
import QtQuick
|
|
import qs.config
|
|
import qs.services
|
|
|
|
SettingsPage {
|
|
id: root
|
|
|
|
objectName: "updates"
|
|
title: "Software Update"
|
|
lede: "Packages, applications, and firmware, each from the place it actually comes from."
|
|
|
|
property string expandedSource: ""
|
|
|
|
Component.onCompleted: {
|
|
Updates.refresh();
|
|
// A first visit with nothing cached should not show a confident "up to
|
|
// date" it has no basis for, so it goes and finds out.
|
|
if (!Updates.everChecked && !Updates.checking)
|
|
Updates.check();
|
|
}
|
|
|
|
TextRow {
|
|
visible: Updates.lastError !== ""
|
|
label: "Updates need attention"
|
|
detail: Updates.lastError
|
|
value: ""
|
|
divider: false
|
|
}
|
|
|
|
TextRow {
|
|
visible: Updates.lastApplied !== null
|
|
label: "Updated"
|
|
detail: Updates.lastApplied
|
|
? Updates.sourceLabel(String(Updates.lastApplied.source ?? ""))
|
|
+ (String(Updates.lastApplied.restorePoint ?? "") !== ""
|
|
? " · a snapshot was taken first, number "
|
|
+ String(Updates.lastApplied.restorePoint)
|
|
: "")
|
|
: ""
|
|
value: ""
|
|
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."
|
|
action: Updates.checking ? "Checking…" : "Check now"
|
|
enabled: !Updates.busy
|
|
divider: Updates.rebootNeeded
|
|
onTriggered: Updates.check()
|
|
}
|
|
|
|
// The honest version of "restart required": the running kernel is not
|
|
// the newest installed one, so a reboot would change which kernel runs.
|
|
TextRow {
|
|
visible: Updates.rebootNeeded
|
|
label: "Restart to finish"
|
|
detail: "A newer kernel is installed than the one running. "
|
|
+ String(Updates.kernel?.running ?? "") + " → "
|
|
+ String(Updates.kernel?.newestInstalled ?? "")
|
|
value: "Restart needed"
|
|
divider: false
|
|
}
|
|
}
|
|
|
|
// ── One card per source ──────────────────────────────────────────────────
|
|
|
|
SettingsCard {
|
|
title: "System packages"
|
|
subtitle: Updates.dnf?.available === false
|
|
? "dnf is not available on this machine."
|
|
: (Number(Updates.dnf?.count ?? 0) === 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"
|
|
}
|
|
|
|
Repeater {
|
|
model: root.expandedSource === "dnf" ? (Updates.dnf?.packages ?? []) : []
|
|
|
|
delegate: TextRow {
|
|
required property var modelData
|
|
required property int index
|
|
width: parent.width
|
|
label: String(modelData.name ?? "")
|
|
detail: String(modelData.repository ?? "")
|
|
value: String(modelData.version ?? "")
|
|
divider: index < (Updates.dnf?.packages ?? []).length - 1
|
|
}
|
|
}
|
|
|
|
TextRow {
|
|
visible: Number(Updates.dnf?.count ?? 0) === 0 && Updates.everChecked
|
|
label: "Packages are current"
|
|
detail: "Nothing from the system repositories is waiting"
|
|
value: ""
|
|
divider: false
|
|
}
|
|
}
|
|
|
|
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")
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
SwitchRow {
|
|
label: "Update applications automatically"
|
|
detail: "Once a day, in the background. Applications are not a security boundary the way packages are, so this is safe to leave on; packages still ask."
|
|
checked: Updates.automatic?.flatpakEnabled === true
|
|
enabled: !Updates.busy && Updates.automatic?.flatpakAvailable === true
|
|
divider: false
|
|
onToggled: value => Updates.setAutomaticFlatpak(value)
|
|
}
|
|
}
|
|
|
|
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")
|
|
|
|
Repeater {
|
|
model: Updates.firmware?.devices ?? []
|
|
|
|
delegate: TextRow {
|
|
required property var modelData
|
|
required property int index
|
|
width: parent.width
|
|
label: String(modelData.name ?? "")
|
|
detail: String(modelData.version ?? "") + " → " + String(modelData.target ?? "")
|
|
+ (modelData.needsReboot ? " · installs on restart" : "")
|
|
value: ""
|
|
divider: true
|
|
}
|
|
}
|
|
|
|
ActionRow {
|
|
visible: Number(Updates.firmware?.count ?? 0) > 0
|
|
label: "Install firmware"
|
|
detail: "Some devices only finish updating after a restart"
|
|
action: Updates.applying ? "Working…" : "Install"
|
|
enabled: !Updates.busy
|
|
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
|
|
}
|
|
}
|
|
}
|