"use client"; import { useMemo } from "react"; import { toast } from "sonner"; import { buildDownloadUrl, buildZipUrl, toFilePath, } from "@/modules/files/hooks/useFiles"; import type { FileInfoResponse } from "@/lib/shared/contracts/files"; import type { FileEntry } from "@/modules/files/components/file-manager-presenters"; import type { CreateEntryDialogState, FileManagerAction, OpenFileState, RenameDialogState, } from "@/modules/files/components/manager/file-manager-state"; type AsyncMutation = { mutateAsync: (args: TArgs) => Promise; }; export type UseFileManagerEntryActionsArgs = { createEntryDialog: CreateEntryDialogState | null; createFileMutation: AsyncMutation<{ parentPath: string; name: string }, { path: string }>; createFolderMutation: AsyncMutation<{ parentPath: string; name: string }, { path: string }>; createLocalShareMutation: AsyncMutation<{ path: string }, { sharedPath: string }>; currentPath: string[]; deleteFromTrashMutation: AsyncMutation<{ path: string }>; deleteLocalShareMutation: AsyncMutation; dispatch: (action: FileManagerAction) => void; emptyTrashMutation: AsyncMutation; fileDrafts: Record; filesRootPath: string; getFileInfoMutation: AsyncMutation; includeHidden: boolean; isEmptyingTrash: boolean; isStarredView: boolean; isTrashView: boolean; moveToTrashMutation: AsyncMutation<{ path: string }, { trashPath: string }>; openFile: OpenFileState | null; openFileKey: string | null; openFileViewer: { mode: string; content?: string | null; mtimeMs: number } | null; renameDialog: RenameDialogState | null; renameFileEntryMutation: AsyncMutation<{ path: string; newName: string }, { path: string }>; restoreFromTrashMutation: AsyncMutation<{ path: string; collision: "keep-both" }, { restoredPath: string }>; saveFileContentMutation: AsyncMutation<{ path: string; content: string; expectedMtimeMs: number }>; selectedFiles: Set; setStatusNotice: (message: string | null) => void; sortedEntries: FileEntry[]; systemHostname: string; systemNetworkAddress: string; toggleStarMutation: AsyncMutation; uploadAbortControllerRef: { current: AbortController | null }; uploadFilesMutation: AsyncMutation<{ destinationPath: string; files: File[]; includeHidden: boolean; onProgress: (loaded: number, total: number) => void; signal: AbortSignal; }, { uploaded: unknown[]; skipped: unknown[] }>; unzipFileMutation: AsyncMutation; }; export function useFileManagerEntryActions({ createEntryDialog, createFileMutation, createFolderMutation, createLocalShareMutation, currentPath, deleteFromTrashMutation, deleteLocalShareMutation, dispatch, emptyTrashMutation, fileDrafts, filesRootPath, getFileInfoMutation, includeHidden, isEmptyingTrash, isStarredView, isTrashView, moveToTrashMutation, openFile, openFileKey, openFileViewer, renameDialog, renameFileEntryMutation, restoreFromTrashMutation, saveFileContentMutation, selectedFiles, setStatusNotice, sortedEntries, systemHostname, systemNetworkAddress, toggleStarMutation, uploadAbortControllerRef, uploadFilesMutation, unzipFileMutation, }: UseFileManagerEntryActionsArgs) { return useMemo(() => { async function handleTrashSelected() { const names = [...selectedFiles]; if (names.length === 0) return; dispatch({ type: "CLEAR_SELECTION" }); for (const name of names) { const entry = sortedEntries.find((candidate) => candidate.name === name); if (!entry) continue; try { await moveToTrashMutation.mutateAsync({ path: entry.path }); } catch { // Errors surface through mutation state. } } } async function handleMoveSelectedToTrash(entry: FileEntry) { dispatch({ type: "SET_PENDING_ENTRY_PATH", path: entry.path }); try { const result = await moveToTrashMutation.mutateAsync({ path: entry.path }); if (openFile && toFilePath(openFile.path) === entry.path) dispatch({ type: "CLOSE_FILE" }); dispatch({ type: "SELECT_FILE", name: null }); setStatusNotice(`Moved to Trash: ${result.trashPath}`); } catch (error) { setStatusNotice(error instanceof Error ? error.message : "Failed to move item to Trash"); } finally { dispatch({ type: "SET_PENDING_ENTRY_PATH", path: null }); } } async function handleRestoreFromTrash(entry: FileEntry) { dispatch({ type: "SET_PENDING_ENTRY_PATH", path: entry.path }); try { const result = await restoreFromTrashMutation.mutateAsync({ path: entry.path, collision: "keep-both", }); dispatch({ type: "SELECT_FILE", name: null }); setStatusNotice(`Restored: ${result.restoredPath}`); } catch (error) { setStatusNotice(error instanceof Error ? error.message : "Failed to restore item from Trash"); } finally { dispatch({ type: "SET_PENDING_ENTRY_PATH", path: null }); } } async function handleDeleteFromTrash(entry: FileEntry) { dispatch({ type: "SET_PENDING_ENTRY_PATH", path: entry.path }); try { await deleteFromTrashMutation.mutateAsync({ path: entry.path }); dispatch({ type: "SELECT_FILE", name: null }); setStatusNotice(`Deleted permanently: ${entry.name}`); } catch (error) { setStatusNotice(error instanceof Error ? error.message : "Failed to permanently delete item"); } finally { dispatch({ type: "SET_PENDING_ENTRY_PATH", path: null }); } } function handleEmptyTrash() { if (!isTrashView || sortedEntries.length === 0 || isEmptyingTrash) return; dispatch({ type: "SHOW_EMPTY_TRASH_CONFIRM" }); } async function confirmEmptyTrash() { dispatch({ type: "HIDE_EMPTY_TRASH_CONFIRM" }); dispatch({ type: "SET_IS_EMPTYING_TRASH", value: true }); try { const result = await emptyTrashMutation.mutateAsync(undefined); setStatusNotice(`Trash emptied (${result.deletedCount} item${result.deletedCount !== 1 ? "s" : ""})`); } catch (error) { setStatusNotice(error instanceof Error ? error.message : "Failed to empty Trash"); } finally { dispatch({ type: "SET_IS_EMPTYING_TRASH", value: false }); } } async function submitCreateEntryDialog() { if (!createEntryDialog) return; const name = createEntryDialog.name.trim(); if (!name) { dispatch({ type: "SET_CREATE_ENTRY_ERROR", error: `Invalid ${createEntryDialog.kind} name` }); return; } const parentPath = toFilePath(currentPath); try { const result = createEntryDialog.kind === "folder" ? await createFolderMutation.mutateAsync({ parentPath, name }) : await createFileMutation.mutateAsync({ parentPath, name }); dispatch({ type: "CLOSE_CREATE_ENTRY_DIALOG" }); setStatusNotice(`${createEntryDialog.kind === "folder" ? "Folder" : "File"} created: ${result.path}`); } catch (error) { const message = error instanceof Error ? error.message : `Failed to create ${createEntryDialog.kind}`; dispatch({ type: "SET_CREATE_ENTRY_ERROR", error: message }); setStatusNotice(message); } } function handleRenameEntry(entry: FileEntry) { if (isTrashView) { setStatusNotice("Rename is disabled in Trash"); return; } dispatch({ type: "OPEN_RENAME_DIALOG", entry }); } async function submitRenameDialog() { if (!renameDialog) return; const newName = renameDialog.name.trim(); if (!newName || newName === renameDialog.entry.name) { dispatch({ type: "CLOSE_RENAME_DIALOG" }); return; } try { const result = await renameFileEntryMutation.mutateAsync({ path: renameDialog.entry.path, newName, }); dispatch({ type: "CLOSE_RENAME_DIALOG" }); dispatch({ type: "SELECT_FILE", name: null }); setStatusNotice(`Renamed to ${result.path.split("/").pop() ?? newName}`); } catch (error) { dispatch({ type: "SET_RENAME_ERROR", error: error instanceof Error ? error.message : "Failed to rename item", }); } } async function handleGetInfo(entry: FileEntry) { try { const info = await getFileInfoMutation.mutateAsync(entry.path); dispatch({ type: "OPEN_FILE_INFO_DIALOG", info }); } catch (error) { setStatusNotice(error instanceof Error ? error.message : "Failed to get item info"); } } async function handleToggleStar(entry: FileEntry) { try { const result = await toggleStarMutation.mutateAsync(entry.path); setStatusNotice(result.starred ? `Starred ${entry.name}` : `Unstarred ${entry.name}`); } catch (error) { setStatusNotice(error instanceof Error ? error.message : "Failed to toggle star"); } } async function handleUploadFiles(files: File[]) { if (files.length === 0 || isTrashView || isStarredView) return; const totalSize = files.reduce((sum, file) => sum + file.size, 0); uploadAbortControllerRef.current?.abort(); const controller = new AbortController(); uploadAbortControllerRef.current = controller; dispatch({ type: "SET_UPLOAD_PROGRESS", loaded: 0, total: totalSize }); const isCurrentUpload = () => uploadAbortControllerRef.current === controller; try { const result = await uploadFilesMutation.mutateAsync({ destinationPath: toFilePath(currentPath), files, includeHidden, onProgress: (loaded, total) => { dispatch({ type: "SET_UPLOAD_PROGRESS", loaded, total: total || totalSize }); }, signal: controller.signal, }); if (!isCurrentUpload()) return; dispatch({ type: "CLEAR_UPLOAD_PROGRESS" }); const uploaded = result.uploaded.length; const skipped = result.skipped.length; if (uploaded > 0 && skipped === 0) setStatusNotice(`Uploaded ${uploaded} file${uploaded !== 1 ? "s" : ""}`); else if (uploaded > 0) setStatusNotice(`Uploaded ${uploaded}, skipped ${skipped} (already exist)`); else setStatusNotice(`Skipped ${skipped} file${skipped !== 1 ? "s" : ""} (already exist)`); } catch (error) { if (!isCurrentUpload()) return; dispatch({ type: "CLEAR_UPLOAD_PROGRESS" }); if (error instanceof Error && error.name === "AbortError") { setStatusNotice("Upload canceled"); } else { setStatusNotice(error instanceof Error ? error.message : "Upload failed"); } } finally { if (uploadAbortControllerRef.current === controller) { uploadAbortControllerRef.current = null; } } } function handleDownloadEntry(entry: FileEntry) { window.open(entry.type === "folder" ? buildZipUrl(entry.path) : buildDownloadUrl(entry.path), "_blank", "noopener,noreferrer"); } async function handleUnzipEntry(entry: FileEntry) { try { await unzipFileMutation.mutateAsync(entry.path); setStatusNotice("Unzipped successfully"); } catch (error) { setStatusNotice(error instanceof Error ? error.message : "Failed to unzip"); } } async function handleShareFolder(entry: FileEntry) { try { const result = await createLocalShareMutation.mutateAsync({ path: entry.path }); setStatusNotice(`Shared over network: /${result.sharedPath}`); } catch (error) { setStatusNotice(error instanceof Error ? error.message : "Failed to share folder"); } } async function handleUnshareFolder(shareId: string) { try { await deleteLocalShareMutation.mutateAsync(shareId); setStatusNotice("Shared folder removed"); } catch (error) { setStatusNotice(error instanceof Error ? error.message : "Failed to remove shared folder"); } } async function handleCopyEntryPath(entry: FileEntry) { const basePath = filesRootPath ? `${filesRootPath}/${entry.path}` : `/${entry.path}`; const networkBase = systemNetworkAddress || systemHostname; const fullPath = entry.path === "Shared" || entry.path.startsWith("Shared/") ? networkBase ? `smb://${networkBase}${basePath}` : basePath : basePath; try { if (navigator.clipboard?.writeText) await navigator.clipboard.writeText(fullPath); else { const el = document.createElement("textarea"); el.value = fullPath; el.style.position = "fixed"; el.style.opacity = "0"; document.body.appendChild(el); el.select(); const ok = document.execCommand("copy"); document.body.removeChild(el); if (!ok) throw new Error("execCommand failed"); } toast.success("Path copied to clipboard"); } catch { toast.error("Could not copy automatically", { description: fullPath }); } } async function handleSaveOpenFile() { if (!openFileKey || openFileViewer?.mode !== "text") return; let nextContent = fileDrafts[openFileKey] ?? openFileViewer.content ?? ""; const ext = openFileKey.split(".").pop()?.toLowerCase(); if (ext === "json") { try { nextContent = JSON.stringify(JSON.parse(nextContent), null, 2); dispatch({ type: "SET_FILE_DRAFT", key: openFileKey, value: nextContent }); } catch { // malformed JSON — save as-is } } try { await saveFileContentMutation.mutateAsync({ path: openFileKey, content: nextContent, expectedMtimeMs: openFileViewer.mtimeMs, }); dispatch({ type: "SET_EDITOR_NOTICE", notice: "Saved" }); } catch (error) { dispatch({ type: "SET_EDITOR_NOTICE", notice: error instanceof Error ? error.message : "Failed to save file", }); } } return { confirmEmptyTrash, handleCopyEntryPath, handleDeleteFromTrash, handleDownloadEntry, handleEmptyTrash, handleGetInfo, handleMoveSelectedToTrash, handleRenameEntry, handleRestoreFromTrash, handleSaveOpenFile, handleShareFolder, handleToggleStar, handleTrashSelected, handleUnshareFolder, handleUnzipEntry, handleUploadFiles, submitCreateEntryDialog, submitRenameDialog, }; }, [ createEntryDialog, createFileMutation, createFolderMutation, createLocalShareMutation, currentPath, deleteFromTrashMutation, deleteLocalShareMutation, dispatch, emptyTrashMutation, fileDrafts, filesRootPath, getFileInfoMutation, includeHidden, isEmptyingTrash, isStarredView, isTrashView, moveToTrashMutation, openFile, openFileKey, openFileViewer, renameDialog, renameFileEntryMutation, restoreFromTrashMutation, saveFileContentMutation, selectedFiles, setStatusNotice, sortedEntries, systemHostname, systemNetworkAddress, toggleStarMutation, uploadAbortControllerRef, uploadFilesMutation, unzipFileMutation, ]); }