Skip to content

Commit

Permalink
feat: Add grid view for file explorer
Browse files Browse the repository at this point in the history
  • Loading branch information
NriotHrreion committed Aug 7, 2024
1 parent 8623c4a commit fe9734c
Show file tree
Hide file tree
Showing 12 changed files with 343 additions and 119 deletions.
4 changes: 2 additions & 2 deletions app/(pages)/explorer/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import Navbar from "@/components/explorer/navbar";

export default function Layout({ children }: PropsWithChildren) {
return (
<div className="w-full h-full pb-10 flex flex-col items-center space-y-3">
<div className="w-full h-full pb-10 flex flex-col items-center">
<Navbar />

<div className="w-[1000px] h-[78vh] flex gap-7">
<div className="w-[1000px] h-[79vh] flex gap-7">
{children}
</div>
</div>
Expand Down
68 changes: 68 additions & 0 deletions components/explorer/explorer-grid-view-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"use client";

import type { ViewItemProps } from "@/types";

import React from "react";
import { Checkbox } from "@nextui-org/checkbox";
import { Tooltip } from "@nextui-org/tooltip";

import { getFileIcon, getFolderIcon } from "./explorer-item";

import { formatSize, getFileTypeName } from "@/lib/utils";

interface GridViewItemProps extends ViewItemProps {}

const ExplorerGridViewItem: React.FC<GridViewItemProps> = ({
extname, size, selected, contextMenu, setSelected, handleSelection, handleOpen, onContextMenu, ...props
}) => {
return (
<div
className="w-[6.5rem] h-28 flex flex-col items-center overflow-hidden relative"
onClick={() => handleSelection()}
onKeyDown={({ key }) => {
key === "Enter" && handleSelection();
}}
role="button"
tabIndex={0}>
<div className="absolute top-1 left-1">
<Checkbox
className=""
size="sm"
isSelected={selected}
onValueChange={(value) => setSelected(value)}/>
</div>

<Tooltip
content={
<div className="text-left">
<p>名称:{props.name}</p>
<p>类型:{props.type === "folder" ? "文件夹" : getFileTypeName(extname)}</p>
{props.type === "file" && <p>大小:{formatSize(size)}</p>}
</div>
}
placement="bottom">
<div
className="flex-1 flex justify-center items-center pt-5 cursor-pointer"
onDoubleClick={() => handleOpen()}
onContextMenu={onContextMenu}>
{
props.type === "folder"
? getFolderIcon(props.name, 34)
: getFileIcon(extname ?? "", 34)
}
</div>
</Tooltip>

<button
className="w-full text-center text-sm overflow-hidden whitespace-nowrap text-ellipsis cursor-pointer hover:underline hover:text-primary-500"
onDoubleClick={() => handleOpen()}
onContextMenu={onContextMenu}>
{props.name}
</button>

{contextMenu}
</div>
);
}

export default ExplorerGridViewItem;
37 changes: 37 additions & 0 deletions components/explorer/explorer-grid-view.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"use client";

import type { ViewProps } from "@/types";

import React from "react";
import { cn } from "@nextui-org/theme";

import ExplorerItem from "./explorer-item";
import ExplorerError from "./explorer-error";

import { scrollbarStyle } from "@/lib/style";

interface GridViewProps extends ViewProps {}

const ExplorerGridView: React.FC<GridViewProps> = ({ items, error, contextMenu, onContextMenu }) => {
return (
<div className="w-[730px] flex mt-2">
<div
className={cn("w-full flex-1 grid grid-rows-[repeat(auto-fill,7rem)] grid-cols-[repeat(auto-fill,6.5rem)] gap-4 overflow-y-auto pr-2", scrollbarStyle)}
onContextMenu={onContextMenu}>
{
!error
? items.map((item, index) => (
item.access
? <ExplorerItem {...item} displayingMode="grid" key={index}/>
: null // To hide inaccessible items
))
: <ExplorerError error={error}/>
}
</div>

{contextMenu}
</div>
);
}

export default ExplorerGridView;
57 changes: 12 additions & 45 deletions components/explorer/explorer-item.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
/* eslint-disable react-hooks/rules-of-hooks */
"use client";

import type { DirectoryItem } from "@/types";
import type { DirectoryItem, DisplayingMode } from "@/types";

import React, { useState, useMemo, useEffect } from "react";
import { Divider } from "@nextui-org/divider";
import { Checkbox } from "@nextui-org/checkbox";
import {
Folder,
FolderGit2,
Expand All @@ -32,8 +30,11 @@ import { useRouter } from "next/navigation";
import { toast } from "react-toastify";
import { useContextMenu, ContextMenuItem, ContextMenuDivider } from "use-context-menu";

import ExplorerListViewItem from "./explorer-list-view-item";
import ExplorerGridViewItem from "./explorer-grid-view-item";

import { useExplorer } from "@/hooks/useExplorer";
import { concatPath, formatSize, getFileType, getFileTypeName } from "@/lib/utils";
import { concatPath, getFileType } from "@/lib/utils";
import { getViewer } from "@/lib/viewers";
import { useDialog } from "@/hooks/useDialog";
import { useFile } from "@/hooks/useFile";
Expand Down Expand Up @@ -87,11 +88,12 @@ export function getFileIcon(extname: string, size: number = 18, color?: string):
return <File size={size} color={color} className={className}/>;
}

interface ExplorerItemProps extends DirectoryItem {}
interface ExplorerItemProps extends DirectoryItem {
displayingMode: DisplayingMode
}

const ExplorerItem: React.FC<ExplorerItemProps> = (props) => {
const ExplorerItem: React.FC<ExplorerItemProps> = ({ displayingMode, ...props }) => {
const extname = useMemo(() => props.name.split(".").findLast(() => true), [props.name]);
const size = useMemo(() => formatSize(props.size), [props.size]);

const [selected, setSelected] = useState<boolean>(false);

Expand Down Expand Up @@ -178,44 +180,9 @@ const ExplorerItem: React.FC<ExplorerItemProps> = (props) => {
);

return (
<div
className="w-full min-h-8 text-md flex items-center gap-4"
onClick={() => handleSelection()}
onKeyDown={({ key }) => {
key === "Enter" && handleSelection();
}}
role="button"
tabIndex={0}>
<div className="w-[2%] flex items-center">
<Checkbox
className=""
size="sm"
isSelected={selected}
onValueChange={(value) => setSelected(value)}/>
</div>

<div className="flex-[2] min-w-0 flex items-center gap-2">
{(
props.type === "folder" ? getFolderIcon(props.name, 20, "#9e9e9e") : getFileIcon(extname ?? "txt", 20, "#9e9e9e")
) as React.ReactNode}
<button
className="text-ellipsis whitespace-nowrap cursor-pointer overflow-hidden hover:underline hover:text-primary-500"
onDoubleClick={() => handleOpen()}
onContextMenu={onContextMenu}>
{props.name}
</button>
</div>

<Divider orientation="vertical" className="bg-transparent"/>

<span className="flex-1 text-default-400 text-sm cursor-default">{props.type === "folder" ? "文件夹" : getFileTypeName(extname)}</span>

<Divider orientation="vertical" className="bg-transparent"/>

<span className="flex-1 text-default-400 text-right text-sm cursor-default">{props.type === "file" ? (size) : ""}</span>

{contextMenu}
</div>
displayingMode === "list"
? <ExplorerListViewItem {...props} extname={extname} selected={selected} contextMenu={contextMenu} setSelected={setSelected} handleSelection={handleSelection} handleOpen={handleOpen} onContextMenu={onContextMenu}/>
: <ExplorerGridViewItem {...props} extname={extname} selected={selected} contextMenu={contextMenu} setSelected={setSelected} handleSelection={handleSelection} handleOpen={handleOpen} onContextMenu={onContextMenu}/>
);
};

Expand Down
60 changes: 60 additions & 0 deletions components/explorer/explorer-list-view-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"use client";

import type { ViewItemProps } from "@/types";

import React from "react";
import { Checkbox } from "@nextui-org/checkbox";
import { Divider } from "@nextui-org/divider";

import { getFileIcon, getFolderIcon } from "./explorer-item";

import { formatSize, getFileTypeName } from "@/lib/utils";

interface ListViewItemProps extends ViewItemProps {}

const ExplorerListViewItem: React.FC<ListViewItemProps> = ({
extname, selected, contextMenu, setSelected, handleSelection, handleOpen, onContextMenu, ...props
}) => {
return (
<div
className="w-full min-h-8 text-md flex items-center gap-4"
onClick={() => handleSelection()}
onKeyDown={({ key }) => {
key === "Enter" && handleSelection();
}}
role="button"
tabIndex={0}>
<div className="w-[2%] flex items-center">
<Checkbox
className=""
size="sm"
isSelected={selected}
onValueChange={(value) => setSelected(value)}/>
</div>

<div className="flex-[2] min-w-0 flex items-center gap-2">
{(
props.type === "folder" ? getFolderIcon(props.name, 20, "#9e9e9e") : getFileIcon(extname ?? "txt", 20, "#9e9e9e")
) as React.ReactNode}
<button
className="text-ellipsis whitespace-nowrap cursor-pointer overflow-hidden hover:underline hover:text-primary-500"
onDoubleClick={() => handleOpen()}
onContextMenu={onContextMenu}>
{props.name}
</button>
</div>

<Divider orientation="vertical" className="bg-transparent"/>

<span className="flex-1 text-default-400 text-sm cursor-default">{props.type === "folder" ? "文件夹" : getFileTypeName(extname)}</span>

<Divider orientation="vertical" className="bg-transparent"/>

<span className="flex-1 text-default-400 text-right text-sm cursor-default">{props.type === "file" ? formatSize(props.size) : ""}</span>

{contextMenu}
</div>
);
}

export default ExplorerListViewItem;
47 changes: 47 additions & 0 deletions components/explorer/explorer-list-view.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"use client";

import type { ViewProps } from "@/types";

import React from "react";
import { cn } from "@nextui-org/theme";
import { Divider } from "@nextui-org/divider";

import ExplorerItem from "./explorer-item";
import ExplorerError from "./explorer-error";

import { scrollbarStyle } from "@/lib/style";

interface ListViewProps extends ViewProps {}

const ExplorerListView: React.FC<ListViewProps> = ({ items, error, contextMenu, onContextMenu }) => {
return (
<div className="w-[730px] flex flex-col gap-1">
<div className="w-full h-6 text-sm flex items-center gap-4 pr-5">
<div className="w-[2%]"/> {/* placeholder */}
<span className="flex-[2] cursor-default">名称</span>
<Divider orientation="vertical"/>
<span className="flex-1 cursor-default">类型</span>
<Divider orientation="vertical"/>
<span className="flex-1 cursor-default">大小</span>
</div>

<div
className={cn("w-full relative flex-1 flex flex-col overflow-y-auto pr-5", scrollbarStyle)}
onContextMenu={onContextMenu}>
{
!error
? items.map((item, index) => (
item.access
? <ExplorerItem {...item} displayingMode="list" key={index}/>
: null // To hide inaccessible items
))
: <ExplorerError error={error}/>
}
</div>

{contextMenu}
</div>
);
}

export default ExplorerListView;
43 changes: 12 additions & 31 deletions components/explorer/explorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,16 @@ import type { BaseResponseData, DirectoryItem } from "@/types";

import React, { useState, useEffect } from "react";
import axios, { type AxiosError } from "axios";
import { Divider } from "@nextui-org/divider";
import { toast } from "react-toastify";
import { cn } from "@nextui-org/theme";
import { ContextMenuItem, useContextMenu } from "use-context-menu";

import ExplorerItem from "./explorer-item";
import ExplorerError from "./explorer-error";
import ExplorerListView from "./explorer-list-view";
import ExplorerGridView from "./explorer-grid-view";

import { useExplorer } from "@/hooks/useExplorer";
import { scrollbarStyle } from "@/lib/style";
import { useDialog } from "@/hooks/useDialog";
import { useForceUpdate } from "@/hooks/useForceUpdate";
import { useEmitter } from "@/hooks/useEmitter";

interface FolderResponseData extends BaseResponseData {
items: DirectoryItem[]
Expand All @@ -24,6 +23,7 @@ interface FolderResponseData extends BaseResponseData {
const Explorer: React.FC = () => {
const explorer = useExplorer();
const dialog = useDialog();
const forceUpdate = useForceUpdate();

const [items, setItems] = useState<DirectoryItem[]>([]);
const [error, setError] = useState<string | null>(null);
Expand Down Expand Up @@ -73,6 +73,10 @@ const Explorer: React.FC = () => {
return () => setError(null);
}, [explorer]);

useEmitter([
["displaying-mode-change", () => forceUpdate()]
]);

const { contextMenu, onContextMenu } = useContextMenu(
<>
<ContextMenuItem onSelect={() => dialog.open("createFolder", { path: explorer.stringifyPath() })}>新建文件夹</ContextMenuItem>
Expand All @@ -82,32 +86,9 @@ const Explorer: React.FC = () => {
);

return (
<div className="w-[730px] flex flex-col gap-1">
<div className="w-full h-6 text-sm flex items-center gap-4 pr-5">
<div className="w-[2%]"/> {/* placeholder */}
<span className="flex-[2] cursor-default">名称</span>
<Divider orientation="vertical"/>
<span className="flex-1 cursor-default">类型</span>
<Divider orientation="vertical"/>
<span className="flex-1 cursor-default">大小</span>
</div>

<div
className={cn("w-full relative flex-1 flex flex-col overflow-y-auto pr-5", scrollbarStyle)}
onContextMenu={onContextMenu}>
{
!error
? items.map((item, index) => (
item.access
? <ExplorerItem {...item} key={index}/>
: null // To hide inaccessible items
))
: <ExplorerError error={error}/>
}
</div>

{contextMenu}
</div>
explorer.displayingMode === "list"
? <ExplorerListView items={items} error={error} contextMenu={contextMenu} onContextMenu={onContextMenu}/>
: <ExplorerGridView items={items} error={error} contextMenu={contextMenu} onContextMenu={onContextMenu}/>
);
}

Expand Down
Loading

0 comments on commit fe9734c

Please sign in to comment.