Collapse cog menu to a single item to stop the sheet overflowing
Release Plugin / release (push) Failing after 21s
Release Plugin / release (push) Failing after 21s
Two injected menu items made the settings action sheet tall enough to run off the bottom of the screen (jellyfin measures/positions the sheet before our async injection adds height). Now inject just one item, "SyncPlay stats", and move the sync button back into the panel it opens alongside the drift readout -- so the sheet barely grows and both actions live in our own overflow-proof UI. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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 =
|
||||
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">' +
|
||||
'<span style="color:#999;">SyncPlay stats</span>' +
|
||||
'<button id="syncPlusClose" style="background:none;border:none;color:#999;cursor:pointer;font-size:14px;padding:0 2px;">✕</button>' +
|
||||
'</div>' +
|
||||
'<div id="syncPlusRows">Not in a SyncPlay group.</div>';
|
||||
'<div id="syncPlusRows">Not in a SyncPlay group.</div>' +
|
||||
'<button id="syncPlusSyncBtn" style="margin-top:10px;background:#00a4dc;color:#fff;border:none;border-radius:3px;padding:6px 12px;cursor:pointer;font-size:12px;">Sync me to group</button>' +
|
||||
'<div id="syncPlusResult" style="margin-top:6px;color:#999;font-size:12px;min-height:1em;"></div>';
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user