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
97 lines
3.4 KiB
QML
97 lines
3.4 KiB
QML
pragma Singleton
|
|
|
|
// Online accounts, via GNOME Online Accounts.
|
|
//
|
|
// The daemon already runs in this session -- gvfs activates it, and accounts
|
|
// work without gnome-shell anywhere. Only the panel was GNOME's; the accounts
|
|
// are D-Bus objects anything may read and modify. So listing, per-service
|
|
// toggles, and removal all happen here, natively.
|
|
//
|
|
// Signing in is the exception, and only for OAuth providers. The daemon's
|
|
// AddAccount takes credentials as an argument rather than obtaining them, and
|
|
// the code that runs Google's OAuth exchange lives in libgoa-backend, which
|
|
// Fedora ships without a GIR binding. So that one step is handed to GNOME's
|
|
// panel and the user comes straight back here.
|
|
//
|
|
// Read on demand and after every change: accounts are added and removed by
|
|
// people, not by the system, so there is nothing to poll for.
|
|
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import QtQuick
|
|
|
|
Singleton {
|
|
id: root
|
|
|
|
readonly property string helperPath: Quickshell.shellDir + "/scripts/panama-accounts"
|
|
|
|
// [{ path, provider, providerName, providerIcons, identity, needsAttention,
|
|
// services: [{key,label,enabled}] }]
|
|
property var accounts: []
|
|
property bool scanned: false
|
|
property bool busy: false
|
|
property string lastError: ""
|
|
|
|
// Accounts whose stored credentials have stopped working -- an expired
|
|
// token, a changed password. GOA knows, and nothing outside its own panel
|
|
// ever says so, which is how an account quietly stops syncing for weeks.
|
|
readonly property int attentionCount: root.accounts.filter(a => a.needsAttention).length
|
|
|
|
readonly property bool available: root.lastError === ""
|
|
|
|
function refresh(): void {
|
|
if (!list.running)
|
|
list.running = true;
|
|
}
|
|
|
|
// Enabling a service clears GOA's "disabled" flag; the helper owns that
|
|
// inversion so the UI can speak in terms of what is on.
|
|
function setService(path: string, service: string, enabled: bool): void {
|
|
if (root.busy)
|
|
return;
|
|
root.busy = true;
|
|
write.command = [root.helperPath, "set", path, service, enabled ? "true" : "false"];
|
|
write.running = true;
|
|
}
|
|
|
|
function remove(path: string): void {
|
|
if (root.busy)
|
|
return;
|
|
root.busy = true;
|
|
write.command = [root.helperPath, "remove", path];
|
|
write.running = true;
|
|
}
|
|
|
|
Process {
|
|
id: list
|
|
command: [root.helperPath, "list"]
|
|
stdout: StdioCollector {
|
|
onStreamFinished: {
|
|
try {
|
|
const parsed = JSON.parse(this.text);
|
|
root.accounts = Array.isArray(parsed.accounts) ? parsed.accounts : [];
|
|
root.lastError = String(parsed.error ?? "");
|
|
} catch (error) {
|
|
root.accounts = [];
|
|
root.lastError = "Could not read the accounts helper's output.";
|
|
console.warn("OnlineAccounts: could not parse helper output:", error);
|
|
}
|
|
root.scanned = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: write
|
|
stderr: StdioCollector {
|
|
onStreamFinished: if (this.text.trim() !== "") root.lastError = this.text.trim()
|
|
}
|
|
// Re-read rather than assuming the write landed: GOA may refuse, and a
|
|
// toggle that sprang back is the honest outcome.
|
|
onExited: {
|
|
root.busy = false;
|
|
root.refresh();
|
|
}
|
|
}
|
|
}
|