summaryrefslogtreecommitdiff
path: root/client/main.js
diff options
context:
space:
mode:
authorJoel Klinghed <the_jk@spawned.biz>2025-10-23 00:20:35 +0200
committerJoel Klinghed <the_jk@spawned.biz>2025-10-23 00:20:35 +0200
commitbb8ef2203469e949700499499e101354dfb1fe1f (patch)
treea9f68f89fd84a6ce6a5c04e558cc962d520604b6 /client/main.js
parent2d7b7c84cd843b75ca7131549f99d0671dfe3268 (diff)
client: Add very basic client
Diffstat (limited to 'client/main.js')
-rw-r--r--client/main.js286
1 files changed, 286 insertions, 0 deletions
diff --git a/client/main.js b/client/main.js
new file mode 100644
index 0000000..e6c46f6
--- /dev/null
+++ b/client/main.js
@@ -0,0 +1,286 @@
+import { api } from "./api.js";
+
+let bad_monitor = false;
+let last_controller = undefined;
+
+function show_error(msg) {
+ const err = document.getElementById("error");
+ err.textContent = msg;
+ err.classList.remove("invisible");
+}
+
+function reset_error() {
+ const err = document.getElementById("error");
+ err.classList.add("invisible");
+}
+
+function create_device(device) {
+ const element = document.createElement("li");
+ element.id = `dev_${device.address}`;
+ const text = document.createElement("span");
+ text.classList.add("device_text");
+ element.appendChild(text);
+ const play = document.createElement("button");
+ play.classList.add("device_play");
+ play.classList.add("material-symbols-outlined");
+ play.textContent = "play_arrow";
+ play.addEventListener("click", () => {
+ api.device_action(device.address, "play")
+ .then(() => {
+ reset_error();
+ if (bad_monitor) {
+ fetch_device(device.address);
+ }
+ })
+ .catch((err) => {
+ show_error(`Unable to start playing: ${err}`);
+ });
+ });
+ element.appendChild(play);
+ const pause = document.createElement("button");
+ pause.classList.add("device_pause");
+ pause.classList.add("material-symbols-outlined");
+ pause.textContent = "pause";
+ pause.addEventListener("click", () => {
+ api.device_action(device.address, "pause")
+ .then(() => {
+ reset_error();
+ if (bad_monitor) {
+ fetch_device(device.address);
+ }
+ })
+ .catch((err) => {
+ show_error(`Unable to pause: ${err}`);
+ });
+ });
+ element.appendChild(pause);
+ return element;
+}
+
+function update_device(child, device) {
+ const text = child.children.item(0);
+ const play = child.children.item(1);
+ const pause = child.children.item(2);
+ if (device.playing !== null) {
+ text.textContent = `${device.name} ${device.playing.status} ${device.playing.title}`;
+ play.classList.remove("invisible");
+ play.disabled = device.playing.status === "playing";
+ pause.classList.remove("invisible");
+ pause.disabled = !play.disabled;
+ } else {
+ text.textContent = device.name;
+ play.classList.add("invisible");
+ pause.classList.add("invisible");
+ }
+}
+
+function upsert_device(list, index, device) {
+ let child;
+ if (index >= list.childElementCount) {
+ child = create_device(device);
+ list.appendChild(child);
+ } else {
+ child = list.children.item(index);
+ }
+ child.id = `dev_${device.address}`;
+ update_device(child, device);
+}
+
+function remove_devices(list, index) {
+ while (index < list.childElementCount) {
+ list.removeChild(list.children.item(index));
+ }
+}
+
+function update_playing(controller) {
+ const list = document.getElementById("playing");
+ let index = 0;
+ controller.devices.forEach((device) => {
+ if (device.paired && device.connected && device.playing !== null) {
+ upsert_device(list, index++, device);
+ }
+ });
+ remove_devices(list, index);
+}
+
+function update_connected(controller) {
+ const list = document.getElementById("connected");
+ let index = 0;
+ controller.devices.forEach((device) => {
+ if (device.paired && device.connected && device.playing === null) {
+ upsert_device(list, index++, device);
+ }
+ });
+ remove_devices(list, index);
+}
+
+function update_paired(controller) {
+ const list = document.getElementById("paired");
+ let index = 0;
+ controller.devices.forEach((device) => {
+ if (device.paired && !device.connected) {
+ upsert_device(list, index++, device);
+ }
+ });
+ remove_devices(list, index);
+}
+
+function fetch_controller() {
+ api.controller()
+ .then((controller) => {
+ reset_error();
+ last_controller = controller;
+
+ const enable_pairing = document.getElementById("enable_pairing");
+ const disable_pairing = document.getElementById("disable_pairing");
+ const pairing_status = document.getElementById("pairing_status");
+ if (controller.discoverable === true) {
+ enable_pairing.classList.add("invisible");
+ enable_pairing.disabled = true;
+ disable_pairing.classList.remove("invisible");
+ disable_pairing.disabled = false;
+ pairing_status.textContent = `Pair with ${controller.name}...`;
+ pairing_status.classList.remove("invisible");
+ } else {
+ enable_pairing.classList.remove("invisible");
+ enable_pairing.disabled = controller.pairable !== true;
+ disable_pairing.classList.add("invisible");
+ disable_pairing.disabled = true;
+ pairing_status.classList.add("invisible");
+ }
+
+ update_playing(controller);
+ update_connected(controller);
+ update_paired(controller);
+ })
+ .catch((err) => {
+ console.err(err);
+ show_error("Unable to get controller from api");
+ });
+}
+
+function fetch_device(address) {
+ api.device(address)
+ .then((device) => {
+ reset_error();
+
+ let parent;
+ if (device.paired && device.connected) {
+ if (device.playing !== null) {
+ parent = document.getElementById("playing");
+ } else {
+ parent = document.getElementById("connected");
+ }
+ } else if (device.paired) {
+ parent = document.getElementById("paired");
+ } else {
+ parent = null;
+ }
+
+ let child = document.getElementById(`dev_${device.address}`);
+ if (child === null) {
+ if (parent === null) return;
+
+ child = create_device(device);
+ }
+ update_device(child, device);
+
+ if (parent === null) {
+ child.parentElement.removeChild(child);
+ } else if (parent !== child.parentElement) {
+ parent.appendChild(child);
+ }
+ })
+ .catch(() => {
+ // Fallback to updating whole controller if device fails
+ fetch_controller();
+ });
+}
+
+function monitor() {
+ const schema = location.protocol === "https:" ? "wss:" : "ws:";
+ let path = location.pathname;
+ if (path.endsWith("/index.html")) {
+ path = path.substring(0, path.length - 10);
+ }
+ const url = `${schema}//${location.host}${path}api/v1/events`;
+ const socket = new WebSocket(url);
+ socket.addEventListener("message", (event) => {
+ if (event.data === "controller/update") {
+ fetch_controller();
+ } else if (event.data.startsWith("device/update/")) {
+ const device_address = event.data.substring(14);
+ fetch_device(device_address);
+ }
+ });
+ socket.addEventListener("error", (event) => {
+ bad_monitor = true;
+ console.error(event);
+ });
+ socket.addEventListener("close", () => {
+ bad_monitor = true;
+ });
+ socket.addEventListener("open", () => {
+ bad_monitor = false;
+ });
+}
+
+function init() {
+ const enable_pairing = document.getElementById("enable_pairing");
+ const disable_pairing = document.getElementById("disable_pairing");
+
+ enable_pairing.addEventListener("click", () => {
+ enable_pairing.disabled = true;
+ api.controller_discoverable("true")
+ .then(() => {
+ if (bad_monitor) {
+ fetch_controller();
+ if (
+ last_controller !== undefined &&
+ last_controller.discover_timeout_seconds > 0
+ ) {
+ setTimeout(() => {
+ fetch_controller();
+ }, last_controller.discover_timeout_seconds * 1000);
+ }
+ }
+ })
+ .catch((err) => {
+ show_error(`Unable to enable pairing: ${err}`);
+ });
+ });
+ disable_pairing.addEventListener("click", () => {
+ disable_pairing.disabled = true;
+ api.controller_discoverable("false")
+ .then(() => {
+ if (bad_monitor) fetch_controller();
+ })
+ .catch((err) => {
+ show_error(`Unable to disable pairing: ${err}`);
+ });
+ });
+
+ api.status()
+ .then(() => {
+ fetch_controller();
+
+ monitor();
+ document.addEventListener("visibilitychange", () => {
+ if (bad_monitor && !document.hidden) {
+ // Try to reconnect websocket when page is activated.
+ monitor();
+ }
+ });
+ })
+ .catch((err) => {
+ show_error(`Unable to connect to api: ${err}`);
+ });
+}
+
+if (document.readyState === "loading") {
+ addEventListener("DOMContentLoaded", () => {
+ init();
+ });
+} else {
+ init();
+}