Give each online account its provider's icon
Four accounts distinguished only by a line of small grey text are four rows that have to be read rather than recognised. GOA already knows what each one is. It hands back a serialised GThemedIcon -- ". GThemedIcon goa-account-google goa-account goa …" -- a preference-ordered fallback chain. The helper passes the whole chain on rather than resolving it, because which of those names exists is a property of the icon theme in use and not something a python script talking to D-Bus should be deciding. The page walks the chain and takes the first name the active theme actually has. Both simpler readings were wrong and looked right: taking the first name blindly assumes it resolves, and taking the last as a fallback assumes the most generic name is the most likely to exist. On Adwaita the tails of these very chains -- "mail", "goa-symbolic" -- do not exist at all, so a miss would have drawn nothing. Checked by asking Quickshell.iconPath directly, which returns empty for a name the theme lacks; the fallback is avatar-default-symbolic, which is present. SettingsCard grew an optional icon for this. It is empty by default and the header lays out exactly as before when unset, so no other card moves. Last-sync is not here because there is nothing to show: GOA exposes no sync-related property at all, on any of these accounts. Better to say so than to invent a timestamp from when the page last refreshed. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
// quietly stops syncing for weeks.
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.config
|
||||
import qs.services
|
||||
|
||||
@@ -57,6 +58,22 @@ SettingsPage {
|
||||
id: accountCard
|
||||
required property var modelData
|
||||
|
||||
// GOA hands back a preference-ordered chain; this walks it and takes
|
||||
// the first name the active icon theme actually has. Taking the
|
||||
// first name 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.
|
||||
icon: {
|
||||
const chain = accountCard.modelData.providerIcons ?? [];
|
||||
for (const name of chain) {
|
||||
if (Quickshell.iconPath(String(name), true) !== "")
|
||||
return String(name);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
iconFallback: "avatar-default-symbolic"
|
||||
|
||||
title: accountCard.modelData.identity || accountCard.modelData.providerName
|
||||
subtitle: accountCard.modelData.needsAttention
|
||||
? accountCard.modelData.providerName + " · sign-in expired, so this account has stopped syncing"
|
||||
|
||||
@@ -9,6 +9,13 @@ Rectangle {
|
||||
property string title: ""
|
||||
property string subtitle: ""
|
||||
|
||||
// An optional themed icon beside the title, for cards that represent a
|
||||
// specific thing rather than a topic -- an online account is one of four
|
||||
// that otherwise differ only by a line of small grey text. Empty by
|
||||
// default, and when empty the header lays out exactly as it did before.
|
||||
property string icon: ""
|
||||
property string iconFallback: "dialog-information-symbolic"
|
||||
|
||||
width: parent ? parent.width : 680
|
||||
implicitHeight: body.implicitHeight + 30
|
||||
radius: Theme.cardRadius + 2
|
||||
@@ -32,15 +39,38 @@ Rectangle {
|
||||
anchors.margins: 15
|
||||
spacing: 0
|
||||
|
||||
Text {
|
||||
Row {
|
||||
width: parent.width
|
||||
visible: root.title !== ""
|
||||
text: root.title
|
||||
color: Theme.fg
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.DemiBold
|
||||
bottomPadding: root.subtitle === "" ? 10 : 3
|
||||
spacing: root.icon === "" ? 0 : 10
|
||||
|
||||
ThemedIcon {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: root.icon !== ""
|
||||
width: root.icon === "" ? 0 : 22
|
||||
icon: root.icon
|
||||
iconFallback: root.iconFallback
|
||||
size: 22
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - (root.icon === "" ? 0 : 32)
|
||||
text: root.title
|
||||
color: Theme.fg
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.DemiBold
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
// The title's bottom padding lived on the title Text; it moves here so
|
||||
// the Row above can centre an icon against it without the padding
|
||||
// pushing the icon off-centre.
|
||||
Item {
|
||||
width: 1
|
||||
height: root.title === "" ? 0 : (root.subtitle === "" ? 10 : 3)
|
||||
}
|
||||
|
||||
Text {
|
||||
|
||||
@@ -68,6 +68,11 @@ def describe(obj):
|
||||
"path": obj.get_object_path(),
|
||||
"provider": account.props.provider_type,
|
||||
"providerName": account.props.provider_name,
|
||||
# GOA hands back a serialised GThemedIcon: ". GThemedIcon name1 name2 …",
|
||||
# a preference-ordered fallback chain. Passed on as that list rather than
|
||||
# resolved here, because which of those names exists is a property of the
|
||||
# icon theme in use, which this has no business deciding.
|
||||
"providerIcons": themed_icon_names(account.props.provider_icon),
|
||||
# PresentationIdentity is the human one (an email address); Identity is
|
||||
# the internal handle and is not always readable.
|
||||
"identity": account.props.presentation_identity or account.props.identity,
|
||||
@@ -79,6 +84,26 @@ def describe(obj):
|
||||
}
|
||||
|
||||
|
||||
def themed_icon_names(icon) -> list[str]:
|
||||
"""The icon names out of a serialised GThemedIcon, best first.
|
||||
|
||||
The string form is ". GThemedIcon mail-unread-symbolic mail-symbolic mail",
|
||||
where the leading "." and the type name are structure rather than content.
|
||||
Anything that is not that shape yields nothing, so a caller gets an empty
|
||||
list rather than a name that will never resolve.
|
||||
"""
|
||||
if icon is None:
|
||||
return []
|
||||
try:
|
||||
text = icon.to_string()
|
||||
except Exception:
|
||||
text = str(icon)
|
||||
parts = str(text or "").split()
|
||||
if len(parts) < 3 or parts[0] != "." or parts[1] != "GThemedIcon":
|
||||
return []
|
||||
return [name for name in parts[2:] if name]
|
||||
|
||||
|
||||
def find(client, path):
|
||||
for obj in client.get_accounts():
|
||||
if obj.get_object_path() == path:
|
||||
|
||||
@@ -25,7 +25,8 @@ Singleton {
|
||||
|
||||
readonly property string helperPath: Quickshell.shellDir + "/scripts/panama-accounts"
|
||||
|
||||
// [{ path, provider, providerName, identity, needsAttention, services: [{key,label,enabled}] }]
|
||||
// [{ path, provider, providerName, providerIcons, identity, needsAttention,
|
||||
// services: [{key,label,enabled}] }]
|
||||
property var accounts: []
|
||||
property bool scanned: false
|
||||
property bool busy: false
|
||||
|
||||
Reference in New Issue
Block a user