import Adw from "gi://Adw"; import Gio from "gi://Gio"; import GLib from "gi://GLib"; import Gtk from "gi://Gtk"; import Soup from "gi://Soup"; import { ExtensionPreferences } from "resource:///org/gnome/shell/extensions/prefs.js"; function jsonRpc(method, params = {}) { return JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }); } async function postJson(session, url, bodyStr) { const msg = Soup.Message.new("POST", url); msg.request_headers.append("Content-Type", "application/json"); msg.set_request_body_from_bytes( "application/json", new GLib.Bytes(bodyStr), ); const bytes = await session.send_and_read_async( msg, GLib.PRIORITY_DEFAULT, null, ); const text = new TextDecoder().decode(bytes.get_data()); return JSON.parse(text); } function connectedCount(group) { return (group?.clients ?? []).filter((c) => c?.connected).length; } function groupLabel(group) { const base = group?.name && group.name.trim() ? group.name.trim() : (group?.stream_id ?? "Group"); const connected = connectedCount(group); const total = group?.clients?.length ?? 0; return `${base} (${connected}/${total})`; } export default class SnapshellPreferences extends ExtensionPreferences { fillPreferencesWindow(window) { const settings = this.getSettings( "org.gnome.shell.extensions.snapshell", ); const session = new Soup.Session(); // --- UI models --- const groupNames = new Gtk.StringList(); let groupIds = []; // parallel array: index -> group id // --- Page --- const page = new Adw.PreferencesPage({ title: "Snapshell", icon_name: "audio-volume-high-symbolic", }); window.add(page); // --- Group: Connection --- const connectionGroup = new Adw.PreferencesGroup({ title: "Connection", description: "Configure Snapserver JSON-RPC endpoint.", }); page.add(connectionGroup); const urlRow = new Adw.EntryRow({ title: "Server URL", text: settings.get_string("server-url"), }); urlRow.set_tooltip_text("Example: https://snap.unbl.ink/jsonrpc"); connectionGroup.add(urlRow); urlRow.connect("notify::text", () => { settings.set_string("server-url", urlRow.text.trim()); }); // --- Group: Target Group --- const targetGroup = new Adw.PreferencesGroup({ title: "Target group", description: "Choose which Snapcast group the Quick Settings slider controls.", }); page.add(targetGroup); const groupRow = new Adw.ComboRow({ title: "Snapcast group", model: groupNames, }); targetGroup.add(groupRow); const refreshRow = new Adw.ActionRow({ title: "Refresh groups", subtitle: "Fetch groups from Server.GetStatus", }); const refreshBtn = new Gtk.Button({ label: "Refresh" }); refreshRow.add_suffix(refreshBtn); refreshRow.activatable_widget = refreshBtn; targetGroup.add(refreshRow); // --- Group: Behavior --- const behaviorGroup = new Adw.PreferencesGroup({ title: "Behavior", }); page.add(behaviorGroup); const pollRow = new Adw.SpinRow({ title: "Poll interval (seconds)", adjustment: new Gtk.Adjustment({ lower: 2, upper: 60, step_increment: 1, page_increment: 5, value: settings.get_uint("poll-seconds") || 5, }), }); behaviorGroup.add(pollRow); pollRow.connect("notify::value", () => { settings.set_uint("poll-seconds", pollRow.value); }); // --- Helpers to load + sync groups --- const setStatus = (text) => { refreshRow.subtitle = text; }; const clearGroups = () => { // Remove all items from StringList while (groupNames.get_n_items() > 0) groupNames.remove(0); groupIds = []; }; const syncComboSelectionFromSettings = () => { const selectedId = settings.get_string("group-id"); const idx = groupIds.indexOf(selectedId); groupRow.selected = idx >= 0 ? idx : Gtk.INVALID_LIST_POSITION; }; const fetchGroups = async () => { const url = settings.get_string("server-url").trim(); if (!url) { clearGroups(); setStatus("Set Server URL first."); return; } setStatus("Fetching…"); try { const resp = await postJson( session, url, jsonRpc("Server.GetStatus", {}), ); const groups = resp?.result?.server?.groups ?? []; clearGroups(); // Populate list for (const g of groups) { groupNames.append(groupLabel(g)); groupIds.push(g.id); } setStatus(`Loaded ${groups.length} group(s).`); syncComboSelectionFromSettings(); } catch (e) { clearGroups(); setStatus(`Failed: ${e?.message ?? e}`); } }; // When user picks a row, store its group id groupRow.connect("notify::selected", () => { const idx = groupRow.selected; if (idx === Gtk.INVALID_LIST_POSITION) return; const id = groupIds[idx]; if (id) settings.set_string("group-id", id); }); // Refresh button refreshBtn.connect("clicked", () => { fetchGroups(); }); // If URL changes, offer fresh fetch (don’t auto-spam) urlRow.connect("notify::text", () => { setStatus("URL changed. Click Refresh."); clearGroups(); syncComboSelectionFromSettings(); }); // Initial load fetchGroups(); } }