💄 style: Update file list style and add file name display
This commit is contained in:
parent
b12a6eed1e
commit
6d1f17e99d
10 changed files with 462 additions and 330 deletions
|
|
@ -34,31 +34,39 @@ import { Separator } from "./ui/separator";
|
||||||
import { Badge } from "./ui/badge";
|
import { Badge } from "./ui/badge";
|
||||||
import { formatDistanceToNow } from "date-fns";
|
import { formatDistanceToNow } from "date-fns";
|
||||||
import FileStatus from "@/components/file-status";
|
import FileStatus from "@/components/file-status";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useFileSpeed } from "@/hooks/use-file-speed";
|
||||||
|
|
||||||
interface FileCardProps {
|
interface FileCardProps {
|
||||||
file: TelegramFile;
|
file: TelegramFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FileCard({ file }: FileCardProps) {
|
export function FileCard({ file }: FileCardProps) {
|
||||||
|
const { downloadProgress, downloadSpeed } = useFileSpeed(file.id);
|
||||||
return (
|
return (
|
||||||
<FileDrawer file={file}>
|
<FileDrawer file={file} downloadProgress={downloadProgress}>
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<FileAvatar file={file} />
|
<FileAvatar file={file} />
|
||||||
<div className="flex-1">
|
<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">
|
<div className="mb-2 flex items-center justify-between text-sm text-muted-foreground">
|
||||||
<span>
|
<span>
|
||||||
{prettyBytes(file.size)} • {file.type}
|
{prettyBytes(file.size)} • {file.type}
|
||||||
</span>
|
</span>
|
||||||
<FileStatus status={file.downloadStatus} />
|
<FileStatus status={file.downloadStatus} />
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-2 w-full overflow-hidden">
|
<div
|
||||||
<FileProgress file={file} showSize={false} />
|
className={cn(
|
||||||
|
"mb-2 w-full overflow-hidden",
|
||||||
|
file.downloadStatus === "idle" && "hidden",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<FileProgress file={file} downloadProgress={downloadProgress} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-end">
|
<div className="flex items-center justify-end">
|
||||||
<FileControl file={file} />
|
<FileControl file={file} downloadSpeed={downloadSpeed} isMobile={true}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -96,14 +104,14 @@ function FileAvatar({
|
||||||
{loadPreview ? (
|
{loadPreview ? (
|
||||||
<PhotoPreview
|
<PhotoPreview
|
||||||
thumbnail={file.thumbnail}
|
thumbnail={file.thumbnail}
|
||||||
name={file.name}
|
name={file.fileName}
|
||||||
chatId={file.chatId}
|
chatId={file.chatId}
|
||||||
messageId={file.messageId}
|
messageId={file.messageId}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Image
|
<Image
|
||||||
src={`data:image/jpeg;base64,${file.thumbnail}`}
|
src={`data:image/jpeg;base64,${file.thumbnail}`}
|
||||||
alt={file.name ?? "File thumbnail"}
|
alt={file.fileName ?? "File thumbnail"}
|
||||||
width={32}
|
width={32}
|
||||||
height={32}
|
height={32}
|
||||||
className="inline-block h-16 w-16 rounded object-cover"
|
className="inline-block h-16 w-16 rounded object-cover"
|
||||||
|
|
@ -126,9 +134,11 @@ function FileAvatar({
|
||||||
|
|
||||||
function FileDrawer({
|
function FileDrawer({
|
||||||
file,
|
file,
|
||||||
|
downloadProgress,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
file: TelegramFile;
|
file: TelegramFile;
|
||||||
|
downloadProgress: number;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -139,21 +149,24 @@ function FileDrawer({
|
||||||
aria-describedby={undefined}
|
aria-describedby={undefined}
|
||||||
>
|
>
|
||||||
<DrawerHeader className="text-center">
|
<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} />
|
<FileAvatar file={file} loadPreview={true} />
|
||||||
<span className="max-w-md truncate">{file.name}</span>
|
<span className="max-w-64 truncate">{file.fileName}</span>
|
||||||
</DrawerTitle>
|
</DrawerTitle>
|
||||||
<div className="py-1">
|
<div className="py-1">
|
||||||
<FileStatus status={file.downloadStatus} />
|
<FileStatus status={file.downloadStatus} />
|
||||||
</div>
|
</div>
|
||||||
{file.caption && (
|
{file.caption && (
|
||||||
<SpoiledWrapper hasSensitiveContent={file.hasSensitiveContent}>
|
<SpoiledWrapper hasSensitiveContent={file.hasSensitiveContent}>
|
||||||
<DrawerDescription className="mx-auto mt-2 max-w-md">
|
<DrawerDescription
|
||||||
{file.caption}
|
className="mx-auto mt-2 max-h-48 max-w-md overflow-y-auto text-start"
|
||||||
</DrawerDescription>
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: file.caption.replaceAll("\n", "<br />"),
|
||||||
|
}}
|
||||||
|
></DrawerDescription>
|
||||||
</SpoiledWrapper>
|
</SpoiledWrapper>
|
||||||
)}
|
)}
|
||||||
<FileProgress file={file} showSize={false} />
|
<FileProgress file={file} downloadProgress={downloadProgress} />
|
||||||
</DrawerHeader>
|
</DrawerHeader>
|
||||||
|
|
||||||
<div className="px-2">
|
<div className="px-2">
|
||||||
|
|
@ -204,7 +217,7 @@ function FileDrawer({
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(file.downloadStatus !== 'idle' && file.startDate !== 0) && (
|
{file.downloadStatus !== "idle" && file.startDate !== 0 && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
<Clock10 className="h-4 w-4" />
|
<Clock10 className="h-4 w-4" />
|
||||||
|
|
|
||||||
|
|
@ -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 { useFileControl } from "@/hooks/use-file-control";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ArrowDown, Loader2, Pause, SquareX, StepForward } from "lucide-react";
|
import { ArrowDown, Loader2, Pause, SquareX, StepForward } from "lucide-react";
|
||||||
|
|
@ -8,8 +8,50 @@ import {
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} 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 } =
|
const { start, starting, togglePause, togglingPause, cancel, cancelling } =
|
||||||
useFileControl(file);
|
useFileControl(file);
|
||||||
|
|
||||||
|
|
@ -17,87 +59,103 @@ export default function FileControl({ file }: { file: TelegramFile }) {
|
||||||
return null;
|
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 (
|
return (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<div className="flex w-full justify-end md:justify-around">
|
<div className="relative h-6 overflow-hidden">
|
||||||
{(file.downloadStatus === "idle" ||
|
<AnimatePresence mode="wait">
|
||||||
file.downloadStatus === "error") && (
|
{showDownloadInfo ? (
|
||||||
<Tooltip>
|
<motion.div
|
||||||
<TooltipTrigger asChild>
|
key="downloadInfo"
|
||||||
<Button variant="ghost" size="xs" onClick={() => start(file.id)}>
|
initial={{ y: 20, opacity: 0 }}
|
||||||
{starting ? (
|
animate={{ y: 0, opacity: 1 }}
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
exit={{ y: -20, opacity: 0 }}
|
||||||
) : (
|
transition={{ duration: 0.2, ease: "easeOut" }}
|
||||||
<ArrowDown className="h-4 w-4" />
|
className="absolute w-full"
|
||||||
)}
|
>
|
||||||
</Button>
|
<span className="text-nowrap text-xs">
|
||||||
</TooltipTrigger>
|
{file.downloadStatus === "downloading"
|
||||||
<TooltipContent>
|
? `${prettyBytes(downloadSpeed, { bits: true })}/s`
|
||||||
<p>
|
: prettyBytes(file.downloadedSize)}
|
||||||
{file.downloadStatus === "idle"
|
</span>
|
||||||
? "Start Download"
|
</motion.div>
|
||||||
: file.downloadStatus === "error"
|
) : (
|
||||||
? "Retry"
|
<motion.div
|
||||||
: "Downloading"}
|
key="actionButtons"
|
||||||
</p>
|
initial={{ y: 20, opacity: 0 }}
|
||||||
</TooltipContent>
|
animate={{ y: 0, opacity: 1 }}
|
||||||
</Tooltip>
|
exit={{ y: -20, opacity: 0 }}
|
||||||
)}
|
transition={{ duration: 0.2, ease: "easeOut" }}
|
||||||
{(file.downloadStatus === "downloading" ||
|
className="absolute w-full"
|
||||||
file.downloadStatus === "paused") && (
|
>
|
||||||
<>
|
{actionButtons}
|
||||||
<Tooltip>
|
</motion.div>
|
||||||
<TooltipTrigger asChild>
|
)}
|
||||||
<Button
|
</AnimatePresence>
|
||||||
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>
|
</div>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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 SpoiledWrapper from "@/components/spoiled-wrapper";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
|
|
@ -11,6 +18,7 @@ import { type TelegramFile } from "@/lib/types";
|
||||||
import { type RowHeight } from "@/components/table-row-height-switch";
|
import { type RowHeight } from "@/components/table-row-height-switch";
|
||||||
import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard";
|
import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard";
|
||||||
import { formatDistanceToNow } from "date-fns";
|
import { formatDistanceToNow } from "date-fns";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface FileExtraProps {
|
interface FileExtraProps {
|
||||||
file: TelegramFile;
|
file: TelegramFile;
|
||||||
|
|
@ -23,21 +31,39 @@ export default function FileExtra({ file, rowHeight }: FileExtraProps) {
|
||||||
return (
|
return (
|
||||||
<div className="relative flex flex-col space-y-1 overflow-hidden">
|
<div className="relative flex flex-col space-y-1 overflow-hidden">
|
||||||
<TooltipProvider>
|
<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 && (
|
{rowHeight !== "s" && file.caption && (
|
||||||
<SpoiledWrapper hasSensitiveContent={file.hasSensitiveContent}>
|
<SpoiledWrapper hasSensitiveContent={file.hasSensitiveContent}>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Captions className="h-4 w-4 flex-shrink-0" />
|
<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}
|
{file.caption}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent asChild>
|
<TooltipContent>
|
||||||
<div className="max-w-80 text-wrap rounded p-2">
|
<p
|
||||||
{file.caption}
|
className="max-w-80 text-wrap rounded p-2"
|
||||||
</div>
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: file.caption.replaceAll("\n", "<br />"),
|
||||||
|
}}
|
||||||
|
></p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</SpoiledWrapper>
|
</SpoiledWrapper>
|
||||||
|
|
@ -63,31 +89,14 @@ export default function FileExtra({ file, rowHeight }: FileExtraProps) {
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center gap-2">
|
{rowHeight !== "s" && (
|
||||||
<Tooltip>
|
<div className="flex items-center gap-2">
|
||||||
<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 && (
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<p className="flex items-center gap-2">
|
<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">
|
<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,
|
addSuffix: true,
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -95,12 +104,31 @@ export default function FileExtra({ file, rowHeight }: FileExtraProps) {
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<div className="max-w-80 text-wrap rounded p-2">
|
<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>
|
</div>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
{file.completionDate && (
|
||||||
</div>
|
<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>
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { type TelegramFile } from "@/lib/types";
|
|
||||||
import { FileCard } from "./file-card";
|
import { FileCard } from "./file-card";
|
||||||
import React, {
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||||
memo,
|
|
||||||
type ReactNode,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
|
|
@ -19,41 +11,17 @@ import {
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import { Download, LoaderCircle, LoaderPinwheel } from "lucide-react";
|
||||||
Download,
|
|
||||||
FileAudioIcon,
|
|
||||||
FileIcon,
|
|
||||||
ImageIcon,
|
|
||||||
LoaderCircle,
|
|
||||||
LoaderPinwheel,
|
|
||||||
VideoIcon,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useFiles } from "@/hooks/use-files";
|
import { useFiles } from "@/hooks/use-files";
|
||||||
import { FileFilters } from "@/components/file-filters";
|
import { FileFilters } from "@/components/file-filters";
|
||||||
import {
|
import { useRowHeightLocalStorage } from "@/components/table-row-height-switch";
|
||||||
getRowHeightTailwindClass,
|
|
||||||
useRowHeightLocalStorage,
|
|
||||||
} from "@/components/table-row-height-switch";
|
|
||||||
import { type Column } from "@/components/table-column-filter";
|
import { type Column } from "@/components/table-column-filter";
|
||||||
import { cn } from "@/lib/utils";
|
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 FileNotFount from "@/components/file-not-found";
|
||||||
import FileExtra from "@/components/file-extra";
|
|
||||||
import useIsMobile from "@/hooks/use-is-mobile";
|
import useIsMobile from "@/hooks/use-is-mobile";
|
||||||
import FileStatus from "@/components/file-status";
|
|
||||||
import useSWRMutation from "swr/mutation";
|
import useSWRMutation from "swr/mutation";
|
||||||
import { POST } from "@/lib/api";
|
import { POST } from "@/lib/api";
|
||||||
|
import FileRow from "@/components/file-row";
|
||||||
|
|
||||||
interface FileListProps {
|
interface FileListProps {
|
||||||
accountId: string;
|
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) {
|
export function FileList({ accountId, chatId }: FileListProps) {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const [selectedFiles, setSelectedFiles] = useState<Set<number>>(new Set());
|
const [selectedFiles, setSelectedFiles] = useState<Set<number>>(new Set());
|
||||||
|
|
@ -155,12 +103,12 @@ export function FileList({ accountId, chatId }: FileListProps) {
|
||||||
return () => observer.disconnect();
|
return () => observer.disconnect();
|
||||||
}, [handleLoadMore]);
|
}, [handleLoadMore]);
|
||||||
|
|
||||||
const dynamicClass = useCallback(() => {
|
const dynamicClass = useMemo(() => {
|
||||||
switch (rowHeight) {
|
switch (rowHeight) {
|
||||||
case "s":
|
case "s":
|
||||||
return {
|
return {
|
||||||
content: "h-6 w-6",
|
content: "h-6 w-6",
|
||||||
contentCell: "w-6",
|
contentCell: "w-4",
|
||||||
};
|
};
|
||||||
case "m":
|
case "m":
|
||||||
return {
|
return {
|
||||||
|
|
@ -234,84 +182,6 @@ export function FileList({ accountId, chatId }: FileListProps) {
|
||||||
setSelectedFiles(newSelected);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<FileFilters
|
<FileFilters
|
||||||
|
|
@ -381,7 +251,7 @@ export function FileList({ accountId, chatId }: FileListProps) {
|
||||||
suppressHydrationWarning
|
suppressHydrationWarning
|
||||||
className={cn(
|
className={cn(
|
||||||
col.className ?? "",
|
col.className ?? "",
|
||||||
col.id === "content" ? dynamicClass().contentCell : "",
|
col.id === "content" ? dynamicClass.contentCell : "",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{col.label}
|
{col.label}
|
||||||
|
|
@ -392,47 +262,13 @@ export function FileList({ accountId, chatId }: FileListProps) {
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody className="[&_tr:last-child]:border-b">
|
<TableBody className="[&_tr:last-child]:border-b">
|
||||||
{files.map((file, index) => (
|
{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}`}
|
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 && (
|
{!isLoading && files.length === 0 && (
|
||||||
<TableRow className="border-b-0">
|
<TableRow className="border-b-0">
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,14 @@
|
||||||
import { Progress } from "@/components/ui/progress";
|
import { Progress } from "@/components/ui/progress";
|
||||||
import prettyBytes from "pretty-bytes";
|
|
||||||
import { type TelegramFile } from "@/lib/types";
|
import { type TelegramFile } from "@/lib/types";
|
||||||
import { useFileSpeed } from "@/hooks/use-file-speed";
|
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { ClockArrowDown, Zap } from "lucide-react";
|
|
||||||
|
|
||||||
export default function FileProgress({
|
export default function FileProgress({
|
||||||
file,
|
file,
|
||||||
showSize,
|
downloadProgress,
|
||||||
}: {
|
}: {
|
||||||
file: TelegramFile;
|
file: TelegramFile;
|
||||||
showSize: boolean;
|
downloadProgress: number;
|
||||||
}) {
|
}) {
|
||||||
const { downloadProgress, downloadSpeed } = useFileSpeed(file.id);
|
|
||||||
const progress = useMemo(() => {
|
const progress = useMemo(() => {
|
||||||
const fileDownloadProgress =
|
const fileDownloadProgress =
|
||||||
file.size > 0
|
file.size > 0
|
||||||
|
|
@ -37,22 +33,30 @@ export default function FileProgress({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-end justify-between gap-2">
|
<div className="flex items-end justify-between gap-2">
|
||||||
{showSize && file.downloadedSize > 0 && (
|
{/*<div className="hidden min-w-20 md:flex">*/}
|
||||||
<div className="flex min-w-32 items-center gap-1 bg-gray-100 px-1">
|
{/* <div className="group flex items-center gap-1 px-1">*/}
|
||||||
<ClockArrowDown className="h-3 w-3" />
|
{/* <AnimatePresence mode="wait">*/}
|
||||||
<span className="text-nowrap text-xs">
|
{/* <motion.div*/}
|
||||||
{prettyBytes(file.downloadedSize)}
|
{/* key="content"*/}
|
||||||
</span>
|
{/* className="flex items-center gap-1"*/}
|
||||||
</div>
|
{/* initial={{ opacity: 1 }}*/}
|
||||||
)}
|
{/* exit={{ opacity: 0 }}*/}
|
||||||
<div className="hidden min-w-32 items-center gap-1 bg-gray-100 px-1 md:flex">
|
{/* >*/}
|
||||||
<Zap className="h-3 w-3" />
|
{/* <Zap className="h-3 w-3 group-hover:hidden" />*/}
|
||||||
<span className="text-nowrap text-xs">
|
{/* <ClockArrowDown className="hidden h-3 w-3 group-hover:block" />*/}
|
||||||
{file.downloadStatus === "downloading"
|
{/* <span className="text-nowrap text-xs">*/}
|
||||||
? `${prettyBytes(downloadSpeed, { bits: true })}/s`
|
{/* <span className="group-hover:hidden">*/}
|
||||||
: "0/s"}
|
{/* {`${prettyBytes(downloadSpeed, { bits: true })}/s`}*/}
|
||||||
</span>
|
{/* </span>*/}
|
||||||
</div>
|
{/* <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" />
|
<Progress value={progress} className="flex-1 rounded-none md:w-32" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
189
web/src/components/file-row.tsx
Normal file
189
web/src/components/file-row.tsx
Normal 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")}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
@ -27,7 +27,11 @@ export default function FileStatus({ status }: { status: DownloadStatus }) {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return <Badge className={cn("text-xs", STATUS[status].className)}>
|
return (
|
||||||
{STATUS[status].text}
|
<Badge
|
||||||
</Badge>;
|
className={cn("text-xs hover:bg-gray-200", STATUS[status].className)}
|
||||||
|
>
|
||||||
|
{STATUS[status].text}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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="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 className="h-2.5 w-2.5 rounded-full bg-white"></div>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-gray-700 peer-checked:text-primary">
|
<span className="text-gray-400 peer-checked:text-primary">
|
||||||
HTTP
|
HTTP
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
|
||||||
|
|
@ -118,7 +118,7 @@ const SelectItem = React.forwardRef<
|
||||||
<SelectPrimitive.Item
|
<SelectPrimitive.Item
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ export type TelegramFile = {
|
||||||
uniqueId: string;
|
uniqueId: string;
|
||||||
messageId: number;
|
messageId: number;
|
||||||
chatId: number;
|
chatId: number;
|
||||||
name: string;
|
fileName: string;
|
||||||
type: FileType;
|
type: FileType;
|
||||||
size: number;
|
size: number;
|
||||||
downloadedSize: number;
|
downloadedSize: number;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue