Fix settings page compatibility and add debug access

This commit is contained in:
ajp_anton
2026-08-02 15:28:45 +00:00
parent a0a535c64a
commit ef4cd3dc9c
7 changed files with 128 additions and 12 deletions
@@ -157,6 +157,22 @@
<div id="ml-storage-info" class="fieldDescription"></div> <div id="ml-storage-info" class="fieldDescription"></div>
</fieldset> </fieldset>
<fieldset class="ml-section">
<legend>Debug</legend>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="ml-debug-item-id">Jellyfin item ID</label>
<input id="ml-debug-item-id" type="text" class="emby-input ml-input" placeholder="32-character item ID">
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="ml-debug-user-id">User ID</label>
<input id="ml-debug-user-id" type="text" class="emby-input ml-input" placeholder="Optional; leave blank for your own settings">
</div>
<div class="ml-actions">
<button id="ml-debug-open" is="emby-button" type="button" class="raised"><span>Open debug view</span></button>
</div>
<div class="fieldDescription">The selected users classification and translation rules are shown. Viewing another user requires administrator access.</div>
</fieldset>
<button id="ml-save" is="emby-button" type="submit" class="raised button-submit"><span>Save</span></button> <button id="ml-save" is="emby-button" type="submit" class="raised button-submit"><span>Save</span></button>
<button id="ml-reload" is="emby-button" type="button" class="raised"><span>Reload</span></button> <button id="ml-reload" is="emby-button" type="button" class="raised"><span>Reload</span></button>
</form> </form>
@@ -1030,6 +1046,14 @@
document.getElementById("ml-cache-refresh").addEventListener("click", loadCacheDiagnostics); document.getElementById("ml-cache-refresh").addEventListener("click", loadCacheDiagnostics);
document.getElementById("ml-cache-clear").addEventListener("click", clearCacheEntries); document.getElementById("ml-cache-clear").addEventListener("click", clearCacheEntries);
document.getElementById("ml-refresh-activity-refresh").addEventListener("click", loadRefreshDiagnostics); 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-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-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"); }); document.getElementById("ml-cleanup-open").addEventListener("click", function () { document.getElementById("ml-cleanup-confirm").checked = false; M.showModal("ml-cleanup-modal"); });
@@ -9,7 +9,7 @@
<label class="ml-inline-check"> <label class="ml-inline-check">
<span>View:</span> <span>View:</span>
<select id="ml-debug-mode" class="emby-select ml-select"> <select id="ml-debug-mode" class="emby-select ml-select">
<option value="resolved">Resolved for current user</option> <option value="resolved">Resolved for selected user</option>
<option value="stored">Stored facts and translations</option> <option value="stored">Stored facts and translations</option>
</select> </select>
</label> </label>
@@ -44,12 +44,17 @@
return match ? decodeURIComponent(match[1]) : ""; return match ? decodeURIComponent(match[1]) : "";
} }
function targetUserId() {
return new URLSearchParams(location.search).get("userId") || "";
}
function endpoint() { function endpoint() {
var id = encodeURIComponent(itemId()); var id = encodeURIComponent(itemId());
var locale = document.documentElement.lang || navigator.language || ""; var locale = document.documentElement.lang || navigator.language || "";
var suffix = locale ? "?mlLocale=" + encodeURIComponent(locale) : ""; var suffix = locale ? "?mlLocale=" + encodeURIComponent(locale) : "";
return mode.value === "stored" if (mode.value === "stored") return "/Multilang/Debug/" + id;
? "/Multilang/Debug/" + id return targetUserId()
? "/Multilang/Debug/" + id + "/" + encodeURIComponent(targetUserId()) + suffix
: "/Multilang/Debug/" + id + "/self" + suffix; : "/Multilang/Debug/" + id + "/self" + suffix;
} }
@@ -193,7 +198,7 @@
async function load() { async function load() {
M.setStatus(status, "Loading..."); 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 { try {
var data = await M.get(endpoint()); var data = await M.get(endpoint());
lastJson = JSON.stringify(data, null, 2); lastJson = JSON.stringify(data, null, 2);
@@ -1,4 +1,11 @@
(function () { (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 basePath = () => {
const path = window.location.pathname || ""; const path = window.location.pathname || "";
const marker = "/web/"; const marker = "/web/";
@@ -37,6 +44,15 @@
return h; 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) { async function request(path, options) {
const response = await fetch(`${basePath()}${path}`, { const response = await fetch(`${basePath()}${path}`, {
cache: "no-store", cache: "no-store",
@@ -142,6 +158,7 @@
get: (path) => request(path), get: (path) => request(path),
post: (path, body) => request(path, { method: "POST", body: JSON.stringify(body) }), post: (path, body) => request(path, { method: "POST", body: JSON.stringify(body) }),
basePath, basePath,
debugUrl,
authHeaders, authHeaders,
splitList, splitList,
setStatus, setStatus,
@@ -39,6 +39,18 @@
</div> </div>
</fieldset> </fieldset>
<fieldset class="ml-section">
<legend>Debug</legend>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="ml-debug-item-id">Jellyfin item ID</label>
<input id="ml-debug-item-id" type="text" class="emby-input ml-input" placeholder="32-character item ID">
</div>
<div class="ml-actions">
<button id="ml-debug-open" is="emby-button" type="button" class="raised"><span>Open debug view</span></button>
</div>
<div class="fieldDescription">Shows how your classification and translation rules resolve for this item.</div>
</fieldset>
<button id="ml-save-user" is="emby-button" type="submit" class="raised button-submit"><span>Save</span></button> <button id="ml-save-user" is="emby-button" type="submit" class="raised button-submit"><span>Save</span></button>
<button id="ml-reset-user" is="emby-button" type="button" class="raised"><span>Reload</span></button> <button id="ml-reset-user" is="emby-button" type="button" class="raised"><span>Reload</span></button>
</form> </form>
@@ -1264,6 +1276,14 @@
state.rules.SortLocale = $("ml-sort-locale").value || "Auto"; state.rules.SortLocale = $("ml-sort-locale").value || "Auto";
renderSortLocaleAutoText(); 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 () { $("ml-user-export").addEventListener("click", function () {
downloadUserExport().catch(function (err) { M.setStatus(status, String(err), true); }); downloadUserExport().catch(function (err) { M.setStatus(status, String(err), true); });
}); });
@@ -7,9 +7,9 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<GenerateDocumentationFile>false</GenerateDocumentationFile> <GenerateDocumentationFile>false</GenerateDocumentationFile>
<Version>0.2.2</Version> <Version>0.2.3</Version>
<AssemblyVersion>0.2.2.0</AssemblyVersion> <AssemblyVersion>0.2.3.0</AssemblyVersion>
<FileVersion>0.2.2.0</FileVersion> <FileVersion>0.2.3.0</FileVersion>
<Authors>ajp_anton</Authors> <Authors>ajp_anton</Authors>
</PropertyGroup> </PropertyGroup>
@@ -44,6 +44,11 @@
return parts.path === USER_RULES_ROUTE && parts.params.get("multilang") === "1"; 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) { function pathSegments(pathname) {
return String(pathname || "") return String(pathname || "")
.toLowerCase() .toLowerCase()
@@ -120,7 +125,7 @@
function ensureUserRulesLink() { function ensureUserRulesLink() {
let menuItem = document.getElementById(MENU_ID); let menuItem = document.getElementById(MENU_ID);
if (!(location.hash || "").toLowerCase().includes("mypreferences")) { if (!isPreferencesMenuRoute()) {
menuItem?.remove(); menuItem?.remove();
return; return;
} }
@@ -132,7 +137,8 @@
return; 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]; const insertAfter = links[links.length - 1];
if (!insertAfter) return; if (!insertAfter) return;
@@ -421,7 +427,10 @@
}; };
const observer = new MutationObserver((mutations) => mutations.forEach((m) => { 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); if (m.type === "attributes") rewriteElementImages(m.target);
})); }));
observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ["style", "src", "data-src"] }); observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ["style", "src", "data-src"] });
+43 -2
View File
@@ -185,10 +185,51 @@ test('saves admin configuration through the dashboard page', async ({ page, requ
expect(config.PrecacheTvShowLibraries).toBeFalsy(); 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 }) => { test('saves a structured classification rule through the user settings page', async ({ page, request }) => {
await signIn(page); await signIn(page);
await page.goto(`/web/#/mypreferences?userId=${fixture.userId}`); await page.goto(`/web/#/mypreferencesmenu?userId=${fixture.userId}`);
await page.getByRole('button', { name: 'User Menu' }).click();
await expect(page.locator('#mlUserRulesMenuItem')).toBeVisible(); await expect(page.locator('#mlUserRulesMenuItem')).toBeVisible();
await page.locator('#mlUserRulesMenuItem').click(); await page.locator('#mlUserRulesMenuItem').click();
await expect(page.locator('#ml-user')).toBeVisible(); await expect(page.locator('#ml-user')).toBeVisible();