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));
}
}