💄 style: Update file list style and add file name display

This commit is contained in:
jarvis2f 2025-01-10 15:54:04 +08:00
parent b12a6eed1e
commit 6d1f17e99d
10 changed files with 462 additions and 330 deletions

View file

@ -34,31 +34,39 @@ import { Separator } from "./ui/separator";
import { Badge } from "./ui/badge";
import { formatDistanceToNow } from "date-fns";
import FileStatus from "@/components/file-status";
import { cn } from "@/lib/utils";
import { useFileSpeed } from "@/hooks/use-file-speed";
interface FileCardProps {
file: TelegramFile;
}
export function FileCard({ file }: FileCardProps) {
const { downloadProgress, downloadSpeed } = useFileSpeed(file.id);
return (
<FileDrawer file={file}>
<FileDrawer file={file} downloadProgress={downloadProgress}>
<Card>
<CardContent className="p-4">
<div className="flex items-center gap-4">
<FileAvatar file={file} />
<div className="flex-1">
<h3 className="mb-1 font-medium">{file.name}</h3>
<h3 className="max-w-40 truncate font-medium">{file.fileName}</h3>
<div className="mb-2 flex items-center justify-between text-sm text-muted-foreground">
<span>
{prettyBytes(file.size)} {file.type}
</span>
<FileStatus status={file.downloadStatus} />
</div>
<div className="mb-2 w-full overflow-hidden">
<FileProgress file={file} showSize={false} />
<div
className={cn(
"mb-2 w-full overflow-hidden",
file.downloadStatus === "idle" && "hidden",
)}
>
<FileProgress file={file} downloadProgress={downloadProgress} />
</div>
<div className="flex items-center justify-end">
<FileControl file={file} />
<FileControl file={file} downloadSpeed={downloadSpeed} isMobile={true}/>
</div>
</div>
</div>
@ -96,14 +104,14 @@ function FileAvatar({
{loadPreview ? (
<PhotoPreview
thumbnail={file.thumbnail}
name={file.name}
name={file.fileName}
chatId={file.chatId}
messageId={file.messageId}
/>
) : (
<Image
src={`data:image/jpeg;base64,${file.thumbnail}`}
alt={file.name ?? "File thumbnail"}
alt={file.fileName ?? "File thumbnail"}
width={32}
height={32}
className="inline-block h-16 w-16 rounded object-cover"
@ -126,9 +134,11 @@ function FileAvatar({
function FileDrawer({
file,
downloadProgress,
children,
}: {
file: TelegramFile;
downloadProgress: number;
children: React.ReactNode;
}) {
return (
@ -139,21 +149,24 @@ function FileDrawer({
aria-describedby={undefined}
>
<DrawerHeader className="text-center">
<DrawerTitle className="flex items-center justify-center gap-2 text-xl">
<DrawerTitle className="flex flex-col items-center justify-center gap-2">
<FileAvatar file={file} loadPreview={true} />
<span className="max-w-md truncate">{file.name}</span>
<span className="max-w-64 truncate">{file.fileName}</span>
</DrawerTitle>
<div className="py-1">
<FileStatus status={file.downloadStatus} />
</div>
{file.caption && (
<SpoiledWrapper hasSensitiveContent={file.hasSensitiveContent}>
<DrawerDescription className="mx-auto mt-2 max-w-md">
{file.caption}
</DrawerDescription>
<DrawerDescription
className="mx-auto mt-2 max-h-48 max-w-md overflow-y-auto text-start"
dangerouslySetInnerHTML={{
__html: file.caption.replaceAll("\n", "<br />"),
}}
></DrawerDescription>
</SpoiledWrapper>
)}
<FileProgress file={file} showSize={false} />
<FileProgress file={file} downloadProgress={downloadProgress} />
</DrawerHeader>
<div className="px-2">
@ -204,7 +217,7 @@ function FileDrawer({
</Badge>
</div>
{(file.downloadStatus !== 'idle' && file.startDate !== 0) && (
{file.downloadStatus !== "idle" && file.startDate !== 0 && (
<div className="space-y-2">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Clock10 className="h-4 w-4" />

View file

@ -1,4 +1,4 @@
import { type TelegramFile } from "@/lib/types";
import { type DownloadStatus, type TelegramFile } from "@/lib/types";
import { useFileControl } from "@/hooks/use-file-control";
import { Button } from "@/components/ui/button";
import { ArrowDown, Loader2, Pause, SquareX, StepForward } from "lucide-react";
@ -8,8 +8,50 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { type ReactNode } from "react";
import prettyBytes from "pretty-bytes";
import { AnimatePresence, motion } from "framer-motion";
interface ActionButtonProps {
tooltipText: string;
icon: ReactNode;
onClick: () => void;
loading: boolean;
}
const ActionButton = ({
tooltipText,
icon,
onClick,
loading,
}: ActionButtonProps) => (
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="xs" onClick={onClick}>
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : icon}
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{tooltipText}</p>
</TooltipContent>
</Tooltip>
);
export default function FileControl({
file,
downloadSpeed,
hovered,
isMobile,
}: {
file: TelegramFile;
downloadSpeed: number;
hovered?: boolean;
isMobile?: boolean;
}) {
const showDownloadInfo =
!hovered &&
(file.downloadStatus === "downloading" || file.downloadStatus === "paused");
export default function FileControl({ file }: { file: TelegramFile }) {
const { start, starting, togglePause, togglingPause, cancel, cancelling } =
useFileControl(file);
@ -17,87 +59,103 @@ export default function FileControl({ file }: { file: TelegramFile }) {
return null;
}
const statusMapping: Record<DownloadStatus, ActionButtonProps[]> = {
idle: [
{
onClick: () => start(file.id),
tooltipText: "Start Download",
icon: <ArrowDown className="h-4 w-4" />,
loading: starting,
},
],
error: [
{
onClick: () => start(file.id),
tooltipText: "Retry",
icon: <ArrowDown className="h-4 w-4" />,
loading: starting,
},
],
downloading: [
{
onClick: () => togglePause(file.id),
tooltipText: "Pause",
icon: <Pause className="h-4 w-4" />,
loading: togglingPause,
},
{
onClick: () => cancel(file.id),
tooltipText: "Cancel",
icon: <SquareX className="h-4 w-4" />,
loading: cancelling,
},
],
paused: [
{
onClick: () => togglePause(file.id),
tooltipText: "Resume",
icon: <StepForward className="h-4 w-4" />,
loading: togglingPause,
},
{
onClick: () => cancel(file.id),
tooltipText: "Cancel",
icon: <SquareX className="h-4 w-4" />,
loading: cancelling,
},
],
completed: [],
};
const actionButtons = (
<div className="w-full">
<div
className="flex w-full items-center justify-end space-x-2 md:justify-around"
onClick={(e) => e.preventDefault()}
>
{statusMapping[file.downloadStatus].map((btnProps, index) => (
<ActionButton key={index} {...btnProps} />
))}
</div>
</div>
);
if (isMobile) {
return <TooltipProvider>{actionButtons}</TooltipProvider>;
}
return (
<TooltipProvider>
<div className="flex w-full justify-end md:justify-around">
{(file.downloadStatus === "idle" ||
file.downloadStatus === "error") && (
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="xs" onClick={() => start(file.id)}>
{starting ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<ArrowDown className="h-4 w-4" />
)}
</Button>
</TooltipTrigger>
<TooltipContent>
<p>
{file.downloadStatus === "idle"
? "Start Download"
: file.downloadStatus === "error"
? "Retry"
: "Downloading"}
</p>
</TooltipContent>
</Tooltip>
)}
{(file.downloadStatus === "downloading" ||
file.downloadStatus === "paused") && (
<>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="xs"
onClick={() => togglePause(file.id)}
>
{togglingPause ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : file.downloadStatus === "downloading" ? (
<Pause className="h-4 w-4" />
) : (
<StepForward className="h-4 w-4" />
)}
</Button>
</TooltipTrigger>
<TooltipContent>
<p>
{file.downloadStatus === "downloading"
? "Pause"
: file.downloadStatus === "paused"
? "Resume"
: "Paused"}
</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="xs"
onClick={() => cancel(file.id)}
>
{cancelling ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<SquareX className="h-4 w-4" />
)}
</Button>
</TooltipTrigger>
<TooltipContent>
<p>
{file.downloadStatus === "downloading" ||
file.downloadStatus === "paused" ||
file.downloadStatus === "error"
? "Cancel"
: "Cancelled"}
</p>
</TooltipContent>
</Tooltip>
</>
)}
<div className="relative h-6 overflow-hidden">
<AnimatePresence mode="wait">
{showDownloadInfo ? (
<motion.div
key="downloadInfo"
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: -20, opacity: 0 }}
transition={{ duration: 0.2, ease: "easeOut" }}
className="absolute w-full"
>
<span className="text-nowrap text-xs">
{file.downloadStatus === "downloading"
? `${prettyBytes(downloadSpeed, { bits: true })}/s`
: prettyBytes(file.downloadedSize)}
</span>
</motion.div>
) : (
<motion.div
key="actionButtons"
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: -20, opacity: 0 }}
transition={{ duration: 0.2, ease: "easeOut" }}
className="absolute w-full"
>
{actionButtons}
</motion.div>
)}
</AnimatePresence>
</div>
</TooltipProvider>
);

View file

@ -1,4 +1,11 @@
import { Captions, Clock, ClockArrowDown, Copy, FileCheck } from "lucide-react";
import {
Captions,
Clock,
ClockArrowDown,
Copy,
FileCheck,
Mountain,
} from "lucide-react";
import SpoiledWrapper from "@/components/spoiled-wrapper";
import {
Tooltip,
@ -11,6 +18,7 @@ import { type TelegramFile } from "@/lib/types";
import { type RowHeight } from "@/components/table-row-height-switch";
import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard";
import { formatDistanceToNow } from "date-fns";
import { cn } from "@/lib/utils";
interface FileExtraProps {
file: TelegramFile;
@ -23,21 +31,39 @@ export default function FileExtra({ file, rowHeight }: FileExtraProps) {
return (
<div className="relative flex flex-col space-y-1 overflow-hidden">
<TooltipProvider>
{file.fileName && (
<SpoiledWrapper hasSensitiveContent={file.hasSensitiveContent}>
<p className="flex items-center gap-2">
<Mountain className="h-4 w-4" />
<span className="rounded px-1 text-sm hover:bg-gray-100">
{file.fileName}
</span>
</p>
</SpoiledWrapper>
)}
{rowHeight !== "s" && file.caption && (
<SpoiledWrapper hasSensitiveContent={file.hasSensitiveContent}>
<Tooltip>
<TooltipTrigger>
<div className="flex items-center gap-2">
<Captions className="h-4 w-4 flex-shrink-0" />
<p className="line-clamp-2 overflow-hidden truncate text-ellipsis text-wrap text-sm">
<p
className={cn(
rowHeight !== "l" && "line-clamp-1",
"overflow-hidden truncate text-ellipsis text-wrap text-start text-sm",
)}
>
{file.caption}
</p>
</div>
</TooltipTrigger>
<TooltipContent asChild>
<div className="max-w-80 text-wrap rounded p-2">
{file.caption}
</div>
<TooltipContent>
<p
className="max-w-80 text-wrap rounded p-2"
dangerouslySetInnerHTML={{
__html: file.caption.replaceAll("\n", "<br />"),
}}
></p>
</TooltipContent>
</Tooltip>
</SpoiledWrapper>
@ -63,31 +89,14 @@ export default function FileExtra({ file, rowHeight }: FileExtraProps) {
</TooltipContent>
</Tooltip>
)}
<div className="flex items-center gap-2">
<Tooltip>
<TooltipTrigger asChild>
<p className="flex items-center gap-2">
<Clock className="h-4 w-4" />
<span className="rounded px-1 text-sm text-muted-foreground hover:bg-gray-100">
{formatDistanceToNow(new Date(file.date * 1000), {
addSuffix: true,
})}
</span>
</p>
</TooltipTrigger>
<TooltipContent>
<div className="max-w-80 text-wrap rounded p-2">
{`Message received at ${new Date(file.date * 1000).toLocaleString()}`}
</div>
</TooltipContent>
</Tooltip>
{file.completionDate && (
{rowHeight !== "s" && (
<div className="flex items-center gap-2">
<Tooltip>
<TooltipTrigger asChild>
<p className="flex items-center gap-2">
<ClockArrowDown className="h-4 w-4" />
<Clock className="h-4 w-4" />
<span className="rounded px-1 text-sm text-muted-foreground hover:bg-gray-100">
{formatDistanceToNow(new Date(file.completionDate), {
{formatDistanceToNow(new Date(file.date * 1000), {
addSuffix: true,
})}
</span>
@ -95,12 +104,31 @@ export default function FileExtra({ file, rowHeight }: FileExtraProps) {
</TooltipTrigger>
<TooltipContent>
<div className="max-w-80 text-wrap rounded p-2">
{`File downloaded at ${new Date(file.completionDate).toLocaleString()}`}
{`Message received at ${new Date(file.date * 1000).toLocaleString()}`}
</div>
</TooltipContent>
</Tooltip>
)}
</div>
{file.completionDate && (
<Tooltip>
<TooltipTrigger asChild>
<p className="flex items-center gap-2">
<ClockArrowDown className="h-4 w-4" />
<span className="rounded px-1 text-sm text-muted-foreground hover:bg-gray-100">
{formatDistanceToNow(new Date(file.completionDate), {
addSuffix: true,
})}
</span>
</p>
</TooltipTrigger>
<TooltipContent>
<div className="max-w-80 text-wrap rounded p-2">
{`File downloaded at ${new Date(file.completionDate).toLocaleString()}`}
</div>
</TooltipContent>
</Tooltip>
)}
</div>
)}
</TooltipProvider>
</div>
);

View file

@ -1,14 +1,6 @@
"use client";
import { type TelegramFile } from "@/lib/types";
import { FileCard } from "./file-card";
import React, {
memo,
type ReactNode,
useCallback,
useEffect,
useRef,
useState,
} from "react";
import React, { useEffect, useMemo, useRef, useState } from "react";
import {
Table,
TableBody,
@ -19,41 +11,17 @@ import {
} from "@/components/ui/table";
import { Checkbox } from "@/components/ui/checkbox";
import { Button } from "@/components/ui/button";
import {
Download,
FileAudioIcon,
FileIcon,
ImageIcon,
LoaderCircle,
LoaderPinwheel,
VideoIcon,
} from "lucide-react";
import { Download, LoaderCircle, LoaderPinwheel } from "lucide-react";
import { useFiles } from "@/hooks/use-files";
import { FileFilters } from "@/components/file-filters";
import {
getRowHeightTailwindClass,
useRowHeightLocalStorage,
} from "@/components/table-row-height-switch";
import { useRowHeightLocalStorage } from "@/components/table-row-height-switch";
import { type Column } from "@/components/table-column-filter";
import { cn } from "@/lib/utils";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import PhotoPreview from "@/components/photo-preview";
import SpoiledWrapper from "@/components/spoiled-wrapper";
import Image from "next/image";
import FileControl from "@/components/file-control";
import prettyBytes from "pretty-bytes";
import FileProgress from "@/components/file-progress";
import FileNotFount from "@/components/file-not-found";
import FileExtra from "@/components/file-extra";
import useIsMobile from "@/hooks/use-is-mobile";
import FileStatus from "@/components/file-status";
import useSWRMutation from "swr/mutation";
import { POST } from "@/lib/api";
import FileRow from "@/components/file-row";
interface FileListProps {
accountId: string;
@ -84,26 +52,6 @@ const COLUMNS: Column[] = [
},
];
const PhotoColumnImage = memo(function PhotoColumnImage({
thumbnail,
name,
wh,
}: {
thumbnail: string;
name: string;
wh: string;
}) {
return (
<Image
src={`data:image/jpeg;base64,${thumbnail}`}
alt={name ?? "File thumbnail"}
width={32}
height={32}
className={cn(wh, "rounded object-cover")}
/>
);
});
export function FileList({ accountId, chatId }: FileListProps) {
const isMobile = useIsMobile();
const [selectedFiles, setSelectedFiles] = useState<Set<number>>(new Set());
@ -155,12 +103,12 @@ export function FileList({ accountId, chatId }: FileListProps) {
return () => observer.disconnect();
}, [handleLoadMore]);
const dynamicClass = useCallback(() => {
const dynamicClass = useMemo(() => {
switch (rowHeight) {
case "s":
return {
content: "h-6 w-6",
contentCell: "w-6",
contentCell: "w-4",
};
case "m":
return {
@ -234,84 +182,6 @@ export function FileList({ accountId, chatId }: FileListProps) {
setSelectedFiles(newSelected);
};
const getFileIcon = (type: TelegramFile["type"]) => {
let icon;
switch (type) {
case "photo":
icon = <ImageIcon className="h-4 w-4" />;
break;
case "video":
icon = <VideoIcon className="h-4 w-4" />;
break;
case "audio":
icon = <FileAudioIcon className="h-4 w-4" />;
break;
default:
icon = <FileIcon className="h-4 w-4" />;
}
return (
<div
className={cn(
dynamicClass().content,
"flex items-center justify-center rounded bg-muted",
)}
>
{icon}
</div>
);
};
const columnRenders: Record<
string,
(file: TelegramFile, index: number) => ReactNode
> = {
content: (file: TelegramFile) => (
<div className="flex items-center gap-2">
{file.type === "photo" || file.type === "video" ? (
file.thumbnail ? (
<SpoiledWrapper hasSensitiveContent={file.hasSensitiveContent}>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<PhotoColumnImage
thumbnail={file.thumbnail}
name={file.name}
wh={dynamicClass().content}
/>
</TooltipTrigger>
<TooltipContent asChild>
<PhotoPreview
thumbnail={file.thumbnail}
name={file.name}
chatId={file.chatId}
messageId={file.messageId}
/>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</SpoiledWrapper>
) : (
getFileIcon(file.type)
)
) : (
getFileIcon(file.type)
)}
</div>
),
type: (file: TelegramFile) => (
<div className="flex flex-col items-center">
<span className="capitalize">{file.type}</span>
<span className="text-xs">{file.id}</span>
</div>
),
size: (file: TelegramFile) => <span>{prettyBytes(file.size)}</span>,
status: (file: TelegramFile) => <FileStatus status={file.downloadStatus} />,
extra: (file: TelegramFile) => (
<FileExtra file={file} rowHeight={rowHeight} />
),
actions: (file: TelegramFile) => <FileControl file={file} />,
};
return (
<>
<FileFilters
@ -381,7 +251,7 @@ export function FileList({ accountId, chatId }: FileListProps) {
suppressHydrationWarning
className={cn(
col.className ?? "",
col.id === "content" ? dynamicClass().contentCell : "",
col.id === "content" ? dynamicClass.contentCell : "",
)}
>
{col.label}
@ -392,47 +262,13 @@ export function FileList({ accountId, chatId }: FileListProps) {
</TableHeader>
<TableBody className="[&_tr:last-child]:border-b">
{files.map((file, index) => (
<React.Fragment
<FileRow
file={file}
checked={selectedFiles.has(file.id)}
onCheckedChange={() => handleSelectFile(file.id)}
properties={{ rowHeight, dynamicClass, columns }}
key={`${file.messageId}-${file.uniqueId}-${index}`}
>
<TableRow
className={cn(
getRowHeightTailwindClass(rowHeight),
"border-b-0",
)}
>
<TableCell className="text-center">
<Checkbox
checked={selectedFiles.has(file.id)}
onCheckedChange={() => handleSelectFile(file.id)}
disabled={file.downloadStatus !== "idle"}
/>
</TableCell>
{columns.map((col) =>
col.isVisible ? (
<TableCell
key={col.id}
className={cn(
col.className ?? "",
col.id === "content"
? dynamicClass().contentCell
: "",
)}
>
{columnRenders[col.id]!(file, index)}
</TableCell>
) : null,
)}
</TableRow>
<TableRow>
<TableCell
colSpan={columns.length + 1}
className="h-px p-0"
>
<FileProgress file={file} showSize={true} />
</TableCell>
</TableRow>
</React.Fragment>
/>
))}
{!isLoading && files.length === 0 && (
<TableRow className="border-b-0">

View file

@ -1,18 +1,14 @@
import { Progress } from "@/components/ui/progress";
import prettyBytes from "pretty-bytes";
import { type TelegramFile } from "@/lib/types";
import { useFileSpeed } from "@/hooks/use-file-speed";
import { useMemo } from "react";
import { ClockArrowDown, Zap } from "lucide-react";
export default function FileProgress({
file,
showSize,
downloadProgress,
}: {
file: TelegramFile;
showSize: boolean;
downloadProgress: number;
}) {
const { downloadProgress, downloadSpeed } = useFileSpeed(file.id);
const progress = useMemo(() => {
const fileDownloadProgress =
file.size > 0
@ -37,22 +33,30 @@ export default function FileProgress({
return (
<div className="flex items-end justify-between gap-2">
{showSize && file.downloadedSize > 0 && (
<div className="flex min-w-32 items-center gap-1 bg-gray-100 px-1">
<ClockArrowDown className="h-3 w-3" />
<span className="text-nowrap text-xs">
{prettyBytes(file.downloadedSize)}
</span>
</div>
)}
<div className="hidden min-w-32 items-center gap-1 bg-gray-100 px-1 md:flex">
<Zap className="h-3 w-3" />
<span className="text-nowrap text-xs">
{file.downloadStatus === "downloading"
? `${prettyBytes(downloadSpeed, { bits: true })}/s`
: "0/s"}
</span>
</div>
{/*<div className="hidden min-w-20 md:flex">*/}
{/* <div className="group flex items-center gap-1 px-1">*/}
{/* <AnimatePresence mode="wait">*/}
{/* <motion.div*/}
{/* key="content"*/}
{/* className="flex items-center gap-1"*/}
{/* initial={{ opacity: 1 }}*/}
{/* exit={{ opacity: 0 }}*/}
{/* >*/}
{/* <Zap className="h-3 w-3 group-hover:hidden" />*/}
{/* <ClockArrowDown className="hidden h-3 w-3 group-hover:block" />*/}
{/* <span className="text-nowrap text-xs">*/}
{/* <span className="group-hover:hidden">*/}
{/* {`${prettyBytes(downloadSpeed, { bits: true })}/s`}*/}
{/* </span>*/}
{/* <span className="hidden group-hover:inline">*/}
{/* {prettyBytes(file.downloadedSize)}*/}
{/* </span>*/}
{/* </span>*/}
{/* </motion.div>*/}
{/* </AnimatePresence>*/}
{/* </div>*/}
{/*</div>*/}
<Progress value={progress} className="flex-1 rounded-none md:w-32" />
</div>
);

View file

@ -0,0 +1,189 @@
import { TableCell, TableRow } from "@/components/ui/table";
import { cn } from "@/lib/utils";
import {
getRowHeightTailwindClass,
type RowHeight,
} from "@/components/table-row-height-switch";
import { Checkbox } from "@/components/ui/checkbox";
import FileProgress from "@/components/file-progress";
import React, { memo, type ReactNode, useState } from "react";
import { type TelegramFile } from "@/lib/types";
import SpoiledWrapper from "@/components/spoiled-wrapper";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import PhotoPreview from "@/components/photo-preview";
import { env } from "@/env";
import prettyBytes from "pretty-bytes";
import FileStatus from "@/components/file-status";
import FileExtra from "@/components/file-extra";
import FileControl from "@/components/file-control";
import { FileAudioIcon, FileIcon, ImageIcon, VideoIcon } from "lucide-react";
import Image from "next/image";
import { type Column } from "./table-column-filter";
import { useFileSpeed } from "@/hooks/use-file-speed";
interface FileRowProps {
file: TelegramFile;
checked: boolean;
properties: {
rowHeight: RowHeight;
dynamicClass: {
content: string;
contentCell: string;
};
columns: Column[];
};
onCheckedChange: (checked: boolean) => void;
}
export default function FileRow({
file,
checked,
properties,
onCheckedChange,
}: FileRowProps) {
const { rowHeight, dynamicClass, columns } = properties;
const { downloadProgress, downloadSpeed } = useFileSpeed(file.id);
const [hovered, setHovered] = useState(false);
const getFileIcon = (type: TelegramFile["type"]) => {
let icon;
switch (type) {
case "photo":
icon = <ImageIcon className="h-4 w-4" />;
break;
case "video":
icon = <VideoIcon className="h-4 w-4" />;
break;
case "audio":
icon = <FileAudioIcon className="h-4 w-4" />;
break;
default:
icon = <FileIcon className="h-4 w-4" />;
}
return (
<div
className={cn(
dynamicClass.content,
"flex items-center justify-center rounded border",
)}
>
{icon}
</div>
);
};
const columnRenders: Record<string, ReactNode> = {
content: (
<div className="flex items-center justify-center gap-2">
{file.type === "photo" || file.type === "video" ? (
file.thumbnail ? (
<SpoiledWrapper hasSensitiveContent={file.hasSensitiveContent}>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<PhotoColumnImage
thumbnail={file.thumbnail}
name={file.fileName}
wh={dynamicClass.content}
/>
</TooltipTrigger>
<TooltipContent asChild>
<PhotoPreview
thumbnail={file.thumbnail}
name={file.fileName}
chatId={file.chatId}
messageId={file.messageId}
/>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</SpoiledWrapper>
) : (
getFileIcon(file.type)
)
) : (
getFileIcon(file.type)
)}
</div>
),
type: (
<div className="flex flex-col items-center">
<span className="capitalize">{file.type}</span>
{env.NEXT_PUBLIC_MOCK_DATA === true && (
<span className="text-xs">{file.id}</span>
)}
</div>
),
size: <span>{prettyBytes(file.size)}</span>,
status: <FileStatus status={file.downloadStatus} />,
extra: <FileExtra file={file} rowHeight={rowHeight} />,
actions: (
<FileControl
file={file}
downloadSpeed={downloadSpeed}
hovered={hovered}
/>
),
};
return (
<React.Fragment>
<TableRow
className={cn(getRowHeightTailwindClass(rowHeight), "border-b-0")}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
<TableCell className="text-center">
<Checkbox
checked={checked}
onCheckedChange={onCheckedChange}
disabled={file.downloadStatus !== "idle"}
/>
</TableCell>
{columns.map((col) =>
col.isVisible ? (
<TableCell
key={col.id}
className={cn(
col.className ?? "",
col.id === "content" ? dynamicClass.contentCell : "",
)}
>
{columnRenders[col.id]}
</TableCell>
) : null,
)}
</TableRow>
<TableRow>
<TableCell colSpan={columns.length + 1} className="h-px p-0">
<FileProgress file={file} downloadProgress={downloadProgress} />
</TableCell>
</TableRow>
</React.Fragment>
);
}
const PhotoColumnImage = memo(function PhotoColumnImage({
thumbnail,
name,
wh,
}: {
thumbnail: string;
name: string;
wh: string;
}) {
return (
<Image
src={`data:image/jpeg;base64,${thumbnail}`}
alt={name ?? "File thumbnail"}
width={32}
height={32}
className={cn(wh, "rounded object-cover")}
/>
);
});

View file

@ -27,7 +27,11 @@ export default function FileStatus({ status }: { status: DownloadStatus }) {
},
};
return <Badge className={cn("text-xs", STATUS[status].className)}>
{STATUS[status].text}
</Badge>;
return (
<Badge
className={cn("text-xs hover:bg-gray-200", STATUS[status].className)}
>
{STATUS[status].text}
</Badge>
);
}

View file

@ -246,7 +246,7 @@ export default function Proxys({
<div className="mr-2 flex h-5 w-5 items-center justify-center rounded-full border-2 border-gray-300 peer-checked:border-primary peer-checked:bg-primary">
<div className="h-2.5 w-2.5 rounded-full bg-white"></div>
</div>
<span className="text-gray-700 peer-checked:text-primary">
<span className="text-gray-400 peer-checked:text-primary">
HTTP
</span>
</label>

View file

@ -118,7 +118,7 @@ const SelectItem = React.forwardRef<
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 hover:bg-accent hover:text-accent-foreground",
className,
)}
{...props}

View file

@ -36,7 +36,7 @@ export type TelegramFile = {
uniqueId: string;
messageId: number;
chatId: number;
name: string;
fileName: string;
type: FileType;
size: number;
downloadedSize: number;