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
+185 -95
View File
@@ -46,11 +46,10 @@ let moveState = {
let renameState = {
source: null,
name: "",
submitAction: null,
};
let deleteConfirmState = {
pane: "left",
items: [],
recursivePaths: [],
resolver: null,
};
let contextMenuState = {
open: false,
@@ -292,6 +291,8 @@ function moveElements() {
function renameElements() {
return {
overlay: document.getElementById("rename-popup"),
title: document.getElementById("rename-title"),
label: document.getElementById("rename-label"),
input: document.getElementById("rename-input"),
error: document.getElementById("rename-error"),
applyButton: document.getElementById("rename-apply-btn"),
@@ -2583,7 +2584,28 @@ function navigateTo(pane, path) {
async function createFolderForPane(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) {
return;
}
@@ -2609,49 +2631,36 @@ async function renameSelected() {
if (selectedItems.length !== 1) {
return;
}
const selected = selectedItems[0];
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);
}
openRenamePopup();
}
function closeDeleteConfirmModal() {
const elements = deleteConfirmElements();
deleteConfirmState.pane = "left";
deleteConfirmState.items = [];
deleteConfirmState.recursivePaths = [];
const resolver = deleteConfirmState.resolver;
deleteConfirmState.resolver = null;
elements.error.textContent = "";
elements.title.textContent = "Delete folder and contents?";
elements.message.textContent = "This will permanently delete the folder and all files and subfolders inside it.";
elements.path.textContent = "";
elements.applyButton.textContent = "Delete";
elements.overlay.classList.add("hidden");
if (typeof resolver === "function") {
resolver(false);
}
}
function openDeleteConfirmModal(pane, items, recursivePaths) {
function openConfirmModal({ title, message, path, applyText = "Confirm" }) {
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.message.textContent = "This will permanently delete the folder and all files and subfolders inside it.";
elements.path.textContent = items[0].path;
} else {
elements.title.textContent = "Delete selected items and folder contents?";
elements.message.textContent = `This will permanently delete ${items.length} selected items, including all files and subfolders inside the selected folders.`;
elements.path.textContent = `${items.length} selected items`;
}
elements.title.textContent = title;
elements.message.textContent = message;
elements.path.textContent = path || "";
elements.applyButton.textContent = applyText;
elements.overlay.classList.remove("hidden");
elements.applyButton.focus();
return new Promise((resolve) => {
deleteConfirmState.resolver = resolve;
});
}
async function executeDeleteItems(pane, items, recursivePaths) {
@@ -2690,20 +2699,19 @@ async function executeDeleteItems(pane, items, recursivePaths) {
}
async function submitDeleteConfirmModal() {
const elements = deleteConfirmElements();
if (!deleteConfirmState.items.length) {
const resolver = deleteConfirmState.resolver;
if (typeof resolver !== "function") {
return;
}
deleteConfirmState.resolver = null;
const elements = deleteConfirmElements();
elements.error.textContent = "";
try {
const pane = deleteConfirmState.pane;
const items = [...deleteConfirmState.items];
const recursivePaths = new Set(deleteConfirmState.recursivePaths);
closeDeleteConfirmModal();
await executeDeleteItems(pane, items, recursivePaths);
} catch (err) {
elements.error.textContent = err.message;
}
elements.title.textContent = "Delete folder and contents?";
elements.message.textContent = "This will permanently delete the folder and all files and subfolders inside it.";
elements.path.textContent = "";
elements.applyButton.textContent = "Delete";
elements.overlay.classList.add("hidden");
resolver(true);
}
async function collectDeleteRecursivePaths(selectedItems) {
@@ -2734,10 +2742,29 @@ async function deleteSelected() {
try {
const recursivePaths = await collectDeleteRecursivePaths(selectedItems);
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;
}
await executeDeleteItems(pane, selectedItems, recursivePaths);
return;
}
if (!window.confirm(`Delete ${selectedItems.length} selected item(s)?`)) {
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;
}
await executeDeleteItems(pane, selectedItems, new Set());
@@ -2841,7 +2868,28 @@ async function executeMoveSelection(baseDestination) {
async function addBookmark() {
const pane = state.activePane;
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) {
return;
}
@@ -3145,15 +3193,43 @@ function resetRenameState() {
renameState = {
source: null,
name: "",
submitAction: null,
};
}
function closeRenamePopup() {
function settleRenamePopup(value = null, cancelled = false, notify = true) {
const elements = renameElements();
const submitAction = renameState.submitAction;
elements.overlay.classList.add("hidden");
elements.error.textContent = "";
elements.input.value = "";
elements.title.textContent = "Rename";
elements.label.textContent = "Name";
elements.applyButton.textContent = "Rename";
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() {
@@ -3162,15 +3238,49 @@ function openRenamePopup() {
return false;
}
const source = selectedItems[0];
const elements = renameElements();
renameState.source = source;
renameState.name = source.name;
elements.input.value = source.name;
elements.error.textContent = "";
elements.overlay.classList.remove("hidden");
elements.input.focus();
elements.input.select();
return true;
return openTextInputModal({
title: "Rename",
label: "Name",
applyText: "Rename",
initialValue: source.name,
onSubmit: async (rawValue, elements, cancelled) => {
if (cancelled) {
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() {
@@ -3253,40 +3363,12 @@ function openF6Flow() {
async function submitRenamePopup() {
const elements = renameElements();
const source = renameState.source;
if (!source) {
if (typeof renameState.submitAction !== "function") {
return;
}
const newName = elements.input.value.trim();
elements.error.textContent = "";
if (!newName) {
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;
const shouldClose = await renameState.submitAction(elements.input.value, elements, false);
if (shouldClose !== false) {
settleRenamePopup("", false, false);
}
}
@@ -3764,9 +3846,17 @@ function resetEditorState() {
};
}
function attemptCloseEditor() {
if (editorIsDirty() && !window.confirm("Discard unsaved changes?")) {
return;
async function attemptCloseEditor() {
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;
}
}
closeEditor();
}