import Adw from "gi://Adw"; import GLib from "gi://GLib"; import Gtk from "gi://Gtk"; import Soup from "gi://Soup"; import { ExtensionPreferences } from "resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js"; export default class SnapshellPreferences extends ExtensionPreferences { fillPreferencesWindow(window) { const settings = this.getSettings(); const session = new Soup.Session({ timeout: 5, idle_timeout: 5, user_agent: "Snapshell/1", }); const groupNames = new Gtk.StringList(); let groupIds = []; function jsonRpc(method, params) { return JSON.stringify({ jsonrpc: "2.0", id: 1, method: method, params: params || {}, }); } function postJson(url, bodyStr) { return new Promise((resolve, reject) => { 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), ); session.send_and_read_async( msg, GLib.PRIORITY_DEFAULT, null, (sess, res) => { try { const bytes = sess.send_and_read_finish(res); const text = new TextDecoder().decode( bytes.get_data(), ); resolve(JSON.parse(text)); } catch (e) { reject(e); } }, ); }); } function labelForGroup(g) { const clients = g && g.clients ? g.clients : []; let connected = 0; for (const c of clients) { if (c && c.connected) connected++; } const total = clients.length; let base = "Group"; if (g && g.name && String(g.name).trim().length > 0) base = String(g.name).trim(); else if (g && g.stream_id) base = String(g.stream_id); return `${base} (${connected}/${total})`; } function clearGroups() { while (groupNames.get_n_items() > 0) groupNames.remove(0); groupIds = []; } function syncSelectionFromSettings(groupRow) { const selectedId = settings.get_string("group-id"); const idx = groupIds.indexOf(selectedId); groupRow.selected = idx >= 0 ? idx : Gtk.INVALID_LIST_POSITION; } async function fetchGroups(statusRow, groupRow) { const url = settings.get_string("server-url").trim(); if (!url) { clearGroups(); statusRow.subtitle = "Set Server URL first."; return; } statusRow.subtitle = "Fetching…"; try { const resp = await postJson( url, jsonRpc("Server.GetStatus", {}), ); const groups = resp && resp.result && resp.result.server && resp.result.server.groups ? resp.result.server.groups : []; clearGroups(); for (const g of groups) { groupNames.append(labelForGroup(g)); groupIds.push(g.id); } statusRow.subtitle = `Loaded ${groups.length} group(s).`; syncSelectionFromSettings(groupRow); } catch (e) { clearGroups(); statusRow.subtitle = `Failed: ${e && e.message ? e.message : e}`; } } // ----- UI ----- const page = new Adw.PreferencesPage({ title: "Snapshell", icon_name: "audio-volume-high-symbolic", }); window.add(page); 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"), }); connectionGroup.add(urlRow); urlRow.connect("notify::text", () => { settings.set_string("server-url", urlRow.text.trim()); }); 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 statusRow = new Adw.ActionRow({ title: "Refresh groups", subtitle: "Fetch groups from Server.GetStatus", }); const refreshBtn = new Gtk.Button({ label: "Refresh" }); statusRow.add_suffix(refreshBtn); statusRow.activatable_widget = refreshBtn; targetGroup.add(statusRow); 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); }); 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); }); refreshBtn.connect("clicked", () => { fetchGroups(statusRow, groupRow); }); urlRow.connect("notify::text", () => { statusRow.subtitle = "URL changed. Click Refresh."; clearGroups(); syncSelectionFromSettings(groupRow); }); // Initial load fetchGroups(statusRow, groupRow); } }