This commit is contained in:
kodi
2026-03-11 15:25:32 +01:00
parent 6a1a575383
commit d1f018a130
22 changed files with 909 additions and 49 deletions
+300 -17
View File
@@ -1,7 +1,7 @@
let state = {
panes: {
left: {
currentPath: "storage1",
currentPath: "/Volumes",
showHidden: false,
selectedItem: null,
selectedItems: [],
@@ -9,7 +9,7 @@ let state = {
currentRowIndex: -1,
},
right: {
currentPath: "storage1",
currentPath: "/Volumes",
showHidden: false,
selectedItem: null,
selectedItems: [],
@@ -28,6 +28,14 @@ let editorState = {
originalContent: "",
modified: null,
};
let renameMoveState = {
source: null,
destination: "",
};
let batchMoveState = {
destinationBase: "",
count: 0,
};
function paneState(pane) {
return state.panes[pane];
@@ -89,6 +97,28 @@ function editorElements() {
};
}
function renameMoveElements() {
return {
overlay: document.getElementById("rename-move-popup"),
source: document.getElementById("rename-move-source"),
input: document.getElementById("rename-move-input"),
error: document.getElementById("rename-move-error"),
applyButton: document.getElementById("rename-move-apply-btn"),
cancelButton: document.getElementById("rename-move-cancel-btn"),
};
}
function batchMoveElements() {
return {
overlay: document.getElementById("batch-move-popup"),
count: document.getElementById("batch-move-count"),
destination: document.getElementById("batch-move-destination"),
error: document.getElementById("batch-move-error"),
applyButton: document.getElementById("batch-move-apply-btn"),
cancelButton: document.getElementById("batch-move-cancel-btn"),
};
}
async function apiRequest(method, url, body) {
const options = { method, headers: {} };
if (body !== undefined) {
@@ -181,7 +211,7 @@ function updateActionButtons() {
document.getElementById("rename-btn").disabled = !exactlyOne;
document.getElementById("delete-btn").disabled = !hasSelection;
document.getElementById("copy-btn").disabled = !allFiles;
document.getElementById("move-btn").disabled = !allFiles;
document.getElementById("move-btn").disabled = !hasSelection;
}
function isEditableSelection(item) {
@@ -197,23 +227,66 @@ function isEditableSelection(item) {
}
function currentParentPath(path) {
if (!path.includes("/")) {
const normalized = (path || "").trim();
if (!normalized) {
return null;
}
const segments = path.split("/");
if (normalized === "/Volumes") {
return null;
}
if (normalized.startsWith("/")) {
const segments = normalized.split("/").filter(Boolean);
if (segments.length <= 1) {
return null;
}
if (segments.length === 2) {
return `/${segments[0]}`;
}
return `/${segments.slice(0, -1).join("/")}`;
}
if (!normalized.includes("/")) {
return null;
}
const segments = normalized.split("/");
if (segments.length === 2) {
return segments[0];
}
return segments.slice(0, -1).join("/");
}
function baseName(path) {
const index = path.lastIndexOf("/");
return index >= 0 ? path.slice(index + 1) : path;
}
function renderBreadcrumbs(pane, path) {
const nav = document.getElementById(`${pane}-breadcrumbs`);
nav.innerHTML = "";
const parts = path.split("/");
let aggregate = "";
const normalized = (path || "").trim();
const isHostPath = normalized.startsWith("/");
const parts = normalized.split("/").filter(Boolean);
if (isHostPath) {
const rootCrumb = createButton("/", () => {
setActivePane(pane);
navigateTo(pane, "/Volumes");
});
rootCrumb.type = "button";
rootCrumb.onclick = (ev) => {
ev.preventDefault();
ev.stopPropagation();
setActivePane(pane);
navigateTo(pane, "/Volumes");
};
nav.append(rootCrumb);
if (parts.length > 0) {
const sep = document.createElement("span");
sep.textContent = "/";
nav.append(sep);
}
}
let aggregate = isHostPath ? "" : "";
for (let i = 0; i < parts.length; i += 1) {
aggregate = i === 0 ? parts[i] : `${aggregate}/${parts[i]}`;
aggregate = isHostPath ? `/${parts.slice(0, i + 1).join("/")}` : (i === 0 ? parts[i] : `${aggregate}/${parts[i]}`);
const crumbPath = aggregate;
const crumb = createButton(parts[i], () => {
setActivePane(pane);
@@ -552,7 +625,7 @@ async function deleteSelected() {
}
function defaultDestination(sourcePath, targetBasePath) {
const sourceName = sourcePath.slice(sourcePath.lastIndexOf("/") + 1);
const sourceName = baseName(sourcePath);
return `${targetBasePath}/${sourceName}`;
}
@@ -593,13 +666,15 @@ async function startCopySelected() {
}
async function startMoveSelected() {
await executeMoveSelection(paneState(otherPane(state.activePane)).currentPath);
}
async function executeMoveSelection(baseDestination) {
const sourcePane = state.activePane;
const destinationPane = otherPane(sourcePane);
const selectedItems = [...paneState(sourcePane).selectedItems];
if (selectedItems.length === 0) {
return;
}
const baseDestination = paneState(destinationPane).currentPath;
setError("actions-error", "");
let successes = 0;
let failures = 0;
@@ -690,7 +765,7 @@ function actionShortcutHandled(event) {
return triggerActionButton("copy-btn");
}
if (event.key === "F6") {
return triggerActionButton("move-btn");
return openF6Flow();
}
if (event.key === "F7") {
return triggerActionButton("mkdir-btn");
@@ -712,7 +787,7 @@ function actionShortcutHandled(event) {
return triggerActionButton("copy-btn");
}
if (key === "6") {
return triggerActionButton("move-btn");
return openF6Flow();
}
if (key === "7") {
return triggerActionButton("mkdir-btn");
@@ -720,9 +795,6 @@ function actionShortcutHandled(event) {
if (key === "8") {
return triggerActionButton("delete-btn");
}
if (key === "r") {
return triggerActionButton("rename-btn");
}
}
return false;
@@ -744,6 +816,14 @@ function isWildcardPopupOpen() {
return !wildcardPopupElements().overlay.classList.contains("hidden");
}
function isRenameMovePopupOpen() {
return !renameMoveElements().overlay.classList.contains("hidden");
}
function isBatchMovePopupOpen() {
return !batchMoveElements().overlay.classList.contains("hidden");
}
function isViewerOpen() {
return !viewerElements().overlay.classList.contains("hidden");
}
@@ -806,6 +886,155 @@ function closeWildcardPopup() {
elements.input.value = "";
}
function showDirectoryMoveNotSupported() {
const message = "Directory move is not supported in v1";
setError("actions-error", message);
setStatus(message);
}
function resetRenameMoveState() {
renameMoveState = {
source: null,
destination: "",
};
}
function resetBatchMoveState() {
batchMoveState = {
destinationBase: "",
count: 0,
};
}
function closeRenameMovePopup() {
const elements = renameMoveElements();
elements.overlay.classList.add("hidden");
elements.error.textContent = "";
elements.input.value = "";
resetRenameMoveState();
}
function closeBatchMovePopup() {
const elements = batchMoveElements();
elements.overlay.classList.add("hidden");
elements.error.textContent = "";
resetBatchMoveState();
}
function openRenameMovePopup() {
const selectedItems = activePaneState().selectedItems;
if (selectedItems.length !== 1) {
return false;
}
const source = selectedItems[0];
const destination = defaultDestination(source.path, paneState(otherPane(state.activePane)).currentPath);
const elements = renameMoveElements();
renameMoveState.source = source;
renameMoveState.destination = destination;
elements.source.textContent = `Source: ${source.path}`;
elements.input.value = destination;
elements.error.textContent = "";
elements.overlay.classList.remove("hidden");
elements.input.focus();
elements.input.select();
return true;
}
function openBatchMovePopup(selectedItems) {
if (selectedItems.length === 0) {
return false;
}
const destinationBase = paneState(otherPane(state.activePane)).currentPath;
const elements = batchMoveElements();
batchMoveState.destinationBase = destinationBase;
batchMoveState.count = selectedItems.length;
elements.count.textContent = `${selectedItems.length} selected item(s)`;
elements.destination.textContent = `Destination: ${destinationBase}`;
elements.error.textContent = "";
elements.overlay.classList.remove("hidden");
elements.applyButton.focus();
return true;
}
function openF6Flow() {
const selectedItems = activePaneState().selectedItems;
if (selectedItems.length === 0) {
return false;
}
if (selectedItems.length === 1) {
return openRenameMovePopup();
}
if (selectedItems.some((item) => item.kind !== "file")) {
showDirectoryMoveNotSupported();
return true;
}
return openBatchMovePopup(selectedItems);
}
async function submitRenameMovePopup() {
const elements = renameMoveElements();
const source = renameMoveState.source;
if (!source) {
return;
}
const destination = elements.input.value.trim();
const sourceParent = currentParentPath(source.path);
const destinationParent = currentParentPath(destination);
const destinationName = baseName(destination);
elements.error.textContent = "";
if (!destination) {
elements.error.textContent = "Destination path is required";
return;
}
if (destination === source.path) {
elements.error.textContent = "Destination must differ from source";
return;
}
if (source.kind === "directory" && destinationParent !== sourceParent) {
elements.error.textContent = "Directory move is not supported in v1";
return;
}
try {
if (destinationParent === sourceParent) {
await apiRequest("POST", "/api/files/rename", {
path: source.path,
new_name: destinationName,
});
closeRenameMovePopup();
setSelectedItem(state.activePane, null);
await loadBrowsePane(state.activePane);
setStatus(`Renamed ${source.path}`);
return;
}
const result = await apiRequest("POST", "/api/files/move", {
source: source.path,
destination,
});
state.selectedTaskId = result.task_id;
await refreshTasksSnapshot();
closeRenameMovePopup();
setSelectedItem(state.activePane, null);
await Promise.all([loadBrowsePane("left"), loadBrowsePane("right")]);
setStatus(`Move: 1 success, 0 failed`);
} catch (err) {
elements.error.textContent = err.message;
}
}
async function submitBatchMovePopup() {
const elements = batchMoveElements();
elements.error.textContent = "";
try {
await executeMoveSelection(batchMoveState.destinationBase);
closeBatchMovePopup();
} catch (err) {
elements.error.textContent = err.message;
}
}
function submitWildcardPopup() {
const elements = wildcardPopupElements();
const pattern = elements.input.value.trim();
@@ -1003,6 +1232,31 @@ function clearSelectionForActivePane() {
}
function handleKeyboardShortcuts(event) {
if (isBatchMovePopupOpen()) {
if (event.key === "Escape") {
event.preventDefault();
closeBatchMovePopup();
return;
}
if (event.key === "Enter") {
event.preventDefault();
submitBatchMovePopup();
return;
}
return;
}
if (isRenameMovePopupOpen()) {
if (event.key === "Escape") {
event.preventDefault();
closeRenameMovePopup();
return;
}
if (event.key === "Enter") {
event.preventDefault();
submitRenameMovePopup();
}
return;
}
if (isEditorOpen()) {
if (event.key === "Escape") {
event.preventDefault();
@@ -1108,7 +1362,7 @@ function setupEvents() {
document.getElementById("rename-btn").onclick = renameSelected;
document.getElementById("delete-btn").onclick = deleteSelected;
document.getElementById("copy-btn").onclick = startCopySelected;
document.getElementById("move-btn").onclick = startMoveSelected;
document.getElementById("move-btn").onclick = openF6Flow;
document.getElementById("mkdir-btn").onclick = createFolderForActivePane;
const wildcard = wildcardPopupElements();
@@ -1131,6 +1385,35 @@ function setupEvents() {
}
};
const renameMove = renameMoveElements();
renameMove.cancelButton.onclick = closeRenameMovePopup;
renameMove.applyButton.onclick = submitRenameMovePopup;
renameMove.input.onkeydown = (event) => {
if (event.key === "Enter") {
event.preventDefault();
submitRenameMovePopup();
return;
}
if (event.key === "Escape") {
event.preventDefault();
closeRenameMovePopup();
}
};
renameMove.overlay.onclick = (event) => {
if (event.target === renameMove.overlay) {
closeRenameMovePopup();
}
};
const batchMove = batchMoveElements();
batchMove.cancelButton.onclick = closeBatchMovePopup;
batchMove.applyButton.onclick = submitBatchMovePopup;
batchMove.overlay.onclick = (event) => {
if (event.target === batchMove.overlay) {
closeBatchMovePopup();
}
};
const viewer = viewerElements();
viewer.closeButton.onclick = closeViewer;
viewer.overlay.onclick = (event) => {