IKHSAN
ENID Baca dalam Bahasa Indonesia

homeio

Public

Forked from doctor-io/homeio

Branch
main
Latest commit
958d74616 October 2026

file-manager-derived.ts

Open raw
import { createElement, type ReactNode } from "react";
import { Folder, HardDrive, Home, StarFilled, Usb } from "@/components/icons/platform-icons";
import { OsIcon } from "@/components/icons/OsIcon";
import { DEVICE_ICONS, FILE_SIDEBAR_ICONS, FOLDER_ICONS } from "@/components/icons/icon-assets";
import type { FileListEntry, FileReadResponse } from "@/lib/shared/contracts/files";
import type { FileManagerSidebarSection, RemovableSidebarItem } from "@/modules/files/components/chrome/file-manager-sidebar";
import type { UsbDrive } from "@/lib/shared/contracts/usb";
import type { GoogleDriveConnection } from "@/modules/files/hooks/useGoogleDrive";
import {
  normalizePathForDisplay,
  toUiFileEntry,
  type FileEntry,
} from "@/modules/files/components/file-manager-presenters";
import type { SortBy } from "@/modules/files/components/manager/file-manager-state";

const BYTES_PER_TB = 1024 ** 4;
const BYTES_PER_GB = 1024 ** 3;
const BYTES_PER_MB = 1024 ** 2;

export const STARRED_VIRTUAL_PATH = ["⭐Starred"] as const;

export const DRIVE_PATH_PREFIX = "Drive" as const;

function createKoraIcon(src: string, fallback: ReactNode) {
  return createElement(OsIcon, {
    src,
    className: "size-4 object-contain",
    fallback,
  });
}

export const sidebarSections: FileManagerSidebarSection[] = [
  {
    title: "Favorites",
    items: [
      {
        name: "Home",
        icon: createKoraIcon(
          FILE_SIDEBAR_ICONS.home,
          createElement(Home, { className: "size-4 text-muted-foreground" }),
        ),
        path: [],
      },
      {
        name: "Starred",
        icon: createKoraIcon(
          FILE_SIDEBAR_ICONS.starred,
          createElement(StarFilled, { className: "size-4 text-amber-400" }),
        ),
        path: [...STARRED_VIRTUAL_PATH],
      },
      {
        name: "Documents",
        icon: createKoraIcon(
          FOLDER_ICONS.document,
          createElement(Folder, { className: "size-4 text-sky-400" }),
        ),
        path: ["Documents"],
      },
      {
        name: "Downloads",
        icon: createKoraIcon(
          FOLDER_ICONS.download,
          createElement(Folder, { className: "size-4 text-emerald-400" }),
        ),
        path: ["Downloads"],
      },
      {
        name: "Media",
        icon: createKoraIcon(
          FOLDER_ICONS.video,
          createElement(Folder, { className: "size-4 text-amber-400" }),
        ),
        path: ["Media"],
      },
      {
        name: "Apps",
        icon: createKoraIcon(
          FOLDER_ICONS.app,
          createElement(Folder, { className: "size-4 text-violet-400" }),
        ),
        path: ["AppData"],
      },
    ],
  },
  {
    title: "Locations",
    items: [],
  },
  {
    title: "Cloud",
    items: [],
  },
];

export function formatStorageValue(bytes: number): string {
  if (bytes >= BYTES_PER_TB) {
    const value = (bytes / BYTES_PER_TB).toFixed(1);
    return `${value.endsWith(".0") ? value.slice(0, -2) : value} TB`;
  }
  if (bytes >= BYTES_PER_GB) {
    const value = (bytes / BYTES_PER_GB).toFixed(1);
    return `${value.endsWith(".0") ? value.slice(0, -2) : value} GB`;
  }
  return `${(bytes / BYTES_PER_MB).toFixed(0)} MB`;
}

export function clampPercent(value: number | null | undefined) {
  if (typeof value !== "number" || Number.isNaN(value)) return 0;
  return Math.max(0, Math.min(value, 100));
}

export function sortEntries({
  currentEntries,
  isGlobalSearchActive,
  searchQuery,
  sortBy,
  sortDir,
}: {
  currentEntries: FileEntry[];
  isGlobalSearchActive: boolean;
  searchQuery: string;
  sortBy: SortBy;
  sortDir: "asc" | "desc";
}) {
  let entries = [...currentEntries];

  if (searchQuery && !isGlobalSearchActive) {
    const normalized = searchQuery.toLowerCase();
    entries = entries.filter((entry) => entry.name.toLowerCase().includes(normalized));
  }

  const folders = entries.filter((entry) => entry.type === "folder");
  const files = entries.filter((entry) => entry.type === "file");
  const dir = sortDir === "asc" ? 1 : -1;
  const sortFn = (left: FileEntry, right: FileEntry) => {
    if (sortBy === "name") return dir * left.name.localeCompare(right.name);
    if (sortBy === "modified") {
      return dir * (new Date(left.modifiedAt).getTime() - new Date(right.modifiedAt).getTime());
    }
    return dir * ((left.sizeBytes ?? 0) - (right.sizeBytes ?? 0));
  };

  return [...folders.sort(sortFn), ...files.sort(sortFn)];
}

