Give identity its due: native enrollment, honest deletion, and sign-in that stays home

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
Gabriel Brown
2026-08-24 18:55:38 -04:00
parent 5a0643357f
commit 4ec8bd94d9
25 changed files with 4713 additions and 407 deletions
@@ -0,0 +1,247 @@
// One GNOME Online Accounts account: a line you can read, unfolding into the
// services it lends to the rest of the desktop.
//
// Built like InstalledAppRow rather than out of SettingRow, because the head
// carries the provider's own icon on the left and SettingRow's only icon slot
// is a mono glyph. What unfolds is small -- a toggle per service and a removal
// -- but it is per-account state, and four accounts each showing four toggles
// was most of a screen of switches with no way to tell which belonged to whom.
//
// Nothing here writes. Every control leaves through a signal so the page owns
// the service calls, which keeps the removal confirmation one piece of page
// state instead of something each row remembers for itself.
import QtQuick
import Quickshell
import qs.config
import qs.widgets
Column {
id: root
// { path, provider, providerName, providerIcons, identity, needsAttention,
// services: [{ key, label, enabled }] }
required property var account
property bool expanded: false
property bool busy: false
property bool confirmingRemoval: false
property bool divider: true
// The words on the re-authorisation button come from the page, because the
// page is what that button leads to: it is the one hand-off to GNOME's own
// dialog, and the sentence and the call belong together rather than one of
// them living out here. Left empty the button does not appear at all.
property string signInAction: ""
signal activated
signal serviceToggled(key: string, enabled: bool)
signal removeArmed
signal removeCancelled
signal removeConfirmed
signal signInAgainRequested
readonly property bool needsAttention: root.account?.needsAttention === true
readonly property string providerName: String(root.account?.providerName ?? "")
readonly property string identity: String(root.account?.identity ?? "")
readonly property var services: root.account?.services ?? []
// GOA hands back a preference-ordered chain; this walks it and takes the
// first name the active icon theme actually has. Taking the first blindly,
// or the last as a fallback, both looked right and were not: on Adwaita the
// tail of these chains ("mail", "goa-symbolic") does not exist, so a miss
// would have rendered nothing at all.
readonly property string iconName: {
const chain = root.account?.providerIcons ?? [];
for (const name of chain) {
if (Quickshell.iconPath(String(name), true) !== "")
return String(name);
}
return "";
}
width: parent ? parent.width : 620
spacing: 0
Item {
id: head
width: parent.width
implicitHeight: Math.max(56, copy.implicitHeight + 20)
Rectangle {
anchors.fill: parent
anchors.bottomMargin: 1
radius: 9
z: -1
visible: headHover.hovered
color: Theme.alpha(Theme.fg, 0.05)
border.width: 0
}
Rectangle {
id: tile
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: 32
height: 32
radius: 9
color: Theme.alpha(Theme.fg, 0.07)
border.width: 0
ThemedIcon {
anchors.centerIn: parent
icon: root.iconName
iconFallback: "avatar-default-symbolic"
tint: root.needsAttention ? Theme.warn : Theme.fg
size: 19
}
}
Column {
id: copy
anchors.left: tile.right
anchors.leftMargin: 12
anchors.right: trailing.left
anchors.rightMargin: 16
anchors.verticalCenter: parent.verticalCenter
spacing: 2
Text {
width: parent.width
text: root.identity !== "" ? root.identity : root.providerName
color: Theme.fg
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize
font.weight: Font.Medium
elide: Text.ElideRight
}
Text {
width: parent.width
text: root.needsAttention
? root.providerName + " · sign-in expired, so this account has stopped syncing"
: (root.services.length === 0
? root.providerName
: root.providerName + " · "
+ root.services.filter(service => service.enabled)
.map(service => String(service.label)).join(", "))
color: root.needsAttention ? Theme.warn : Theme.fgDim
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
elide: Text.ElideRight
}
}
Row {
id: trailing
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 9
SettingsButton {
anchors.verticalCenter: parent.verticalCenter
visible: root.needsAttention && root.signInAction !== ""
text: root.signInAction
enabled: !root.busy
onClicked: root.signInAgainRequested()
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: root.expanded ? "▴" : "▾"
color: Theme.fgMuted
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
}
}
Rectangle {
anchors.left: copy.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: 1
visible: root.divider && !root.expanded
color: Theme.alpha(Theme.fg, 0.065)
}
HoverHandler {
id: headHover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
onTapped: root.activated()
}
}
Column {
id: expansion
width: parent.width
leftPadding: 44
visible: root.expanded
Repeater {
model: root.services
delegate: SettingRow {
id: serviceRow
required property var modelData
width: expansion.width - expansion.leftPadding
label: String(serviceRow.modelData.label ?? "")
detail: serviceRow.modelData.enabled
? "Applications using " + String(serviceRow.modelData.label ?? "").toLowerCase()
+ " can see this account"
: "Hidden from applications"
controlWidth: 54
SettingsToggle {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
checked: serviceRow.modelData.enabled === true
// A toggle that moves while GOA is still answering the last
// one springs back, which reads as a broken switch.
enabled: !root.busy
onToggled: value => root.serviceToggled(
String(serviceRow.modelData.key ?? ""), value)
}
}
}
SettingRow {
width: expansion.width - expansion.leftPadding
label: root.confirmingRemoval ? "Remove this account?" : "Remove this account"
detail: root.confirmingRemoval
? "It signs out here and disappears from every application that was using it. Nothing on the provider's side is touched."
: "Signs out and removes it from every application that was using it"
controlWidth: 230
divider: false
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 8
SettingsButton {
text: root.confirmingRemoval ? "Keep it" : "Remove…"
enabled: !root.busy
onClicked: root.confirmingRemoval ? root.removeCancelled() : root.removeArmed()
}
SettingsButton {
visible: root.confirmingRemoval
text: "Remove"
tone: "danger"
enabled: !root.busy
onClicked: root.removeConfirmed()
}
}
}
}
}