Add backup export and local artwork storage

This commit is contained in:
ajp_anton
2026-06-08 04:19:08 +00:00
parent 63dbc3a6d4
commit a4bb9a98c2
14 changed files with 1745 additions and 46 deletions
@@ -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();