mirror of
https://github.com/ranaldsgift/KefinTweaks.git
synced 2026-11-05 15:52:47 +01:00
Move to single global document observer where possible and relevant
This commit is contained in:
+28
-13
@@ -9,15 +9,11 @@
|
||||
|
||||
LOG('Script loaded - JS Injector mode');
|
||||
|
||||
// Step 1: watch for .backdropContainer to appear
|
||||
const waitForContainer = new MutationObserver(() => {
|
||||
const container = document.querySelector('.backdropContainer');
|
||||
if (!container) return;
|
||||
function attachPruneObserver(container) {
|
||||
if (!container || container.dataset.kefinBackdropPrune === 'true') return;
|
||||
container.dataset.kefinBackdropPrune = 'true';
|
||||
|
||||
LOG('Found .backdropContainer, attaching prune observer');
|
||||
waitForContainer.disconnect();
|
||||
|
||||
// Step 2: prune children as needed
|
||||
const pruneObserver = new MutationObserver(() => {
|
||||
LOG('Mutation detected, checking children count:', container.children.length);
|
||||
while (container.children.length > 2) {
|
||||
@@ -25,16 +21,35 @@
|
||||
container.removeChild(container.firstElementChild);
|
||||
}
|
||||
});
|
||||
|
||||
pruneObserver.observe(container, { childList: true });
|
||||
LOG('Prune observer is now active');
|
||||
});
|
||||
}
|
||||
|
||||
waitForContainer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
function startWait() {
|
||||
const utils = window.KefinTweaksUtils;
|
||||
const existing = document.querySelector('.backdropContainer');
|
||||
if (existing) {
|
||||
attachPruneObserver(existing);
|
||||
return;
|
||||
}
|
||||
|
||||
if (utils?.waitForElement) {
|
||||
utils.waitForElement('.backdropContainer', { timeout: 60000, quiet: true }).then((container) => {
|
||||
if (container) attachPruneObserver(container);
|
||||
else WARN_MISSING();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback if utils not ready yet
|
||||
setTimeout(startWait, 500);
|
||||
}
|
||||
|
||||
function WARN_MISSING() {
|
||||
console.warn('[KefinTweaks BackdropLeakFix] .backdropContainer not found within timeout');
|
||||
}
|
||||
|
||||
startWait();
|
||||
LOG('Waiting for .backdropContainer to appear...');
|
||||
LOG('Backdrop leak fix functionality initialized');
|
||||
})();
|
||||
|
||||
@@ -6199,13 +6199,9 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof MutationObserver === 'undefined' || !document.body) {
|
||||
console.log('[KefinTweaks Configuration] Cannot observe #app-user-menu (MutationObserver/body unavailable)');
|
||||
return;
|
||||
}
|
||||
|
||||
const utils = window.KefinTweaksUtils;
|
||||
let scheduled = false;
|
||||
appUserMenuConfigObserver = new MutationObserver(() => {
|
||||
const onMutations = () => {
|
||||
if (document.querySelector('[data-kefintweaks-user-menu-config-button]')) {
|
||||
return;
|
||||
}
|
||||
@@ -6215,8 +6211,19 @@
|
||||
scheduled = false;
|
||||
addConfigButtonToAppUserMenu();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
if (utils?.onBodyMutation) {
|
||||
appUserMenuConfigObserver = utils.onBodyMutation('config-appUserMenu', onMutations);
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof MutationObserver === 'undefined' || !document.body) {
|
||||
console.log('[KefinTweaks Configuration] Cannot observe #app-user-menu (MutationObserver/body unavailable)');
|
||||
return;
|
||||
}
|
||||
|
||||
appUserMenuConfigObserver = new MutationObserver(onMutations);
|
||||
appUserMenuConfigObserver.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
|
||||
@@ -4,34 +4,64 @@
|
||||
// This script sends you to home.html in these scenarios instead of leaving the app
|
||||
|
||||
(function () {
|
||||
function attachHandler() {
|
||||
const btn = document.querySelector('button.MuiButtonBase-root');
|
||||
if (!btn) return;
|
||||
'use strict';
|
||||
|
||||
if (btn._backCheckAttached) return;
|
||||
function attachHandler(btn) {
|
||||
if (!btn || btn._backCheckAttached) return true;
|
||||
btn._backCheckAttached = true;
|
||||
|
||||
btn.addEventListener('click', function (e) {
|
||||
if (window.history.state?.idx === 0) {
|
||||
// Stop normal handlers
|
||||
e.stopImmediatePropagation();
|
||||
e.preventDefault();
|
||||
|
||||
console.log("[KefinTweaks DashboardButtonFix]: No history to go back. Redirecting to home page.");
|
||||
console.log('[KefinTweaks DashboardButtonFix]: No history to go back. Redirecting to home page.');
|
||||
|
||||
// Changes this to the page you'd like to return to by default
|
||||
const homeUrl = ApiClient.serverVersion().split('.')[1] > 10 ? 'home' : 'home.html';
|
||||
Dashboard.navigate(`/${homeUrl}`);
|
||||
}
|
||||
}, true);
|
||||
|
||||
observer.disconnect();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Observe DOM in case button is recreated
|
||||
const observer = new MutationObserver(attachHandler);
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
function tryAttach() {
|
||||
const btn = document.querySelector('button.MuiButtonBase-root');
|
||||
return attachHandler(btn);
|
||||
}
|
||||
|
||||
// Initial run
|
||||
attachHandler();
|
||||
function armForDashboard() {
|
||||
if (tryAttach()) return;
|
||||
|
||||
const utils = window.KefinTweaksUtils;
|
||||
if (utils?.waitForElement) {
|
||||
utils.waitForElement('button.MuiButtonBase-root', {
|
||||
timeout: 15000,
|
||||
quiet: true,
|
||||
predicate: (el) => !el._backCheckAttached
|
||||
}).then((btn) => {
|
||||
if (btn) attachHandler(btn);
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (utils?.pollUntil) {
|
||||
utils.pollUntil(() => (tryAttach() ? true : null), { intervalMs: 200, timeoutMs: 15000 });
|
||||
return;
|
||||
}
|
||||
setTimeout(armForDashboard, 500);
|
||||
}
|
||||
|
||||
function start() {
|
||||
const utils = window.KefinTweaksUtils;
|
||||
if (!utils?.onViewPage) {
|
||||
setTimeout(start, 500);
|
||||
return;
|
||||
}
|
||||
utils.onViewPage(() => {
|
||||
armForDashboard();
|
||||
}, { pages: ['dashboard', 'configurationpage'] });
|
||||
// Also try immediately (direct load)
|
||||
armForDashboard();
|
||||
}
|
||||
|
||||
start();
|
||||
})();
|
||||
|
||||
@@ -30,29 +30,70 @@
|
||||
const config = window.KefinTweaksConfig?.exclusiveElsewhere || {
|
||||
hideServerName: false // Default fallback
|
||||
};
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
|
||||
let pollTimer = null;
|
||||
|
||||
function stopPoll() {
|
||||
if (pollTimer) {
|
||||
clearInterval(pollTimer);
|
||||
pollTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function tryApplyExclusive() {
|
||||
const elsewhereContainer = document.querySelector('.itemDetailPage:not(.hide) .streaming-lookup-container>div');
|
||||
if (elsewhereContainer && elsewhereContainer.children && elsewhereContainer.children.length > 2) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
const serverName = config.hideServerName ? '' : ApiClient.serverName();
|
||||
|
||||
const serverName = config.hideServerName ? '' : (typeof ApiClient !== 'undefined' ? ApiClient.serverName() : '');
|
||||
const link = document.querySelector('.itemDetailPage:not(.hide) .streaming-lookup-container>div>div:first-child a');
|
||||
if (link && !link.classList.contains('exclusive')) {
|
||||
LOG('Modifying elsewhere link to show exclusive branding');
|
||||
link.innerHTML = `Only available on ${serverName}`;
|
||||
link.classList.add("exclusive");
|
||||
link.title = "Exclusive";
|
||||
link.classList.add('exclusive');
|
||||
link.title = 'Exclusive';
|
||||
link.disable = true;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
|
||||
LOG('Exclusive elsewhere functionality initialized');
|
||||
})();
|
||||
function armItemDetailPoll() {
|
||||
stopPoll();
|
||||
if (tryApplyExclusive()) return;
|
||||
|
||||
const started = Date.now();
|
||||
const TTL_MS = 15000;
|
||||
pollTimer = setInterval(() => {
|
||||
if (tryApplyExclusive() || Date.now() - started >= TTL_MS) {
|
||||
stopPoll();
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
function start() {
|
||||
const utils = window.KefinTweaksUtils;
|
||||
if (!utils?.onViewPage) {
|
||||
setTimeout(start, 500);
|
||||
return;
|
||||
}
|
||||
|
||||
utils.onViewPage((view) => {
|
||||
stopPoll();
|
||||
// item / details views
|
||||
if (view === 'item' || view === 'details' || /item\.html|details/i.test(window.location.hash || '')) {
|
||||
armItemDetailPoll();
|
||||
}
|
||||
});
|
||||
|
||||
// Direct load on item page
|
||||
if (document.querySelector('.itemDetailPage:not(.hide)')) {
|
||||
armItemDetailPoll();
|
||||
}
|
||||
|
||||
LOG('Exclusive elsewhere functionality initialized (onViewPage + TTL poll)');
|
||||
}
|
||||
|
||||
start();
|
||||
})();
|
||||
|
||||
+25
-18
@@ -701,30 +701,37 @@
|
||||
return isAppVersionAtLeast12();
|
||||
}
|
||||
|
||||
function ensureToolbarObserver() {
|
||||
if (toolbarObserver || typeof MutationObserver === 'undefined' || !document.body) return;
|
||||
let scheduled = false;
|
||||
toolbarObserver = new MutationObserver(() => {
|
||||
if (scheduled) return;
|
||||
scheduled = true;
|
||||
requestAnimationFrame(() => {
|
||||
scheduled = false;
|
||||
ensureHamburgerButton();
|
||||
});
|
||||
});
|
||||
toolbarObserver.observe(document.body, { childList: true, subtree: true });
|
||||
}
|
||||
|
||||
function start() {
|
||||
ensureStyles();
|
||||
function armToolbarPoll() {
|
||||
ensureHamburgerButton();
|
||||
ensureToolbarObserver();
|
||||
// Retry briefly for late toolbar mount
|
||||
let tries = 0;
|
||||
const poll = setInterval(() => {
|
||||
tries += 1;
|
||||
if (ensureHamburgerButton() || tries >= 40) clearInterval(poll);
|
||||
}, 250);
|
||||
}
|
||||
|
||||
function start() {
|
||||
ensureStyles();
|
||||
armToolbarPoll();
|
||||
|
||||
const utils = window.KefinTweaksUtils;
|
||||
if (utils?.onViewPage) {
|
||||
// Toolbar remounts across navigations — short poll per view, no permanent body MO
|
||||
utils.onViewPage(() => {
|
||||
armToolbarPoll();
|
||||
});
|
||||
} else if (utils?.onBodyMutation) {
|
||||
let scheduled = false;
|
||||
toolbarObserver = utils.onBodyMutation('hamburger-toolbar', () => {
|
||||
if (scheduled) return;
|
||||
scheduled = true;
|
||||
requestAnimationFrame(() => {
|
||||
scheduled = false;
|
||||
ensureHamburgerButton();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
LOG('Desktop hamburger menu initialized');
|
||||
}
|
||||
|
||||
|
||||
+60
-74
@@ -380,20 +380,16 @@
|
||||
}
|
||||
|
||||
const HOME_SECTIONS_SELECTOR = '.homePage:not(.hide) #homeTab.is-active .sections';
|
||||
let homeSectionsReadyObserver = null;
|
||||
let homeSectionsReadyWaitGeneration = 0;
|
||||
|
||||
function disconnectHomeSectionsReadyObserver() {
|
||||
if (!homeSectionsReadyObserver) return;
|
||||
homeSectionsReadyObserver.disconnect();
|
||||
homeSectionsReadyObserver = null;
|
||||
LOG('Disconnected home sections ready observer');
|
||||
// Cancels in-flight waitForElement/poll by bumping generation
|
||||
homeSectionsReadyWaitGeneration += 1;
|
||||
LOG('Cancelled home sections ready wait');
|
||||
}
|
||||
|
||||
function connectHomeSectionsReadyObserver() {
|
||||
if (homeSectionsReadyObserver) {
|
||||
LOG('Home sections ready observer already connected');
|
||||
return;
|
||||
}
|
||||
const generation = ++homeSectionsReadyWaitGeneration;
|
||||
|
||||
const existing = document.querySelector(HOME_SECTIONS_SELECTOR);
|
||||
if (existing) {
|
||||
@@ -402,68 +398,34 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof MutationObserver === 'undefined') {
|
||||
WARN('MutationObserver unavailable; cannot wait for home sections container');
|
||||
return;
|
||||
}
|
||||
|
||||
homeSectionsReadyObserver = new MutationObserver(() => {
|
||||
const onFound = () => {
|
||||
if (generation !== homeSectionsReadyWaitGeneration) return;
|
||||
if (!document.querySelector(HOME_SECTIONS_SELECTOR)) return;
|
||||
LOG('Home sections container appeared; enhancing');
|
||||
enqueueHomeViewJob({ type: 'home', hash: window.location.hash || '' });
|
||||
});
|
||||
homeSectionsReadyObserver.observe(document.documentElement, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
LOG('Connected home sections ready observer');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get section index N from a .sectionN element (e.g. section0 -> 0).
|
||||
* Returns -1 if no matching class.
|
||||
*/
|
||||
function getSectionIndexFromElement(sectionEl) {
|
||||
const sectionClass = Array.from(sectionEl.classList || []).find(c => /^section\d+$/.test(c));
|
||||
if (!sectionClass) return -1;
|
||||
const match = sectionClass.match(/^section(\d+)$/);
|
||||
return match ? parseInt(match[1], 10) : -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the section is ready to clone: either there are no .itemsContainer descendants,
|
||||
* or every .itemsContainer in the section has at least one child (items have been rendered).
|
||||
*/
|
||||
function sectionHasAllItemsContainersPopulated(sectionEl) {
|
||||
const itemsContainers = sectionEl.querySelectorAll ? sectionEl.querySelectorAll('.itemsContainer') : [];
|
||||
if (itemsContainers.length === 0) return false;
|
||||
return Array.from(itemsContainers).every(container => container.children.length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Observe a Jellyfin section node until every .itemsContainer in it has children, then clone and append to kefinHome once.
|
||||
* Observer is stored in sectionCache.pendingJellyfinSectionObservers so handleJellyfinRender can disconnect it on re-run.
|
||||
*/
|
||||
function observeSectionUntilChildrenThenClone(sectionEl, kefinHome) {
|
||||
if (!sectionEl || typeof MutationObserver === 'undefined') return;
|
||||
const observer = new MutationObserver(() => {
|
||||
if (!sectionHasAllItemsContainersPopulated(sectionEl)) return;
|
||||
const target = document.querySelector('.homePage:not(.hide) #homeTab .kefinHomeSectionsContainer');
|
||||
if (!target) {
|
||||
observer.disconnect();
|
||||
const idx = sectionCache.pendingJellyfinSectionObservers.indexOf(observer);
|
||||
if (idx !== -1) sectionCache.pendingJellyfinSectionObservers.splice(idx, 1);
|
||||
return;
|
||||
}
|
||||
const clone = sectionEl.cloneNode(true);
|
||||
clone.dataset.jellyfinSectionClone = 'true';
|
||||
target.appendChild(clone);
|
||||
observer.disconnect();
|
||||
const idx = sectionCache.pendingJellyfinSectionObservers.indexOf(observer);
|
||||
if (idx !== -1) sectionCache.pendingJellyfinSectionObservers.splice(idx, 1);
|
||||
});
|
||||
sectionCache.pendingJellyfinSectionObservers.push(observer);
|
||||
observer.observe(sectionEl, { childList: true, subtree: true });
|
||||
const utils = window.KefinTweaksUtils;
|
||||
if (utils?.waitForElement) {
|
||||
LOG('Waiting for home sections via waitForElement');
|
||||
utils.waitForElement(HOME_SECTIONS_SELECTOR, { timeout: 20000, quiet: true }).then((el) => {
|
||||
if (el) onFound();
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (utils?.pollUntil) {
|
||||
LOG('Waiting for home sections via pollUntil');
|
||||
utils.pollUntil(
|
||||
() => (generation === homeSectionsReadyWaitGeneration
|
||||
? document.querySelector(HOME_SECTIONS_SELECTOR)
|
||||
: true),
|
||||
{ intervalMs: 200, timeoutMs: 20000 }
|
||||
).then((el) => {
|
||||
if (el && el !== true) onFound();
|
||||
});
|
||||
return;
|
||||
}
|
||||
WARN('KefinTweaksUtils wait helpers unavailable; cannot wait for home sections container');
|
||||
}
|
||||
|
||||
function mulberry32(seed) {
|
||||
@@ -2177,15 +2139,36 @@
|
||||
syncCategoryRailTvFocus();
|
||||
}
|
||||
|
||||
function disconnectHomeChromeVisibilityObserver() {
|
||||
if (!homeChromeVisibilityObserver) return;
|
||||
homeChromeVisibilityObserver.disconnect();
|
||||
homeChromeVisibilityObserver = null;
|
||||
}
|
||||
|
||||
function ensureHomeChromeVisibilityObserver() {
|
||||
if (homeChromeVisibilityObserver) return;
|
||||
// Attributes+class needed for tab hide/show — dedicated MO, page-scoped when possible.
|
||||
// Disconnect off home so we do not keep a body-wide attributes+subtree watcher alive.
|
||||
if (!isHomePageVisible()) {
|
||||
disconnectHomeChromeVisibilityObserver();
|
||||
return;
|
||||
}
|
||||
|
||||
const homePage = document.querySelector('.homePage:not(.hide)') || document.querySelector('.homePage');
|
||||
const target = homePage || document.documentElement;
|
||||
const config = homePage
|
||||
? { childList: true, subtree: true, attributes: true, attributeFilter: ['class'] }
|
||||
: { attributes: true, attributeFilter: ['data-active-page', 'class'] };
|
||||
|
||||
if (homeChromeVisibilityObserver) {
|
||||
const sameTarget = homeChromeVisibilityObserver.__kefinTarget === target;
|
||||
const sameMode = homeChromeVisibilityObserver.__kefinNarrow === !homePage;
|
||||
if (sameTarget && sameMode) return;
|
||||
disconnectHomeChromeVisibilityObserver();
|
||||
}
|
||||
homeChromeVisibilityObserver = new MutationObserver(() => syncHomeScreenChromeVisibility());
|
||||
homeChromeVisibilityObserver.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: ['class']
|
||||
});
|
||||
homeChromeVisibilityObserver.__kefinTarget = target;
|
||||
homeChromeVisibilityObserver.__kefinNarrow = !homePage;
|
||||
homeChromeVisibilityObserver.observe(target, config);
|
||||
}
|
||||
|
||||
async function mountCreateSectionButton() {
|
||||
@@ -2810,6 +2793,7 @@
|
||||
|
||||
if (next.type === 'chrome') {
|
||||
syncHomeScreenChromeVisibility();
|
||||
ensureHomeChromeVisibilityObserver();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2822,6 +2806,7 @@
|
||||
|
||||
if (currentTabIndex !== 0) {
|
||||
syncHomeScreenChromeVisibility();
|
||||
ensureHomeChromeVisibilityObserver();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2830,6 +2815,7 @@
|
||||
});
|
||||
await enhanceHomeScreen();
|
||||
syncHomeScreenChromeVisibility();
|
||||
ensureHomeChromeVisibilityObserver();
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
|
||||
+26
-10
@@ -862,8 +862,9 @@
|
||||
|
||||
document.addEventListener('contextmenu', captureFromEvent, true);
|
||||
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach(mutation => {
|
||||
const onMutations = (mutations) => {
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type !== 'childList') continue;
|
||||
mutation.addedNodes.forEach(node => {
|
||||
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
||||
const sheets = node.matches?.('.actionSheetContent')
|
||||
@@ -871,9 +872,16 @@
|
||||
: [...(node.querySelectorAll?.('.actionSheetContent') || [])];
|
||||
sheets.forEach(injectPinActionSheetItem);
|
||||
});
|
||||
});
|
||||
});
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
}
|
||||
};
|
||||
|
||||
const utils = window.KefinTweaksUtils;
|
||||
if (utils?.onBodyMutation) {
|
||||
utils.onBodyMutation('homeScreenPin-actionSheet', onMutations);
|
||||
} else if (typeof MutationObserver !== 'undefined' && document.body) {
|
||||
const observer = new MutationObserver(onMutations);
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
}
|
||||
}
|
||||
|
||||
function isPinnedSectionId(sectionId) {
|
||||
@@ -1245,8 +1253,9 @@
|
||||
document.querySelectorAll('.cardOverlayContainer').forEach(processOverlay);
|
||||
document.querySelectorAll('[data-section-id^="pinned-list-"] .card[data-id]').forEach(processCard);
|
||||
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach(mutation => {
|
||||
const onMutations = (mutations) => {
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type !== 'childList') continue;
|
||||
mutation.addedNodes.forEach(node => {
|
||||
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
||||
if (node.classList?.contains('cardOverlayContainer')) processOverlay(node);
|
||||
@@ -1254,9 +1263,16 @@
|
||||
if (node.classList?.contains('card')) processCard(node);
|
||||
node.querySelectorAll?.('.card[data-id]').forEach(processCard);
|
||||
});
|
||||
});
|
||||
});
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
}
|
||||
};
|
||||
|
||||
const utils = window.KefinTweaksUtils;
|
||||
if (utils?.onBodyMutation) {
|
||||
utils.onBodyMutation('homeScreenPin-pinnedCards', onMutations);
|
||||
} else if (typeof MutationObserver !== 'undefined' && document.body) {
|
||||
const observer = new MutationObserver(onMutations);
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
}
|
||||
}
|
||||
|
||||
function isPinnedParentSection(sectionId) {
|
||||
|
||||
+36
-11
@@ -787,21 +787,46 @@
|
||||
}
|
||||
|
||||
// Scroll state loading removed - not needed
|
||||
|
||||
// Wait for page to mount
|
||||
const observer = new MutationObserver(() => {
|
||||
|
||||
function tryInitializeFromView() {
|
||||
if (shouldInitialize()) {
|
||||
initialize().catch(err => ERR('Initialization failed:', err));
|
||||
return true;
|
||||
}
|
||||
});
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
|
||||
// Also try to initialize immediately if page is already loaded
|
||||
if (document.readyState === 'complete') {
|
||||
setTimeout(() => {
|
||||
initialize().catch(err => ERR('Initial initialization failed:', err));
|
||||
}, 100);
|
||||
return false;
|
||||
}
|
||||
|
||||
function armLibraryPageInit() {
|
||||
if (tryInitializeFromView()) return;
|
||||
const utils = window.KefinTweaksUtils;
|
||||
if (utils?.pollUntil) {
|
||||
utils.pollUntil(() => (tryInitializeFromView() ? true : null), {
|
||||
intervalMs: 200,
|
||||
timeoutMs: 15000
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Fallback brief poll
|
||||
let tries = 0;
|
||||
const timer = setInterval(() => {
|
||||
tries += 1;
|
||||
if (tryInitializeFromView() || tries >= 75) clearInterval(timer);
|
||||
}, 200);
|
||||
}
|
||||
|
||||
function startInfiniteScrollHook() {
|
||||
const utils = window.KefinTweaksUtils;
|
||||
if (!utils?.onViewPage) {
|
||||
setTimeout(startInfiniteScrollHook, 500);
|
||||
return;
|
||||
}
|
||||
utils.onViewPage(() => {
|
||||
armLibraryPageInit();
|
||||
}, { pages: SUPPORTED_PAGES });
|
||||
armLibraryPageInit();
|
||||
}
|
||||
|
||||
startInfiniteScrollHook();
|
||||
|
||||
// Cleanup function
|
||||
function cleanup() {
|
||||
|
||||
+20
-22
@@ -1242,30 +1242,28 @@
|
||||
}
|
||||
|
||||
function setupPlaylistCardPlayObserver() {
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.type !== 'childList') return;
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
||||
const processNode = (node) => {
|
||||
if (!node || node.nodeType !== Node.ELEMENT_NODE) return;
|
||||
if (node.classList?.contains('cardOverlayContainer')) {
|
||||
overridePlaylistCardPlayButton(node);
|
||||
}
|
||||
node.querySelectorAll?.('.cardOverlayContainer').forEach(overridePlaylistCardPlayButton);
|
||||
};
|
||||
|
||||
if (node.classList && node.classList.contains('cardOverlayContainer')) {
|
||||
overridePlaylistCardPlayButton(node);
|
||||
}
|
||||
const onMutations = (mutations) => {
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type !== 'childList') continue;
|
||||
mutation.addedNodes.forEach(processNode);
|
||||
}
|
||||
};
|
||||
|
||||
const overlayContainers = node.querySelectorAll && node.querySelectorAll('.cardOverlayContainer');
|
||||
if (overlayContainers && overlayContainers.length > 0) {
|
||||
overlayContainers.forEach((overlayContainer) => {
|
||||
overridePlaylistCardPlayButton(overlayContainer);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
const utils = window.KefinTweaksUtils;
|
||||
if (utils?.onBodyMutation) {
|
||||
utils.onBodyMutation('playlist-cardPlay', onMutations);
|
||||
} else if (typeof MutationObserver !== 'undefined' && document.body) {
|
||||
const observer = new MutationObserver(onMutations);
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
}
|
||||
|
||||
processExistingPlaylistCardOverlays();
|
||||
}
|
||||
|
||||
+35
-38
@@ -237,50 +237,47 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Set up MutationObserver to watch for new overlay containers
|
||||
// Shared body observer: inject when card overlays appear
|
||||
function setupRemoveContinueButtonObserver() {
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.type === 'childList') {
|
||||
// Check for added nodes
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
// Check if the added node is an overlay container
|
||||
if (node.classList && node.classList.contains('cardOverlayContainer')) {
|
||||
const buttonContainer = node.querySelector('.cardOverlayButton-br');
|
||||
if (buttonContainer) {
|
||||
addRemoveContinueButton(node);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for overlay containers within the added node
|
||||
const overlayContainers = node.querySelectorAll && node.querySelectorAll('.cardOverlayContainer');
|
||||
if (overlayContainers && overlayContainers.length > 0) {
|
||||
overlayContainers.forEach((overlayContainer) => {
|
||||
const buttonContainer = overlayContainer.querySelector('.cardOverlayButton-br');
|
||||
if (buttonContainer) {
|
||||
addRemoveContinueButton(overlayContainer);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
const processNode = (node) => {
|
||||
if (!node || node.nodeType !== Node.ELEMENT_NODE) return;
|
||||
if (node.classList?.contains('cardOverlayContainer')) {
|
||||
if (node.querySelector('.cardOverlayButton-br')) addRemoveContinueButton(node);
|
||||
}
|
||||
node.querySelectorAll?.('.cardOverlayContainer').forEach((overlayContainer) => {
|
||||
if (overlayContainer.querySelector('.cardOverlayButton-br')) {
|
||||
addRemoveContinueButton(overlayContainer);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Start observing
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
|
||||
// Process any existing overlay containers
|
||||
};
|
||||
|
||||
const onMutations = (mutations) => {
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type !== 'childList') continue;
|
||||
mutation.addedNodes.forEach(processNode);
|
||||
}
|
||||
};
|
||||
|
||||
const utils = window.KefinTweaksUtils;
|
||||
if (utils?.onBodyMutation) {
|
||||
utils.onBodyMutation('removeContinue-overlays', onMutations);
|
||||
} else if (typeof MutationObserver !== 'undefined' && document.body) {
|
||||
const observer = new MutationObserver(onMutations);
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
}
|
||||
|
||||
processExistingOverlayContainers();
|
||||
}
|
||||
|
||||
// Initialize the observer
|
||||
setupRemoveContinueButtonObserver();
|
||||
function startRemoveContinue() {
|
||||
if (!window.KefinTweaksUtils?.onBodyMutation && !document.body) {
|
||||
setTimeout(startRemoveContinue, 500);
|
||||
return;
|
||||
}
|
||||
setupRemoveContinueButtonObserver();
|
||||
}
|
||||
|
||||
startRemoveContinue();
|
||||
|
||||
// Debug function for troubleshooting (available in console)
|
||||
window.debugRemoveContinueButtons = function() {
|
||||
|
||||
@@ -1341,7 +1341,11 @@ window.KefinTweaksConfig = ${JSON.stringify(configToBackup, null, 2)};`;
|
||||
dialog.style.position = 'absolute';
|
||||
dialog.style.minWidth = '280px';
|
||||
dialog.style.pointerEvents = 'auto';
|
||||
dialog.style.animation = '160ms ease-out 0s 1 normal both running scaleup';
|
||||
if (typeof window.ModalSystem?.applyOpenAnimation === 'function') {
|
||||
window.ModalSystem.applyOpenAnimation(dialog);
|
||||
} else {
|
||||
dialog.style.animation = '160ms ease-out 0s 1 normal both running scaleup';
|
||||
}
|
||||
|
||||
// Create dialog content
|
||||
const dialogContent = document.createElement('div');
|
||||
|
||||
+418
-20
@@ -1711,12 +1711,15 @@
|
||||
|
||||
function disconnectCustomMenuLinkObserver() {
|
||||
if (!customMenuLinkObserver) return;
|
||||
customMenuLinkObserver.disconnect();
|
||||
if (typeof customMenuLinkObserver.disconnect === 'function') {
|
||||
customMenuLinkObserver.disconnect();
|
||||
}
|
||||
offBodyMutation('customMenuLinks');
|
||||
customMenuLinkObserver = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persistent MO: any custom-menu host remount → idempotent reapplyAll.
|
||||
* Shared body subscriber: any custom-menu host remount → idempotent reapplyAll.
|
||||
* No host is assumed stable (drawer, prefs, #app-user-menu, top-nav).
|
||||
*/
|
||||
function ensureCustomMenuLinkObserver() {
|
||||
@@ -1724,7 +1727,7 @@
|
||||
disconnectCustomMenuLinkObserver();
|
||||
return;
|
||||
}
|
||||
if (customMenuLinkObserver || typeof MutationObserver === 'undefined' || !document.body) {
|
||||
if (customMenuLinkObserver) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1737,7 +1740,7 @@
|
||||
reapplyAllCustomMenuLinks();
|
||||
});
|
||||
};
|
||||
customMenuLinkObserver = new MutationObserver((mutations) => {
|
||||
const onMutations = (mutations) => {
|
||||
if (customMenuLinkReapplyDepth > 0) return;
|
||||
if (!customMenuLinkRegistry.length) {
|
||||
disconnectCustomMenuLinkObserver();
|
||||
@@ -1750,12 +1753,9 @@
|
||||
}
|
||||
if (!mutations.some(mutationShouldTriggerCustomMenuReapply)) return;
|
||||
scheduleCustomMenuReapply();
|
||||
});
|
||||
};
|
||||
|
||||
customMenuLinkObserver.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
customMenuLinkObserver = onBodyMutation('customMenuLinks', onMutations);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3051,6 +3051,7 @@ window.KefinTweaksConfig = ${JSON.stringify(config, null, 2)};`;
|
||||
customPageTitleGuardObserver.disconnect();
|
||||
customPageTitleGuardObserver = null;
|
||||
}
|
||||
offBodyMutation('customPageTitle-discover');
|
||||
activeCustomPageTitle = null;
|
||||
}
|
||||
|
||||
@@ -3087,21 +3088,29 @@ window.KefinTweaksConfig = ${JSON.stringify(config, null, 2)};`;
|
||||
subtree: true
|
||||
});
|
||||
|
||||
// childList only: Jellyfin typically replaces pageTitle text via textContent (childList)
|
||||
const root = document.getElementById('reactRoot') || document.body || document.documentElement;
|
||||
if (root) {
|
||||
customPageTitleGuardObserver.observe(root, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll('h3.pageTitle').forEach((el) => {
|
||||
// Narrow watch: page title nodes only (no body/reactRoot subtree)
|
||||
const observePageTitleEl = (el) => {
|
||||
if (!el || el.dataset?.kefinTitleGuardObserved === 'true') return;
|
||||
if (el.dataset) el.dataset.kefinTitleGuardObserved = 'true';
|
||||
customPageTitleGuardObserver.observe(el, {
|
||||
childList: true,
|
||||
characterData: true,
|
||||
subtree: true
|
||||
});
|
||||
};
|
||||
|
||||
document.querySelectorAll('h3.pageTitle').forEach(observePageTitleEl);
|
||||
|
||||
// When new h3.pageTitle nodes mount, attach narrow observers via shared body MO
|
||||
onBodyMutation('customPageTitle-discover', (mutations) => {
|
||||
for (const mutation of mutations) {
|
||||
for (const node of mutation.addedNodes) {
|
||||
if (node.nodeType !== Node.ELEMENT_NODE) continue;
|
||||
if (node.matches?.('h3.pageTitle')) observePageTitleEl(node);
|
||||
node.querySelectorAll?.('h3.pageTitle').forEach(observePageTitleEl);
|
||||
}
|
||||
}
|
||||
syncIfNeeded();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3313,6 +3322,384 @@ window.KefinTweaksConfig = ${JSON.stringify(config, null, 2)};`;
|
||||
};
|
||||
}
|
||||
|
||||
// ── Shared DOM observation (body MO multiplexer) ─────────────────────────
|
||||
// Inventory / migration waves (body + childList + subtree unless noted):
|
||||
// Wave A (oneshot / onViewPage+TTL): infiniteScroll, dashboardButtonFix,
|
||||
// backdropLeakFix wait, homeSectionsReadyObserver, exclusiveElsewhere
|
||||
// Wave B (shared body — overlays): removeContinue, homeScreenPin×2, playlist card play
|
||||
// Wave C (shared body — chrome): customMenuLinkObserver, hamburger toolbar, config user-menu
|
||||
// Wave D (scoped / attributes): watchlist empty (page-scoped attrs), discovery→shared,
|
||||
// slides wait→waitForElement, homeChromeVisibility (home-rooted + off-home disconnect),
|
||||
// pageTitleGuard (title + h3.pageTitle only; discover via shared)
|
||||
// Extra: updoot SPA reinit → onViewPage (was body childList+subtree)
|
||||
// Prefer onViewPage + waitForElement/poll for one-shots; shared body only for ongoing inject.
|
||||
// Measure: KefinTweaksUtils.debugBodyObservers() — expect one shared body MO + listed subscribers.
|
||||
|
||||
const bodySubscribers = new Map();
|
||||
/** @type {MutationObserver|null} */
|
||||
let bodyObserver = null;
|
||||
/** @type {Map<string, { disconnect: Function }>} */
|
||||
const managedObservers = new Map();
|
||||
/** @type {MutationRecord[]} */
|
||||
let pendingBodyMutations = [];
|
||||
let bodyFlushScheduled = false;
|
||||
let bodyFlushGeneration = 0;
|
||||
/** @type {Function[]} */
|
||||
let paintQueue = [];
|
||||
let paintScheduled = false;
|
||||
let waitForElementSeq = 0;
|
||||
|
||||
const PLAYER_NOISE_SELECTOR =
|
||||
'.osdPositionText, .osdDurationText, .startTimeText, .endTimeText, .endsAtText, .videoSubtitlesInner, .videoSecondarySubtitlesInner';
|
||||
|
||||
function runPaintQueue() {
|
||||
paintScheduled = false;
|
||||
const batch = paintQueue;
|
||||
paintQueue = [];
|
||||
for (const fn of batch) {
|
||||
try {
|
||||
fn();
|
||||
} catch (err) {
|
||||
ERR('Error in afterNextPaint callback:', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run fn after the next paint (shared rAF + timeout fallback for hidden tabs).
|
||||
* @param {Function} fn
|
||||
*/
|
||||
function afterNextPaint(fn) {
|
||||
paintQueue.push(fn);
|
||||
if (paintScheduled) return;
|
||||
paintScheduled = true;
|
||||
let done = false;
|
||||
const run = () => {
|
||||
if (done) return;
|
||||
done = true;
|
||||
clearTimeout(fallback);
|
||||
runPaintQueue();
|
||||
};
|
||||
const fallback = setTimeout(run, 250);
|
||||
requestAnimationFrame(() => setTimeout(run, 0));
|
||||
}
|
||||
|
||||
function isStructuralChange(record) {
|
||||
if (record.addedNodes.length === 0 && record.removedNodes.length === 0) return false;
|
||||
const target = record.target;
|
||||
return !(target.nodeType === 1 && /** @type {Element} */ (target).closest(PLAYER_NOISE_SELECTOR));
|
||||
}
|
||||
|
||||
function flushPendingBodyMutations() {
|
||||
bodyFlushScheduled = false;
|
||||
const batch = pendingBodyMutations;
|
||||
pendingBodyMutations = [];
|
||||
if (batch.length === 0) return;
|
||||
for (const [id, sub] of Array.from(bodySubscribers)) {
|
||||
if (bodySubscribers.get(id) !== sub) continue;
|
||||
if (sub.priority > 0) continue;
|
||||
try {
|
||||
sub.callback(batch);
|
||||
} catch (err) {
|
||||
ERR(`Error in body observer subscriber "${id}":`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resortBodySubscribers() {
|
||||
const sorted = [...bodySubscribers.entries()].sort((a, b) => b[1].priority - a[1].priority);
|
||||
bodySubscribers.clear();
|
||||
for (const [id, sub] of sorted) {
|
||||
bodySubscribers.set(id, sub);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureBodyObserver() {
|
||||
if (bodyObserver || typeof MutationObserver === 'undefined' || !document.body) return;
|
||||
bodyObserver = new MutationObserver((mutations) => {
|
||||
let hasStructuralChange = false;
|
||||
for (let i = 0; i < mutations.length; i++) {
|
||||
if (isStructuralChange(mutations[i])) {
|
||||
hasStructuralChange = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hasStructuralChange) return;
|
||||
|
||||
for (const [id, sub] of Array.from(bodySubscribers)) {
|
||||
if (bodySubscribers.get(id) !== sub) continue;
|
||||
if (sub.priority <= 0) break;
|
||||
try {
|
||||
sub.callback(mutations);
|
||||
} catch (err) {
|
||||
ERR(`Error in body observer subscriber "${id}":`, err);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < mutations.length; i++) pendingBodyMutations.push(mutations[i]);
|
||||
if (!bodyFlushScheduled) {
|
||||
bodyFlushScheduled = true;
|
||||
const generation = bodyFlushGeneration;
|
||||
afterNextPaint(() => {
|
||||
if (generation === bodyFlushGeneration) flushPendingBodyMutations();
|
||||
});
|
||||
}
|
||||
});
|
||||
bodyObserver.observe(document.body, { childList: true, subtree: true });
|
||||
LOG('Shared body observer started');
|
||||
}
|
||||
|
||||
function stopBodyObserverIfEmpty() {
|
||||
if (bodyObserver && bodySubscribers.size === 0) {
|
||||
bodyObserver.disconnect();
|
||||
bodyObserver = null;
|
||||
bodyFlushGeneration++;
|
||||
bodyFlushScheduled = false;
|
||||
pendingBodyMutations = [];
|
||||
LOG('Shared body observer stopped (no subscribers)');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a callback on the shared document.body MutationObserver.
|
||||
* @param {string} id
|
||||
* @param {Function} callback - (mutations: MutationRecord[]) => void
|
||||
* @param {{ priority?: number }} [options]
|
||||
* @returns {{ unsubscribe: Function, disconnect: Function }}
|
||||
*/
|
||||
function onBodyMutation(id, callback, options = {}) {
|
||||
if (typeof callback !== 'function') {
|
||||
ERR('onBodyMutation callback must be a function');
|
||||
return { unsubscribe() {}, disconnect() {} };
|
||||
}
|
||||
const priority = typeof options.priority === 'number' ? options.priority : 0;
|
||||
if (bodySubscribers.has(id)) {
|
||||
WARN(`Replacing body observer subscriber: ${id}`);
|
||||
}
|
||||
const subscription = { callback, priority };
|
||||
bodySubscribers.set(id, subscription);
|
||||
resortBodySubscribers();
|
||||
ensureBodyObserver();
|
||||
const cleanup = () => {
|
||||
if (bodySubscribers.get(id) !== subscription) return;
|
||||
bodySubscribers.delete(id);
|
||||
stopBodyObserverIfEmpty();
|
||||
};
|
||||
return { unsubscribe: cleanup, disconnect: cleanup };
|
||||
}
|
||||
|
||||
function offBodyMutation(id) {
|
||||
const removed = bodySubscribers.delete(id);
|
||||
if (removed) stopBodyObserverIfEmpty();
|
||||
return removed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a managed observer. Body + childList+subtree (no attributes/characterData)
|
||||
* is routed to the shared multiplexer.
|
||||
* @param {string} id
|
||||
* @param {MutationCallback} callback
|
||||
* @param {Node} target
|
||||
* @param {MutationObserverInit} config
|
||||
* @returns {{ disconnect: Function, unsubscribe: Function, observe: Function, takeRecords: Function }}
|
||||
*/
|
||||
function createManagedObserver(id, callback, target, config) {
|
||||
if (managedObservers.has(id)) {
|
||||
managedObservers.get(id).disconnect();
|
||||
managedObservers.delete(id);
|
||||
}
|
||||
|
||||
const isBodyTarget =
|
||||
target === document.body || target === document.documentElement || target === document;
|
||||
const isSubtreeWatch = !!(config && config.childList && config.subtree);
|
||||
const isSimpleChildList =
|
||||
isSubtreeWatch && !config.attributes && !config.attributeFilter && !config.characterData;
|
||||
|
||||
if (isBodyTarget && isSimpleChildList) {
|
||||
const handle = onBodyMutation(id, callback);
|
||||
const cleanup = () => {
|
||||
handle.disconnect();
|
||||
managedObservers.delete(id);
|
||||
};
|
||||
const proxy = {
|
||||
disconnect: cleanup,
|
||||
unsubscribe: cleanup,
|
||||
observe() {},
|
||||
takeRecords() {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
managedObservers.set(id, proxy);
|
||||
return proxy;
|
||||
}
|
||||
|
||||
if (typeof MutationObserver === 'undefined' || !target) {
|
||||
const noop = { disconnect() {}, unsubscribe() {}, observe() {}, takeRecords() { return []; } };
|
||||
managedObservers.set(id, noop);
|
||||
return noop;
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(callback);
|
||||
observer.observe(target, config);
|
||||
const wrap = {
|
||||
disconnect() {
|
||||
observer.disconnect();
|
||||
managedObservers.delete(id);
|
||||
},
|
||||
unsubscribe() {
|
||||
wrap.disconnect();
|
||||
},
|
||||
observe(t, c) {
|
||||
observer.observe(t, c);
|
||||
},
|
||||
takeRecords() {
|
||||
return observer.takeRecords();
|
||||
}
|
||||
};
|
||||
managedObservers.set(id, wrap);
|
||||
return wrap;
|
||||
}
|
||||
|
||||
function disconnectManagedObserver(id) {
|
||||
if (bodySubscribers.has(id)) {
|
||||
offBodyMutation(id);
|
||||
managedObservers.delete(id);
|
||||
return true;
|
||||
}
|
||||
if (managedObservers.has(id)) {
|
||||
managedObservers.get(id).disconnect();
|
||||
managedObservers.delete(id);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for an element (uses shared body observer for re-checks).
|
||||
* @param {string} selector
|
||||
* @param {number|{ timeout?: number, root?: ParentNode, predicate?: (el: Element) => boolean, quiet?: boolean }} [options]
|
||||
* @returns {Promise<Element|null>}
|
||||
*/
|
||||
function waitForElement(selector, options = {}) {
|
||||
const opts = typeof options === 'number' ? { timeout: options } : options || {};
|
||||
const timeout = opts.timeout ?? 10000;
|
||||
const root = opts.root || document;
|
||||
const predicate = typeof opts.predicate === 'function' ? opts.predicate : null;
|
||||
|
||||
const find = () => {
|
||||
if (!predicate) return root.querySelector(selector);
|
||||
for (const el of Array.from(root.querySelectorAll(selector))) {
|
||||
try {
|
||||
if (predicate(el)) return el;
|
||||
} catch (_) {
|
||||
/* treat throw as non-match */
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const existing = find();
|
||||
if (existing) {
|
||||
resolve(existing);
|
||||
return;
|
||||
}
|
||||
|
||||
const observerId = `wait-${selector}-${++waitForElementSeq}`;
|
||||
let timeoutId = null;
|
||||
const handle = onBodyMutation(observerId, () => {
|
||||
const element = find();
|
||||
if (element) {
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
handle.unsubscribe();
|
||||
resolve(element);
|
||||
}
|
||||
});
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
handle.unsubscribe();
|
||||
if (!opts.quiet) {
|
||||
WARN(`Timeout waiting for element: ${selector}`);
|
||||
}
|
||||
resolve(null);
|
||||
}, timeout);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll until fn returns truthy or timeout. Useful when onViewPage fires before DOM settles.
|
||||
* @param {() => *} fn
|
||||
* @param {{ intervalMs?: number, timeoutMs?: number }} [options]
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
function pollUntil(fn, options = {}) {
|
||||
const intervalMs = options.intervalMs ?? 100;
|
||||
const timeoutMs = options.timeoutMs ?? 10000;
|
||||
return new Promise((resolve) => {
|
||||
const start = Date.now();
|
||||
const tick = () => {
|
||||
try {
|
||||
const value = fn();
|
||||
if (value) {
|
||||
resolve(value);
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
ERR('pollUntil callback error:', err);
|
||||
}
|
||||
if (Date.now() - start >= timeoutMs) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
setTimeout(tick, intervalMs);
|
||||
};
|
||||
tick();
|
||||
});
|
||||
}
|
||||
|
||||
function getBodySubscriberCount() {
|
||||
return bodySubscribers.size;
|
||||
}
|
||||
|
||||
function listBodySubscriberIds() {
|
||||
return Array.from(bodySubscribers.keys());
|
||||
}
|
||||
|
||||
function getManagedObserverCount() {
|
||||
return managedObservers.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug: log shared-body subscriber ids + managed dedicated observer count.
|
||||
* Steady-state (utils loaded, overlays/chrome subscribed) should show one live
|
||||
* body childList+subtree MutationObserver with these typical ids:
|
||||
* customMenuLinks, removeContinue-overlays, homeScreenPin-actionSheet,
|
||||
* homeScreenPin-pinnedCards, playlist-cardPlay, config-appUserMenu,
|
||||
* watchlist-overlay-discovery, (+ customPageTitle-discover when a custom page is active)
|
||||
*/
|
||||
function debugBodyObservers() {
|
||||
const ids = listBodySubscriberIds();
|
||||
LOG('Shared body MO active:', !!bodyObserver, '| subscribers:', ids.length, ids);
|
||||
LOG('Managed dedicated observers:', getManagedObserverCount());
|
||||
return { bodyObserverActive: !!bodyObserver, subscribers: ids, managed: getManagedObserverCount() };
|
||||
}
|
||||
|
||||
window.addEventListener('beforeunload', () => {
|
||||
managedObservers.forEach((obs) => {
|
||||
try {
|
||||
obs.disconnect();
|
||||
} catch (_) {}
|
||||
});
|
||||
managedObservers.clear();
|
||||
bodySubscribers.clear();
|
||||
if (bodyObserver) {
|
||||
bodyObserver.disconnect();
|
||||
bodyObserver = null;
|
||||
}
|
||||
bodyFlushGeneration++;
|
||||
pendingBodyMutations = [];
|
||||
});
|
||||
|
||||
// Expose utilities to global scope
|
||||
window.KefinTweaksUtils = {
|
||||
onViewPage,
|
||||
@@ -3342,7 +3729,18 @@ window.KefinTweaksConfig = ${JSON.stringify(config, null, 2)};`;
|
||||
waitForApiClient,
|
||||
waitForLogin,
|
||||
isModernUI,
|
||||
getWatchlistUrl
|
||||
getWatchlistUrl,
|
||||
onBodyMutation,
|
||||
offBodyMutation,
|
||||
createManagedObserver,
|
||||
disconnectManagedObserver,
|
||||
waitForElement,
|
||||
pollUntil,
|
||||
afterNextPaint,
|
||||
getBodySubscriberCount,
|
||||
listBodySubscriberIds,
|
||||
getManagedObserverCount,
|
||||
debugBodyObservers
|
||||
};
|
||||
|
||||
ensureNavigationChromeHandler();
|
||||
|
||||
+107
-86
@@ -7153,7 +7153,7 @@ In the Custom Tabs plugin, add a new tab with the following HTML content:
|
||||
}
|
||||
}
|
||||
|
||||
// Set up observer to watch for watchlist section mount (custom page / library page)
|
||||
// Page-scoped observer for watchlist section remount (attributes+class required)
|
||||
function setupWatchlistSectionObserver() {
|
||||
function isVisibleLibraryOrCustomPage(el) {
|
||||
return el &&
|
||||
@@ -7170,34 +7170,17 @@ In the Custom Tabs plugin, add a new tab with the following HTML content:
|
||||
return isVisibleLibraryOrCustomPage(node);
|
||||
}
|
||||
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
let shouldCheck = false;
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.type === 'childList') {
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
if (nodeMayContainWatchlist(node)) shouldCheck = true;
|
||||
});
|
||||
}
|
||||
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
|
||||
const target = mutation.target;
|
||||
if (isVisibleLibraryOrCustomPage(target)) shouldCheck = true;
|
||||
}
|
||||
});
|
||||
if (shouldCheck) {
|
||||
checkForEmptyWatchlist();
|
||||
}
|
||||
});
|
||||
let emptyWatchlistObserver = null;
|
||||
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: ['class']
|
||||
});
|
||||
function disconnectEmptyWatchlistObserver() {
|
||||
if (!emptyWatchlistObserver) return;
|
||||
emptyWatchlistObserver.disconnect();
|
||||
emptyWatchlistObserver = null;
|
||||
}
|
||||
|
||||
function checkForEmptyWatchlist() {
|
||||
const onWatchlistPage = window.location.pathname.includes(window.KefinTweaksUtils._watchlistUrl) || window.location.hash.includes(window.KefinTweaksUtils._watchlistUrl);
|
||||
|
||||
|
||||
if (!onWatchlistPage) {
|
||||
return;
|
||||
}
|
||||
@@ -7216,10 +7199,60 @@ In the Custom Tabs plugin, add a new tab with the following HTML content:
|
||||
});
|
||||
}
|
||||
|
||||
// In case #/watchlist is already visible
|
||||
checkForEmptyWatchlist();
|
||||
}
|
||||
function armEmptyWatchlistObserver() {
|
||||
const onWatchlistPage = window.location.pathname.includes(window.KefinTweaksUtils._watchlistUrl) || window.location.hash.includes(window.KefinTweaksUtils._watchlistUrl);
|
||||
if (!onWatchlistPage) {
|
||||
disconnectEmptyWatchlistObserver();
|
||||
return;
|
||||
}
|
||||
|
||||
checkForEmptyWatchlist();
|
||||
|
||||
const pageRoot =
|
||||
document.querySelector('.libraryPage:not(.hide)') ||
|
||||
document.querySelector('.customPage:not(.hide)') ||
|
||||
document.querySelector('.homePage:not(.hide)');
|
||||
const target = pageRoot || document.body;
|
||||
|
||||
if (emptyWatchlistObserver && emptyWatchlistObserver.__kefinTarget === target) {
|
||||
return;
|
||||
}
|
||||
disconnectEmptyWatchlistObserver();
|
||||
|
||||
emptyWatchlistObserver = new MutationObserver((mutations) => {
|
||||
let shouldCheck = false;
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type === 'childList') {
|
||||
for (const node of mutation.addedNodes) {
|
||||
if (nodeMayContainWatchlist(node)) {
|
||||
shouldCheck = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
|
||||
if (isVisibleLibraryOrCustomPage(mutation.target)) shouldCheck = true;
|
||||
}
|
||||
if (shouldCheck) break;
|
||||
}
|
||||
if (shouldCheck) checkForEmptyWatchlist();
|
||||
});
|
||||
emptyWatchlistObserver.__kefinTarget = target;
|
||||
emptyWatchlistObserver.observe(target, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: ['class']
|
||||
});
|
||||
}
|
||||
|
||||
const utils = window.KefinTweaksUtils;
|
||||
if (utils?.onViewPage) {
|
||||
utils.onViewPage(() => {
|
||||
armEmptyWatchlistObserver();
|
||||
});
|
||||
}
|
||||
armEmptyWatchlistObserver();
|
||||
}
|
||||
|
||||
// Initialize the watchlist section observer
|
||||
setupWatchlistSectionObserver();
|
||||
@@ -7808,7 +7841,7 @@ In the Custom Tabs plugin, add a new tab with the following HTML content:
|
||||
root.querySelectorAll?.('.itemsContainer').forEach(attachItemsContainerObserver);
|
||||
}
|
||||
|
||||
const discoveryObserver = new MutationObserver((mutations) => {
|
||||
function onDiscoveryMutations(mutations) {
|
||||
for (const mutation of mutations) {
|
||||
for (const node of mutation.addedNodes) {
|
||||
if (node.nodeType !== Node.ELEMENT_NODE) continue;
|
||||
@@ -7820,12 +7853,15 @@ In the Custom Tabs plugin, add a new tab with the following HTML content:
|
||||
discoverItemsContainers(node);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
discoveryObserver.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
const utils = window.KefinTweaksUtils;
|
||||
if (utils?.onBodyMutation) {
|
||||
utils.onBodyMutation('watchlist-overlay-discovery', onDiscoveryMutations);
|
||||
} else if (typeof MutationObserver !== 'undefined' && document.body) {
|
||||
const discoveryObserver = new MutationObserver(onDiscoveryMutations);
|
||||
discoveryObserver.observe(document.body, { childList: true, subtree: true });
|
||||
}
|
||||
|
||||
discoverItemsContainers(document.body);
|
||||
processExistingOverlayContainers();
|
||||
@@ -8065,72 +8101,57 @@ In the Custom Tabs plugin, add a new tab with the following HTML content:
|
||||
|
||||
// Function to monitor slides container for MediaBar Plugin
|
||||
function monitorSlidesContainer() {
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.type === 'childList') {
|
||||
// Check for added nodes
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
// Check if the added node is a slide
|
||||
if (node.classList && node.classList.contains('slide')) {
|
||||
addSlideWatchlistButton(node);
|
||||
}
|
||||
|
||||
// Check for slides within the added node
|
||||
const slides = node.querySelectorAll && node.querySelectorAll('.slide');
|
||||
if (slides && slides.length > 0) {
|
||||
slides.forEach((slide) => {
|
||||
addSlideWatchlistButton(slide);
|
||||
});
|
||||
}
|
||||
}
|
||||
const slidesObserver = new MutationObserver((mutations) => {
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type !== 'childList') continue;
|
||||
for (const node of mutation.addedNodes) {
|
||||
if (node.nodeType !== Node.ELEMENT_NODE) continue;
|
||||
if (node.classList?.contains('slide')) {
|
||||
addSlideWatchlistButton(node);
|
||||
}
|
||||
node.querySelectorAll?.('.slide')?.forEach((slide) => {
|
||||
addSlideWatchlistButton(slide);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Check if slides container already exists
|
||||
const slidesContainer = document.querySelector('#slides-container');
|
||||
if (slidesContainer) {
|
||||
// Process existing slides
|
||||
const existingSlides = slidesContainer.querySelectorAll('.slide');
|
||||
existingSlides.forEach((slide) => {
|
||||
function bindSlidesContainer(slidesContainer) {
|
||||
if (!slidesContainer) return;
|
||||
slidesContainer.querySelectorAll('.slide').forEach((slide) => {
|
||||
addSlideWatchlistButton(slide);
|
||||
});
|
||||
|
||||
// Start observing the slides container
|
||||
observer.observe(slidesContainer, {
|
||||
slidesObserver.observe(slidesContainer, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
}
|
||||
|
||||
// Also watch for slides container to be added
|
||||
const containerObserver = new MutationObserver(() => {
|
||||
const slidesContainer = document.querySelector('#slides-container');
|
||||
if (slidesContainer) {
|
||||
// Process existing slides
|
||||
const existingSlides = slidesContainer.querySelectorAll('.slide');
|
||||
existingSlides.forEach((slide) => {
|
||||
addSlideWatchlistButton(slide);
|
||||
});
|
||||
const existing = document.querySelector('#slides-container');
|
||||
if (existing) {
|
||||
bindSlidesContainer(existing);
|
||||
return;
|
||||
}
|
||||
|
||||
// Start observing the slides container
|
||||
observer.observe(slidesContainer, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
const utils = window.KefinTweaksUtils;
|
||||
if (utils?.waitForElement) {
|
||||
utils.waitForElement('#slides-container', { timeout: 60000, quiet: true }).then((el) => {
|
||||
bindSlidesContainer(el);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Disconnect the container observer since we've found it
|
||||
containerObserver.disconnect();
|
||||
// Fallback: short TTL poll if shared wait helper unavailable
|
||||
const start = Date.now();
|
||||
const pollId = setInterval(() => {
|
||||
const el = document.querySelector('#slides-container');
|
||||
if (el) {
|
||||
clearInterval(pollId);
|
||||
bindSlidesContainer(el);
|
||||
} else if (Date.now() - start > 60000) {
|
||||
clearInterval(pollId);
|
||||
}
|
||||
});
|
||||
|
||||
// Start observing the entire document body for the slides container
|
||||
containerObserver.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
}, 250);
|
||||
}
|
||||
|
||||
// Initialize the slides container observer
|
||||
|
||||
Reference in New Issue
Block a user