(function () { var hasAccess = false; var panel = null; function getAuthToken() { try { var creds = JSON.parse(localStorage.getItem('jellyfin_credentials')); var server = creds && creds.Servers && creds.Servers[0]; return (server && server.AccessToken) || null; } catch (e) { return null; } } function api(path, options) { var token = getAuthToken(); var opts = Object.assign({ headers: {} }, options); if (token) { opts.headers['X-Emby-Token'] = token; } 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; } var btn = document.createElement('button'); btn.setAttribute('is', 'paper-icon-button-light'); btn.setAttribute('type', 'button'); btn.className = 'headerLmsRemoteButton headerButton headerButtonRight paper-icon-button-light'; btn.title = 'Multi-room Remote'; btn.innerHTML = ''; btn.addEventListener('click', function (e) { e.stopPropagation(); togglePanel(btn); }); var castButton = headerRight.querySelector('.headerCastButton'); if (castButton) { headerRight.insertBefore(btn, castButton); } else { headerRight.appendChild(btn); } } function tryInject() { if (!hasAccess) { return; } var headerRight = document.querySelector('.headerRight'); if (headerRight) { addButton(headerRight); } } // ---------- bootstrap ---------- function init() { checkAccess().then(function (ok) { hasAccess = ok; if (!ok) { return; } 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(); } }); }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();