// 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() } } } } }