export function getCurrentEntries({
  directoryEntries,
  globalEntries,
  isGlobalSearchActive,
  isStarredView,
  starredEntries,
}: {
  directoryEntries: FileListEntry[] | undefined;
  globalEntries: FileListEntry[] | undefined;
  isGlobalSearchActive: boolean;
  isStarredView: boolean;
  starredEntries: FileListEntry[] | undefined;
}) {
  if (isGlobalSearchActive) {
    return (globalEntries ?? []).map(toUiFileEntry);
  }
  const rawEntries = isStarredView ? starredEntries ?? [] : directoryEntries ?? [];
  return rawEntries.map(toUiFileEntry);
}

export function getOpenFileDetails({
  fileDrafts,
  filePath,
  language,
  viewer,
}: {
  fileDrafts: Record<string, string>;
  filePath: string | null;
  language: string;
  viewer: FileReadResponse | null;
}) {
  const content =
    filePath && viewer?.mode === "text" ? (fileDrafts[filePath] ?? viewer.content ?? "") : "";
  const badgeLabel = viewer
    ? viewer.mode === "text"
      ? language
      : viewer.mode.replaceAll("_", " ")
    : language;
  const hasUnsavedChanges =
    filePath !== null &&
    viewer?.mode === "text" &&
    fileDrafts[filePath] !== undefined &&
    fileDrafts[filePath] !== (viewer.content ?? "");
  return { badgeLabel, content, hasUnsavedChanges };
}

export function getStorageSummary(
  storage: { totalBytes: number; usedBytes: number; usedPercent: number } | undefined,
) {
  if (!storage || storage.totalBytes <= 0) {
    return { percent: 0, text: "-- / --" };
  }
  return {
    percent: clampPercent(storage.usedPercent),
    text: `${formatStorageValue(storage.usedBytes)} / ${formatStorageValue(storage.totalBytes)}`,
  };
}

export function getViewFlags(currentPath: string[]) {
  return {
    isSharedView: currentPath[0] === "Shared",
    isStarredView: currentPath.length === 1 && currentPath[0] === STARRED_VIRTUAL_PATH[0],
    isTrashView: currentPath[0] === "Trash",
    isDriveView: currentPath[0] === DRIVE_PATH_PREFIX && Boolean(currentPath[1]),
  };
}

export function getDriveItems(
  connections: GoogleDriveConnection[] | undefined,
): Array<{ id: string; name: string; email: string; path: string[] }> {
  return (connections ?? []).map((conn) => ({
    id: conn.id,
    name: conn.displayName ?? conn.email,
    email: conn.email,
    path: [DRIVE_PATH_PREFIX, conn.id],
  }));
}

export function getLocationItems(
  shares: Array<{ mountPath: string }> | undefined,
) {
  const hosts = new Map<string, string[]>();
  for (const share of shares ?? []) {
    const segments = share.mountPath.split("/").filter(Boolean);
    const hostSegment = segments[1];
    const rootSegment = segments[0];
    if (!hostSegment || !rootSegment) continue;
    hosts.set(hostSegment, [rootSegment, hostSegment]);
  }

  return Array.from(hosts.entries())
    .sort(([left], [right]) => left.localeCompare(right))
    .map(([host, hostPath]) => ({
      name: host,
      icon: createKoraIcon(
        DEVICE_ICONS.network,
        createElement(HardDrive, { className: "size-4 text-cyan-400" }),
      ),
      path: hostPath,
    }));
}

function usbPathSegment(label: string, id: string): string {
  return label.replace(/[^a-zA-Z0-9\-_]/g, "_").replace(/_+/g, "_").slice(0, 32) || id.slice(0, 8);
}

export function getRemovableItems(drives: UsbDrive[]): RemovableSidebarItem[] {
  return drives.map((drive) => ({
    name: drive.label,
    icon: createKoraIcon(
      DEVICE_ICONS.usb,
      createElement(Usb, { className: "size-4 text-amber-400" }),
    ),
    path: ["Removable", usbPathSegment(drive.label, drive.id)],
    driveId: drive.id,
    isMounted: drive.isMounted,
  }));
}

export function getLocalSharesByPath(
  shares:
    | Array<{
        id: string;
        shareName: string;
        sourcePath: string;
        sharedPath: string;
        isMounted: boolean;
        isExported: boolean;
      }>
    | undefined,
) {
  const map = new Map<
    string,
    {
      id: string;
      shareName: string;
      sourcePath: string;
      sharedPath: string;
      isMounted: boolean;
      isExported: boolean;
    }
  >();
  for (const share of shares ?? []) {
    map.set(share.sourcePath, share);
    map.set(share.sharedPath, share);
  }
  return map;
}

export function getBrowserCounts(entries: FileEntry[]) {
  return {
    fileCount: entries.filter((entry) => entry.type === "file").length,
    folderCount: entries.filter((entry) => entry.type === "folder").length,
  };
}

export function getClipboardDisplayName(
  clipboardState: { names: string[] } | null,
) {
  if (!clipboardState) return null;
  return clipboardState.names.length === 1
    ? clipboardState.names[0]
    : `${clipboardState.names.length} items`;
}

export function getCurrentPathForDisplay(currentPath: string[]) {
  return normalizePathForDisplay(currentPath);
}

export function validateEntryName(name: string): string | null {
  if (name.includes("/") || name.includes("\0")) {
    return 'Name cannot contain "/" or null characters';
  }
  return null;
}
bytes
9,409
lines
322
Type
ts
Latest commit
16 October 2026