const APPS = [ { name: "Netflix", package: "com.netflix.ninja" }, { name: "YouTube", package: "com.google.android.youtube.tv" }, { name: "Prime Video", package: "com.amazon.amazonvideo.livingroom" }, { name: "Disney+", package: "com.disney.disneyplus" }, { name: "Spotify", package: "com.spotify.tv.android" }, { name: "Plex", package: "com.plexapp.android" }, ]; const state = { devices: [], selectedId: localStorage.getItem("selectedDeviceId") || null, }; const els = { deviceSelect: document.getElementById("deviceSelect"), statusDot: document.getElementById("statusDot"), settingsBtn: document.getElementById("settingsBtn"), settingsDialog: document.getElementById("settingsDialog"), closeSettings: document.getElementById("closeSettings"), deviceList: document.getElementById("deviceList"), addDeviceForm: document.getElementById("addDeviceForm"), newDeviceName: document.getElementById("newDeviceName"), newDeviceHost: document.getElementById("newDeviceHost"), pairForm: document.getElementById("pairForm"), pairHost: document.getElementById("pairHost"), pairCode: document.getElementById("pairCode"), settingsMessage: document.getElementById("settingsMessage"), searchForm: document.getElementById("searchForm"), searchInput: document.getElementById("searchInput"), appsRow: document.getElementById("appsRow"), toast: document.getElementById("toast"), }; function setSettingsMessage(text, isError = false) { els.settingsMessage.textContent = text; els.settingsMessage.classList.toggle("error", isError); } function showToast(message) { els.toast.textContent = message; els.toast.classList.add("show"); clearTimeout(showToast._t); showToast._t = setTimeout(() => els.toast.classList.remove("show"), 1800); } function vibrate() { if (navigator.vibrate) navigator.vibrate(12); } async function api(path, options) { const res = await fetch(`/api${path}`, { headers: { "Content-Type": "application/json" }, ...options, }); const body = await res.json().catch(() => ({})); if (!res.ok) { throw new Error(body.error || `Request failed (${res.status})`); } return body; } function currentDevice() { return state.devices.find((d) => d.id === state.selectedId) || null; } function renderDeviceSelect() { els.deviceSelect.innerHTML = ""; if (!state.devices.length) { const opt = document.createElement("option"); opt.textContent = "No TVs added — tap ⚙"; els.deviceSelect.appendChild(opt); els.statusDot.className = "status-dot"; return; } for (const d of state.devices) { const opt = document.createElement("option"); opt.value = d.id; opt.textContent = d.name; els.deviceSelect.appendChild(opt); } if (!state.selectedId || !state.devices.some((d) => d.id === state.selectedId)) { state.selectedId = state.devices[0].id; } els.deviceSelect.value = state.selectedId; localStorage.setItem("selectedDeviceId", state.selectedId); updateStatusDot(); } function updateStatusDot() { const device = currentDevice(); els.statusDot.className = "status-dot"; if (device?.status === "device") els.statusDot.classList.add("online"); else if (device) els.statusDot.classList.add("offline"); } function renderDeviceList() { els.deviceList.innerHTML = ""; for (const d of state.devices) { const li = document.createElement("li"); li.innerHTML = `
${escapeHtml(d.name)}
${escapeHtml(d.host)} · ${d.status || "unknown"}
`; els.deviceList.appendChild(li); } } function escapeHtml(str) { return String(str).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); } async function refreshDevices() { const { devices } = await api("/devices"); state.devices = devices; renderDeviceSelect(); renderDeviceList(); } async function sendKey(key) { const device = currentDevice(); if (!device) return showToast("Add a TV first (⚙)"); vibrate(); try { await api(`/devices/${device.id}/key`, { method: "POST", body: JSON.stringify({ key }) }); } catch (err) { showToast(err.message); } } async function launchApp(packageName, label) { const device = currentDevice(); if (!device) return showToast("Add a TV first (⚙)"); vibrate(); try { await api(`/devices/${device.id}/app`, { method: "POST", body: JSON.stringify({ package: packageName }) }); showToast(`Opening ${label}…`); } catch (err) { showToast(err.message); } } function renderApps() { els.appsRow.innerHTML = ""; for (const app of APPS) { const btn = document.createElement("button"); btn.className = "btn"; btn.textContent = app.name; btn.addEventListener("click", () => launchApp(app.package, app.name)); els.appsRow.appendChild(btn); } } document.querySelectorAll("[data-key]").forEach((btn) => { btn.addEventListener("click", () => sendKey(btn.dataset.key)); }); document.querySelector('[data-action="mute"]').addEventListener("click", () => sendKey("VOLUME_MUTE")); els.deviceSelect.addEventListener("change", () => { state.selectedId = els.deviceSelect.value; localStorage.setItem("selectedDeviceId", state.selectedId); updateStatusDot(); }); els.settingsBtn.addEventListener("click", async () => { setSettingsMessage(""); await refreshDevices().catch((err) => showToast(err.message)); els.settingsDialog.showModal(); }); els.closeSettings.addEventListener("click", () => els.settingsDialog.close()); els.addDeviceForm.addEventListener("submit", async (e) => { e.preventDefault(); setSettingsMessage(""); try { const { device } = await api("/devices", { method: "POST", body: JSON.stringify({ name: els.newDeviceName.value.trim(), host: els.newDeviceHost.value.trim() }), }); els.newDeviceName.value = ""; els.newDeviceHost.value = ""; state.selectedId = device.id; await refreshDevices(); setSettingsMessage(`Added ${device.name}. Connecting…`); await api(`/devices/${device.id}/connect`, { method: "POST" }); setSettingsMessage(`Connected to ${device.name}.`); await refreshDevices(); } catch (err) { setSettingsMessage(err.message, true); await refreshDevices(); } }); els.deviceList.addEventListener("click", async (e) => { const target = e.target.closest("button"); if (!target) return; const { action, id } = target.dataset; setSettingsMessage(""); try { if (action === "connect") { setSettingsMessage("Connecting…"); await api(`/devices/${id}/connect`, { method: "POST" }); setSettingsMessage("Connected."); } else if (action === "remove") { await api(`/devices/${id}`, { method: "DELETE" }); } await refreshDevices(); } catch (err) { setSettingsMessage(err.message, true); await refreshDevices(); } }); els.pairForm.addEventListener("submit", async (e) => { e.preventDefault(); setSettingsMessage("Pairing…"); try { const device = currentDevice(); if (!device) throw new Error("Add the TV's IP first, then pair."); await api(`/devices/${device.id}/pair`, { method: "POST", body: JSON.stringify({ pairHost: els.pairHost.value.trim(), code: els.pairCode.value.trim() }), }); setSettingsMessage("Paired! Reconnecting…"); els.pairHost.value = ""; els.pairCode.value = ""; await refreshDevices(); } catch (err) { setSettingsMessage(err.message, true); } }); els.searchForm.addEventListener("submit", async (e) => { e.preventDefault(); const device = currentDevice(); const text = els.searchInput.value.trim(); if (!device || !text) return; try { await api(`/devices/${device.id}/text`, { method: "POST", body: JSON.stringify({ text }) }); els.searchInput.value = ""; showToast("Sent"); } catch (err) { showToast(err.message); } }); renderApps(); refreshDevices().catch((err) => showToast(err.message)); setInterval(() => refreshDevices().catch(() => {}), 8000); if ("serviceWorker" in navigator) { window.addEventListener("load", () => { navigator.serviceWorker.register("/sw.js").catch(() => {}); }); }