From ef4cd3dc9cf67e0596319a5be0b41537b88f3d7d Mon Sep 17 00:00:00 2001 From: ajp_anton Date: Sun, 2 Aug 2026 15:28:45 +0000 Subject: [PATCH] Fix settings page compatibility and add debug access --- .../Configuration/configPage.html | 24 ++++++++++ .../Configuration/debugPage.html | 13 ++++-- .../Configuration/shared.js | 17 +++++++ .../Configuration/userRulesPage.html | 20 +++++++++ .../Jellyfin.Plugin.Multilang.csproj | 6 +-- .../wwwroot/inject.js | 15 +++++-- tests/browser/multilang.spec.js | 45 ++++++++++++++++++- 7 files changed, 128 insertions(+), 12 deletions(-) diff --git a/src/Jellyfin.Plugin.Multilang/Configuration/configPage.html b/src/Jellyfin.Plugin.Multilang/Configuration/configPage.html index 1c181ee..e9c73cd 100644 --- a/src/Jellyfin.Plugin.Multilang/Configuration/configPage.html +++ b/src/Jellyfin.Plugin.Multilang/Configuration/configPage.html @@ -157,6 +157,22 @@
+
+ Debug +
+ + +
+
+ + +
+
+ +
+
The selected user’s classification and translation rules are shown. Viewing another user requires administrator access.
+
+ @@ -1030,6 +1046,14 @@ document.getElementById("ml-cache-refresh").addEventListener("click", loadCacheDiagnostics); document.getElementById("ml-cache-clear").addEventListener("click", clearCacheEntries); document.getElementById("ml-refresh-activity-refresh").addEventListener("click", loadRefreshDiagnostics); + document.getElementById("ml-debug-open").addEventListener("click", function () { + var itemId = document.getElementById("ml-debug-item-id").value.trim(); + if (!itemId) { + M.setStatus(status, "Enter a Jellyfin item ID.", true); + return; + } + window.open(M.debugUrl(itemId, document.getElementById("ml-debug-user-id").value.trim()), "_blank", "noopener"); + }); document.getElementById("ml-export-open").addEventListener("click", function () { loadExportInfo(); syncAssetCheckboxes(); M.showModal("ml-export-modal"); }); document.getElementById("ml-import-open").addEventListener("click", function () { loadExportInfo(); syncAssetCheckboxes(); M.showModal("ml-import-modal"); }); document.getElementById("ml-cleanup-open").addEventListener("click", function () { document.getElementById("ml-cleanup-confirm").checked = false; M.showModal("ml-cleanup-modal"); }); diff --git a/src/Jellyfin.Plugin.Multilang/Configuration/debugPage.html b/src/Jellyfin.Plugin.Multilang/Configuration/debugPage.html index 66a915c..f2b5e29 100644 --- a/src/Jellyfin.Plugin.Multilang/Configuration/debugPage.html +++ b/src/Jellyfin.Plugin.Multilang/Configuration/debugPage.html @@ -9,7 +9,7 @@ @@ -44,12 +44,17 @@ return match ? decodeURIComponent(match[1]) : ""; } + function targetUserId() { + return new URLSearchParams(location.search).get("userId") || ""; + } + function endpoint() { var id = encodeURIComponent(itemId()); var locale = document.documentElement.lang || navigator.language || ""; var suffix = locale ? "?mlLocale=" + encodeURIComponent(locale) : ""; - return mode.value === "stored" - ? "/Multilang/Debug/" + id + if (mode.value === "stored") return "/Multilang/Debug/" + id; + return targetUserId() + ? "/Multilang/Debug/" + id + "/" + encodeURIComponent(targetUserId()) + suffix : "/Multilang/Debug/" + id + "/self" + suffix; } @@ -193,7 +198,7 @@ async function load() { M.setStatus(status, "Loading..."); - meta.textContent = "Item: " + itemId() + " | " + mode.options[mode.selectedIndex].text; + meta.textContent = "Item: " + itemId() + " | " + mode.options[mode.selectedIndex].text + (targetUserId() ? " | User: " + targetUserId() : ""); try { var data = await M.get(endpoint()); lastJson = JSON.stringify(data, null, 2); diff --git a/src/Jellyfin.Plugin.Multilang/Configuration/shared.js b/src/Jellyfin.Plugin.Multilang/Configuration/shared.js index 4dd9290..4f34c11 100644 --- a/src/Jellyfin.Plugin.Multilang/Configuration/shared.js +++ b/src/Jellyfin.Plugin.Multilang/Configuration/shared.js @@ -1,4 +1,11 @@ (function () { + if (!Element.prototype.replaceChildren) { + Element.prototype.replaceChildren = function () { + this.textContent = ""; + for (let index = 0; index < arguments.length; index++) this.appendChild(arguments[index]); + }; + } + const basePath = () => { const path = window.location.pathname || ""; const marker = "/web/"; @@ -37,6 +44,15 @@ return h; } + function debugUrl(itemId, userId) { + const query = new URLSearchParams(); + const t = token() || new URLSearchParams(location.search).get("token") || new URLSearchParams(location.search).get("api_key") || ""; + if (t) query.set("token", t); + if (userId) query.set("userId", userId); + const suffix = query.toString(); + return `${basePath()}/Multilang/DebugUi/${encodeURIComponent(String(itemId || "").trim())}${suffix ? `?${suffix}` : ""}`; + } + async function request(path, options) { const response = await fetch(`${basePath()}${path}`, { cache: "no-store", @@ -142,6 +158,7 @@ get: (path) => request(path), post: (path, body) => request(path, { method: "POST", body: JSON.stringify(body) }), basePath, + debugUrl, authHeaders, splitList, setStatus, diff --git a/src/Jellyfin.Plugin.Multilang/Configuration/userRulesPage.html b/src/Jellyfin.Plugin.Multilang/Configuration/userRulesPage.html index 6206164..7a5b590 100644 --- a/src/Jellyfin.Plugin.Multilang/Configuration/userRulesPage.html +++ b/src/Jellyfin.Plugin.Multilang/Configuration/userRulesPage.html @@ -39,6 +39,18 @@ +
+ Debug +
+ + +
+
+ +
+
Shows how your classification and translation rules resolve for this item.
+
+ @@ -1264,6 +1276,14 @@ state.rules.SortLocale = $("ml-sort-locale").value || "Auto"; renderSortLocaleAutoText(); }); + $("ml-debug-open").addEventListener("click", function () { + var itemId = $("ml-debug-item-id").value.trim(); + if (!itemId) { + M.setStatus(status, "Enter a Jellyfin item ID.", true); + return; + } + window.open(M.debugUrl(itemId), "_blank", "noopener"); + }); $("ml-user-export").addEventListener("click", function () { downloadUserExport().catch(function (err) { M.setStatus(status, String(err), true); }); }); diff --git a/src/Jellyfin.Plugin.Multilang/Jellyfin.Plugin.Multilang.csproj b/src/Jellyfin.Plugin.Multilang/Jellyfin.Plugin.Multilang.csproj index f4378ca..bacfb8f 100644 --- a/src/Jellyfin.Plugin.Multilang/Jellyfin.Plugin.Multilang.csproj +++ b/src/Jellyfin.Plugin.Multilang/Jellyfin.Plugin.Multilang.csproj @@ -7,9 +7,9 @@ enable enable false - 0.2.2 - 0.2.2.0 - 0.2.2.0 + 0.2.3 + 0.2.3.0 + 0.2.3.0 ajp_anton diff --git a/src/Jellyfin.Plugin.Multilang/wwwroot/inject.js b/src/Jellyfin.Plugin.Multilang/wwwroot/inject.js index 1b25fbe..0c6f825 100644 --- a/src/Jellyfin.Plugin.Multilang/wwwroot/inject.js +++ b/src/Jellyfin.Plugin.Multilang/wwwroot/inject.js @@ -44,6 +44,11 @@ return parts.path === USER_RULES_ROUTE && parts.params.get("multilang") === "1"; } + function isPreferencesMenuRoute() { + const path = hashParts().path; + return path === "mypreferences" || path === "mypreferencesmenu"; + } + function pathSegments(pathname) { return String(pathname || "") .toLowerCase() @@ -120,7 +125,7 @@ function ensureUserRulesLink() { let menuItem = document.getElementById(MENU_ID); - if (!(location.hash || "").toLowerCase().includes("mypreferences")) { + if (!isPreferencesMenuRoute()) { menuItem?.remove(); return; } @@ -132,7 +137,8 @@ return; } - const links = [...document.querySelectorAll('a[href^="#/mypreferences"]')]; + const preferencesPage = visiblePage(); + const links = preferencesPage ? [...preferencesPage.querySelectorAll('a[href^="#/mypreferences"]')] : []; const insertAfter = links[links.length - 1]; if (!insertAfter) return; @@ -421,7 +427,10 @@ }; const observer = new MutationObserver((mutations) => mutations.forEach((m) => { - if (m.type === "childList") m.addedNodes.forEach(scanNode); + if (m.type === "childList") { + m.addedNodes.forEach(scanNode); + ensureUserRulesLink(); + } if (m.type === "attributes") rewriteElementImages(m.target); })); observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ["style", "src", "data-src"] }); diff --git a/tests/browser/multilang.spec.js b/tests/browser/multilang.spec.js index b6a760c..b89397f 100644 --- a/tests/browser/multilang.spec.js +++ b/tests/browser/multilang.spec.js @@ -185,10 +185,51 @@ test('saves admin configuration through the dashboard page', async ({ page, requ expect(config.PrecacheTvShowLibraries).toBeFalsy(); }); +test('renders provider controls without native replaceChildren support', async ({ page }) => { + await page.addInitScript(() => { + Object.defineProperty(Element.prototype, 'replaceChildren', { configurable: true, value: undefined }); + }); + await signIn(page); + await page.goto('/web/#/configurationpage?name=Multilang'); + await expect(page.locator('#ml-provider-inputs input[data-provider]')).toHaveCount(2); +}); + +test('opens a resolved debug view from both settings pages', async ({ page }) => { + await signIn(page); + + await page.goto('/web/#/configurationpage?name=Multilang'); + await expect(page.locator('#ml-admin')).toBeVisible(); + await page.locator('#ml-debug-item-id').fill(fixture.orderedItemIds[0]); + await page.locator('#ml-debug-user-id').fill(fixture.userId); + const [adminDebug] = await Promise.all([ + page.waitForEvent('popup'), + page.locator('#ml-debug-open').click() + ]); + await expect(adminDebug).toHaveURL(new RegExp(`/Multilang/DebugUi/${fixture.orderedItemIds[0]}.*userId=${fixture.userId}`)); + await expect(adminDebug.locator('#ml-debug-status')).toHaveText('Loaded.'); + await adminDebug.close(); + + await page.goto(`/web/#/mypreferencesmenu?userId=${fixture.userId}`); + await expect(page.locator('#mlUserRulesMenuItem')).toBeVisible(); + await page.getByRole('button', { name: 'User Menu' }).click(); + await expect(page.locator('.MuiMenu-list #mlUserRulesMenuItem')).toHaveCount(0); + await page.keyboard.press('Escape'); + await page.locator('#mlUserRulesMenuItem').click(); + await expect(page.locator('#ml-user')).toBeVisible(); + await page.locator('#ml-debug-item-id').fill(fixture.orderedItemIds[0]); + const [userDebug] = await Promise.all([ + page.waitForEvent('popup'), + page.locator('#ml-debug-open').click() + ]); + await expect(userDebug).toHaveURL(new RegExp(`/Multilang/DebugUi/${fixture.orderedItemIds[0]}`)); + await expect(userDebug).not.toHaveURL(/userId=/); + await expect(userDebug.locator('#ml-debug-status')).toHaveText('Loaded.'); + await userDebug.close(); +}); + test('saves a structured classification rule through the user settings page', async ({ page, request }) => { await signIn(page); - await page.goto(`/web/#/mypreferences?userId=${fixture.userId}`); - await page.getByRole('button', { name: 'User Menu' }).click(); + await page.goto(`/web/#/mypreferencesmenu?userId=${fixture.userId}`); await expect(page.locator('#mlUserRulesMenuItem')).toBeVisible(); await page.locator('#mlUserRulesMenuItem').click(); await expect(page.locator('#ml-user')).toBeVisible();