201 lines
6.1 KiB
JavaScript
201 lines
6.1 KiB
JavaScript
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();
|
||
}
|
||
}
|