Lead Appearance with light and dark, and stop pages listing whole datasets

Appearance was six cards deep and Light/Dark was the third of them, below the
wallpaper grid and the entire lock screen -- so the control reached most often
was the last one you got to. It is five tabs now, Theme first. The mock showed
four; the page turned out to have eleven cards, so Titlebars and Windows became
Windows, and Clock and vitals became Shell, rather than pretending four would
hold them.

Region, Date & Time and Displays each rendered a complete dataset as rows: every
installed locale, the whole tz database, every mode the monitor advertises. The
chooser was never the problem -- SearchPicker already existed and worked. It was
simply rendered always-expanded, so the one line saying what is currently set sat
under hundreds that were not. PickerRow collapses each behind its current value
and closes again once something is picked.

The avatar never appeared to change because accountsservice writes every picture
to the same path, leaving the URL byte-identical while Qt served its cached
image. cache:false was already set and could not have helped: an unchanged source
is never re-read at all. avatarUrl now carries a revision fragment, bumped only
when a write actually succeeds. Pictures are cropped before they are set, in the
picture's own pixel coordinates so the result does not depend on the size it
happened to be displayed at, and written out at 512x512 through GdkPixbuf --
already a dependency here, so nothing new is required.

Snapshots listed nothing. The timeline and its Delete buttons existed the whole
time, behind a row labelled "Browse...", a word that promises a file browser. The
three most recent points are shown inline now, with the rest one press away.

qmldir-registration-contract exists because an unregistered component is not a
quiet problem: Quickshell fails the entire configuration on it, so the settings
window dies and the bar and dock go with it. That happened twice while writing
this, both times on a machine somebody was using. It is pure file inspection, so
it runs before a change ever reaches the running shell.

Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
This commit is contained in:
Gabriel Brown
2026-08-19 22:36:41 -04:00
parent ac5e6e2130
commit 1aa1324083
14 changed files with 749 additions and 70 deletions
@@ -24,6 +24,12 @@ SettingsPage {
title: "Snapshots"
lede: "Points in time you can go back to, taken automatically for each volume."
// Points in time shown without asking. The page used to show none: the
// timeline and its Delete buttons sat behind a row labelled "Browse…", a
// word that promises a file browser, so a page about points in time
// appeared to list only how many there were.
readonly property int previewCount: 3
property string openConfig: ""
property string confirmingDelete: ""
property string confirmingRestore: ""
@@ -90,6 +96,9 @@ SettingsPage {
readonly property string configName: String(volumeCard.modelData.name ?? "")
readonly property var snapshots: volumeCard.modelData.snapshots ?? []
readonly property bool open: root.openConfig === volumeCard.configName
readonly property int shownCount: volumeCard.open
? volumeCard.snapshots.length
: Math.min(root.previewCount, volumeCard.snapshots.length)
title: Snapshots.labelFor(volumeCard.modelData)
subtitle: String(volumeCard.modelData.subvolume ?? "")
@@ -118,30 +127,24 @@ SettingsPage {
onTriggered: Snapshots.take(volumeCard.configName, "Taken from Settings")
}
ActionRow {
label: "History"
detail: volumeCard.snapshots.length === 0
? "Nothing taken yet"
: volumeCard.snapshots.length + " point"
+ (volumeCard.snapshots.length === 1 ? "" : "s") + " in time"
action: volumeCard.open ? "Hide" : "Browse…"
enabled: volumeCard.snapshots.length > 0
divider: volumeCard.open
onTriggered: {
root.confirmingDelete = "";
Snapshots.closeBrowser();
root.openConfig = volumeCard.open ? "" : volumeCard.configName;
}
TextRow {
visible: volumeCard.snapshots.length === 0
label: "No points in time yet"
detail: "One is taken automatically on the schedule above."
value: ""
divider: false
}
// ── The timeline ─────────────────────────────────────────────────
Column {
width: parent.width
visible: volumeCard.open && !root.browsingOpen
visible: !root.browsingOpen
Repeater {
model: volumeCard.snapshots
model: volumeCard.open
? volumeCard.snapshots
: volumeCard.snapshots.slice(0, root.previewCount)
delegate: SettingRow {
id: pointRow
@@ -162,7 +165,7 @@ SettingsPage {
+ " · #" + pointRow.modelData.number
+ (pointRow.modelData.kept ? " · kept" : "")
controlWidth: 250
divider: pointRow.index < volumeCard.snapshots.length - 1
divider: pointRow.index < volumeCard.shownCount - 1
Row {
anchors.right: parent.right
@@ -197,6 +200,36 @@ SettingsPage {
}
}
}
SettingRow {
width: parent.width
visible: volumeCard.snapshots.length > root.previewCount
activatable: true
divider: false
label: volumeCard.open
? "Showing all " + volumeCard.snapshots.length + " points in time"
: (volumeCard.snapshots.length - root.previewCount)
+ " older point"
+ (volumeCard.snapshots.length - root.previewCount === 1 ? "" : "s")
+ " in time"
detail: volumeCard.open
? ""
: "Oldest is " + String(volumeCard.snapshots[volumeCard.snapshots.length - 1]?.date ?? "")
controlWidth: 110
onActivated: {
root.confirmingDelete = "";
root.openConfig = volumeCard.open ? "" : volumeCard.configName;
}
Text {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: volumeCard.open ? "Show fewer \u25B4" : "Show all \u25BE"
color: Theme.fgDim
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
}
}
}
// ── Inside one point in time ─────────────────────────────────────