diff --git a/src/JellyfinSyncPlus/SyncPlay/SyncPlusStatsController.cs b/src/JellyfinSyncPlus/SyncPlay/SyncPlusStatsController.cs index 00e2b70..a21c6be 100644 --- a/src/JellyfinSyncPlus/SyncPlay/SyncPlusStatsController.cs +++ b/src/JellyfinSyncPlus/SyncPlay/SyncPlusStatsController.cs @@ -435,14 +435,17 @@ public class SyncPlusStatsController : ControllerBase """; // The in-player UI. Entry point is jellyfin-web's own player settings (cog) menu: - // we watch for the action sheet it opens and append two native-looking menu items. - // This intentionally depends on a small, verified slice of the minified client's DOM - // (.btnVideoOsdSettings, .actionSheet, .actionSheetScroller, .actionSheetMenuItem -- - // all confirmed present in the bundled 10.11.6 web client, including the sheet's own - // delegated click handler that auto-closes it on any .actionSheetMenuItem click, - // which gives injected items native close behavior for free). If a future - // jellyfin-web rename breaks these, the failure mode is just "menu items don't - // appear" -- nothing errors, and the standalone stats page still works. + // we watch for the action sheet it opens and append a single native-looking menu + // item ("SyncPlay stats") that opens our own floating panel holding both the drift + // readout and the sync button. Just one item on purpose -- the sheet is measured and + // positioned before our async injection runs, so every added item risks pushing it + // off the bottom of the screen (which is exactly what happened with two). Depends on + // a small, verified slice of the minified client's DOM (.btnVideoOsdSettings, + // .actionSheet, .actionSheetScroller, .actionSheetMenuItem -- all confirmed present + // in the bundled 10.11.6 client, including the sheet's own delegated click handler + // that auto-closes it on any .actionSheetMenuItem click). If a future jellyfin-web + // rename breaks these, the failure mode is just "the item doesn't appear" -- nothing + // errors, and the standalone stats page still works. private const string ClientScriptJs = """ (function () { 'use strict'; @@ -455,29 +458,19 @@ public class SyncPlusStatsController : ControllerBase var panel = document.createElement('div'); panel.id = 'syncPlusPanel'; - panel.style.cssText = 'position:fixed;top:70px;left:12px;z-index:99999;display:none;font-family:sans-serif;font-size:13px;color:#ddd;background:rgba(0,0,0,.75);border:1px solid rgba(255,255,255,.15);border-radius:6px;padding:10px 12px;min-width:260px;'; + panel.style.cssText = 'position:fixed;top:70px;left:12px;z-index:99999;display:none;font-family:sans-serif;font-size:13px;color:#ddd;background:rgba(0,0,0,.8);border:1px solid rgba(255,255,255,.15);border-radius:6px;padding:10px 12px;min-width:260px;'; panel.innerHTML = '
' + 'SyncPlay stats' + '' + '
' + - '
Not in a SyncPlay group.
'; + '
Not in a SyncPlay group.
' + + '' + + '
'; document.body.appendChild(panel); - var toast = document.createElement('div'); - toast.id = 'syncPlusToast'; - toast.style.cssText = 'position:fixed;bottom:90px;left:50%;transform:translateX(-50%);z-index:99999;display:none;font-family:sans-serif;font-size:13px;color:#eee;background:rgba(0,0,0,.8);border-radius:6px;padding:8px 16px;'; - document.body.appendChild(toast); - var toastHandle = null; - - function showToast(text) { - toast.textContent = text; - toast.style.display = 'block'; - if (toastHandle) { clearTimeout(toastHandle); } - toastHandle = setTimeout(function () { toast.style.display = 'none'; }, 4000); - } - var rows = panel.querySelector('#syncPlusRows'); + var result = panel.querySelector('#syncPlusResult'); var pollHandle = null; function closePanel() { @@ -521,24 +514,29 @@ public class SyncPlusStatsController : ControllerBase function togglePanel() { if (panel.style.display !== 'none') { closePanel(); return; } + result.textContent = ''; panel.style.display = 'block'; refresh(); pollHandle = setInterval(refresh, 1000); } - function syncMe() { + panel.querySelector('#syncPlusSyncBtn').addEventListener('click', function () { var t = token(); - if (!t) { showToast('Not logged in.'); return; } + if (!t) { result.textContent = 'Not logged in.'; return; } + result.textContent = 'Syncing...'; fetch('/SyncPlus/Stats/Sync', { method: 'POST', headers: { 'X-Emby-Token': t } }) .then(function (r) { return r.json(); }) - .then(function (d) { showToast(d.Message || 'Done.'); }) - .catch(function (e) { showToast('Sync error: ' + e.message); }); - } + .then(function (d) { result.textContent = d.Message || 'Done.'; }) + .catch(function (e) { result.textContent = 'Error: ' + e.message; }); + }); // The cog menu is an action sheet built fresh on every open, so watch for it appearing - // shortly after a click on the OSD settings button and append our items. The sheet's - // own delegated click handler closes it on any .actionSheetMenuItem click, so injected - // items get native close behavior without us touching its code. + // shortly after a click on the OSD settings button and append a single item. Just one + // item (both stats + the sync button live in our own panel it opens) keeps the sheet + // from growing tall enough to overflow off-screen -- jellyfin measures and positions + // the sheet before our async injection runs, so every added item risks pushing content + // past the viewport bottom. The sheet's own delegated click handler closes it on any + // .actionSheetMenuItem click, so our item gets native close behavior for free. var lastCogClick = 0; document.addEventListener('click', function (e) { if (e.target && e.target.closest && e.target.closest('.btnVideoOsdSettings')) { @@ -565,10 +563,9 @@ public class SyncPlusStatsController : ControllerBase var node = added[j]; if (!(node instanceof HTMLElement)) { continue; } var sheet = node.classList && node.classList.contains('actionSheet') ? node : node.querySelector && node.querySelector('.actionSheet'); - if (!sheet || sheet.querySelector('[data-id="syncplus-sync"]')) { continue; } + if (!sheet || sheet.querySelector('[data-id="syncplus-stats"]')) { continue; } var scroller = sheet.querySelector('.actionSheetScroller'); if (!scroller) { continue; } - scroller.appendChild(makeItem('syncplus-sync', 'Sync me to group', syncMe)); scroller.appendChild(makeItem('syncplus-stats', 'SyncPlay stats', togglePanel)); } }