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:
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user