mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-10-09 15:58:34 +00:00
a01d1f770f
* feat(sftp): add Open with system default for native Windows file association (fixes #1236) Adds a new 'Open with system default' option to the SFTP context menu that uses Electron's shell.openPath() to invoke the native OS file opening mechanism. On Windows, this uses ShellExecute, which works with UWP/WinUI apps (Photos, Paint, etc.) whose executable paths change with updates. The existing 'Open with...' (browse for executable) is preserved. Closes #1236 * fix(sftp): add file watch support to openWithSystemDefault for SFTP auto-sync P2: The new default-app open flow was not starting a file watch when SFTP auto-sync was enabled, so edits saved in the system default app were not uploaded back to the remote host. Mirrors the downloadToTempAndOpen behavior — accepts an optional { enableWatch } parameter and starts a file watch when set. * fix(sftp): mark transfer as failed when system default open fails P2: When shell.openPath fails for a remote file, the transfer queue still shows success because downloadToTemp had already completed the temp download. Now updates externalTransferId to 'failed' before throwing, matching downloadToTempAndOpen's behavior.
252 lines
10 KiB
TypeScript
252 lines
10 KiB
TypeScript
import { useEffect, useMemo, useRef } from "react";
|
|
import type { MutableRefObject } from "react";
|
|
import type { SftpStateApi } from "../../../application/state/useSftpState";
|
|
import type { RemoteFile, SftpFilenameEncoding } from "../../../types";
|
|
import type { SftpPaneCallbacks } from "../SftpContext";
|
|
import type { SftpPane } from "../../../application/state/sftp/types";
|
|
import { useSftpViewPaneActions } from "./useSftpViewPaneActions";
|
|
import { useSftpViewFileOps } from "./useSftpViewFileOps";
|
|
import type { FileOpenerType, SystemAppInfo } from "../../../lib/sftpFileUtils";
|
|
import { formatFileSize, formatDate } from '../../../application/state/sftp/utils';
|
|
import { isSessionError } from "../../../application/state/sftp/errors";
|
|
import { filterHiddenFiles } from "../utils";
|
|
|
|
interface UseSftpViewPaneCallbacksParams {
|
|
sftpRef: MutableRefObject<SftpStateApi>;
|
|
behaviorRef: MutableRefObject<string>;
|
|
autoSyncRef: MutableRefObject<boolean>;
|
|
getOpenerForFileRef: MutableRefObject<
|
|
(fileName: string) => { openerType?: FileOpenerType; systemApp?: SystemAppInfo } | null
|
|
>;
|
|
setOpenerForExtension: (
|
|
extension: string,
|
|
openerType: FileOpenerType,
|
|
systemApp?: SystemAppInfo,
|
|
) => void;
|
|
t: (key: string, vars?: Record<string, string | number>) => string;
|
|
listSftp?: (sftpId: string, path: string, encoding?: SftpFilenameEncoding) => Promise<RemoteFile[]>;
|
|
showSaveDialog?: (defaultPath: string, filters?: Array<{ name: string; extensions: string[] }>) => Promise<string | null>;
|
|
selectDirectory?: (title?: string, defaultPath?: string) => Promise<string | null>;
|
|
startStreamTransfer?: (
|
|
options: {
|
|
transferId: string;
|
|
sourcePath: string;
|
|
targetPath: string;
|
|
sourceType: 'local' | 'sftp';
|
|
targetType: 'local' | 'sftp';
|
|
sourceSftpId?: string;
|
|
targetSftpId?: string;
|
|
totalBytes?: number;
|
|
sourceEncoding?: SftpFilenameEncoding;
|
|
targetEncoding?: SftpFilenameEncoding;
|
|
},
|
|
onProgress?: (transferred: number, total: number, speed: number) => void,
|
|
onComplete?: () => void,
|
|
onError?: (error: string) => void
|
|
) => Promise<{ transferId: string; totalBytes?: number; error?: string }>;
|
|
getSftpIdForConnection?: (connectionId: string) => string | undefined;
|
|
listLocalFiles: (path: string) => Promise<RemoteFile[]>;
|
|
mkdirLocal?: (path: string) => Promise<void>;
|
|
deleteLocalFile?: (path: string) => Promise<void>;
|
|
listDrives: () => Promise<string[]>;
|
|
}
|
|
|
|
export const useSftpViewPaneCallbacks = ({
|
|
sftpRef,
|
|
behaviorRef,
|
|
autoSyncRef,
|
|
getOpenerForFileRef,
|
|
setOpenerForExtension,
|
|
t,
|
|
listSftp,
|
|
showSaveDialog,
|
|
selectDirectory,
|
|
startStreamTransfer,
|
|
getSftpIdForConnection,
|
|
listLocalFiles,
|
|
listDrives,
|
|
}: UseSftpViewPaneCallbacksParams) => {
|
|
const paneActions = useSftpViewPaneActions({ sftpRef });
|
|
const fileOps = useSftpViewFileOps({
|
|
sftpRef,
|
|
behaviorRef,
|
|
autoSyncRef,
|
|
getOpenerForFileRef,
|
|
setOpenerForExtension,
|
|
t,
|
|
showSaveDialog,
|
|
selectDirectory,
|
|
startStreamTransfer,
|
|
getSftpIdForConnection,
|
|
});
|
|
|
|
const listLocalFilesRef = useRef(listLocalFiles);
|
|
const listSftpRef = useRef(listSftp);
|
|
const getSftpIdForConnectionRef = useRef(getSftpIdForConnection);
|
|
|
|
useEffect(() => {
|
|
listLocalFilesRef.current = listLocalFiles;
|
|
listSftpRef.current = listSftp;
|
|
getSftpIdForConnectionRef.current = getSftpIdForConnection;
|
|
}, [listLocalFiles, listSftp, getSftpIdForConnection]);
|
|
|
|
const makeListDirectory = (side: "left" | "right", getPane: () => SftpPane) =>
|
|
async (path: string) => {
|
|
const pane = getPane();
|
|
if (!pane.connection) return [];
|
|
const toSize = (raw: string) => parseInt(raw) || 0;
|
|
const toTs = (raw: string) => new Date(raw).getTime();
|
|
const normalizeEntries = (rawFiles: RemoteFile[]) =>
|
|
filterHiddenFiles(
|
|
rawFiles.map(f => {
|
|
const s = toSize(f.size);
|
|
const ms = toTs(f.lastModified);
|
|
return {
|
|
name: f.name,
|
|
type: f.type as 'file' | 'directory' | 'symlink',
|
|
size: s,
|
|
sizeFormatted: formatFileSize(s),
|
|
lastModified: ms,
|
|
lastModifiedFormatted: formatDate(ms),
|
|
permissions: f.permissions,
|
|
linkTarget: f.linkTarget as 'file' | 'directory' | null | undefined,
|
|
hidden: f.hidden,
|
|
};
|
|
}),
|
|
pane.showHiddenFiles,
|
|
);
|
|
if (pane.connection.isLocal) {
|
|
return normalizeEntries(await listLocalFilesRef.current(path));
|
|
}
|
|
const sftpId = getSftpIdForConnectionRef.current?.(pane.connection.id);
|
|
if (!sftpId) {
|
|
const error = new Error("SFTP session not found");
|
|
sftpRef.current.reportSessionError(side, error);
|
|
throw error;
|
|
}
|
|
|
|
let rawFiles: RemoteFile[] | undefined;
|
|
try {
|
|
rawFiles = await listSftpRef.current?.(sftpId, path, pane.filenameEncoding);
|
|
} catch (err) {
|
|
if (isSessionError(err)) {
|
|
sftpRef.current.reportSessionError(side, err as Error);
|
|
}
|
|
throw err;
|
|
}
|
|
|
|
if (!rawFiles) return [];
|
|
return normalizeEntries(rawFiles);
|
|
};
|
|
|
|
/* eslint-disable react-hooks/exhaustive-deps -- Handlers use refs, so they are stable */
|
|
const leftCallbacks = useMemo<SftpPaneCallbacks>(
|
|
() => ({
|
|
onConnect: paneActions.onConnectLeft,
|
|
onDisconnect: paneActions.onDisconnectLeft,
|
|
onPrepareSelection: paneActions.onPrepareSelectionLeft,
|
|
onNavigateTo: paneActions.onNavigateToLeft,
|
|
onNavigateUp: paneActions.onNavigateUpLeft,
|
|
onRefresh: paneActions.onRefreshLeft,
|
|
onRefreshTab: paneActions.onRefreshTabLeft,
|
|
onSetFilenameEncoding: paneActions.onSetFilenameEncodingLeft,
|
|
onOpenEntry: fileOps.onOpenEntryLeft,
|
|
onToggleSelection: paneActions.onToggleSelectionLeft,
|
|
onRangeSelect: paneActions.onRangeSelectLeft,
|
|
onClearSelection: paneActions.onClearSelectionLeft,
|
|
onSetFilter: paneActions.onSetFilterLeft,
|
|
onCreateDirectory: paneActions.onCreateDirectoryLeft,
|
|
onCreateDirectoryAtPath: paneActions.onCreateDirectoryAtPathLeft,
|
|
onCreateFile: paneActions.onCreateFileLeft,
|
|
onCreateFileAtPath: paneActions.onCreateFileAtPathLeft,
|
|
onDeleteFiles: paneActions.onDeleteFilesLeft,
|
|
onDeleteFilesAtPath: paneActions.onDeleteFilesAtPathLeft,
|
|
onRenameFile: paneActions.onRenameFileLeft,
|
|
onRenameFileAtPath: paneActions.onRenameFileAtPathLeft,
|
|
onMoveEntriesToPath: paneActions.onMoveEntriesToPathLeft,
|
|
onCopyToOtherPane: paneActions.onCopyToOtherPaneLeft,
|
|
onReceiveFromOtherPane: paneActions.onReceiveFromOtherPaneLeft,
|
|
onEditPermissions: fileOps.onEditPermissionsLeft,
|
|
onEditFile: fileOps.onEditFileLeft,
|
|
onOpenFile: fileOps.onOpenFileLeft,
|
|
onOpenFileWithSystemDefault: fileOps.onOpenFileWithSystemDefaultLeft,
|
|
onOpenFileWith: fileOps.onOpenFileWithLeft,
|
|
onDownloadFile: fileOps.onDownloadFileLeft,
|
|
onDownloadFiles: fileOps.onDownloadFilesLeft,
|
|
onUploadExternalFiles: fileOps.onUploadExternalFilesLeft,
|
|
onUploadExternalFileList: fileOps.onUploadExternalFileListLeft,
|
|
onUploadExternalFolder: fileOps.onUploadExternalFolderLeft,
|
|
onListDirectory: makeListDirectory("left", () => sftpRef.current.leftPane),
|
|
onListDrives: listDrives,
|
|
}),
|
|
[],
|
|
);
|
|
|
|
const rightCallbacks = useMemo<SftpPaneCallbacks>(
|
|
() => ({
|
|
onConnect: paneActions.onConnectRight,
|
|
onDisconnect: paneActions.onDisconnectRight,
|
|
onPrepareSelection: paneActions.onPrepareSelectionRight,
|
|
onNavigateTo: paneActions.onNavigateToRight,
|
|
onNavigateUp: paneActions.onNavigateUpRight,
|
|
onRefresh: paneActions.onRefreshRight,
|
|
onRefreshTab: paneActions.onRefreshTabRight,
|
|
onSetFilenameEncoding: paneActions.onSetFilenameEncodingRight,
|
|
onOpenEntry: fileOps.onOpenEntryRight,
|
|
onToggleSelection: paneActions.onToggleSelectionRight,
|
|
onRangeSelect: paneActions.onRangeSelectRight,
|
|
onClearSelection: paneActions.onClearSelectionRight,
|
|
onSetFilter: paneActions.onSetFilterRight,
|
|
onCreateDirectory: paneActions.onCreateDirectoryRight,
|
|
onCreateDirectoryAtPath: paneActions.onCreateDirectoryAtPathRight,
|
|
onCreateFile: paneActions.onCreateFileRight,
|
|
onCreateFileAtPath: paneActions.onCreateFileAtPathRight,
|
|
onDeleteFiles: paneActions.onDeleteFilesRight,
|
|
onDeleteFilesAtPath: paneActions.onDeleteFilesAtPathRight,
|
|
onRenameFile: paneActions.onRenameFileRight,
|
|
onRenameFileAtPath: paneActions.onRenameFileAtPathRight,
|
|
onMoveEntriesToPath: paneActions.onMoveEntriesToPathRight,
|
|
onCopyToOtherPane: paneActions.onCopyToOtherPaneRight,
|
|
onReceiveFromOtherPane: paneActions.onReceiveFromOtherPaneRight,
|
|
onEditPermissions: fileOps.onEditPermissionsRight,
|
|
onEditFile: fileOps.onEditFileRight,
|
|
onOpenFile: fileOps.onOpenFileRight,
|
|
onOpenFileWithSystemDefault: fileOps.onOpenFileWithSystemDefaultRight,
|
|
onOpenFileWith: fileOps.onOpenFileWithRight,
|
|
onDownloadFile: fileOps.onDownloadFileRight,
|
|
onDownloadFiles: fileOps.onDownloadFilesRight,
|
|
onUploadExternalFiles: fileOps.onUploadExternalFilesRight,
|
|
onUploadExternalFileList: fileOps.onUploadExternalFileListRight,
|
|
onUploadExternalFolder: fileOps.onUploadExternalFolderRight,
|
|
onListDirectory: makeListDirectory("right", () => sftpRef.current.rightPane),
|
|
onListDrives: listDrives,
|
|
}),
|
|
[],
|
|
);
|
|
/* eslint-enable react-hooks/exhaustive-deps */
|
|
|
|
return {
|
|
leftCallbacks,
|
|
rightCallbacks,
|
|
dragCallbacks: paneActions.dragCallbacks,
|
|
draggedFiles: paneActions.draggedFiles,
|
|
permissionsState: fileOps.permissionsState,
|
|
setPermissionsState: fileOps.setPermissionsState,
|
|
showTextEditor: fileOps.showTextEditor,
|
|
setShowTextEditor: fileOps.setShowTextEditor,
|
|
textEditorTarget: fileOps.textEditorTarget,
|
|
setTextEditorTarget: fileOps.setTextEditorTarget,
|
|
textEditorContent: fileOps.textEditorContent,
|
|
setTextEditorContent: fileOps.setTextEditorContent,
|
|
loadingTextContent: fileOps.loadingTextContent,
|
|
showFileOpenerDialog: fileOps.showFileOpenerDialog,
|
|
setShowFileOpenerDialog: fileOps.setShowFileOpenerDialog,
|
|
fileOpenerTarget: fileOps.fileOpenerTarget,
|
|
setFileOpenerTarget: fileOps.setFileOpenerTarget,
|
|
handleSaveTextFile: fileOps.handleSaveTextFile,
|
|
onPromoteToTab: fileOps.onPromoteToTab,
|
|
handleFileOpenerSelect: fileOps.handleFileOpenerSelect,
|
|
handleSelectSystemApp: fileOps.handleSelectSystemApp,
|
|
};
|
|
};
|