This commit is contained in:
kodi
2026-03-14 15:29:50 +01:00
parent 3fb8528b0e
commit e85e51d64a
5 changed files with 197 additions and 96 deletions
Binary file not shown.
@@ -117,6 +117,7 @@ class UiSmokeGoldenTest(unittest.TestCase):
self.assertIn('id="search-results"', body) self.assertIn('id="search-results"', body)
self.assertIn('id="info-modal"', body) self.assertIn('id="info-modal"', body)
self.assertIn('id="rename-popup"', body) self.assertIn('id="rename-popup"', body)
self.assertIn('id="rename-label"', body)
self.assertIn('id="rename-input"', body) self.assertIn('id="rename-input"', body)
self.assertIn('id="rename-apply-btn"', body) self.assertIn('id="rename-apply-btn"', body)
self.assertIn('id="settings-general-tab"', body) self.assertIn('id="settings-general-tab"', body)
@@ -227,6 +228,8 @@ class UiSmokeGoldenTest(unittest.TestCase):
self.assertIn('function feedbackElements()', app_js) self.assertIn('function feedbackElements()', app_js)
self.assertIn('function openFeedbackModal(message)', app_js) self.assertIn('function openFeedbackModal(message)', app_js)
self.assertIn('function closeFeedbackModal()', app_js) self.assertIn('function closeFeedbackModal()', app_js)
self.assertIn('function openConfirmModal({ title, message, path, applyText = "Confirm" })', app_js)
self.assertIn('function openTextInputModal({ title, label, applyText, initialValue = "", onSubmit })', app_js)
self.assertIn('function downloadModalElements()', app_js) self.assertIn('function downloadModalElements()', app_js)
self.assertIn('function isZipDownloadSelection(items)', app_js) self.assertIn('function isZipDownloadSelection(items)', app_js)
self.assertIn('function singleFileDownloadRequestKey(path)', app_js) self.assertIn('function singleFileDownloadRequestKey(path)', app_js)
@@ -328,6 +331,11 @@ class UiSmokeGoldenTest(unittest.TestCase):
self.assertIn('startCopySelected();', app_js) self.assertIn('startCopySelected();', app_js)
self.assertIn('openF6Flow();', app_js) self.assertIn('openF6Flow();', app_js)
self.assertIn('deleteSelected();', app_js) self.assertIn('deleteSelected();', app_js)
self.assertIn('const confirmed = await openConfirmModal({', app_js)
self.assertIn('title: selectedItems.length === 1 ? "Delete item?" : "Delete selected items?"', app_js)
self.assertIn('title: "Discard unsaved changes?"', app_js)
self.assertIn('title: "Create Folder"', app_js)
self.assertIn('title: "Add Bookmark"', app_js)
self.assertIn('openInfo();', app_js) self.assertIn('openInfo();', app_js)
self.assertIn('elements.title.textContent = "Properties";', app_js) self.assertIn('elements.title.textContent = "Properties";', app_js)
self.assertIn('if (selectedItems.length > 1) {', app_js) self.assertIn('if (selectedItems.length > 1) {', app_js)
@@ -338,6 +346,9 @@ class UiSmokeGoldenTest(unittest.TestCase):
self.assertNotIn('Only files are supported for copy', app_js) self.assertNotIn('Only files are supported for copy', app_js)
self.assertIn('document.getElementById("upload-menu-toggle").onclick = (event) => {', app_js) self.assertIn('document.getElementById("upload-menu-toggle").onclick = (event) => {', app_js)
self.assertIn('document.getElementById("upload-folder-btn").onclick = openFolderPicker;', app_js) self.assertIn('document.getElementById("upload-folder-btn").onclick = openFolderPicker;', app_js)
self.assertNotIn('window.confirm(', app_js)
self.assertNotIn('window.prompt(', app_js)
self.assertNotIn('window.alert(', app_js)
self.assertIn('throw createApiError(response, data);', app_js) self.assertIn('throw createApiError(response, data);', app_js)
self.assertIn('function closeUploadMenu()', app_js) self.assertIn('function closeUploadMenu()', app_js)
self.assertIn('function toggleUploadMenu()', app_js) self.assertIn('function toggleUploadMenu()', app_js)
+185 -95
View File
@@ -46,11 +46,10 @@ let moveState = {
let renameState = { let renameState = {
source: null, source: null,
name: "", name: "",
submitAction: null,
}; };
let deleteConfirmState = { let deleteConfirmState = {
pane: "left", resolver: null,
items: [],
recursivePaths: [],
}; };
let contextMenuState = { let contextMenuState = {
open: false, open: false,
@@ -292,6 +291,8 @@ function moveElements() {
function renameElements() { function renameElements() {
return { return {
overlay: document.getElementById("rename-popup"), overlay: document.getElementById("rename-popup"),
title: document.getElementById("rename-title"),
label: document.getElementById("rename-label"),
input: document.getElementById("rename-input"), input: document.getElementById("rename-input"),
error: document.getElementById("rename-error"), error: document.getElementById("rename-error"),
applyButton: document.getElementById("rename-apply-btn"), applyButton: document.getElementById("rename-apply-btn"),
@@ -2583,7 +2584,28 @@ function navigateTo(pane, path) {
async function createFolderForPane(pane) { async function createFolderForPane(pane) {
setActivePane(pane); setActivePane(pane);
const name = window.prompt("Folder name"); const name = await new Promise((resolve) => {
openTextInputModal({
title: "Create Folder",
label: "Folder name",
applyText: "Create",
initialValue: "",
onSubmit: async (rawValue, elements, cancelled) => {
if (cancelled) {
resolve("");
return true;
}
const value = rawValue.trim();
elements.error.textContent = "";
if (!value) {
elements.error.textContent = "Folder name is required";
return false;
}
resolve(value);
return true;
},
});
});
if (!name) { if (!name) {
return; return;
} }
@@ -2609,49 +2631,36 @@ async function renameSelected() {
if (selectedItems.length !== 1) { if (selectedItems.length !== 1) {
return; return;
} }
const selected = selectedItems[0]; openRenamePopup();
const newName = window.prompt("New name", selected.name);
if (!newName) {
return;
}
setError("actions-error", "");
try {
await apiRequest("POST", "/api/files/rename", {
path: selected.path,
new_name: newName,
});
setSelectedItem(pane, null);
await loadBrowsePane(pane);
} catch (err) {
setActionError("Rename", err);
}
} }
function closeDeleteConfirmModal() { function closeDeleteConfirmModal() {
const elements = deleteConfirmElements(); const elements = deleteConfirmElements();
deleteConfirmState.pane = "left"; const resolver = deleteConfirmState.resolver;
deleteConfirmState.items = []; deleteConfirmState.resolver = null;
deleteConfirmState.recursivePaths = [];
elements.error.textContent = ""; elements.error.textContent = "";
elements.overlay.classList.add("hidden");
}
function openDeleteConfirmModal(pane, items, recursivePaths) {
const elements = deleteConfirmElements();
deleteConfirmState.pane = pane;
deleteConfirmState.items = items.map((item) => ({ ...item }));
deleteConfirmState.recursivePaths = Array.from(recursivePaths);
elements.error.textContent = "";
if (items.length === 1) {
elements.title.textContent = "Delete folder and contents?"; elements.title.textContent = "Delete folder and contents?";
elements.message.textContent = "This will permanently delete the folder and all files and subfolders inside it."; elements.message.textContent = "This will permanently delete the folder and all files and subfolders inside it.";
elements.path.textContent = items[0].path; elements.path.textContent = "";
} else { elements.applyButton.textContent = "Delete";
elements.title.textContent = "Delete selected items and folder contents?"; elements.overlay.classList.add("hidden");
elements.message.textContent = `This will permanently delete ${items.length} selected items, including all files and subfolders inside the selected folders.`; if (typeof resolver === "function") {
elements.path.textContent = `${items.length} selected items`; resolver(false);
} }
}
function openConfirmModal({ title, message, path, applyText = "Confirm" }) {
const elements = deleteConfirmElements();
elements.error.textContent = "";
elements.title.textContent = title;
elements.message.textContent = message;
elements.path.textContent = path || "";
elements.applyButton.textContent = applyText;
elements.overlay.classList.remove("hidden"); elements.overlay.classList.remove("hidden");
elements.applyButton.focus();
return new Promise((resolve) => {
deleteConfirmState.resolver = resolve;
});
} }
async function executeDeleteItems(pane, items, recursivePaths) { async function executeDeleteItems(pane, items, recursivePaths) {
@@ -2690,20 +2699,19 @@ async function executeDeleteItems(pane, items, recursivePaths) {
} }
async function submitDeleteConfirmModal() { async function submitDeleteConfirmModal() {
const elements = deleteConfirmElements(); const resolver = deleteConfirmState.resolver;
if (!deleteConfirmState.items.length) { if (typeof resolver !== "function") {
return; return;
} }
deleteConfirmState.resolver = null;
const elements = deleteConfirmElements();
elements.error.textContent = ""; elements.error.textContent = "";
try { elements.title.textContent = "Delete folder and contents?";
const pane = deleteConfirmState.pane; elements.message.textContent = "This will permanently delete the folder and all files and subfolders inside it.";
const items = [...deleteConfirmState.items]; elements.path.textContent = "";
const recursivePaths = new Set(deleteConfirmState.recursivePaths); elements.applyButton.textContent = "Delete";
closeDeleteConfirmModal(); elements.overlay.classList.add("hidden");
await executeDeleteItems(pane, items, recursivePaths); resolver(true);
} catch (err) {
elements.error.textContent = err.message;
}
} }
async function collectDeleteRecursivePaths(selectedItems) { async function collectDeleteRecursivePaths(selectedItems) {
@@ -2734,10 +2742,29 @@ async function deleteSelected() {
try { try {
const recursivePaths = await collectDeleteRecursivePaths(selectedItems); const recursivePaths = await collectDeleteRecursivePaths(selectedItems);
if (recursivePaths.size > 0) { if (recursivePaths.size > 0) {
openDeleteConfirmModal(pane, selectedItems, recursivePaths); const confirmed = await openConfirmModal({
title: selectedItems.length === 1 ? "Delete folder and contents?" : "Delete selected items and folder contents?",
message: selectedItems.length === 1
? "This will permanently delete the folder and all files and subfolders inside it."
: `This will permanently delete ${selectedItems.length} selected items, including all files and subfolders inside the selected folders.`,
path: selectedItems.length === 1 ? selectedItems[0].path : `${selectedItems.length} selected items`,
applyText: "Delete",
});
if (!confirmed) {
return; return;
} }
if (!window.confirm(`Delete ${selectedItems.length} selected item(s)?`)) { await executeDeleteItems(pane, selectedItems, recursivePaths);
return;
}
const confirmed = await openConfirmModal({
title: selectedItems.length === 1 ? "Delete item?" : "Delete selected items?",
message: selectedItems.length === 1
? "This will permanently delete the selected item."
: `This will permanently delete ${selectedItems.length} selected items.`,
path: selectedItems.length === 1 ? selectedItems[0].path : `${selectedItems.length} selected items`,
applyText: "Delete",
});
if (!confirmed) {
return; return;
} }
await executeDeleteItems(pane, selectedItems, new Set()); await executeDeleteItems(pane, selectedItems, new Set());
@@ -2841,7 +2868,28 @@ async function executeMoveSelection(baseDestination) {
async function addBookmark() { async function addBookmark() {
const pane = state.activePane; const pane = state.activePane;
const path = paneState(pane).currentPath; const path = paneState(pane).currentPath;
const label = window.prompt("Bookmark label", path); const label = await new Promise((resolve) => {
openTextInputModal({
title: "Add Bookmark",
label: "Bookmark label",
applyText: "Add",
initialValue: path,
onSubmit: async (rawValue, elements, cancelled) => {
if (cancelled) {
resolve("");
return true;
}
const value = rawValue.trim();
elements.error.textContent = "";
if (!value) {
elements.error.textContent = "Bookmark label is required";
return false;
}
resolve(value);
return true;
},
});
});
if (!label) { if (!label) {
return; return;
} }
@@ -3145,15 +3193,43 @@ function resetRenameState() {
renameState = { renameState = {
source: null, source: null,
name: "", name: "",
submitAction: null,
}; };
} }
function closeRenamePopup() { function settleRenamePopup(value = null, cancelled = false, notify = true) {
const elements = renameElements(); const elements = renameElements();
const submitAction = renameState.submitAction;
elements.overlay.classList.add("hidden"); elements.overlay.classList.add("hidden");
elements.error.textContent = ""; elements.error.textContent = "";
elements.input.value = ""; elements.input.value = "";
elements.title.textContent = "Rename";
elements.label.textContent = "Name";
elements.applyButton.textContent = "Rename";
resetRenameState(); resetRenameState();
if (notify && typeof submitAction === "function") {
return submitAction(value, null, cancelled);
}
return undefined;
}
function closeRenamePopup() {
settleRenamePopup(null, true);
}
function openTextInputModal({ title, label, applyText, initialValue = "", onSubmit }) {
const elements = renameElements();
renameState.source = null;
renameState.name = initialValue;
renameState.submitAction = onSubmit;
elements.title.textContent = title;
elements.label.textContent = label;
elements.applyButton.textContent = applyText;
elements.input.value = initialValue;
elements.error.textContent = "";
elements.overlay.classList.remove("hidden");
elements.input.focus();
elements.input.select();
} }
function openRenamePopup() { function openRenamePopup() {
@@ -3162,16 +3238,50 @@ function openRenamePopup() {
return false; return false;
} }
const source = selectedItems[0]; const source = selectedItems[0];
const elements = renameElements(); return openTextInputModal({
renameState.source = source; title: "Rename",
renameState.name = source.name; label: "Name",
elements.input.value = source.name; applyText: "Rename",
elements.error.textContent = ""; initialValue: source.name,
elements.overlay.classList.remove("hidden"); onSubmit: async (rawValue, elements, cancelled) => {
elements.input.focus(); if (cancelled) {
elements.input.select();
return true; return true;
} }
const newName = rawValue.trim();
elements.error.textContent = "";
if (!newName) {
elements.error.textContent = "Name is required";
return false;
}
if (newName === source.name) {
elements.error.textContent = "Name must differ from current name";
return false;
}
if (newName.includes("/")) {
elements.error.textContent = "Name cannot contain /";
return false;
}
if (newName === "." || newName === "..") {
elements.error.textContent = "Invalid name";
return false;
}
try {
await apiRequest("POST", "/api/files/rename", {
path: source.path,
new_name: newName,
});
setSelectedItem(state.activePane, null);
await loadBrowsePane(state.activePane);
setStatus(`Renamed ${source.path}`);
return true;
} catch (err) {
elements.error.textContent = err.message;
return false;
}
},
});
}
function resetBatchMoveState() { function resetBatchMoveState() {
batchMoveState = { batchMoveState = {
@@ -3253,40 +3363,12 @@ function openF6Flow() {
async function submitRenamePopup() { async function submitRenamePopup() {
const elements = renameElements(); const elements = renameElements();
const source = renameState.source; if (typeof renameState.submitAction !== "function") {
if (!source) {
return; return;
} }
const newName = elements.input.value.trim(); const shouldClose = await renameState.submitAction(elements.input.value, elements, false);
elements.error.textContent = ""; if (shouldClose !== false) {
if (!newName) { settleRenamePopup("", false, false);
elements.error.textContent = "Name is required";
return;
}
if (newName === source.name) {
elements.error.textContent = "Name must differ from current name";
return;
}
if (newName.includes("/")) {
elements.error.textContent = "Name cannot contain /";
return;
}
if (newName === "." || newName === "..") {
elements.error.textContent = "Invalid name";
return;
}
try {
await apiRequest("POST", "/api/files/rename", {
path: source.path,
new_name: newName,
});
closeRenamePopup();
setSelectedItem(state.activePane, null);
await loadBrowsePane(state.activePane);
setStatus(`Renamed ${source.path}`);
} catch (err) {
elements.error.textContent = err.message;
} }
} }
@@ -3764,10 +3846,18 @@ function resetEditorState() {
}; };
} }
function attemptCloseEditor() { async function attemptCloseEditor() {
if (editorIsDirty() && !window.confirm("Discard unsaved changes?")) { if (editorIsDirty()) {
const discard = await openConfirmModal({
title: "Discard unsaved changes?",
message: "Your unsaved editor changes will be lost.",
path: editorState.path || "",
applyText: "Discard",
});
if (!discard) {
return; return;
} }
}
closeEditor(); closeEditor();
} }
+1 -1
View File
@@ -302,7 +302,7 @@
<div class="popup-card"> <div class="popup-card">
<button id="rename-close-btn" class="viewer-close" type="button" aria-label="Close rename">X</button> <button id="rename-close-btn" class="viewer-close" type="button" aria-label="Close rename">X</button>
<h3 id="rename-title">Rename</h3> <h3 id="rename-title">Rename</h3>
<label for="rename-input" class="popup-label">Name</label> <label id="rename-label" for="rename-input" class="popup-label">Name</label>
<input id="rename-input" type="text" autocomplete="off"> <input id="rename-input" type="text" autocomplete="off">
<div id="rename-error" class="error"></div> <div id="rename-error" class="error"></div>
<div class="popup-actions"> <div class="popup-actions">