💄 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 { 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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
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)}>
|
||||
{STATUS[status].text}
|
||||
</Badge>;
|
||||
return (
|
||||
<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="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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export type TelegramFile = {
|
|||
uniqueId: string;
|
||||
messageId: number;
|
||||
chatId: number;
|
||||
name: string;
|
||||
fileName: string;
|
||||
type: FileType;
|
||||
size: number;
|
||||
downloadedSize: number;
|
||||
|
|
|
|||
Loading…
Reference in a new issue