Files
jellyfin-multilang/src/Jellyfin.Plugin.Multilang/Configuration/configPage.html
T

753 lines
35 KiB
HTML

<div id="ml-admin" data-role="page" class="page type-interior pluginConfigurationPage">
<div class="content-primary ml-root">
<link rel="stylesheet" href="/Multilang/shared.css">
<h1>Multilang</h1>
<form id="ml-admin-form">
<fieldset class="ml-section">
<legend>Providers</legend>
<div id="ml-provider-inputs"></div>
<div class="inputContainer">
<div class="fieldDescription">Provider priority order</div>
<div class="ml-buckets">
<div class="ml-bucket-col">
<div class="ml-bucket-title"><span class="ml-provider-cap ml-provider-cap-on">M</span>etadata</div>
<div class="ml-bucket ml-provider-bucket">
<ul id="ml-metadata-providers" class="ml-provider-list"></ul>
</div>
</div>
<div class="ml-bucket-col">
<div class="ml-bucket-title"><span class="ml-provider-cap ml-provider-cap-on">A</span>rtwork</div>
<div class="ml-bucket ml-provider-bucket">
<ul id="ml-artwork-providers" class="ml-provider-list"></ul>
</div>
</div>
</div>
</div>
</fieldset>
<fieldset class="ml-section">
<legend>Languages</legend>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="ml-languages">Languages</label>
<input id="ml-languages" type="text" class="emby-input ml-input" placeholder="en-US, fi-FI, sv-SE">
<div class="fieldDescription">Use BCP-47 language tags. These are the languages refresh jobs fetch and user rules can select.</div>
</div>
<details id="ml-article-details" class="ml-articles">
<summary>Articles</summary>
<div class="fieldDescription">Comma- or semicolon-separated leading articles to ignore per language.</div>
<div id="ml-article-list" class="ml-articles-list"></div>
<div class="ml-articles-actions">
<button id="ml-article-add" is="emby-button" type="button" class="raised"><span>Add language</span></button>
<button id="ml-article-repair" is="emby-button" type="button" class="raised"><span>Repair built-in</span></button>
<button id="ml-article-reset" is="emby-button" type="button" class="raised"><span>Reset to default</span></button>
</div>
</details>
</fieldset>
<fieldset class="ml-section">
<legend>Scanning task</legend>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="ml-missing-days">Wait days before trying to find missing data</label>
<input id="ml-missing-days" type="number" class="emby-input ml-input" min="0" step="1">
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="ml-existing-days">Wait days before refreshing existing data</label>
<input id="ml-existing-days" type="number" class="emby-input ml-input" min="0" step="1">
</div>
</fieldset>
<fieldset class="ml-section">
<legend>Refresh activity</legend>
<button id="ml-refresh-activity-refresh" is="emby-button" type="button" class="raised"><span>Refresh activity</span></button>
<h3 class="ml-subheading">Current scan</h3>
<div id="ml-refresh-scan" class="ml-cache-entries"></div>
<h3 class="ml-subheading">Queue</h3>
<div id="ml-refresh-queue" class="ml-cache-entries"></div>
<h3 class="ml-subheading">Provider HTTP</h3>
<div id="ml-provider-http" class="ml-cache-entries"></div>
</fieldset>
<fieldset class="ml-section">
<legend>Items cache</legend>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="ml-cache-threshold">ItemsProxy cache threshold ms</label>
<input id="ml-cache-threshold" type="number" class="emby-input ml-input" min="0" step="100">
<div class="fieldDescription">Responses slower than this threshold can be stored in the in-memory ItemsProxy cache.</div>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="ml-cache-ttl">ItemsProxy cache TTL minutes</label>
<input id="ml-cache-ttl" type="number" class="emby-input ml-input" min="1" step="1">
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="ml-cache-max">ItemsProxy cache max MiB</label>
<input id="ml-cache-max" type="number" class="emby-input ml-input" min="1" step="1">
</div>
<div class="ml-actions">
<button id="ml-cache-refresh" is="emby-button" type="button" class="raised"><span>Refresh cache diagnostics</span></button>
<button id="ml-cache-clear" is="emby-button" type="button" class="raised ml-danger"><span>Clear cache</span></button>
</div>
<h3 class="ml-subheading">Stored responses</h3>
<div id="ml-cache-entries" class="ml-cache-entries"></div>
<h3 class="ml-subheading">Recent requests</h3>
<div id="ml-proxy-requests" class="ml-cache-entries"></div>
</fieldset>
<fieldset class="ml-section">
<legend>Logging</legend>
<div class="inputContainer">
<label><input id="ml-log-enabled" type="checkbox"> Enable logging</label>
</div>
<div class="inputContainer">
<label><input id="ml-log-verbose" type="checkbox"> Verbose logging</label>
</div>
</fieldset>
<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>
</form>
<div id="ml-status" class="ml-status"></div>
<script src="/Multilang/shared.js"></script>
<script>
(function () {
var M = window.Multilang;
var state = { config: null, providers: [], articles: {} };
var page = document.getElementById("ml-admin");
var form = document.getElementById("ml-admin-form");
var status = document.getElementById("ml-status");
var providerInputs = document.getElementById("ml-provider-inputs");
var metadataList = document.getElementById("ml-metadata-providers");
var artworkList = document.getElementById("ml-artwork-providers");
var articleList = document.getElementById("ml-article-list");
function make(tag, className, text) {
var el = document.createElement(tag);
if (className) el.className = className;
if (text != null) el.textContent = text;
return el;
}
function providerId(provider) {
return String(provider.Id || provider.id || "").trim().toLowerCase();
}
function providerName(id) {
var provider = state.providers.find(function (p) { return providerId(p) === id; });
return provider ? (provider.Name || provider.name || id) : id;
}
function providerKeyField(provider) {
return provider.ApiKeyField || provider.apiKeyField || "";
}
function providerSupports(provider, bucket) {
return bucket === "metadata"
? !!(provider.SupportsMetadata || provider.supportsMetadata)
: !!(provider.SupportsArtwork || provider.supportsArtwork);
}
function providerById(id) {
return state.providers.find(function (p) { return providerId(p) === id; }) || null;
}
function splitList(value) {
return M.splitList(String(value || "").replace(/;/g, ","));
}
function normalizeLang(value) {
return String(value || "").trim().toLowerCase().replace(/_/g, "-");
}
function syncProviderSourceWidth() {
var sources = Array.prototype.slice.call(page.querySelectorAll(".ml-provider-source"));
if (!sources.length) return;
page.style.setProperty("--ml-provider-source-width", "auto");
var max = 0;
sources.forEach(function (source) {
max = Math.max(max, Math.ceil(source.getBoundingClientRect().width));
});
page.style.setProperty("--ml-provider-source-width", max + "px");
}
function renderProviderInputs() {
providerInputs.replaceChildren();
state.providers.forEach(function (provider) {
var id = providerId(provider);
var keyField = providerKeyField(provider);
var row = make("div", "inputContainer");
var inner = make("div", "ml-provider-input-row");
var source = make("span", "ml-provider-chip ml-provider-source ml-chip", providerName(id));
source.dataset.provider = id;
source.draggable = false;
var meta = make("span", "ml-provider-cap " + (providerSupports(provider, "metadata") ? "ml-provider-cap-on" : "ml-provider-cap-off"), "M");
var art = make("span", "ml-provider-cap " + (providerSupports(provider, "artwork") ? "ml-provider-cap-on" : "ml-provider-cap-off"), "A");
var input = make("input", "emby-input ml-provider-key");
input.type = "password";
input.autocomplete = "off";
input.dataset.provider = id;
input.dataset.key = keyField;
input.placeholder = provider.ApiKeyPlaceholder || provider.apiKeyPlaceholder || "";
input.value = state.config[keyField] || "";
var test = make("button", "raised");
test.type = "button";
test.setAttribute("is", "emby-button");
test.dataset.test = id;
test.appendChild(make("span", "", "Test"));
var result = make("span", "ml-muted");
result.dataset.testResult = id;
inner.append(source, meta, art, input, test, result);
row.appendChild(inner);
providerInputs.appendChild(row);
});
syncProviderSourceWidth();
}
function providerState(id) {
var states = state.config.Providers || [];
return states.find(function (p) { return String(p.Id || p.id || "").toLowerCase() === id; }) || {};
}
function orderFor(id, bucket) {
var p = providerState(id);
return bucket === "metadata" ? (p.MetadataOrder ?? p.metadataOrder ?? -1) : (p.ArtworkOrder ?? p.artworkOrder ?? -1);
}
function makeProviderChip(id) {
var chip = make("li", "ml-provider-chip ml-chip", providerName(id));
chip.dataset.provider = id;
chip.draggable = false;
return chip;
}
function renderBucket(list, bucket) {
var ids = state.providers
.map(providerId)
.filter(function (id) { return providerSupports(providerById(id), bucket) && orderFor(id, bucket) >= 0; })
.sort(function (a, b) { return orderFor(a, bucket) - orderFor(b, bucket); });
list.replaceChildren();
ids.forEach(function (id) { list.appendChild(makeProviderChip(id)); });
}
function renderProviderBuckets() {
renderBucket(metadataList, "metadata");
renderBucket(artworkList, "artwork");
}
function addArticleRow(lang, articles, builtIn, alwaysApply) {
var row = make("div", "ml-articles-row");
row.dataset.builtin = builtIn ? "1" : "";
var langInput = make("input", "emby-input ml-articles-lang");
langInput.type = "text";
langInput.placeholder = "en";
langInput.value = lang || "";
var articlesInput = make("input", "emby-input ml-articles-words");
articlesInput.type = "text";
articlesInput.placeholder = builtIn ? "Can't delete this language, but leaving empty will disable it." : "a, an, the";
articlesInput.value = articles || "";
var always = make("label", "ml-articles-apply");
var alwaysInput = make("input", "ml-articles-apply-toggle");
alwaysInput.type = "checkbox";
alwaysInput.checked = !!alwaysApply;
always.append(alwaysInput, make("span", "", "Always"));
var remove = make("button", "raised ml-articles-remove");
remove.type = "button";
remove.setAttribute("is", "emby-button");
remove.appendChild(make("span", "", "Delete"));
remove.addEventListener("click", function () {
if (row.dataset.builtin === "1") {
articlesInput.value = "";
return;
}
row.remove();
});
row.append(langInput, articlesInput, always, remove);
articleList.appendChild(row);
}
function renderArticles() {
articleList.replaceChildren();
var configured = new Map();
(state.config.ArticleEntries || []).forEach(function (entry) {
var lang = normalizeLang(entry.Language || entry.language);
if (lang) configured.set(lang, entry);
});
var builtInKeys = new Set(Object.keys(state.articles).map(normalizeLang));
Object.keys(state.articles).sort().forEach(function (lang) {
var entry = configured.get(normalizeLang(lang));
addArticleRow(
lang,
entry ? (entry.Articles || entry.articles || "") : (state.articles[lang] || []).join(", "),
true,
entry ? !!(entry.AlwaysApply ?? entry.alwaysApply) : normalizeLang(lang) === "en");
});
configured.forEach(function (entry, lang) {
if (builtInKeys.has(lang)) return;
addArticleRow(lang, entry.Articles || entry.articles || "", false, !!(entry.AlwaysApply ?? entry.alwaysApply));
});
}
function readBucket(list) {
return Array.prototype.slice.call(list.querySelectorAll(".ml-provider-chip"))
.map(function (chip) { return chip.dataset.provider; })
.filter(Boolean);
}
function collect() {
document.querySelectorAll("[data-key]").forEach(function (input) {
state.config[input.dataset.key] = input.value;
});
var metadata = readBucket(metadataList);
var artwork = readBucket(artworkList);
state.config.Providers = state.providers.map(function (provider) {
var id = providerId(provider);
return {
Id: id,
MetadataOrder: metadata.indexOf(id),
ArtworkOrder: artwork.indexOf(id)
};
});
state.config.Languages = splitList(document.getElementById("ml-languages").value);
state.config.WaitDaysForMissingData = Number(document.getElementById("ml-missing-days").value || 0);
state.config.WaitDaysForExistingData = Number(document.getElementById("ml-existing-days").value || 0);
state.config.ItemsProxyCacheThresholdMs = Number(document.getElementById("ml-cache-threshold").value || 0);
state.config.ItemsProxyCacheTtlMinutes = Number(document.getElementById("ml-cache-ttl").value || 1);
state.config.ItemsProxyCacheMaxMiB = Number(document.getElementById("ml-cache-max").value || 1);
state.config.EnableLogging = document.getElementById("ml-log-enabled").checked || document.getElementById("ml-log-verbose").checked;
state.config.VerboseLogging = document.getElementById("ml-log-verbose").checked;
state.config.ArticleEntries = Array.prototype.slice.call(articleList.children).map(function (row) {
return {
Language: normalizeLang(row.querySelector(".ml-articles-lang").value),
Articles: row.querySelector(".ml-articles-words").value,
AlwaysApply: row.querySelector(".ml-articles-apply-toggle").checked
};
}).filter(function (entry) { return entry.Language; });
return state.config;
}
function loadIntoForm() {
document.getElementById("ml-languages").value = (state.config.Languages || []).join(", ");
document.getElementById("ml-missing-days").value = state.config.WaitDaysForMissingData ?? 7;
document.getElementById("ml-existing-days").value = state.config.WaitDaysForExistingData ?? 60;
document.getElementById("ml-cache-threshold").value = state.config.ItemsProxyCacheThresholdMs ?? 1000;
document.getElementById("ml-cache-ttl").value = state.config.ItemsProxyCacheTtlMinutes ?? 120;
document.getElementById("ml-cache-max").value = state.config.ItemsProxyCacheMaxMiB ?? 50;
document.getElementById("ml-log-enabled").checked = !!state.config.EnableLogging;
document.getElementById("ml-log-verbose").checked = !!state.config.VerboseLogging;
renderProviderInputs();
renderProviderBuckets();
renderArticles();
loadCacheDiagnostics();
loadRefreshDiagnostics();
}
function renderCacheEntries(entries, error) {
var root = document.getElementById("ml-cache-entries");
root.replaceChildren();
if (error) {
root.appendChild(make("div", "ml-error", error));
return;
}
if (!entries || entries.length === 0) {
root.appendChild(make("div", "ml-muted", "No cache entries."));
return;
}
entries.forEach(function (entry) {
var url = entry.Url || entry.url || "";
root.appendChild(makeDiagnosticRow(url, [
"cached",
(entry.ItemCount || entry.itemCount || 0) + " items",
(entry.DurationMs || entry.durationMs || 0) + " ms",
((entry.SizeBytes || entry.sizeBytes || 0) / 1024).toFixed(1) + " KiB",
(entry.AgeSeconds || entry.ageSeconds || 0) + " s old"
]));
});
}
async function loadCacheEntries() {
try {
renderCacheEntries(await M.get("/Multilang/CacheEntries"));
} catch (err) {
renderCacheEntries([], "Unable to load cache entries: " + String(err && err.message ? err.message : err));
}
}
function renderProxyRequests(entries, error) {
var root = document.getElementById("ml-proxy-requests");
root.replaceChildren();
if (error) {
root.appendChild(make("div", "ml-error", error));
return;
}
if (!entries || entries.length === 0) {
root.appendChild(make("div", "ml-muted", "No recent ItemsProxy requests."));
return;
}
entries.forEach(function (entry) {
var url = entry.Url || entry.url || "";
root.appendChild(makeDiagnosticRow(url, [
(entry.CacheHit || entry.cacheHit) ? "hit" : ((entry.CacheStored || entry.cacheStored) ? "stored" : "miss"),
"HTTP " + (entry.StatusCode || entry.statusCode || 0),
(entry.ItemCount || entry.itemCount || 0) + " items",
"total " + (entry.TotalMs || entry.totalMs || 0) + " ms",
"upstream " + (entry.UpstreamMs || entry.upstreamMs || 0) + " ms",
"transform " + (entry.TransformMs || entry.transformMs || 0) + " ms",
((entry.SizeBytes || entry.sizeBytes || 0) / 1024).toFixed(1) + " KiB",
(entry.AgeSeconds || entry.ageSeconds || 0) + " s old"
]));
});
}
function makeDiagnosticRow(url, parts) {
var row = make("div", "ml-diagnostic-row");
var top = make("div", "ml-diagnostic-meta");
parts.forEach(function (part, index) {
var chip = make("span", index === 0 ? "ml-diagnostic-chip ml-diagnostic-chip-strong" : "ml-diagnostic-chip", part);
top.appendChild(chip);
});
var urlEl = make("div", "ml-cache-url", url);
urlEl.title = url;
row.append(top, urlEl);
return row;
}
async function loadProxyRequests() {
try {
renderProxyRequests(await M.get("/Multilang/ItemsProxyRequests"));
} catch (err) {
renderProxyRequests([], "Unable to load ItemsProxy requests: " + String(err && err.message ? err.message : err));
}
}
function loadCacheDiagnostics() {
loadCacheEntries();
loadProxyRequests();
}
async function clearCacheEntries() {
if (!window.confirm("Clear all Multilang ItemsProxy cache entries?")) return;
M.setStatus(status, "Clearing cache...");
try {
await M.post("/Multilang/ClearCache", {});
await loadCacheDiagnostics();
M.setStatus(status, "Cache cleared.");
} catch (err) {
M.setStatus(status, "Unable to clear cache: " + String(err && err.message ? err.message : err), true);
}
}
function prop(obj, pascal, camel, fallback) {
if (!obj) return fallback;
if (obj[pascal] !== undefined) return obj[pascal];
if (obj[camel] !== undefined) return obj[camel];
return fallback;
}
function formatUnixTime(value) {
value = Number(value || 0);
if (!value) return "never";
return new Date(value * 1000).toLocaleString();
}
function renderRefreshDiagnostics(data, error) {
var scanRoot = document.getElementById("ml-refresh-scan");
var queueRoot = document.getElementById("ml-refresh-queue");
var httpRoot = document.getElementById("ml-provider-http");
scanRoot.replaceChildren();
queueRoot.replaceChildren();
httpRoot.replaceChildren();
if (error) {
scanRoot.appendChild(make("div", "ml-error", error));
return;
}
data = data || {};
var scan = prop(data, "Scan", "scan", {});
var queue = prop(data, "Queue", "queue", {});
var providerHttp = prop(data, "ProviderHttp", "providerHttp", {});
var running = !!prop(scan, "Running", "running", false);
scanRoot.appendChild(makeDiagnosticRow("Last scan started: " + formatUnixTime(prop(data, "LastScanStarted", "lastScanStarted", 0)), [
running ? "running" : "idle",
String(prop(scan, "Mode", "mode", "") || "none"),
String(prop(scan, "Source", "source", "") || "none"),
"items " + Number(prop(scan, "Items", "items", 0) || 0),
"due " + Number(prop(scan, "Due", "due", 0) || 0),
"refreshed " + Number(prop(scan, "Refreshed", "refreshed", 0) || 0),
"skipped " + (Number(prop(scan, "SkippedNotDue", "skippedNotDue", 0) || 0) + Number(prop(scan, "SkippedNoData", "skippedNoData", 0) || 0)),
"new " + Number(prop(scan, "New", "new", 0) || 0),
"deleted " + Number(prop(scan, "Deleted", "deleted", 0) || 0),
"current " + Number(prop(scan, "CurrentIndex", "currentIndex", 0) || 0) + "/" + Number(prop(scan, "Items", "items", 0) || 0),
String(prop(scan, "CurrentAction", "currentAction", "") || "idle"),
Number(prop(scan, "ElapsedMs", "elapsedMs", 0) || 0) + " ms"
]));
var currentItem = String(prop(scan, "CurrentItemId", "currentItemId", "") || "");
if (currentItem) {
scanRoot.appendChild(makeDiagnosticRow("Current item: " + currentItem, [
String(prop(scan, "CurrentName", "currentName", "") || ""),
String(prop(scan, "CurrentKind", "currentKind", "") || "")
]));
}
var scanError = String(prop(scan, "Error", "error", "") || "");
if (scanError) scanRoot.appendChild(make("div", "ml-error", scanError));
var active = prop(queue, "Active", "active", null);
var queued = prop(queue, "Queued", "queued", []) || [];
var recent = prop(queue, "Recent", "recent", []) || [];
if (active) {
queueRoot.appendChild(makeDiagnosticRow("Active item: " + String(prop(active, "ItemId", "itemId", "")), [
"active",
String(prop(active, "SourceTier", "sourceTier", "")),
String(prop(active, "WorkClass", "workClass", "")),
String(prop(active, "JobType", "jobType", ""))
]));
}
queueRoot.appendChild(makeDiagnosticRow("Queued refresh items", [
"queued",
String(Number(prop(queue, "QueuedCount", "queuedCount", queued.length) || 0))
]));
recent.slice(0, 10).forEach(function (item) {
queueRoot.appendChild(makeDiagnosticRow("Item: " + String(prop(item, "ItemId", "itemId", "")), [
prop(item, "Ok", "ok", false) ? "ok" : "failed",
String(prop(item, "SourceTier", "sourceTier", "")),
String(prop(item, "WorkClass", "workClass", "")),
String(prop(item, "JobType", "jobType", "")),
Number(prop(item, "DurationMs", "durationMs", 0) || 0) + " ms",
formatUnixTime(prop(item, "FinishedAt", "finishedAt", 0))
]));
var itemError = String(prop(item, "Error", "error", "") || "");
if (itemError) queueRoot.appendChild(make("div", "ml-error", itemError));
});
var counts = prop(providerHttp, "Counts", "counts", []) || [];
var providerRecent = prop(providerHttp, "Recent", "recent", []) || [];
if (counts.length === 0 && providerRecent.length === 0) {
httpRoot.appendChild(make("div", "ml-muted", "No provider HTTP activity recorded since plugin startup."));
return;
}
counts.forEach(function (count) {
httpRoot.appendChild(makeDiagnosticRow(String(prop(count, "Provider", "provider", "")), [
"count",
String(prop(count, "Outcome", "outcome", "")),
String(Number(prop(count, "Count", "count", 0) || 0))
]));
});
providerRecent.slice(0, 10).forEach(function (entry) {
httpRoot.appendChild(makeDiagnosticRow(String(prop(entry, "Url", "url", "")), [
String(prop(entry, "Provider", "provider", "")),
String(prop(entry, "Outcome", "outcome", "")),
"attempt " + Number(prop(entry, "Attempt", "attempt", 0) || 0),
formatUnixTime(prop(entry, "At", "at", 0))
]));
});
}
async function loadRefreshDiagnostics() {
try {
renderRefreshDiagnostics(await M.get("/Multilang/RefreshDiagnostics"));
} catch (err) {
renderRefreshDiagnostics(null, "Unable to load refresh activity: " + String(err && err.message ? err.message : err));
}
}
async function load() {
M.setStatus(status, "Loading...");
try {
var all = await Promise.all([
M.get("/Multilang/Providers"),
M.get("/Multilang/ArticleBuiltins"),
M.get("/Multilang/AdminConfig")
]);
state.providers = all[0] || [];
state.articles = all[1] || {};
state.config = all[2] || {};
loadIntoForm();
M.setStatus(status, "");
} catch (err) {
M.setStatus(status, String(err), true);
}
}
function supportsList(list, id) {
var provider = providerById(id);
if (!provider) return false;
if (list === metadataList) return providerSupports(provider, "metadata");
if (list === artworkList) return providerSupports(provider, "artwork");
return false;
}
function hasProvider(list, id, except) {
var existing = list.querySelector('.ml-provider-chip[data-provider="' + id + '"]');
return existing && existing !== except;
}
var active = null;
var ghost = null;
var placeholder = make("li", "ml-provider-chip ml-provider-placeholder ml-chip");
function listFromPoint(x, y) {
var el = document.elementFromPoint(x, y);
if (!el || !el.closest) return null;
var list = el.closest("#ml-metadata-providers, #ml-artwork-providers");
if (list) return list;
var bucket = el.closest(".ml-provider-bucket");
return bucket ? bucket.querySelector("ul") : null;
}
function cleanupDrag() {
placeholder.remove();
if (ghost) ghost.remove();
ghost = null;
active = null;
metadataList.classList.remove("ml-bucket-list-over");
artworkList.classList.remove("ml-bucket-list-over");
}
function positionPlaceholder(list, x, y) {
if (!active || !supportsList(list, active.id) || hasProvider(list, active.id, active.sourceEl)) {
placeholder.remove();
return;
}
var chips = Array.prototype.slice.call(list.querySelectorAll(".ml-provider-chip")).filter(function (chip) {
return chip !== placeholder;
});
for (var i = 0; i < chips.length; i++) {
var rect = chips[i].getBoundingClientRect();
if (x < rect.left + rect.width / 2) {
list.insertBefore(placeholder, chips[i]);
return;
}
}
list.appendChild(placeholder);
}
function beginDrag(event) {
var chip = event.target.closest && event.target.closest(".ml-provider-chip");
if (!chip || !chip.dataset.provider) return;
var sourceList = chip.parentElement === metadataList || chip.parentElement === artworkList ? chip.parentElement : null;
var fromSource = chip.classList.contains("ml-provider-source");
if (!sourceList && !fromSource) return;
event.preventDefault();
event.stopPropagation();
var id = chip.dataset.provider;
active = { id: id, pointerId: event.pointerId, sourceEl: null, sourceList: sourceList, offsetX: 0, offsetY: 0 };
var rect = chip.getBoundingClientRect();
active.offsetX = event.clientX - rect.left;
active.offsetY = event.clientY - rect.top;
placeholder.textContent = providerName(id);
if (sourceList) {
active.sourceEl = chip;
sourceList.insertBefore(placeholder, chip);
chip.remove();
}
ghost = chip.cloneNode(true);
ghost.classList.add("ml-provider-drag-ghost");
ghost.style.position = "fixed";
ghost.style.pointerEvents = "none";
ghost.style.margin = "0";
ghost.style.zIndex = "99999";
document.body.appendChild(ghost);
moveDrag(event);
}
function moveDrag(event) {
if (!active || event.pointerId !== active.pointerId) return;
if (ghost) {
ghost.style.left = event.clientX - active.offsetX + "px";
ghost.style.top = event.clientY - active.offsetY + "px";
}
var list = listFromPoint(event.clientX, event.clientY);
metadataList.classList.toggle("ml-bucket-list-over", list === metadataList);
artworkList.classList.toggle("ml-bucket-list-over", list === artworkList);
if (list) positionPlaceholder(list, event.clientX, event.clientY);
else placeholder.remove();
}
function endDrag(event) {
if (!active || event.pointerId !== active.pointerId) return;
var list = listFromPoint(event.clientX, event.clientY);
if (list && supportsList(list, active.id) && !hasProvider(list, active.id, active.sourceEl)) {
var chip = active.sourceEl || makeProviderChip(active.id);
if (placeholder.parentElement === list) list.insertBefore(chip, placeholder);
else list.appendChild(chip);
} else if (list && active.sourceEl && active.sourceList) {
active.sourceList.insertBefore(active.sourceEl, placeholder.parentElement === active.sourceList ? placeholder : null);
} else if (active.sourceEl && active.sourceList) {
active.sourceEl.remove();
}
cleanupDrag();
}
function bindEvents() {
if (page.dataset.bound === "1") return;
page.dataset.bound = "1";
form.addEventListener("submit", async function (event) {
event.preventDefault();
M.setStatus(status, "Saving...");
try {
state.config = await M.post("/Multilang/AdminConfig", collect());
loadIntoForm();
M.setStatus(status, "Saved.");
} catch (err) {
M.setStatus(status, String(err), true);
}
});
document.getElementById("ml-reload").addEventListener("click", load);
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-log-enabled").addEventListener("change", function () {
if (!this.checked) document.getElementById("ml-log-verbose").checked = false;
});
document.getElementById("ml-log-verbose").addEventListener("change", function () {
if (this.checked) document.getElementById("ml-log-enabled").checked = true;
});
document.getElementById("ml-article-add").addEventListener("click", function () { addArticleRow("", "", false, false); });
document.getElementById("ml-article-repair").addEventListener("click", renderArticles);
document.getElementById("ml-article-reset").addEventListener("click", function () {
state.config.ArticleEntries = [];
renderArticles();
});
providerInputs.addEventListener("click", async function (event) {
var button = event.target.closest && event.target.closest("[data-test]");
if (!button) return;
var provider = button.dataset.test;
var input = providerInputs.querySelector('input[data-provider="' + provider + '"]');
var result = providerInputs.querySelector('[data-test-result="' + provider + '"]');
result.textContent = "Testing...";
try {
var response = await M.post("/Multilang/TestProvider", { Provider: provider, ApiKey: input.value || "" });
result.textContent = response.Ok ? "OK" : "Failed" + (response.Status ? " HTTP " + response.Status : "");
} catch (err) {
result.textContent = String(err);
}
});
document.addEventListener("dragstart", function (event) {
if (event.target.closest && event.target.closest(".ml-provider-chip")) event.preventDefault();
}, true);
page.addEventListener("pointerdown", beginDrag, true);
document.addEventListener("pointermove", moveDrag, true);
document.addEventListener("pointerup", endDrag, true);
document.addEventListener("pointercancel", cleanupDrag, true);
}
document.addEventListener("viewshow", function (event) {
if (event.target && event.target.id === "ml-admin") {
bindEvents();
load();
}
});
bindEvents();
load();
})();
</script>
</div>
</div>