IKHSAN
EN Read in EnglishID

homeio

Publik

Fork dari doctor-io/homeio

Branch
main
Commit terakhir
958d74616 Oktober 2026

file-manager-view.tsx

Buka raw
"use client";

import { FileManagerSidebar, FileManagerStatusBar, FileManagerToolbar, type FileManagerSidebarItem, type FileManagerSidebarSection, type RemovableSidebarItem, type CloudSidebarItem } from "@/modules/files/components/file-manager-chrome";
import { FileManagerFileArea } from "@/modules/files/components/content/file-manager-content";
import { FILES_PANEL_SHELL } from "@/modules/files/components/file-manager-surface";
import { FileManagerDialogLayer } from "@/modules/files/components/manager/file-manager-dialog-layer";
import { validateEntryName } from "@/modules/files/components/manager/file-manager-derived";
import { GoogleDrivePanel } from "@/modules/files/components/panels/google-drive-panel";
import { buildAssetUrl, toFilePath } from "@/modules/files/hooks/useFiles";
import type { MouseEvent, RefObject, SetStateAction } from "react";
import type { FileEntry } from "@/modules/files/components/file-manager-presenters";
import type { FileInfoResponse, FileReadResponse } from "@/lib/shared/contracts/files";
import type { ClipboardState, CreateEntryDialogState, FileManagerAction, OpenFileState, PasteConflictState, RenameDialogState, ViewMode } from "@/modules/files/components/manager/file-manager-state";

type FileManagerViewProps = {
  rootRef: RefObject<HTMLDivElement | null>;
  actions: {
    confirmEmptyTrash: () => Promise<void>;
    handleConflictResolution: (choice: "replace" | "keep-both" | "skip" | "skip-all") => Promise<void>;
    handleCopyEntryPath: (entry: FileEntry) => Promise<void>;
    handleDeleteFromTrash: (entry: FileEntry) => Promise<void>;
    handleDownloadEntry: (entry: FileEntry) => void;
    handleUnzipEntry: (entry: FileEntry) => Promise<void>;
    handleEmptyTrash: () => void;
    handleGetInfo: (entry: FileEntry) => Promise<void>;
    handleMoveSelectedToTrash: (entry: FileEntry) => Promise<void>;
    handlePasteToDestination: (destinationPath: string) => Promise<void>;
    handleRenameEntry: (entry: FileEntry) => void;
    handleRestoreFromTrash: (entry: FileEntry) => Promise<void>;
    handleSaveOpenFile: () => Promise<void>;
    handleShareFolder: (entry: FileEntry) => Promise<void>;
    handleToggleStar: (entry: FileEntry) => Promise<void>;
    handleTrashSelected: () => Promise<void>;
    handleUnshareFolder: (shareId: string) => Promise<void>;
    handleUploadFiles: (files: File[]) => Promise<void>;
    setClipboardFromEntries: (entries: FileEntry[], operation: "copy" | "move") => void;
    submitCreateEntryDialog: () => Promise<void>;
    submitRenameDialog: () => Promise<void>;
  };
  canSaveOpenFile: boolean;
  clipboardDisplayName: string | null;
  clipboardState: ClipboardState | null;
  contextShare: { id: string } | undefined;
  counts: { fileCount: number; folderCount: number };
  createDialog: CreateEntryDialogState | null;
  createFilePending: boolean;
  createFolderPending: boolean;
  currentEntriesCount: number;
  currentPath: string[];
  currentPathForDisplay: string[];
  directoryErrorMessage: string;
  directoryIsError: boolean;
  directoryIsLoading: boolean;
  dispatch: (action: FileManagerAction) => void;
  editorNotice: string | null;
  emptyTrashPending: boolean;
  fileContentErrorMessage: string;
  fileContentIsError: boolean;
  fileContentIsLoading: boolean;
  fileInfoDialog: FileInfoResponse | null;
  globalSearch: boolean;
  globalSearchIsFetching: boolean;
  hasMoreSearchResults: boolean;
  includeHidden: boolean;
  isDragOver: boolean;
  isEmptyingTrash: boolean;
  isGlobalSearchActive: boolean;
  isSharedView: boolean;
  isStarredView: boolean;
  isTrashView: boolean;
  locationItems: FileManagerSidebarItem[];
  cloudItems: CloudSidebarItem[];
  activeDriveConnection: { id: string; email: string } | null;
  removableItems: RemovableSidebarItem[];
  onMountDrive: (driveId: string) => void;
  onEjectDrive: (driveId: string) => void;
  onCancelUpload: () => void;
  navigateTo: (entry: FileEntry) => void;
  navigateToPath: (pathSegments: string[]) => void;
  onEntryClick: (event: MouseEvent, entry: FileEntry) => void;
  onPositionBackgroundContextMenu: (event: MouseEvent) => void;
  onPositionEntryContextMenu: (event: MouseEvent, entry: FileEntry) => void;
  hasUnsavedChanges: boolean;
  openFile: OpenFileState | null;
  openFileBadgeLabel: string;
  openFileContent: string;
  openFileKey: string | null;
  openFileLanguage: string;
  openFileViewer: FileReadResponse | null;
  pasteConflict: PasteConflictState | null;
  pastePending: boolean;
  pendingEntryPath: string | null;
  renameDialog: RenameDialogState | null;
  renamePending: boolean;
  rootLabel: string;
  searchQuery: string;
  selectedFiles: Set<string>;
  showBackgroundContextMenu: { x: number; y: number } | null;
  showContextMenu: { x: number; y: number; entry: FileEntry } | null;
  showEmptyTrashConfirm: boolean;
  showNetworkDialog: boolean;
  showGoogleDriveDialog: boolean;
  showUsbDialog: boolean;
  sidebarSections: FileManagerSidebarSection[];
  sortedEntries: FileEntry[];
  storageUsagePercent: number;
  storageUsageText: string;
  sortBy: "name" | "modified" | "size";
  sortDir: "asc" | "desc";
  statusNotice: string | null;
  totalEntriesCount: number;
  trashItemCount: number;
  uploadInputRef: RefObject<HTMLInputElement | null>;
  uploadPending: boolean;
  uploadProgress: { loaded: number; total: number } | null;
  unzipPending: boolean;
  viewMode: ViewMode;
};

