Files
NetMesh/components/sftp/clipboardUpload.ts

165 lines
4.9 KiB
TypeScript
Raw Permalink Normal View History

import type { SftpFileEntry } from "../../types";
import type { DropEntry } from "../../lib/sftpFileUtils";
import type { KeyBinding } from "../../domain/models";
import { joinPath } from "../../application/state/sftp/utils";
import { isNavigableDirectory } from "./utils";
export interface ClipboardLocalFile {
path: string;
name: string;
isDirectory: boolean;
size?: number;
}
export interface SftpClipboardUploadTreeSelection {
name: string;
path: string;
isDirectory: boolean;
}
export interface ResolveSftpClipboardUploadTargetParams {
currentPath: string;
selectedFileNames: string[];
files: SftpFileEntry[];
treeSelection: SftpClipboardUploadTreeSelection[];
}
export interface GetSftpClipboardSystemTextPathsParams {
currentPath: string;
selectedFileNames: string[];
treeSelection: SftpClipboardUploadTreeSelection[];
}
export function resolveSftpClipboardUploadTarget({
currentPath,
selectedFileNames,
files,
treeSelection,
}: ResolveSftpClipboardUploadTargetParams): string {
const selectedTreeFolders = treeSelection.filter((entry) => entry.isDirectory && entry.name !== "..");
if (selectedTreeFolders.length === 1) {
return selectedTreeFolders[0].path;
}
if (selectedFileNames.length === 1) {
const filesByName = new Map(files.map((entry) => [entry.name, entry]));
const selectedEntry = filesByName.get(selectedFileNames[0]);
if (selectedEntry && isNavigableDirectory(selectedEntry)) {
return joinPath(currentPath, selectedEntry.name);
}
}
return currentPath;
}
export function getSftpClipboardSystemTextPaths({
currentPath,
selectedFileNames,
treeSelection,
}: GetSftpClipboardSystemTextPathsParams): string[] {
if (treeSelection.length > 0) {
return treeSelection.map((entry) => entry.path);
}
return selectedFileNames.map((name) => joinPath(currentPath, name));
}
export function createDropEntriesFromClipboardFiles(files: ClipboardLocalFile[]): DropEntry[] {
return files.map((file) => ({
file: null,
localPath: file.path,
relativePath: file.name,
isDirectory: file.isDirectory,
size: file.size,
}));
}
export function getSupportedClipboardUploadFiles(files: ClipboardLocalFile[]): ClipboardLocalFile[] {
return files;
}
export function shouldLetNativePasteEventHandleSftpPaste(
action: string,
key: string | undefined,
): boolean {
if (action !== "sftpPaste" || !key) return false;
const normalized = key.toLowerCase().replace(/\s+/g, "");
return [
"ctrl+v",
"⌘+v",
"cmd+v",
"command+v",
].includes(normalized);
}
export function isSftpNativeClipboardPasteEnabled(
hotkeyScheme: "disabled" | "mac" | "pc",
keyBindings: KeyBinding[],
): boolean {
if (hotkeyScheme === "disabled") return false;
const pasteBinding = keyBindings.find((binding) => (
binding.category === "sftp" && binding.action === "sftpPaste"
));
if (!pasteBinding) return false;
const key = hotkeyScheme === "mac" ? pasteBinding.mac : pasteBinding.pc;
return shouldLetNativePasteEventHandleSftpPaste("sftpPaste", key);
}
export interface SftpClipboardUploadRequest {
scopeId: string;
side: "left" | "right";
targetPath: string;
files: ClipboardLocalFile[];
onConfirm: () => Promise<void>;
}
type ClipboardUploadListener = () => void;
let clipboardUploadRequest: SftpClipboardUploadRequest | null = null;
const clipboardUploadListeners = new Set<ClipboardUploadListener>();
const notifyClipboardUploadListeners = () => {
clipboardUploadListeners.forEach((listener) => listener());
};
export const sftpClipboardUploadStore = {
trigger: (request: SftpClipboardUploadRequest) => {
clipboardUploadRequest = request;
notifyClipboardUploadListeners();
},
clear: (request?: SftpClipboardUploadRequest | null) => {
if (request && clipboardUploadRequest !== request) return;
clipboardUploadRequest = null;
notifyClipboardUploadListeners();
},
getSnapshot: () => clipboardUploadRequest,
subscribe: (listener: ClipboardUploadListener) => {
clipboardUploadListeners.add(listener);
return () => clipboardUploadListeners.delete(listener);
},
};
/**
* Hand off a confirmed clipboard upload without holding the modal open.
* Clears the active request first, then runs the transfer (issue #2478).
*/
export async function confirmSftpClipboardUpload(params: {
request: SftpClipboardUploadRequest;
clear?: (request: SftpClipboardUploadRequest) => void;
onUploaded?: (targetPath: string) => void;
}): Promise<void> {
const { request, onUploaded } = params;
const clear = params.clear ?? sftpClipboardUploadStore.clear;
clear(request);
await request.onConfirm();
onUploaded?.(request.targetPath);
}
/** True when Upload should start for this request (blocks same-request double-click only). */
export function shouldStartClipboardUploadConfirm(
request: SftpClipboardUploadRequest | null | undefined,
alreadyStartedFor: SftpClipboardUploadRequest | null | undefined,
): boolean {
return !!request && alreadyStartedFor !== request;
}