Volumes
This commit is contained in:
+300
-17
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user