export function FileManagerView({
  rootRef,
  actions,
  canSaveOpenFile,
  clipboardDisplayName,
  clipboardState,
  contextShare,
  counts,
  createDialog,
  createFilePending,
  createFolderPending,
  currentEntriesCount,
  currentPath,
  currentPathForDisplay,
  directoryErrorMessage,
  directoryIsError,
  directoryIsLoading,
  dispatch,
  editorNotice,
  emptyTrashPending,
  fileContentErrorMessage,
  fileContentIsError,
  fileContentIsLoading,
  fileInfoDialog,
  globalSearch,
  globalSearchIsFetching,
  hasMoreSearchResults,
  hasUnsavedChanges,
  includeHidden,
  isDragOver,
  isEmptyingTrash,
  isGlobalSearchActive,
  isSharedView,
  isStarredView,
  isTrashView,
  locationItems,
  cloudItems,
  activeDriveConnection,
  removableItems,
  onMountDrive,
  onEjectDrive,
  onCancelUpload,
  navigateTo,
  navigateToPath,
  onEntryClick,
  onPositionBackgroundContextMenu,
  onPositionEntryContextMenu,
  openFile,
  openFileBadgeLabel,
  openFileContent,
  openFileKey,
  openFileLanguage,
  openFileViewer,
  pasteConflict,
  pastePending,
  pendingEntryPath,
  renameDialog,
  renamePending,
  rootLabel,
  searchQuery,
  selectedFiles,
  showBackgroundContextMenu,
  showContextMenu,
  showEmptyTrashConfirm,
  showNetworkDialog,
  showGoogleDriveDialog,
  showUsbDialog,
  sidebarSections,
  sortedEntries,
  storageUsagePercent,
  storageUsageText,
  sortBy,
  sortDir,
  statusNotice,
  totalEntriesCount,
  trashItemCount,
  uploadInputRef,
  uploadPending,
  uploadProgress,
  unzipPending,
  viewMode,
}: FileManagerViewProps) {
  return (
    <div
      ref={rootRef}
      className="relative flex h-full"
      onClick={() => {
        dispatch({ type: "HIDE_CONTEXT_MENU" });
        dispatch({ type: "HIDE_BACKGROUND_CONTEXT_MENU" });
      }}
    >
      <FileManagerSidebar
        currentPath={currentPath}
        isSharedView={isSharedView}
        isTrashView={isTrashView}
        locationItems={locationItems}
        cloudItems={cloudItems}
        removableItems={removableItems}
        sidebarSections={sidebarSections}
        storageUsagePercent={storageUsagePercent}
        storageUsageText={storageUsageText}
        onNavigateToPath={navigateToPath}
        onOpenNetworkDialog={() => dispatch({ type: "SHOW_NETWORK_DIALOG" })}
        onOpenGoogleDriveDialog={() => dispatch({ type: "SHOW_GOOGLE_DRIVE_DIALOG" })}
        onOpenUsbDialog={() => dispatch({ type: "SHOW_USB_DIALOG" })}
        onMountDrive={onMountDrive}
        onEjectDrive={onEjectDrive}
      />
      {activeDriveConnection ? (
        <div className={`m-2 min-w-0 flex-1 overflow-hidden ${FILES_PANEL_SHELL}`}>
          <GoogleDrivePanel
            connectionId={activeDriveConnection.id}
            accountEmail={activeDriveConnection.email}
          />
        </div>
      ) : (
      <div className={`m-2 flex min-w-0 flex-1 flex-col ${FILES_PANEL_SHELL}`}>
        <FileManagerToolbar
          canNavigateUp={currentPath.length > 0}
          currentEntriesCount={currentEntriesCount}
          currentPath={currentPath}
          currentPathForDisplay={currentPathForDisplay}
          emptyTrashPending={emptyTrashPending}
          globalSearch={globalSearch}
          globalSearchIsFetching={globalSearchIsFetching}
          includeHidden={includeHidden}
          isEmptyingTrash={isEmptyingTrash}
          isStarredView={isStarredView}
          isTrashView={isTrashView}
          rootLabel={rootLabel}
          searchQuery={searchQuery}
          sortBy={sortBy}
          sortDir={sortDir}
          uploadFilesPending={uploadPending}
          uploadInputRef={uploadInputRef}
          viewMode={viewMode}
          onCycleSortBy={() => dispatch({ type: "SET_SORT_BY", by: sortBy === "name" ? "modified" : sortBy === "modified" ? "size" : "name" })}
          onEmptyTrash={actions.handleEmptyTrash}
          onNavigateToPath={navigateToPath}
          onNavigateUp={() => dispatch({ type: "NAVIGATE_UP" })}
          onSearchQueryChange={(query) => dispatch({ type: "SET_SEARCH_QUERY", query })}
          onSetViewMode={(mode) => dispatch({ type: "SET_VIEW_MODE", mode })}
          onToggleGlobalSearch={() => dispatch({ type: "TOGGLE_GLOBAL_SEARCH" })}
          onToggleIncludeHidden={() => dispatch({ type: "TOGGLE_INCLUDE_HIDDEN" })}
          onToggleSortDir={() => dispatch({ type: "TOGGLE_SORT_DIR" })}
          onUploadInputChange={(files) => {
            void actions.handleUploadFiles(files);
          }}
        />
        <FileManagerFileArea
          canSaveOpenFile={canSaveOpenFile}
          directoryErrorMessage={directoryErrorMessage}
          directoryIsError={directoryIsError}
          directoryIsLoading={directoryIsLoading}
          editorNotice={editorNotice}
          fileContentErrorMessage={fileContentErrorMessage}
          fileContentIsError={fileContentIsError}
          fileContentIsLoading={fileContentIsLoading}
          globalSearchIsFetching={globalSearchIsFetching}
          hasMoreSearchResults={hasMoreSearchResults}
          hasUnsavedChanges={hasUnsavedChanges}
          isDragOver={isDragOver}
          isGlobalSearchActive={isGlobalSearchActive}
          isStarredView={isStarredView}
          isTrashView={isTrashView}
          openFile={openFile}
          openFileAssetUrl={openFileKey ? buildAssetUrl(openFileKey) : ""}
          openFileBadgeLabel={openFileBadgeLabel}
          openFileContent={openFileContent}
          openFileKey={openFileKey}
          openFileLanguage={openFileLanguage}
          openFileViewer={openFileViewer}
          showSaveOpenFile={openFileViewer?.mode === "text"}
          pendingEntryPath={pendingEntryPath}
          searchQuery={searchQuery}
          selectedFiles={selectedFiles}
          sortedEntries={sortedEntries}
          totalEntriesCount={totalEntriesCount}
          viewMode={viewMode}
          onBackgroundContextMenu={onPositionBackgroundContextMenu}
          onChangeOpenFileDraft={(value: SetStateAction<string>) => {
            if (!openFileKey) return;
            dispatch({
              type: "SET_FILE_DRAFT",
              key: openFileKey,
              value: typeof value === "function" ? value(openFileContent) : value,
            });
          }}
          onCloseOpenFile={() => dispatch({ type: "CLOSE_FILE" })}
          onDragLeave={(event) => {
            if (!event.currentTarget.contains(event.relatedTarget as Node)) {
              dispatch({ type: "SET_DRAG_OVER", value: false });
            }
          }}
          onDragOver={(event) => {
            if (isTrashView || isStarredView) return;
            event.preventDefault();
            event.dataTransfer.dropEffect = "copy";
            dispatch({ type: "SET_DRAG_OVER", value: true });
          }}
          onDrop={(event) => {
            event.preventDefault();
            dispatch({ type: "SET_DRAG_OVER", value: false });
            if (isTrashView || isStarredView) return;
            void actions.handleUploadFiles(Array.from(event.dataTransfer.files));
          }}
          onEntryClick={onEntryClick}
          onEntryContextMenu={onPositionEntryContextMenu}
          onLoadMoreSearch={() => dispatch({ type: "LOAD_MORE_SEARCH" })}
          onOpenEntry={navigateTo}
          onSaveOpenFile={() => {
            void actions.handleSaveOpenFile();
          }}
        />
        <FileManagerStatusBar
          clipboardName={clipboardDisplayName}
          clipboardOperation={clipboardState?.operation ?? null}
          currentPathForDisplay={currentPathForDisplay}
          fileCount={counts.fileCount}
          folderCount={counts.folderCount}
          isStarredView={isStarredView}
          isTrashView={isTrashView}
          rootLabel={rootLabel}
          selectedFilesCount={selectedFiles.size}
          statusNotice={statusNotice}
          onTrashSelected={() => {
            void actions.handleTrashSelected();
          }}
        />
      </div>
      )}
      <FileManagerDialogLayer
        clipboardState={clipboardState}
        contextShare={contextShare}
        createEntryDialog={createDialog}
        createPending={createFilePending || createFolderPending}
        fileInfoDialog={fileInfoDialog}
        isTrashView={isTrashView}
        pasteConflict={pasteConflict}
        pastePending={pastePending}
        renameDialog={renameDialog}
        renamePending={renamePending}
        showBackgroundContextMenu={showBackgroundContextMenu}
        showContextMenu={showContextMenu}
        showEmptyTrashConfirm={showEmptyTrashConfirm}
        showNetworkDialog={showNetworkDialog}
        showGoogleDriveDialog={showGoogleDriveDialog}
        showUsbDialog={showUsbDialog}
        trashItemCount={trashItemCount}
        uploadPending={uploadPending}
        uploadProgress={uploadProgress}
        unzipPending={unzipPending}
        onCancelEmptyTrash={() => dispatch({ type: "HIDE_EMPTY_TRASH_CONFIRM" })}
        onCancelUpload={onCancelUpload}
        onChangeCreateEntryDialog={(value) => dispatch({ type: "UPDATE_CREATE_ENTRY_DIALOG", name: value, error: validateEntryName(value) })}
        onChangeRenameDialog={(value) => dispatch({ type: "UPDATE_RENAME_DIALOG", name: value, error: validateEntryName(value) })}
        onCloseBackgroundContextMenu={() => dispatch({ type: "HIDE_BACKGROUND_CONTEXT_MENU" })}
        onCloseContextMenu={() => dispatch({ type: "HIDE_CONTEXT_MENU" })}
        onCloseCreateEntryDialog={() => dispatch({ type: "CLOSE_CREATE_ENTRY_DIALOG" })}
        onCloseFileInfoDialog={() => dispatch({ type: "CLOSE_FILE_INFO_DIALOG" })}
        onCloseNetworkDialog={() => dispatch({ type: "HIDE_NETWORK_DIALOG" })}
        onCloseGoogleDriveDialog={() => dispatch({ type: "HIDE_GOOGLE_DRIVE_DIALOG" })}
        onCloseUsbDialog={() => dispatch({ type: "HIDE_USB_DIALOG" })}
        onCloseRenameDialog={() => dispatch({ type: "CLOSE_RENAME_DIALOG" })}
        onConfirmEmptyTrash={() => {
          void actions.confirmEmptyTrash();
        }}
        onConflictResolution={(choice) => {
          void actions.handleConflictResolution(choice);
        }}
        onCopyContextEntry={() => showContextMenu && actions.setClipboardFromEntries([showContextMenu.entry], "copy")}
        onCopyContextPath={() => showContextMenu && void actions.handleCopyEntryPath(showContextMenu.entry)}
        onCutContextEntry={() => showContextMenu && actions.setClipboardFromEntries([showContextMenu.entry], "move")}
        onDeleteContextEntry={() => showContextMenu && void actions.handleDeleteFromTrash(showContextMenu.entry)}
        onDownloadContextEntry={() => showContextMenu && actions.handleDownloadEntry(showContextMenu.entry)}
        onUnzipContextEntry={() => showContextMenu && void actions.handleUnzipEntry(showContextMenu.entry)}
        onGetInfoContextEntry={() => showContextMenu && void actions.handleGetInfo(showContextMenu.entry)}
        onMoveContextEntryToTrash={() => showContextMenu && void actions.handleMoveSelectedToTrash(showContextMenu.entry)}
        onNavigateToNetwork={() => {
          dispatch({ type: "NAVIGATE_TO_PATH", path: ["Network"] });
          dispatch({ type: "HIDE_NETWORK_DIALOG" });
        }}
        onNavigateToUsb={(path) => {
          dispatch({ type: "NAVIGATE_TO_PATH", path });
        }}
        onOpenContextEntry={() => showContextMenu && navigateTo(showContextMenu.entry)}
        onOpenCreateEntryDialog={(kind) => dispatch({ type: "OPEN_CREATE_ENTRY_DIALOG", kind })}
        onPasteIntoBackground={() => {
          void actions.handlePasteToDestination(toFilePath(currentPath));
        }}
        onPasteIntoContextEntry={() => {
          if (!showContextMenu) return;
          void actions.handlePasteToDestination(
            showContextMenu.entry.type === "folder" ? showContextMenu.entry.path : toFilePath(currentPath),
          );
        }}
        onRenameContextEntry={() => showContextMenu && actions.handleRenameEntry(showContextMenu.entry)}
        onRestoreContextEntry={() => showContextMenu && void actions.handleRestoreFromTrash(showContextMenu.entry)}
        onSubmitCreateEntryDialog={() => {
          void actions.submitCreateEntryDialog();
        }}
        onSubmitRenameDialog={() => {
          void actions.submitRenameDialog();
        }}
        onToggleContextShare={() => {
          if (!showContextMenu) return;
          if (contextShare) {
            void actions.handleUnshareFolder(contextShare.id);
            return;
          }
          if (
            showContextMenu.entry.path === "Shared" ||
            showContextMenu.entry.path.startsWith("Shared/") ||
            showContextMenu.entry.path === "Network" ||
            showContextMenu.entry.path.startsWith("Network/")
          ) {
            return;
          }
          void actions.handleShareFolder(showContextMenu.entry);
        }}
        onToggleContextStar={() => showContextMenu && void actions.handleToggleStar(showContextMenu.entry)}
      />
    </div>
  );
}
byte
18.383
baris
445
Jenis
tsx
Commit terakhir
16 Oktober 2026