feat: Support transfer status filter.

This commit is contained in:
jarvis2f 2025-02-10 15:32:23 +08:00
parent 1575a84a9b
commit 8f48c7939f
6 changed files with 191 additions and 82 deletions

View file

@ -175,8 +175,8 @@ public class TelegramVerticle extends AbstractVerticle {
}
public Future<JsonObject> getChatFiles(long chatId, Map<String, String> filter) {
String status = filter.get("status");
if (Arrays.asList("downloading", "paused", "completed", "error").contains(status)) {
String downloadStatus = filter.get("downloadStatus");
if (Arrays.asList("downloading", "paused", "completed", "error").contains(downloadStatus)) {
return DataVerticle.fileRepository.getFiles(chatId, filter)
.compose(r -> {
long[] messageIds = r.v1.stream().mapToLong(FileRecord::messageId).toArray();
@ -215,7 +215,7 @@ public class TelegramVerticle extends AbstractVerticle {
searchChatMessages.limit = Convert.toInt(filter.get("limit"), 20);
searchChatMessages.filter = TdApiHelp.getSearchMessagesFilter(filter.get("type"));
return (Objects.equals(filter.get("status"), FileRecord.DownloadStatus.idle.name()) ?
return (Objects.equals(filter.get("downloadStatus"), FileRecord.DownloadStatus.idle.name()) ?
this.getIdleChatFiles(searchChatMessages, 0) :
client.execute(searchChatMessages))
.compose(foundChatMessages ->

View file

@ -78,7 +78,7 @@ public class FileRepositoryImpl implements FileRepository {
@Override
public Future<Tuple3<List<FileRecord>, Long, Long>> getFiles(long chatId, Map<String, String> filter) {
String status = filter.get("status");
String downloadStatus = filter.get("downloadStatus");
String transferStatus = filter.get("transferStatus");
String search = filter.get("search");
Long fromMessageId = Convert.toLong(filter.get("fromMessageId"), 0L);
@ -88,9 +88,9 @@ public class FileRepositoryImpl implements FileRepository {
String whereClause = "chat_id = #{chatId}";
Map<String, Object> params = MapUtil.of("chatId", chatId);
params.put("limit", limit);
if (StrUtil.isNotBlank(status)) {
whereClause += " AND download_status = #{status}";
params.put("status", status);
if (StrUtil.isNotBlank(downloadStatus)) {
whereClause += " AND download_status = #{downloadStatus}";
params.put("downloadStatus", downloadStatus);
}
if (StrUtil.isNotBlank(transferStatus)) {
whereClause += " AND transfer_status = #{transferStatus}";

View file

@ -1,9 +1,14 @@
import { type DownloadStatus, type FileFilter } from "@/lib/types";
import {
type DownloadStatus,
type FileFilter,
type TransferStatus,
} from "@/lib/types";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectGroup,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
@ -14,9 +19,13 @@ import { type RowHeight } from "@/components/table-row-height-switch";
import FileTypeFilter from "@/components/file-type-filter";
import dynamic from "next/dynamic";
import { useDebouncedCallback } from "use-debounce";
import { useEffect, useState } from "react";
import { X } from "lucide-react";
import * as React from "react";
import { useEffect, useMemo, useState } from "react";
import { Check, X } from "lucide-react";
import { Button } from "./ui/button";
import { DOWNLOAD_STATUS, TRANSFER_STATUS } from "@/components/file-status";
import * as SelectPrimitive from "@radix-ui/react-select";
import { cn } from "@/lib/utils";
interface FileFiltersProps {
telegramId: string;
@ -37,6 +46,17 @@ const TableRowHeightSwitch = dynamic(
{ ssr: false },
);
const statusOptions = {
downloadStatus: {
text: "Download Status",
status: DOWNLOAD_STATUS,
},
transferStatus: {
text: "Transfer Status",
status: TRANSFER_STATUS,
},
};
export function FileFilters({
telegramId,
chatId,
@ -53,19 +73,69 @@ export function FileFilters({
onFiltersChange({ ...filters, search });
}, 500);
const handleStatusSelect = (value: string) => {
const [group, item] = value.split("||");
if (group === "downloadStatus") {
onFiltersChange({
...filters,
downloadStatus:
item === filters.downloadStatus
? undefined
: (item as DownloadStatus),
});
} else if (group === "transferStatus") {
onFiltersChange({
...filters,
transferStatus:
item === filters.transferStatus
? undefined
: (item as TransferStatus),
});
}
};
const statusDisplayValue = useMemo(() => {
if (!filters.downloadStatus && !filters.transferStatus) {
console.log("no status filter");
return "Filter Status";
}
const downloadStatus =
filters.downloadStatus && DOWNLOAD_STATUS[filters.downloadStatus];
const transferStatus =
filters.transferStatus && TRANSFER_STATUS[filters.transferStatus];
return (
<div className="flex items-center space-x-2">
{downloadStatus && (
<div className="flex items-center space-x-1 rounded bg-gray-100 p-1 dark:bg-gray-800">
<span className="text-xs">Download</span>
<downloadStatus.icon className="h-3 w-3" />
</div>
)}
{transferStatus && (
<div className="flex items-center space-x-1 rounded bg-gray-100 p-1 dark:bg-gray-800">
<span className="text-xs">Transfer</span>
<transferStatus.icon className="h-3 w-3" />
</div>
)}
</div>
);
}, [filters.downloadStatus, filters.transferStatus]);
useEffect(() => {
handleSearchChange(search);
}, [search, handleSearchChange]);
return (
<div className="mb-6 flex flex-col justify-between md:flex-row">
<div className="grid grid-cols-2 gap-4 md:flex md:flex-row">
<div className="relative">
<div className="grid grid-cols-3 gap-4 md:flex md:flex-row">
<div className="relative col-span-3">
<Input
placeholder="Search files..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="relative col-span-2 md:w-[300px]"
className="relative md:w-[300px]"
/>
{search && (
<Button
@ -87,24 +157,41 @@ export function FileFilters({
/>
<Select
value={filters.status}
onValueChange={(value) =>
onFiltersChange({
...filters,
status: value as DownloadStatus | "all",
})
}
value={`${filters.downloadStatus ?? ""}||${filters.transferStatus ?? ""}`}
onValueChange={(value) => handleStatusSelect(value)}
>
<SelectTrigger className="w-full md:w-[150px]">
<SelectValue placeholder="Download status" />
<SelectTrigger className="col-span-2 w-full md:w-[210px]">
<SelectValue placeholder="Filter Status">
{statusDisplayValue}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All statuses</SelectItem>
<SelectItem value="idle">Not downloaded</SelectItem>
<SelectItem value="downloading">Downloading</SelectItem>
<SelectItem value="paused">Paused</SelectItem>
<SelectItem value="completed">Completed</SelectItem>
<SelectItem value="error">Error</SelectItem>
{Object.entries(statusOptions).map(([groupKey, group]) => (
<SelectGroup key={groupKey}>
<SelectLabel>{group.text}</SelectLabel>
{Object.entries(group.status).map(([statusKey, item]) => (
<SelectPrimitive.Item
key={`${groupKey}||${statusKey}`}
value={`${groupKey}||${statusKey}`}
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 hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
)}
>
{filters[groupKey as keyof FileFilter] === statusKey && (
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<Check className="h-4 w-4" />
</span>
)}
<SelectPrimitive.ItemText>
<div className="flex items-center">
{item.icon && <item.icon className="mr-2 h-4 w-4" />}
{item.text}
</div>
</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))}
</SelectGroup>
))}
</SelectContent>
</Select>
</div>

View file

@ -4,50 +4,60 @@ import React from "react";
import { cn } from "@/lib/utils";
import { TooltipWrapper } from "@/components/ui/tooltip";
import { AnimatePresence, motion } from "framer-motion";
import {CheckCircle2, Clock, Download, FolderSync, Pause, XCircle} from "lucide-react";
export const DOWNLOAD_STATUS = {
idle: {
icon: Clock,
className: "bg-gray-100 text-gray-600",
text: "Idle",
},
downloading: {
icon: Download,
className: "bg-blue-100 text-blue-600",
text: "Downloading",
},
paused: {
icon: Pause,
className: "bg-yellow-100 text-yellow-600",
text: "Paused",
},
completed: {
icon: CheckCircle2,
className: "bg-green-100 text-green-600",
text: "Completed",
},
error: {
icon: XCircle,
className: "bg-red-100 text-red-600",
text: "Error",
},
};
export const TRANSFER_STATUS = {
idle: {
icon: Clock,
className: "bg-gray-100 text-gray-600",
text: "Idle",
},
transferring: {
icon: FolderSync,
className: "bg-blue-100 text-blue-600",
text: "Transferring",
},
completed: {
icon: CheckCircle2,
className: "bg-green-100 text-green-600",
text: "Transferred",
},
error: {
icon: XCircle,
className: "bg-red-100 text-red-600",
text: "Transfer Error",
},
};
export default function FileStatus({ file }: { file: TelegramFile }) {
const DOWNLOAD_STATUS = {
idle: {
className: "bg-gray-100 text-gray-600",
text: "Idle",
},
downloading: {
className: "bg-blue-100 text-blue-600",
text: "Downloading",
},
paused: {
className: "bg-yellow-100 text-yellow-600",
text: "Paused",
},
completed: {
className: "bg-green-100 text-green-600",
text: "Completed",
},
error: {
className: "bg-red-100 text-red-600",
text: "Error",
},
};
const TRANSFER_STATUS = {
idle: {
className: "bg-gray-100 text-gray-600",
text: "Idle",
},
transferring: {
className: "bg-blue-100 text-blue-600",
text: "Transferring",
},
completed: {
className: "bg-green-100 text-green-600",
text: "Transferred",
},
error: {
className: "bg-red-100 text-red-600",
text: "Transfer Error",
},
};
const badgeVariants = {
initial: { opacity: 0, scale: 0.9 },
animate: {

View file

@ -2,7 +2,8 @@ import { useEffect, useMemo, useState } from "react";
import {
type DownloadStatus,
type FileFilter,
type TelegramFile, type TransferStatus,
type TelegramFile,
type TransferStatus,
} from "@/lib/types";
import useSWRInfinite from "swr/infinite";
import { useWebsocket } from "@/hooks/use-websocket";
@ -13,7 +14,8 @@ import { useDebounce } from "use-debounce";
const DEFAULT_FILTERS: FileFilter = {
search: "",
type: "media",
status: "all",
downloadStatus: undefined,
transferStatus: undefined,
};
type FileResponse = {
@ -43,9 +45,12 @@ export function useFiles(accountId: string, chatId: string) {
);
const getKey = (page: number, previousPageData: FileResponse) => {
const params = new URLSearchParams({
...(filters.search && { search: window.encodeURIComponent(filters.search) }),
...(filters.search && {
search: window.encodeURIComponent(filters.search),
}),
...(filters.type && { type: filters.type }),
...(filters.status && { status: filters.status })
...(filters.downloadStatus && { downloadStatus: filters.downloadStatus }),
...(filters.transferStatus && { transferStatus: filters.transferStatus }),
});
if (page === 0) {
@ -133,14 +138,19 @@ export function useFiles(accountId: string, chatId: string) {
...file,
id: latestFilesStatus[file.uniqueId]?.fileId ?? file.id,
downloadStatus:
latestFilesStatus[file.uniqueId]?.downloadStatus ?? file.downloadStatus,
localPath: latestFilesStatus[file.uniqueId]?.localPath ?? file.localPath,
latestFilesStatus[file.uniqueId]?.downloadStatus ??
file.downloadStatus,
localPath:
latestFilesStatus[file.uniqueId]?.localPath ?? file.localPath,
completionDate:
latestFilesStatus[file.uniqueId]?.completionDate ?? file.completionDate,
latestFilesStatus[file.uniqueId]?.completionDate ??
file.completionDate,
downloadedSize:
latestFilesStatus[file.uniqueId]?.downloadedSize ?? file.downloadedSize,
latestFilesStatus[file.uniqueId]?.downloadedSize ??
file.downloadedSize,
transferStatus:
latestFilesStatus[file.uniqueId]?.transferStatus ?? file.transferStatus,
latestFilesStatus[file.uniqueId]?.transferStatus ??
file.transferStatus,
});
});
});
@ -169,7 +179,8 @@ export function useFiles(accountId: string, chatId: string) {
if (
newFilters.search === filters.search &&
newFilters.type === filters.type &&
newFilters.status === filters.status
newFilters.downloadStatus === filters.downloadStatus &&
newFilters.transferStatus === filters.transferStatus
) {
return;
}

View file

@ -80,7 +80,8 @@ export type TDFile = {
export type FileFilter = {
search: string;
type: FileType;
status: DownloadStatus | "all";
downloadStatus?: DownloadStatus;
transferStatus?: TransferStatus;
};
export type TelegramApiResult = {