diff --git a/Jellyfin.Plugin.JellyLMS/Web/remote-button.js b/Jellyfin.Plugin.JellyLMS/Web/remote-button.js
index 20b8350..6981e15 100644
--- a/Jellyfin.Plugin.JellyLMS/Web/remote-button.js
+++ b/Jellyfin.Plugin.JellyLMS/Web/remote-button.js
@@ -1,5 +1,6 @@
(function () {
var hasAccess = false;
+ var panel = null;
function getAuthToken() {
try {
@@ -11,17 +12,233 @@
}
}
- function checkAccess() {
+ function api(path, options) {
var token = getAuthToken();
- if (!token) {
- return Promise.resolve(false);
+ var opts = Object.assign({ headers: {} }, options);
+ if (token) {
+ opts.headers['X-Emby-Token'] = token;
}
- return fetch('/JellyLms/RemoteControl/Access', { headers: { 'X-Emby-Token': token } })
- .then(function (resp) { return resp.ok; })
+ if (opts.body && typeof opts.body === 'object') {
+ opts.body = JSON.stringify(opts.body);
+ opts.headers['Content-Type'] = 'application/json';
+ }
+
+ return fetch('/JellyLms' + path, opts);
+ }
+
+ function checkAccess() {
+ return api('/RemoteControl/Access')
+ .then(function (r) { return r.ok; })
.catch(function () { return false; });
}
+ // ---------- panel ----------
+
+ function createPanel(anchorBtn) {
+ var p = document.createElement('div');
+ p.id = 'jellylms-panel';
+ p.style.cssText = [
+ 'position:fixed',
+ 'top:' + (anchorBtn.getBoundingClientRect().bottom + 4) + 'px',
+ 'right:' + (window.innerWidth - anchorBtn.getBoundingClientRect().right) + 'px',
+ 'width:300px',
+ 'max-height:70vh',
+ 'overflow-y:auto',
+ 'background:#1c1c1c',
+ 'border:1px solid #333',
+ 'border-radius:4px',
+ 'box-shadow:0 4px 24px rgba(0,0,0,.7)',
+ 'z-index:999999',
+ 'font-family:inherit',
+ 'font-size:14px',
+ 'color:#ddd',
+ 'padding:12px',
+ ].join(';');
+ return p;
+ }
+
+ function el(tag, css, html) {
+ var e = document.createElement(tag);
+ if (css) { e.style.cssText = css; }
+ if (html != null) { e.innerHTML = html; }
+ return e;
+ }
+
+ function renderError(p, msg) {
+ p.innerHTML = '
' + msg + '
';
+ }
+
+ function renderPlayers(p, players, groups) {
+ p.innerHTML = '';
+
+ var masterMacs = {};
+ groups.forEach(function (g) {
+ masterMacs[g.masterMac] = g;
+ (g.slaveMacs || []).forEach(function (s) { masterMacs[s] = g; });
+ });
+
+ // ---- Players ----
+ var hdr = el('div', 'font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:#888;margin-bottom:6px', 'Players');
+ p.appendChild(hdr);
+
+ players.forEach(function (player) {
+ var row = el('div', 'display:flex;align-items:center;gap:8px;margin-bottom:10px');
+
+ var dot = el('span',
+ 'width:8px;height:8px;border-radius:50%;flex-shrink:0;background:' +
+ (player.isConnected ? '#4caf50' : '#555'));
+ row.appendChild(dot);
+
+ var name = el('span', 'flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap', player.name);
+ row.appendChild(name);
+
+ var pwrBtn = el('button',
+ 'background:none;border:1px solid #555;border-radius:3px;color:#ddd;' +
+ 'padding:2px 7px;cursor:pointer;font-size:12px;flex-shrink:0',
+ player.isPoweredOn ? 'Off' : 'On');
+ pwrBtn.title = player.isPoweredOn ? 'Power off' : 'Power on';
+ pwrBtn.addEventListener('click', function () {
+ var endpoint = player.isPoweredOn ? '/Players/' + player.mac + '/PowerOff' : '/Players/' + player.mac + '/PowerOn';
+ api(endpoint, { method: 'POST' }).then(function () { refresh(p); });
+ });
+ row.appendChild(pwrBtn);
+
+ p.appendChild(row);
+
+ if (player.isConnected) {
+ var volRow = el('div', 'display:flex;align-items:center;gap:8px;margin-bottom:10px;padding-left:16px');
+ var volIcon = el('span', 'color:#888;font-size:16px;font-family:\'Material Icons\';line-height:1', 'volume_up');
+ volRow.appendChild(volIcon);
+
+ var slider = el('input');
+ slider.type = 'range';
+ slider.min = 0;
+ slider.max = 100;
+ slider.value = player.volume || 0;
+ slider.style.cssText = 'flex:1;accent-color:#00a4dc';
+ slider.addEventListener('change', function () {
+ api('/Players/' + player.mac + '/Volume', { method: 'POST', body: { volume: parseInt(slider.value, 10) } });
+ });
+ volRow.appendChild(slider);
+
+ var volVal = el('span', 'color:#888;font-size:12px;width:28px;text-align:right', player.volume + '%');
+ slider.addEventListener('input', function () { volVal.textContent = slider.value + '%'; });
+ volRow.appendChild(volVal);
+
+ p.appendChild(volRow);
+ }
+ });
+
+ // ---- Sync Groups ----
+ if (groups.length > 0) {
+ var sep = el('div', 'border-top:1px solid #333;margin:8px 0');
+ p.appendChild(sep);
+
+ var ghdr = el('div', 'font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:#888;margin-bottom:6px', 'Sync Groups');
+ p.appendChild(ghdr);
+
+ groups.forEach(function (g) {
+ var master = players.find(function (pl) { return pl.mac === g.masterMac; });
+ var slaves = (g.slaveMacs || []).map(function (m) {
+ return players.find(function (pl) { return pl.mac === m; });
+ }).filter(Boolean);
+
+ var names = [master ? master.name : g.masterMac]
+ .concat(slaves.map(function (s) { return s.name; }))
+ .join(' + ');
+
+ var grow = el('div', 'display:flex;align-items:center;gap:8px;margin-bottom:8px');
+ var glabel = el('span', 'flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px', names);
+ grow.appendChild(glabel);
+
+ var dissolveBtn = el('button',
+ 'background:none;border:1px solid #555;border-radius:3px;color:#f88;' +
+ 'padding:2px 7px;cursor:pointer;font-size:12px;flex-shrink:0',
+ 'Unsync');
+ dissolveBtn.addEventListener('click', function () {
+ api('/SyncGroups/' + g.masterMac, { method: 'DELETE' }).then(function () { refresh(p); });
+ });
+ grow.appendChild(dissolveBtn);
+ p.appendChild(grow);
+ });
+ }
+
+ // ---- Create Sync Group ----
+ var unsynced = players.filter(function (pl) {
+ return !masterMacs[pl.mac] && pl.isConnected;
+ });
+
+ if (unsynced.length >= 2) {
+ var sep2 = el('div', 'border-top:1px solid #333;margin:8px 0');
+ p.appendChild(sep2);
+
+ var shdr = el('div', 'font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:#888;margin-bottom:6px', 'Create Sync Group');
+ p.appendChild(shdr);
+
+ var checkboxes = [];
+ unsynced.forEach(function (pl) {
+ var crow = el('div', 'display:flex;align-items:center;gap:8px;margin-bottom:6px');
+ var cb = document.createElement('input');
+ cb.type = 'checkbox';
+ cb.style.accentColor = '#00a4dc';
+ cb.dataset.mac = pl.mac;
+ checkboxes.push(cb);
+ crow.appendChild(cb);
+ crow.appendChild(el('span', 'flex:1', pl.name));
+ p.appendChild(crow);
+ });
+
+ var syncBtn = el('button',
+ 'margin-top:6px;width:100%;background:#00a4dc;border:none;border-radius:3px;' +
+ 'color:#fff;padding:5px 0;cursor:pointer;font-size:13px',
+ 'Sync Selected');
+ syncBtn.addEventListener('click', function () {
+ var selected = checkboxes.filter(function (cb) { return cb.checked; });
+ if (selected.length < 2) { return; }
+ var macs = selected.map(function (cb) { return cb.dataset.mac; });
+ api('/SyncGroups', {
+ method: 'POST',
+ body: { masterMac: macs[0], slaveMacs: macs.slice(1) }
+ }).then(function () { refresh(p); });
+ });
+ p.appendChild(syncBtn);
+ }
+ }
+
+ function refresh(p) {
+ p.innerHTML = 'Loading…
';
+ Promise.all([
+ api('/Players').then(function (r) { return r.json(); }),
+ api('/SyncGroups').then(function (r) { return r.json(); })
+ ]).then(function (results) {
+ renderPlayers(p, results[0], results[1]);
+ }).catch(function () {
+ renderError(p, 'Failed to load players.');
+ });
+ }
+
+ function togglePanel(btn) {
+ if (panel) {
+ panel.remove();
+ panel = null;
+ return;
+ }
+
+ panel = createPanel(btn);
+ document.body.appendChild(panel);
+ refresh(panel, btn);
+ }
+
+ function closePanel() {
+ if (panel) {
+ panel.remove();
+ panel = null;
+ }
+ }
+
+ // ---------- header button ----------
+
function addButton(headerRight) {
if (headerRight.querySelector('.headerLmsRemoteButton')) {
return;
@@ -33,8 +250,9 @@
btn.className = 'headerLmsRemoteButton headerButton headerButtonRight paper-icon-button-light';
btn.title = 'Multi-room Remote';
btn.innerHTML = '';
- btn.addEventListener('click', function () {
- window.location.href = '/JellyLms/RemoteControl';
+ btn.addEventListener('click', function (e) {
+ e.stopPropagation();
+ togglePanel(btn);
});
var castButton = headerRight.querySelector('.headerCastButton');
@@ -46,27 +264,34 @@
}
function tryInject() {
- if (!hasAccess) {
- return;
- }
-
+ if (!hasAccess) { return; }
var headerRight = document.querySelector('.headerRight');
- if (headerRight) {
- addButton(headerRight);
- }
+ if (headerRight) { addButton(headerRight); }
}
+ // ---------- bootstrap ----------
+
function init() {
checkAccess().then(function (ok) {
hasAccess = ok;
- if (!ok) {
- return;
- }
+ if (!ok) { return; }
tryInject();
- var observer = new MutationObserver(tryInject);
+ var observer = new MutationObserver(function () {
+ // If our panel was removed by SPA navigation, clean up the reference
+ if (panel && !document.body.contains(panel)) {
+ panel = null;
+ }
+ tryInject();
+ });
observer.observe(document.body, { childList: true, subtree: true });
+
+ document.addEventListener('click', function (e) {
+ if (panel && !panel.contains(e.target)) {
+ closePanel();
+ }
+ });
});
}