Add backup export and local artwork storage
This commit is contained in:
@@ -27,6 +27,8 @@ public sealed class PluginConfiguration : BasePluginConfiguration
|
||||
|
||||
public int ItemsProxyCacheMaxMiB { get; set; } = 50;
|
||||
|
||||
public string AssetStorageMode { get; set; } = "url";
|
||||
|
||||
[XmlArray("SortArticles")]
|
||||
[XmlArrayItem("SortArticleEntry")]
|
||||
public SortArticleEntry[] ArticleEntries { get; set; } = [];
|
||||
@@ -34,6 +36,8 @@ public sealed class PluginConfiguration : BasePluginConfiguration
|
||||
public bool EnableLogging { get; set; }
|
||||
|
||||
public bool VerboseLogging { get; set; }
|
||||
|
||||
public bool CleanupDataOnUninstall { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ProviderState
|
||||
|
||||
@@ -49,6 +49,14 @@
|
||||
|
||||
<fieldset class="ml-section">
|
||||
<legend>Scanning task</legend>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel inputLabelUnfocused" for="ml-asset-storage">Artwork storage</label>
|
||||
<select id="ml-asset-storage" is="emby-select" class="emby-select-withcolor emby-select ml-input">
|
||||
<option value="url">Use provider URLs</option>
|
||||
<option value="local">Download locally</option>
|
||||
</select>
|
||||
<div id="ml-asset-storage-info" class="fieldDescription"></div>
|
||||
</div>
|
||||
<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">
|
||||
@@ -105,12 +113,68 @@
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="ml-section">
|
||||
<legend>Backup and cleanup</legend>
|
||||
<div class="inputContainer">
|
||||
<label><input id="ml-cleanup-uninstall" type="checkbox"> Clean up Multilang settings, database, and assets on uninstall</label>
|
||||
</div>
|
||||
<div class="ml-actions">
|
||||
<button id="ml-export-open" is="emby-button" type="button" class="raised"><span>Export...</span></button>
|
||||
<button id="ml-import-open" is="emby-button" type="button" class="raised"><span>Import...</span></button>
|
||||
<button id="ml-cleanup-open" is="emby-button" type="button" class="raised ml-danger"><span>Clean up everything...</span></button>
|
||||
</div>
|
||||
<div id="ml-storage-info" class="fieldDescription"></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>
|
||||
|
||||
<div id="ml-export-modal" class="ml-modal" hidden>
|
||||
<div class="ml-modal-panel">
|
||||
<h2>Export Multilang data</h2>
|
||||
<label><input id="ml-export-plugin" type="checkbox" checked> Admin configuration</label>
|
||||
<label><input id="ml-export-users" type="checkbox" checked> User settings</label>
|
||||
<label><input id="ml-export-db" type="checkbox"> Translations database <span id="ml-export-db-size" class="ml-muted"></span></label>
|
||||
<label><input id="ml-export-assets" type="checkbox"> Downloaded assets <span id="ml-export-assets-size" class="ml-muted"></span></label>
|
||||
<div class="ml-actions">
|
||||
<button id="ml-export-run" is="emby-button" type="button" class="raised button-submit"><span>Export</span></button>
|
||||
<button data-modal-close is="emby-button" type="button" class="raised"><span>Cancel</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="ml-import-modal" class="ml-modal" hidden>
|
||||
<div class="ml-modal-panel">
|
||||
<h2>Import Multilang data</h2>
|
||||
<label><input id="ml-import-plugin" type="checkbox" checked> Admin configuration</label>
|
||||
<label><input id="ml-import-users" type="checkbox" checked> User settings</label>
|
||||
<label><input id="ml-import-db" type="checkbox" checked> Translations database <span id="ml-import-db-size" class="ml-muted"></span></label>
|
||||
<label><input id="ml-import-assets" type="checkbox" checked> Downloaded assets <span id="ml-import-assets-size" class="ml-muted"></span></label>
|
||||
<div class="inputContainer">
|
||||
<input id="ml-import-file" type="file" accept=".zip,application/zip">
|
||||
</div>
|
||||
<div class="ml-actions">
|
||||
<button id="ml-import-run" is="emby-button" type="button" class="raised button-submit"><span>Import</span></button>
|
||||
<button data-modal-close is="emby-button" type="button" class="raised"><span>Cancel</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="ml-cleanup-modal" class="ml-modal" hidden>
|
||||
<div class="ml-modal-panel">
|
||||
<h2>Clean up everything</h2>
|
||||
<p>This resets the admin configuration and deletes all Multilang user settings, translation data, and downloaded assets.</p>
|
||||
<label><input id="ml-cleanup-confirm" type="checkbox"> I understand this cannot be undone unless I have an export.</label>
|
||||
<div class="ml-actions">
|
||||
<button id="ml-cleanup-run" is="emby-button" type="button" class="raised ml-danger"><span>Clean up everything</span></button>
|
||||
<button data-modal-close is="emby-button" type="button" class="raised"><span>Cancel</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/Multilang/shared.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
@@ -297,6 +361,8 @@
|
||||
}
|
||||
|
||||
function collect() {
|
||||
var previousStorageMode = (state.config.AssetStorageMode || state.config.assetStorageMode || "url").toLowerCase() === "local" ? "local" : "url";
|
||||
var nextStorageMode = document.getElementById("ml-asset-storage").value === "local" ? "local" : "url";
|
||||
document.querySelectorAll("[data-key]").forEach(function (input) {
|
||||
state.config[input.dataset.key] = input.value;
|
||||
});
|
||||
@@ -316,8 +382,10 @@
|
||||
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.AssetStorageMode = nextStorageMode;
|
||||
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.CleanupDataOnUninstall = document.getElementById("ml-cleanup-uninstall").checked;
|
||||
state.config.ArticleEntries = Array.prototype.slice.call(articleList.children).map(function (row) {
|
||||
return {
|
||||
Language: normalizeLang(row.querySelector(".ml-articles-lang").value),
|
||||
@@ -325,6 +393,7 @@
|
||||
AlwaysApply: row.querySelector(".ml-articles-apply-toggle").checked
|
||||
};
|
||||
}).filter(function (entry) { return entry.Language; });
|
||||
state.config._previousAssetStorageMode = previousStorageMode;
|
||||
return state.config;
|
||||
}
|
||||
|
||||
@@ -335,13 +404,16 @@
|
||||
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-asset-storage").value = (state.config.AssetStorageMode || state.config.assetStorageMode || "url").toLowerCase() === "local" ? "local" : "url";
|
||||
document.getElementById("ml-log-enabled").checked = !!state.config.EnableLogging;
|
||||
document.getElementById("ml-log-verbose").checked = !!state.config.VerboseLogging;
|
||||
document.getElementById("ml-cleanup-uninstall").checked = !!state.config.CleanupDataOnUninstall;
|
||||
renderProviderInputs();
|
||||
renderProviderBuckets();
|
||||
renderArticles();
|
||||
loadCacheDiagnostics();
|
||||
loadRefreshDiagnostics();
|
||||
loadExportInfo();
|
||||
}
|
||||
|
||||
function renderCacheEntries(entries, error) {
|
||||
@@ -557,6 +629,113 @@
|
||||
}
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
bytes = Number(bytes || 0);
|
||||
if (bytes < 1024) return bytes + " B";
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KiB";
|
||||
if (bytes < 1024 * 1024 * 1024) return (bytes / 1024 / 1024).toFixed(1) + " MiB";
|
||||
return (bytes / 1024 / 1024 / 1024).toFixed(1) + " GiB";
|
||||
}
|
||||
|
||||
async function loadExportInfo() {
|
||||
try {
|
||||
var info = await M.get("/Multilang/ExportInfo");
|
||||
var db = info.DatabaseBytes ?? info.databaseBytes ?? 0;
|
||||
var assets = info.AssetsBytes ?? info.assetsBytes ?? 0;
|
||||
document.getElementById("ml-storage-info").textContent = "Translations database: " + formatBytes(db) + ". Downloaded assets: " + formatBytes(assets) + ".";
|
||||
updateAssetStorageInfo(assets);
|
||||
document.getElementById("ml-export-db-size").textContent = "(" + formatBytes(db) + ")";
|
||||
document.getElementById("ml-export-assets-size").textContent = "(" + formatBytes(assets) + ")";
|
||||
document.getElementById("ml-import-db-size").textContent = "(" + formatBytes(db) + " currently)";
|
||||
document.getElementById("ml-import-assets-size").textContent = "(" + formatBytes(assets) + " currently)";
|
||||
} catch (_) {
|
||||
}
|
||||
}
|
||||
|
||||
function showModal(id) {
|
||||
document.getElementById(id).hidden = false;
|
||||
}
|
||||
|
||||
function hideModals() {
|
||||
Array.prototype.slice.call(document.querySelectorAll(".ml-modal")).forEach(function (modal) { modal.hidden = true; });
|
||||
}
|
||||
|
||||
function syncAssetCheckboxes() {
|
||||
document.getElementById("ml-export-assets").disabled = !document.getElementById("ml-export-db").checked;
|
||||
if (document.getElementById("ml-export-assets").disabled) document.getElementById("ml-export-assets").checked = false;
|
||||
document.getElementById("ml-import-assets").disabled = !document.getElementById("ml-import-db").checked;
|
||||
if (document.getElementById("ml-import-assets").disabled) document.getElementById("ml-import-assets").checked = false;
|
||||
}
|
||||
|
||||
function updateAssetStorageInfo(assetBytes) {
|
||||
var mode = document.getElementById("ml-asset-storage").value;
|
||||
var size = formatBytes(assetBytes || 0);
|
||||
document.getElementById("ml-asset-storage-info").textContent = mode === "local"
|
||||
? "Downloaded artwork currently uses " + size + ". Missing local assets will be downloaded during the next scheduled refresh."
|
||||
: "Artwork is stored as provider URLs. Downloaded local artwork currently uses " + size + " and will be removed by the next scheduled refresh.";
|
||||
}
|
||||
|
||||
async function downloadExport() {
|
||||
var params = new URLSearchParams({
|
||||
pluginSettings: document.getElementById("ml-export-plugin").checked ? "true" : "false",
|
||||
userSettings: document.getElementById("ml-export-users").checked ? "true" : "false",
|
||||
translationsDatabase: document.getElementById("ml-export-db").checked ? "true" : "false",
|
||||
downloadedAssets: document.getElementById("ml-export-assets").checked ? "true" : "false"
|
||||
});
|
||||
M.setStatus(status, "Creating export...");
|
||||
var response = await fetch(M.basePath() + "/Multilang/Export?" + params.toString(), {
|
||||
cache: "no-store",
|
||||
headers: M.authHeaders()
|
||||
});
|
||||
if (!response.ok) throw new Error("HTTP " + response.status);
|
||||
var blob = await response.blob();
|
||||
var url = URL.createObjectURL(blob);
|
||||
var a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = "multilang-export.zip";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
hideModals();
|
||||
M.setStatus(status, "Export created.");
|
||||
}
|
||||
|
||||
async function runImport() {
|
||||
var file = document.getElementById("ml-import-file").files[0];
|
||||
if (!file) throw new Error("Choose an export zip first.");
|
||||
var formData = new FormData();
|
||||
formData.append("file", file);
|
||||
formData.append("pluginSettings", document.getElementById("ml-import-plugin").checked ? "true" : "false");
|
||||
formData.append("userSettings", document.getElementById("ml-import-users").checked ? "true" : "false");
|
||||
formData.append("translationsDatabase", document.getElementById("ml-import-db").checked ? "true" : "false");
|
||||
formData.append("downloadedAssets", document.getElementById("ml-import-assets").checked ? "true" : "false");
|
||||
M.setStatus(status, "Importing...");
|
||||
var response = await fetch(M.basePath() + "/Multilang/Import", {
|
||||
method: "POST",
|
||||
cache: "no-store",
|
||||
headers: M.authHeaders(),
|
||||
body: formData
|
||||
});
|
||||
if (!response.ok) throw new Error("HTTP " + response.status);
|
||||
var result = await response.json();
|
||||
hideModals();
|
||||
await load();
|
||||
M.setStatus(status, "Import complete. Users: " + (result.UserSettingsImported ?? result.userSettingsImported ?? 0) + ", translations: " + (result.TranslationsImported ?? result.translationsImported ?? 0) + ".");
|
||||
}
|
||||
|
||||
async function runCleanupAll() {
|
||||
if (!document.getElementById("ml-cleanup-confirm").checked) {
|
||||
M.setStatus(status, "Confirm cleanup first.", true);
|
||||
return;
|
||||
}
|
||||
M.setStatus(status, "Cleaning up...");
|
||||
await M.post("/Multilang/CleanupAll", { Confirm: true });
|
||||
hideModals();
|
||||
await load();
|
||||
M.setStatus(status, "Multilang data cleaned up.");
|
||||
}
|
||||
|
||||
async function load() {
|
||||
M.setStatus(status, "Loading...");
|
||||
try {
|
||||
@@ -692,7 +871,17 @@
|
||||
event.preventDefault();
|
||||
M.setStatus(status, "Saving...");
|
||||
try {
|
||||
state.config = await M.post("/Multilang/AdminConfig", collect());
|
||||
var next = collect();
|
||||
if (next._previousAssetStorageMode === "local" && next.AssetStorageMode === "url" &&
|
||||
!window.confirm("Switching to provider URLs will remove downloaded local artwork during the next scheduled refresh. Continue?")) {
|
||||
M.setStatus(status, "Not saved.");
|
||||
return;
|
||||
}
|
||||
if (next._previousAssetStorageMode === "url" && next.AssetStorageMode === "local") {
|
||||
window.alert("Local artwork downloads will start during the next scheduled refresh.");
|
||||
}
|
||||
delete next._previousAssetStorageMode;
|
||||
state.config = await M.post("/Multilang/AdminConfig", next);
|
||||
loadIntoForm();
|
||||
M.setStatus(status, "Saved.");
|
||||
} catch (err) {
|
||||
@@ -700,9 +889,21 @@
|
||||
}
|
||||
});
|
||||
document.getElementById("ml-reload").addEventListener("click", load);
|
||||
document.getElementById("ml-asset-storage").addEventListener("change", function () { loadExportInfo(); });
|
||||
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-export-open").addEventListener("click", function () { loadExportInfo(); syncAssetCheckboxes(); showModal("ml-export-modal"); });
|
||||
document.getElementById("ml-import-open").addEventListener("click", function () { loadExportInfo(); syncAssetCheckboxes(); showModal("ml-import-modal"); });
|
||||
document.getElementById("ml-cleanup-open").addEventListener("click", function () { document.getElementById("ml-cleanup-confirm").checked = false; showModal("ml-cleanup-modal"); });
|
||||
document.getElementById("ml-export-db").addEventListener("change", syncAssetCheckboxes);
|
||||
document.getElementById("ml-import-db").addEventListener("change", syncAssetCheckboxes);
|
||||
document.getElementById("ml-export-run").addEventListener("click", function () { downloadExport().catch(function (err) { M.setStatus(status, String(err), true); }); });
|
||||
document.getElementById("ml-import-run").addEventListener("click", function () { runImport().catch(function (err) { M.setStatus(status, String(err), true); }); });
|
||||
document.getElementById("ml-cleanup-run").addEventListener("click", function () { runCleanupAll().catch(function (err) { M.setStatus(status, String(err), true); }); });
|
||||
Array.prototype.slice.call(document.querySelectorAll("[data-modal-close]")).forEach(function (button) {
|
||||
button.addEventListener("click", hideModals);
|
||||
});
|
||||
document.getElementById("ml-log-enabled").addEventListener("change", function () {
|
||||
if (!this.checked) document.getElementById("ml-log-verbose").checked = false;
|
||||
});
|
||||
|
||||
@@ -36,6 +36,42 @@ body {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ml-modal[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ml-modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 99999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
background: rgba(0, 0, 0, .68);
|
||||
}
|
||||
|
||||
.ml-modal-panel {
|
||||
width: min(34rem, 100%);
|
||||
max-height: calc(100vh - 2rem);
|
||||
overflow: auto;
|
||||
box-sizing: border-box;
|
||||
padding: 1.25rem;
|
||||
border: 1px solid rgba(255, 255, 255, .22);
|
||||
border-radius: 8px;
|
||||
background: #181818;
|
||||
box-shadow: 0 18px 60px rgba(0, 0, 0, .45);
|
||||
}
|
||||
|
||||
.ml-modal-panel h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.ml-modal-panel label {
|
||||
display: block;
|
||||
margin: .65rem 0;
|
||||
}
|
||||
|
||||
.ml-grid {
|
||||
display: grid;
|
||||
gap: .8rem;
|
||||
|
||||
@@ -25,6 +25,18 @@
|
||||
return h;
|
||||
}
|
||||
|
||||
function authHeaders(contentType) {
|
||||
const qs = new URLSearchParams(location.search);
|
||||
const t = token() || qs.get("token") || qs.get("api_key") || "";
|
||||
const h = {};
|
||||
if (contentType) h["Content-Type"] = contentType;
|
||||
if (t) {
|
||||
h["X-Emby-Token"] = t;
|
||||
h["X-MediaBrowser-Token"] = t;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
async function request(path, options) {
|
||||
const response = await fetch(`${basePath()}${path}`, {
|
||||
cache: "no-store",
|
||||
@@ -89,6 +101,8 @@
|
||||
window.Multilang = {
|
||||
get: (path) => request(path),
|
||||
post: (path, body) => request(path, { method: "POST", body: JSON.stringify(body) }),
|
||||
basePath,
|
||||
authHeaders,
|
||||
splitList,
|
||||
setStatus,
|
||||
chip,
|
||||
|
||||
@@ -31,12 +31,37 @@
|
||||
<div id="ml-action-matrix" class="ml-action-matrix"></div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="ml-section">
|
||||
<legend>Import and export</legend>
|
||||
<div class="ml-actions">
|
||||
<button id="ml-user-export" is="emby-button" type="button" class="raised"><span>Export my settings</span></button>
|
||||
<button id="ml-user-import-open" is="emby-button" type="button" class="raised"><span>Import settings...</span></button>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<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>
|
||||
</form>
|
||||
|
||||
<div id="ml-user-status" class="ml-status"></div>
|
||||
|
||||
<div id="ml-user-import-modal" class="ml-modal" hidden>
|
||||
<div class="ml-modal-panel">
|
||||
<h2>Import user settings</h2>
|
||||
<div class="inputContainer">
|
||||
<input id="ml-user-import-file" type="file" accept=".zip,application/zip">
|
||||
</div>
|
||||
<div id="ml-user-import-choice" class="inputContainer" hidden>
|
||||
<label class="inputLabel inputLabelUnfocused" for="ml-user-import-source">Import settings from user ID</label>
|
||||
<select id="ml-user-import-source" class="emby-select ml-select"></select>
|
||||
</div>
|
||||
<div class="ml-actions">
|
||||
<button id="ml-user-import-run" is="emby-button" type="button" class="raised button-submit"><span>Import</span></button>
|
||||
<button id="ml-user-import-cancel" is="emby-button" type="button" class="raised"><span>Cancel</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/Multilang/shared.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
@@ -1131,6 +1156,103 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadUserExport() {
|
||||
M.setStatus(status, "Creating export...");
|
||||
var response = await fetch(M.basePath() + "/Multilang/UserRules/self/export", {
|
||||
cache: "no-store",
|
||||
headers: M.authHeaders()
|
||||
});
|
||||
if (!response.ok) throw new Error("HTTP " + response.status);
|
||||
var blob = await response.blob();
|
||||
var url = URL.createObjectURL(blob);
|
||||
var a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = "multilang-user-settings.zip";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
M.setStatus(status, "Export created.");
|
||||
}
|
||||
|
||||
function userImportModal(show) {
|
||||
$("ml-user-import-modal").hidden = !show;
|
||||
if (!show) {
|
||||
$("ml-user-import-choice").hidden = true;
|
||||
$("ml-user-import-source").replaceChildren();
|
||||
}
|
||||
}
|
||||
|
||||
function userImportFormData(sourceUserId) {
|
||||
var file = $("ml-user-import-file").files[0];
|
||||
if (!file) throw new Error("Choose an export zip first.");
|
||||
var formData = new FormData();
|
||||
formData.append("file", file);
|
||||
if (sourceUserId) formData.append("sourceUserId", sourceUserId);
|
||||
return formData;
|
||||
}
|
||||
|
||||
async function inspectUserImport() {
|
||||
var response = await fetch(M.basePath() + "/Multilang/UserRules/self/import/inspect", {
|
||||
method: "POST",
|
||||
cache: "no-store",
|
||||
headers: M.authHeaders(),
|
||||
body: userImportFormData("")
|
||||
});
|
||||
if (!response.ok) throw new Error("HTTP " + response.status);
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async function postUserImport(sourceUserId) {
|
||||
var response = await fetch(M.basePath() + "/Multilang/UserRules/self/import", {
|
||||
method: "POST",
|
||||
cache: "no-store",
|
||||
headers: M.authHeaders(),
|
||||
body: userImportFormData(sourceUserId)
|
||||
});
|
||||
if (!response.ok) throw new Error("HTTP " + response.status);
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async function runUserImport() {
|
||||
M.setStatus(status, "Inspecting import...");
|
||||
var choice = $("ml-user-import-choice");
|
||||
if (!choice.hidden) {
|
||||
var selected = $("ml-user-import-source").value;
|
||||
if (!selected) throw new Error("Choose a source user ID.");
|
||||
var chosen = await postUserImport(selected);
|
||||
if ((chosen.UserSettingsImported ?? chosen.userSettingsImported ?? 0) <= 0) throw new Error("No user settings were imported.");
|
||||
userImportModal(false);
|
||||
await load();
|
||||
M.setStatus(status, "Imported user settings.");
|
||||
return;
|
||||
}
|
||||
|
||||
var inspect = await inspectUserImport();
|
||||
var count = inspect.UserSettingsCount ?? inspect.userSettingsCount ?? 0;
|
||||
var containsCurrent = inspect.ContainsCurrentUser ?? inspect.containsCurrentUser;
|
||||
var ids = inspect.UserIds || inspect.userIds || [];
|
||||
if (count <= 0) {
|
||||
M.setStatus(status, "No user settings were found in this export.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (containsCurrent) {
|
||||
var result = await postUserImport("");
|
||||
if ((result.UserSettingsImported ?? result.userSettingsImported ?? 0) <= 0) throw new Error("No user settings were imported.");
|
||||
userImportModal(false);
|
||||
await load();
|
||||
M.setStatus(status, "Imported user settings.");
|
||||
return;
|
||||
}
|
||||
|
||||
var select = $("ml-user-import-source");
|
||||
select.replaceChildren();
|
||||
ids.forEach(function (id) { select.appendChild(option(id, id, false)); });
|
||||
choice.hidden = false;
|
||||
M.setStatus(status, "No matching user ID was found. Choose which exported user settings to import.", true);
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
if (page.dataset.bound === "1") return;
|
||||
page.dataset.bound = "1";
|
||||
@@ -1147,6 +1269,17 @@
|
||||
});
|
||||
$("ml-reset-user").addEventListener("click", load);
|
||||
$("ml-sort-locale").addEventListener("change", renderSortLocaleAutoText);
|
||||
$("ml-user-export").addEventListener("click", function () {
|
||||
downloadUserExport().catch(function (err) { M.setStatus(status, String(err), true); });
|
||||
});
|
||||
$("ml-user-import-open").addEventListener("click", function () {
|
||||
$("ml-user-import-file").value = "";
|
||||
userImportModal(true);
|
||||
});
|
||||
$("ml-user-import-cancel").addEventListener("click", function () { userImportModal(false); });
|
||||
$("ml-user-import-run").addEventListener("click", function () {
|
||||
runUserImport().catch(function (err) { M.setStatus(status, String(err), true); });
|
||||
});
|
||||
document.addEventListener("click", function () { closeValuePanels(null); });
|
||||
$("ml-add-category").addEventListener("click", function () {
|
||||
syncCategoriesFromDom();
|
||||
|
||||
Reference in New Issue
Block a user