+
+
+
+
+ Type
+
+
+ {file.type}
+
-
-
+
+
+
+
+ Size
+
+
+ {prettyBytes(file.size)}
+
-
-
+
+
+
+
+ Date
+
+
+ {formatDistanceToNow(new Date(file.date * 1000), {
+ addSuffix: true,
+ })}
+
+
+
+
+
+
+ Downloaded Size
+
+
+ {prettyBytes(file.downloadedSize)}
+
-
-
+
+
+
+
+
+
);
}
diff --git a/web/src/components/file-control.tsx b/web/src/components/file-control.tsx
index 6e42c1b..5431a36 100644
--- a/web/src/components/file-control.tsx
+++ b/web/src/components/file-control.tsx
@@ -102,3 +102,51 @@ export default function FileControl({ file }: { file: TelegramFile }) {
);
}
+
+export function MobileFileControl({ file }: { file: TelegramFile }) {
+ const { start, starting, togglePause, togglingPause, cancel, cancelling } =
+ useFileControl(file);
+
+ if (file.downloadStatus === "completed") {
+ return null;
+ }
+
+ return (
+
+ {(file.downloadStatus === "idle" || file.downloadStatus === "error") && (
+
+ )}
+ {(file.downloadStatus === "downloading" ||
+ file.downloadStatus === "paused") && (
+ <>
+
+
+ >
+ )}
+
+ );
+}
diff --git a/web/src/components/file-list.tsx b/web/src/components/file-list.tsx
index ee8d889..5ef1231 100644
--- a/web/src/components/file-list.tsx
+++ b/web/src/components/file-list.tsx
@@ -50,6 +50,7 @@ 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";
interface FileListProps {
accountId: string;
@@ -265,9 +266,7 @@ export function FileList({ accountId, chatId }: FileListProps) {
),
size: (file: TelegramFile) =>
{prettyBytes(file.size)},
- status: (file: TelegramFile) => (
-
{file.downloadStatus}
- ),
+ status: (file: TelegramFile) =>
,
extra: (file: TelegramFile) => (
),
@@ -371,7 +370,7 @@ export function FileList({ accountId, chatId }: FileListProps) {
colSpan={columns.length + 1}
className="h-px p-0"
>
-
+
diff --git a/web/src/components/file-progress.tsx b/web/src/components/file-progress.tsx
index 12050d8..4ce57b2 100644
--- a/web/src/components/file-progress.tsx
+++ b/web/src/components/file-progress.tsx
@@ -5,7 +5,13 @@ import { useFileSpeed } from "@/hooks/use-file-speed";
import { useMemo } from "react";
import { ClockArrowDown, Zap } from "lucide-react";
-export default function FileProgress({ file }: { file: TelegramFile }) {
+export default function FileProgress({
+ file,
+ showSize,
+}: {
+ file: TelegramFile;
+ showSize: boolean;
+}) {
const { downloadProgress, downloadSpeed } = useFileSpeed(file.id);
const progress = useMemo(() => {
const fileDownloadProgress =
@@ -31,7 +37,7 @@ export default function FileProgress({ file }: { file: TelegramFile }) {
return (
- {file.downloadedSize > 0 && (
+ {showSize && file.downloadedSize > 0 && (
diff --git a/web/src/components/file-status.tsx b/web/src/components/file-status.tsx
new file mode 100644
index 0000000..18fc480
--- /dev/null
+++ b/web/src/components/file-status.tsx
@@ -0,0 +1,33 @@
+import { type DownloadStatus } from "@/lib/types";
+import { Badge } from "@/components/ui/badge";
+import React from "react";
+import { cn } from "@/lib/utils";
+
+export default function FileStatus({ status }: { status: DownloadStatus }) {
+ const 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",
+ },
+ };
+
+ return
+ {STATUS[status].text}
+ ;
+}
diff --git a/web/src/components/photo-preview.tsx b/web/src/components/photo-preview.tsx
index e56d37b..b5cb49a 100644
--- a/web/src/components/photo-preview.tsx
+++ b/web/src/components/photo-preview.tsx
@@ -3,12 +3,13 @@ import Image from "next/image";
import useSWR from "swr";
import { CloudAlert, Loader } from "lucide-react";
import { useWebsocket } from "@/hooks/use-websocket";
-import { useEffect, useState } from "react";
+import {useEffect, useRef, useState} from "react";
import { WebSocketMessageType } from "@/lib/websocket-types";
import { toast } from "@/hooks/use-toast";
import { useSettings } from "@/hooks/use-settings";
import { type TDFile } from "@/lib/types";
import { getApiUrl } from "@/lib/api";
+import useIsMobile from "@/hooks/use-is-mobile";
interface PhotoPreviewProps {
thumbnail: string;
@@ -25,7 +26,9 @@ export default function PhotoPreview({
}: PhotoPreviewProps) {
const url = `${getApiUrl()}/file/preview?chatId=${chatId}&messageId=${messageId}`;
const { settings } = useSettings();
+ const isMobile = useIsMobile();
const [isReady, setIsReady] = useState(false);
+ const imageRef = useRef(null);
const [fileStatus, setFileStatus] = useState<{
fileId: number | null;
readyFileIds: number[];
@@ -130,9 +133,29 @@ export default function PhotoPreview({
);
}
+ const toggleFullScreen = () => {
+ if (!isMobile) return;
+ if (!document.fullscreenEnabled) {
+ console.error("Full-screen mode is not supported.");
+ return;
+ }
+ const image = imageRef.current;
+ if (!image) return;
+ if (!document.fullscreenElement) {
+ image.requestFullscreen().catch((err: Error) => {
+ console.error(`Error attempting to enable full-screen mode: ${err.message}`)
+ });
+ } else {
+ if (document.exitFullscreen) {
+ void document.exitFullscreen();
+ }
+ }
+ };
+
return (
);
diff --git a/web/src/components/ui/drawer.tsx b/web/src/components/ui/drawer.tsx
new file mode 100644
index 0000000..350f8b6
--- /dev/null
+++ b/web/src/components/ui/drawer.tsx
@@ -0,0 +1,118 @@
+"use client";
+
+import * as React from "react";
+import { Drawer as DrawerPrimitive } from "vaul";
+
+import { cn } from "@/lib/utils";
+
+const Drawer = ({
+ shouldScaleBackground = true,
+ ...props
+}: React.ComponentProps) => (
+
+);
+Drawer.displayName = "Drawer";
+
+const DrawerTrigger = DrawerPrimitive.Trigger;
+
+const DrawerPortal = DrawerPrimitive.Portal;
+
+const DrawerClose = DrawerPrimitive.Close;
+
+const DrawerOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
+
+const DrawerContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+ {children}
+
+
+));
+DrawerContent.displayName = "DrawerContent";
+
+const DrawerHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+);
+DrawerHeader.displayName = "DrawerHeader";
+
+const DrawerFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+);
+DrawerFooter.displayName = "DrawerFooter";
+
+const DrawerTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
+
+const DrawerDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
+
+export {
+ Drawer,
+ DrawerPortal,
+ DrawerOverlay,
+ DrawerTrigger,
+ DrawerClose,
+ DrawerContent,
+ DrawerHeader,
+ DrawerFooter,
+ DrawerTitle,
+ DrawerDescription,
+};
diff --git a/web/src/components/ui/separator.tsx b/web/src/components/ui/separator.tsx
new file mode 100644
index 0000000..12d81c4
--- /dev/null
+++ b/web/src/components/ui/separator.tsx
@@ -0,0 +1,31 @@
+"use client"
+
+import * as React from "react"
+import * as SeparatorPrimitive from "@radix-ui/react-separator"
+
+import { cn } from "@/lib/utils"
+
+const Separator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(
+ (
+ { className, orientation = "horizontal", decorative = true, ...props },
+ ref
+ ) => (
+
+ )
+)
+Separator.displayName = SeparatorPrimitive.Root.displayName
+
+export { Separator }