(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 rect = anchorBtn.getBoundingClientRect(); var p = document.createElement('div'); p.id = 'jellylms-panel'; p.style.cssText = [ 'position:fixed', 'top:' + (rect.bottom + 4) + 'px', 'right:' + (window.innerWidth - rect.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) { p.innerHTML = ''; // ---- Players ---- p.appendChild(el('div', 'font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:#888;margin-bottom:8px', 'Players')); players.forEach(function (player) { // name row: status dot + name + power button var row = el('div', 'display:flex;align-items:center;gap:8px;margin-bottom:6px'); row.appendChild(el('span', 'width:8px;height:8px;border-radius:50%;flex-shrink:0;background:' + (player.isConnected ? '#4caf50' : '#555'))); var label = player.name || player.macAddress; row.appendChild(el('span', 'flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap', label)); 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.macAddress + '/PowerOff' : '/Players/' + player.macAddress + '/PowerOn'; api(endpoint, { method: 'POST' }).then(function () { refresh(p); }); }); row.appendChild(pwrBtn); p.appendChild(row); // volume row — show when powered on if (player.isPoweredOn) { var volRow = el('div', 'display:flex;align-items:center;gap:8px;margin-bottom:8px;padding-left:16px'); volRow.appendChild(el('span', 'color:#888;font-size:16px;font-family:"Material Icons";line-height:1', 'volume_up')); var slider = document.createElement('input'); slider.type = 'range'; slider.min = 0; slider.max = 100; slider.value = player.volume; slider.style.cssText = 'flex:1;accent-color:#00a4dc'; slider.addEventListener('change', function () { api('/Players/' + player.macAddress + '/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 ---- var synced = {}; players.forEach(function (pl) { if (pl.syncMaster) { synced[pl.macAddress] = true; } if (pl.syncSlaves && pl.syncSlaves.length) { synced[pl.macAddress] = true; pl.syncSlaves.forEach(function (m) { synced[m] = true; }); } }); var masters = players.filter(function (pl) { return pl.syncSlaves && pl.syncSlaves.length > 0; }); if (masters.length > 0) { p.appendChild(el('div', 'border-top:1px solid #333;margin:8px 0')); p.appendChild(el('div', 'font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:#888;margin-bottom:8px', 'Sync Groups')); masters.forEach(function (master) { var slaveNames = master.syncSlaves.map(function (mac) { var found = players.find(function (pl) { return pl.macAddress === mac; }); return found ? (found.name || mac) : mac; }); var label = (master.name || master.macAddress) + ' + ' + slaveNames.join(', '); var grow = el('div', 'display:flex;align-items:center;gap:8px;margin-bottom:8px'); grow.appendChild(el('span', 'flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px', label)); 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/' + master.macAddress, { method: 'DELETE' }) .then(function () { refresh(p); }); }); grow.appendChild(dissolveBtn); p.appendChild(grow); }); } // ---- Create Sync Group ---- var unsynced = players.filter(function (pl) { return !synced[pl.macAddress]; }); if (unsynced.length >= 2) { p.appendChild(el('div', 'border-top:1px solid #333;margin:8px 0')); p.appendChild(el('div', 'font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:#888;margin-bottom:8px', 'Create Sync Group')); 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.macAddress; checkboxes.push(cb); crow.appendChild(cb); crow.appendChild(el('span', 'flex:1', pl.name || pl.macAddress)); 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…
'; api('/Players') .then(function (r) { return r.json(); }) .then(function (players) { renderPlayers(p, players); }) .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); } 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 (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(); } })();