/* eslint-disable react-hooks/exhaustive-deps */ import React, { useState, useEffect, useRef } from "react"; import { DraggableWindow } from "./DraggableWindow.tsx"; import { FileViewer } from "./FileViewer.tsx"; import { useWindowManager } from "./WindowManager.tsx"; import { downloadSSHFile, readSSHFile, writeSSHFile, getSSHStatus, connectSSH, } from "@/main-axios.ts"; import { toast } from "sonner"; import { useTranslation } from "react-i18next"; interface FileItem { name: string; type: "file" | "directory" | "link"; path: string; size?: number; modified?: string; permissions?: string; owner?: string; group?: string; } interface SSHHost { id: number; name: string; ip: string; port: number; username: string; password?: string; key?: string; keyPassword?: string; authType: "password" | "key"; credentialId?: number; userId?: number; } interface FileWindowProps { windowId: string; file: FileItem; sshSessionId: string; sshHost: SSHHost; initialX?: number; initialY?: number; onFileNotFound?: (file: FileItem) => void; } function isDisplayableText(str: string): boolean { let printable = 0; for (let i = 0; i < Math.min(str.length, 1000); i++) { const code = str.charCodeAt(i); if ( (code >= 32 && code <= 126) || code === 9 || code === 10 || code === 13 || code >= 128 ) { printable++; } } return printable / Math.min(str.length, 1000) > 0.85; } export function FileWindow({ windowId, file, sshSessionId, sshHost, initialX = 100, initialY = 100, onFileNotFound, }: FileWindowProps) { const { closeWindow, maximizeWindow, focusWindow, windows } = useWindowManager(); const { t } = useTranslation(); const [content, setContent] = useState(""); const [isLoading, setIsLoading] = useState(false); const [isSaving, setIsSaving] = useState(false); const [isEditable, setIsEditable] = useState(false); const [pendingContent, setPendingContent] = useState(""); const [resetKey, setResetKey] = useState(0); const [mediaDimensions, setMediaDimensions] = useState< { width: number; height: number } | undefined >(); const [externalEditorPath, setExternalEditorPath] = useState(""); const autoSaveTimerRef = useRef(null); const externalEditorRef = useRef<{ editId: string; unsubscribe?: () => void; } | null>(null); const currentWindow = windows.find((w) => w.id === windowId); useEffect(() => { if (!window.electronAPI?.isElectron) return; window.electronAPI .getSetting?.("fileManager.externalEditorPath") .then((value) => { if (typeof value === "string") setExternalEditorPath(value); }) .catch(() => {}); }, []); const ensureSSHConnection = async () => { try { const status = await getSSHStatus(sshSessionId); if (!status.connected) { await connectSSH(sshSessionId, { hostId: sshHost.id, ip: sshHost.ip, port: sshHost.port, username: sshHost.username, password: sshHost.password, sshKey: sshHost.key, keyPassword: sshHost.keyPassword, authType: sshHost.authType, credentialId: sshHost.credentialId, userId: sshHost.userId, }); } } catch (error) { console.error("SSH connection check/reconnect failed:", error); throw error; } }; useEffect(() => { const loadFileContent = async () => { if (file.type !== "file") return; try { setIsLoading(true); await ensureSSHConnection(); const response = await readSSHFile(sshSessionId, file.path); let fileContent = response.content || ""; if (response.encoding === "base64") { try { const bytes = Uint8Array.from(atob(fileContent), (c) => c.charCodeAt(0), ); const decoded = new TextDecoder("utf-8").decode(bytes); if (isDisplayableText(decoded)) { fileContent = decoded; } } catch (err) { console.error("Failed to decode base64 content:", err); } } setContent(fileContent); setPendingContent(fileContent); setResetKey((k) => k + 1); if (!file.size) { const contentSize = new Blob([fileContent]).size; file.size = contentSize; } const mediaExtensions = [ "jpg", "jpeg", "png", "gif", "bmp", "svg", "webp", "tiff", "ico", "mp3", "wav", "ogg", "aac", "flac", "m4a", "wma", "mp4", "avi", "mov", "wmv", "flv", "mkv", "webm", "m4v", "zip", "rar", "7z", "tar", "gz", "bz2", "xz", "exe", "dll", "so", "dylib", "bin", "iso", ]; const extension = file.name.split(".").pop()?.toLowerCase(); setIsEditable(!mediaExtensions.includes(extension || "")); } catch (error: unknown) { console.error("Failed to load file:", error); const err = error as { message?: string; isFileNotFound?: boolean; response?: { status?: number; data?: { tooLarge?: boolean; error?: string; fileNotFound?: boolean; }; }; }; const errorData = err?.response?.data; if (errorData?.tooLarge) { toast.error(`File too large: ${errorData.error}`, { duration: 10000, }); } else if ( err.message?.includes("connection") || err.message?.includes("established") ) { toast.error( `SSH connection failed. Please check your connection to ${sshHost.name} (${sshHost.ip}:${sshHost.port})`, ); } else { const errorMessage = errorData?.error || err.message || "Unknown error"; const isFileNotFound = err.isFileNotFound || errorData?.fileNotFound || err.response?.status === 404 || errorMessage.includes("File not found") || errorMessage.includes("No such file or directory") || errorMessage.includes("cannot access") || errorMessage.includes("not found") || errorMessage.includes("Resource not found"); if (isFileNotFound && onFileNotFound) { onFileNotFound(file); toast.error( t("fileManager.fileNotFoundAndRemoved", { name: file.name }), ); closeWindow(windowId); return; } else { toast.error( t("fileManager.failedToLoadFile", { error: errorMessage.includes("Server error occurred") ? t("fileManager.serverErrorOccurred") : errorMessage, }), ); } } } finally { setIsLoading(false); } }; loadFileContent(); }, [file, sshSessionId, sshHost]); const handleRevert = async () => { const loadFileContent = async () => { if (file.type !== "file") return; try { setIsLoading(true); await ensureSSHConnection(); const response = await readSSHFile(sshSessionId, file.path); const fileContent = response.content || ""; setContent(fileContent); setPendingContent(""); setResetKey((k) => k + 1); if (!file.size) { const contentSize = new Blob([fileContent]).size; file.size = contentSize; } } catch (error: unknown) { console.error("Failed to load file content:", error); const err = error as { message?: string }; toast.error( `${t("fileManager.failedToLoadFile")}: ${err.message || t("fileManager.unknownError")}`, ); } finally { setIsLoading(false); } }; loadFileContent(); }; const handleSave = async (newContent: string) => { try { setIsSaving(true); await ensureSSHConnection(); await writeSSHFile(sshSessionId, file.path, newContent); setContent(newContent); setPendingContent(""); if (autoSaveTimerRef.current) { clearTimeout(autoSaveTimerRef.current); autoSaveTimerRef.current = null; } toast.success(t("fileManager.fileSavedSuccessfully")); } catch (error: unknown) { console.error("Failed to save file:", error); const err = error as { message?: string }; if ( err.message?.includes("connection") || err.message?.includes("established") ) { toast.error( `SSH connection failed. Please check your connection to ${sshHost.name} (${sshHost.ip}:${sshHost.port})`, ); } else { toast.error( `${t("fileManager.failedToSaveFile")}: ${err.message || t("fileManager.unknownError")}`, ); } } finally { setIsSaving(false); } }; const closeExternalEditor = async () => { const session = externalEditorRef.current; if (!session) return; session.unsubscribe?.(); externalEditorRef.current = null; try { await window.electronAPI?.closeExternalEditor?.(session.editId); } catch (error) { console.error("Failed to close external editor session:", error); } }; const handleOpenExternalEditor = async () => { if (!window.electronAPI?.isElectron) { toast.error(t("fileManager.externalEditorDesktopOnly")); return; } try { await closeExternalEditor(); const result = await window.electronAPI.openExternalEditor({ fileName: file.name, content: pendingContent || content, encoding: "utf8", editorPath: externalEditorPath || null, }); if (!result.success || !result.editId) { throw new Error(result.error || "Failed to open external editor"); } const unsubscribe = window.electronAPI.onExternalEditorSaved?.( (payload) => { if (payload.editId !== result.editId) return; void handleSave(payload.content); }, ); externalEditorRef.current = { editId: result.editId, unsubscribe, }; toast.success(t("fileManager.externalEditorOpened")); } catch (error: unknown) { console.error("Failed to open external editor:", error); const err = error as { message?: string }; toast.error( `${t("fileManager.failedToOpenExternalEditor")}: ${ err.message || t("fileManager.unknownError") }`, ); } }; const handleChooseExternalEditor = async () => { if (!window.electronAPI?.isElectron) { toast.error(t("fileManager.externalEditorDesktopOnly")); return; } try { const result = await window.electronAPI.showOpenDialog({ title: t("fileManager.chooseExternalEditor"), properties: ["openFile"], }); const editorPath = result.filePaths?.[0]; if (result.canceled || !editorPath) return; await window.electronAPI.setSetting?.( "fileManager.externalEditorPath", editorPath, ); setExternalEditorPath(editorPath); toast.success(t("fileManager.externalEditorSelected")); } catch (error: unknown) { console.error("Failed to select external editor:", error); const err = error as { message?: string }; toast.error( `${t("fileManager.failedToSelectExternalEditor")}: ${ err.message || t("fileManager.unknownError") }`, ); } }; const handleContentChange = (newContent: string) => { setPendingContent(newContent); if (autoSaveTimerRef.current) { clearTimeout(autoSaveTimerRef.current); autoSaveTimerRef.current = null; } if (newContent !== content) { autoSaveTimerRef.current = setTimeout(async () => { try { await handleSave(newContent); toast.success(t("fileManager.fileAutoSaved")); } catch (error) { console.error("Auto-save failed:", error); toast.error(t("fileManager.autoSaveFailed")); } }, 60000); } }; useEffect(() => { return () => { if (autoSaveTimerRef.current) { clearTimeout(autoSaveTimerRef.current); } void closeExternalEditor(); }; }, []); const handleDownload = async () => { try { await ensureSSHConnection(); const response = await downloadSSHFile(sshSessionId, file.path); if (response?.content) { const byteCharacters = atob(response.content); const byteNumbers = new Array(byteCharacters.length); for (let i = 0; i < byteCharacters.length; i++) { byteNumbers[i] = byteCharacters.charCodeAt(i); } const byteArray = new Uint8Array(byteNumbers); const blob = new Blob([byteArray], { type: response.mimeType || "application/octet-stream", }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = response.fileName || file.name; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); toast.success(t("fileManager.fileDownloadedSuccessfully")); } } catch (error: unknown) { console.error("Failed to download file:", error); const err = error as { message?: string }; if ( err.message?.includes("connection") || err.message?.includes("established") ) { toast.error( `SSH connection failed. Please check your connection to ${sshHost.name} (${sshHost.ip}:${sshHost.port})`, ); } else { toast.error( `Failed to download file: ${err.message || "Unknown error"}`, ); } } }; const handleClose = () => { void closeExternalEditor(); closeWindow(windowId); }; const handleMaximize = () => { maximizeWindow(windowId); }; const handleFocus = () => { focusWindow(windowId); }; const handleMediaDimensionsChange = (dimensions: { width: number; height: number; }) => { setMediaDimensions(dimensions); }; if (!currentWindow) { return null; } return ( handleSave(newContent)} onDownload={handleDownload} onOpenExternal={ window.electronAPI?.isElectron && isEditable ? handleOpenExternalEditor : undefined } onChooseExternalEditor={ window.electronAPI?.isElectron && isEditable ? handleChooseExternalEditor : undefined } onMediaDimensionsChange={handleMediaDimensionsChange} /> ); }