Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 86 additions & 55 deletions app/components/FileItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
import { useState, useRef } from "react";
import { getFileIcon, formatFileSize, formatDate, type FileType } from "../lib/helpers";
import FileItemMenu from "./FileItemMenu";
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemMedia,
ItemTitle,
} from "@/components/ui/item";

export type { FileType };

Expand Down Expand Up @@ -61,7 +69,7 @@ export default function FileItem({
}

clickCountRef.current += 1;

if (clickCountRef.current === 1) {
clickTimeoutRef.current = setTimeout(() => {
if (clickCountRef.current === 1) {
Expand All @@ -88,13 +96,13 @@ export default function FileItem({
setTimeout(() => {
touchHandledRef.current = false;
}, 400);

const currentTime = new Date().getTime();
const tapLength = currentTime - lastTapRef.current;

if (tapLength < 300 && tapLength > 0) {
// Double tap detected

if (clickTimeoutRef.current) {
clearTimeout(clickTimeoutRef.current);
clickTimeoutRef.current = null;
Expand All @@ -112,18 +120,26 @@ export default function FileItem({
clickTimeoutRef.current = null;
}, 300);
}

lastTapRef.current = currentTime;
};

const secondaryLabel =
file.lastModified
? formatDate(file.lastModified)
: file.type === "folder"
? "Folder"
: file.size
? formatFileSize(file.size)
: "File";

if (view === "grid") {
return (
<section
className={`group relative flex cursor-pointer select-none flex-col items-center justify-center rounded-lg border-2 p-2 transition-colors sm:p-4 ${isSelected
? "border-primary bg-accent"
: "border-transparent bg-background hover:border-border hover:bg-muted"
}`}
style={{ touchAction: 'manipulation' }}
<Item
variant={isSelected ? "muted" : "outline"}
size="default"
className="relative h-full cursor-pointer select-none flex-col items-center justify-center gap-3 py-5 text-center hover:bg-muted"
style={{ touchAction: "manipulation" }}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
onClick={handleClick}
Expand All @@ -138,34 +154,42 @@ export default function FileItem({
}}
>
{isHovered && (
<FileItemMenu
file={file}
position="top-right"
onRename={onRename}
onPreview={onPreview}
onDownload={onDownload}
onCopy={onCopy}
onMove={onMove}
onDelete={onDelete}
onShare={onShare}
/>
<ItemActions className="absolute top-1.5 right-1.5 z-10">
<FileItemMenu
file={file}
position="top-right"
onRename={onRename}
onPreview={onPreview}
onDownload={onDownload}
onCopy={onCopy}
onMove={onMove}
onDelete={onDelete}
onShare={onShare}
/>
</ItemActions>
)}
<div className="mb-1 flex h-12 w-12 items-center justify-center sm:mb-2 sm:h-16 sm:w-16">
<ItemMedia className="mb-0 flex size-14 items-center justify-center rounded-2xl bg-muted sm:size-16">
{getFileIcon(file.type, file.mimeType)}
</div>
<p className="max-w-full truncate text-center text-xs font-medium text-foreground sm:text-sm">
{file.name}
</p>
</section>
</ItemMedia>
<ItemContent className="w-full min-w-0 items-center gap-0.5">
<ItemTitle className="w-full justify-center truncate text-xs sm:text-sm">
{file.name}
</ItemTitle>
<ItemDescription className="line-clamp-1 w-full text-center text-xs">
{secondaryLabel}
</ItemDescription>
</ItemContent>
</Item>
);
}

// List view
return (
<section
className={`group flex cursor-pointer select-none items-center gap-2 border-b border-border px-2 py-2 transition-colors sm:gap-4 sm:px-4 sm:py-3 ${isSelected ? "bg-accent" : "bg-background hover:bg-muted"
}`}
style={{ touchAction: 'manipulation' }}
<Item
variant={isSelected ? "muted" : "outline"}
size="sm"
className="cursor-pointer select-none hover:bg-muted"
style={{ touchAction: "manipulation" }}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
onClick={handleClick}
Expand All @@ -179,32 +203,39 @@ export default function FileItem({
onContextMenu?.(file, event);
}}
>
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center sm:h-10 sm:w-10">
<ItemMedia className="flex size-9 items-center justify-center rounded-xl bg-muted sm:size-10">
{getFileIcon(file.type, file.mimeType)}
</div>
<div className="min-w-0 flex-1">
<p className="truncate text-xs font-medium text-foreground sm:text-sm">{file.name}</p>
</div>
<div className="hidden flex-shrink-0 text-xs text-muted-foreground sm:block sm:text-sm">
{file.lastModified && formatDate(file.lastModified)}
</div>
<div className="hidden flex-shrink-0 text-xs text-muted-foreground md:block md:text-sm">
{file.size && formatFileSize(file.size)}
</div>
</ItemMedia>

<ItemContent className="min-w-0 gap-0.5">
<ItemTitle className="w-full truncate">{file.name}</ItemTitle>
<ItemDescription className="line-clamp-1 text-xs">
{secondaryLabel}
</ItemDescription>
</ItemContent>

<ItemActions className="ml-auto hidden min-w-20 shrink-0 justify-end text-muted-foreground md:flex">
<span className="text-xs sm:text-sm">
{file.size ? formatFileSize(file.size) : file.type === "folder" ? "—" : ""}
</span>
</ItemActions>

{isHovered && (
<FileItemMenu
file={file}
position="right"
onRename={onRename}
onPreview={onPreview}
onDownload={onDownload}
onCopy={onCopy}
onMove={onMove}
onDelete={onDelete}
onShare={onShare}
/>
<ItemActions className="shrink-0">
<FileItemMenu
file={file}
position="right"
onRename={onRename}
onPreview={onPreview}
onDownload={onDownload}
onCopy={onCopy}
onMove={onMove}
onDelete={onDelete}
onShare={onShare}
/>
</ItemActions>
)}
</section>
</Item>
);
}

9 changes: 5 additions & 4 deletions app/components/FileList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useState, useEffect } from "react";
import FileItem, { FileItemData } from "./FileItem";
import Toolbar from "./shared/Toolbar";
import EmptyState from "./shared/EmptyState";
import { ItemGroup } from "@/components/ui/item";

interface FileListProps {
files: FileItemData[];
Expand Down Expand Up @@ -60,7 +61,7 @@ export default function FileList({
{files.length === 0 ? (
<EmptyState message="No files or folders" />
) : view === "grid" ? (
<div className="grid grid-cols-2 gap-2 p-2 sm:grid-cols-3 sm:gap-3 sm:p-4 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6">
<ItemGroup className="grid grid-cols-2 gap-3 p-3 sm:grid-cols-3 sm:gap-3 sm:p-4 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6">
{files.map((file) => (
<FileItem
key={file.id}
Expand All @@ -79,9 +80,9 @@ export default function FileList({
onContextMenu={onFileContextMenu}
/>
))}
</div>
</ItemGroup>
) : (
<div className="divide-y divide-border">
<ItemGroup className="gap-1 p-2 sm:p-4" data-size="sm">
{files.map((file) => (
<FileItem
key={file.id}
Expand All @@ -100,7 +101,7 @@ export default function FileList({
onContextMenu={onFileContextMenu}
/>
))}
</div>
</ItemGroup>
)}
</section>
</main>
Expand Down
16 changes: 10 additions & 6 deletions app/components/shared/Toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ export default function Toolbar({
actions,
}: ToolbarProps) {
return (
<header className="flex items-center justify-between border-b border-border px-2 py-2 sm:px-4">
<nav className="flex items-center gap-1 sm:gap-2" aria-label="View options">
<header className="flex items-center justify-between border-b border-border px-3 py-2.5 sm:px-4">
<nav className="flex items-center gap-2" aria-label="View options">
<ToggleGroup
value={[view]}
onValueChange={(groupValue) => {
Expand All @@ -33,15 +33,19 @@ export default function Toolbar({
spacing={0}
>
<ToggleGroupItem value="list" aria-label="List view">
<List className="h-4 w-4 sm:h-5 sm:w-5" />
<List className="size-4" />
</ToggleGroupItem>
<ToggleGroupItem value="grid" aria-label="Grid view">
<LayoutGrid className="h-4 w-4 sm:h-5 sm:w-5" />
<LayoutGrid className="size-4" />
</ToggleGroupItem>
</ToggleGroup>
{actions && <div className="flex items-center gap-1 sm:gap-2">{actions}</div>}
{actions && <div className="flex items-center gap-2">{actions}</div>}
</nav>
<div className="text-xs text-muted-foreground sm:text-sm" role="status" aria-live="polite">
<div
className="text-xs text-muted-foreground sm:text-sm"
role="status"
aria-live="polite"
>
{itemCount} {itemCount === 1 ? "item" : "items"}
</div>
</header>
Expand Down
Loading
Loading