📱 feat: Optimize mobile display
This commit is contained in:
parent
3ce64a3cc3
commit
59a7ab046b
7 changed files with 62 additions and 23 deletions
|
|
@ -37,7 +37,7 @@ export default function ChatSelect({ disabled }: { disabled: boolean }) {
|
||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
className="w-[250px] justify-between"
|
className="w-full md:w-[250px] justify-between"
|
||||||
>
|
>
|
||||||
{selectedChat ? (
|
{selectedChat ? (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ export function FileCard({ file }: FileCardProps) {
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h3 className="mb-1 font-medium">{file.name}</h3>
|
<h3 className="mb-1 font-medium">{file.name}</h3>
|
||||||
<div className="mb-2 text-sm text-muted-foreground">
|
<div className="mb-2 text-sm text-muted-foreground">
|
||||||
{prettyBytes(file.size)} • {file.type}
|
{prettyBytes(file.size)} • {file.type} • {file.downloadStatus}
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-2 w-full overflow-hidden">
|
<div className="mb-2 w-full overflow-hidden">
|
||||||
<FileProgress file={file} />
|
<FileProgress file={file} />
|
||||||
|
|
|
||||||
|
|
@ -39,14 +39,14 @@ export function FileFilters({
|
||||||
}: FileFiltersProps) {
|
}: FileFiltersProps) {
|
||||||
return (
|
return (
|
||||||
<div className="mb-6 flex flex-col justify-between md:flex-row">
|
<div className="mb-6 flex flex-col justify-between md:flex-row">
|
||||||
<div className="flex flex-col gap-4 md:flex-row">
|
<div className="grid grid-cols-2 gap-4 md:flex-row md:flex">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Search files..."
|
placeholder="Search files..."
|
||||||
value={filters.search}
|
value={filters.search}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
onFiltersChange({ ...filters, search: e.target.value })
|
onFiltersChange({ ...filters, search: e.target.value })
|
||||||
}
|
}
|
||||||
className="md:w-[300px]"
|
className="col-span-2 md:w-[300px]"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FileTypeFilter
|
<FileTypeFilter
|
||||||
|
|
@ -65,7 +65,7 @@ export function FileFilters({
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-[150px]">
|
<SelectTrigger className="w-full md:w-[150px]">
|
||||||
<SelectValue placeholder="Download status" />
|
<SelectValue placeholder="Download status" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
@ -78,7 +78,7 @@ export function FileFilters({
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-4">
|
<div className="hidden gap-4 md:flex">
|
||||||
<TableColumnFilter
|
<TableColumnFilter
|
||||||
columns={columns}
|
columns={columns}
|
||||||
onColumnConfigChange={onColumnConfigChange}
|
onColumnConfigChange={onColumnConfigChange}
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ import prettyBytes from "pretty-bytes";
|
||||||
import FileProgress from "@/components/file-progress";
|
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 FileExtra from "@/components/file-extra";
|
||||||
|
import useIsMobile from "@/hooks/use-is-mobile";
|
||||||
|
|
||||||
interface FileListProps {
|
interface FileListProps {
|
||||||
accountId: string;
|
accountId: string;
|
||||||
|
|
@ -100,7 +101,7 @@ const PhotoColumnImage = memo(function PhotoColumnImage({
|
||||||
});
|
});
|
||||||
|
|
||||||
export function FileList({ accountId, chatId }: FileListProps) {
|
export function FileList({ accountId, chatId }: FileListProps) {
|
||||||
const [isMobile, setIsMobile] = useState(false);
|
const isMobile = useIsMobile();
|
||||||
const [selectedFiles, setSelectedFiles] = useState<Set<number>>(new Set());
|
const [selectedFiles, setSelectedFiles] = useState<Set<number>>(new Set());
|
||||||
const observerTarget = useRef(null);
|
const observerTarget = useRef(null);
|
||||||
const [columns, setColumns] = useState<Column[]>(COLUMNS);
|
const [columns, setColumns] = useState<Column[]>(COLUMNS);
|
||||||
|
|
@ -110,16 +111,6 @@ export function FileList({ accountId, chatId }: FileListProps) {
|
||||||
const { filters, handleFilterChange, isLoading, files, handleLoadMore } =
|
const { filters, handleFilterChange, isLoading, files, handleLoadMore } =
|
||||||
useFiles(accountId, chatId);
|
useFiles(accountId, chatId);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const checkMobile = () => {
|
|
||||||
setIsMobile(window.innerWidth < 768);
|
|
||||||
};
|
|
||||||
|
|
||||||
checkMobile();
|
|
||||||
window.addEventListener("resize", checkMobile);
|
|
||||||
return () => window.removeEventListener("resize", checkMobile);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const observer = new IntersectionObserver(
|
const observer = new IntersectionObserver(
|
||||||
(entries) => {
|
(entries) => {
|
||||||
|
|
@ -162,6 +153,16 @@ export function FileList({ accountId, chatId }: FileListProps) {
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
<FileFilters
|
||||||
|
telegramId={accountId}
|
||||||
|
chatId={chatId}
|
||||||
|
filters={filters}
|
||||||
|
onFiltersChange={handleFilterChange}
|
||||||
|
columns={columns}
|
||||||
|
onColumnConfigChange={setColumns}
|
||||||
|
rowHeight={rowHeight}
|
||||||
|
setRowHeight={setRowHeight}
|
||||||
|
/>
|
||||||
<div className="grid grid-cols-1 gap-4">
|
<div className="grid grid-cols-1 gap-4">
|
||||||
{files.map((file, index) => (
|
{files.map((file, index) => (
|
||||||
<FileCard
|
<FileCard
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ export default function FileTypeFilter({
|
||||||
value={type}
|
value={type}
|
||||||
onValueChange={(value) => onTypeChange(value as FileType)}
|
onValueChange={(value) => onTypeChange(value as FileType)}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-[150px]">
|
<SelectTrigger className="w-full md:w-[150px]">
|
||||||
<SelectValue placeholder="File type" />
|
<SelectValue placeholder="File type" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
|
||||||
|
|
@ -30,23 +30,28 @@ import ChatSelect from "@/components/chat-select";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import TelegramIcon from "@/components/telegram-icon";
|
import TelegramIcon from "@/components/telegram-icon";
|
||||||
import AutoDownloadDialog from "@/components/auto-download-dialog";
|
import AutoDownloadDialog from "@/components/auto-download-dialog";
|
||||||
|
import useIsMobile from "@/hooks/use-is-mobile";
|
||||||
|
import { useState } from "react";
|
||||||
|
import {Button} from "@/components/ui/button";
|
||||||
|
|
||||||
export function Header() {
|
export function Header() {
|
||||||
const { isLoading, getAccounts, accountId, account, handleAccountChange } =
|
const { isLoading, getAccounts, accountId, account, handleAccountChange } =
|
||||||
useTelegramAccount();
|
useTelegramAccount();
|
||||||
const { connectionStatus, accountDownloadSpeed } = useWebsocket();
|
const { connectionStatus, accountDownloadSpeed } = useWebsocket();
|
||||||
const accounts = getAccounts("active");
|
const accounts = getAccounts("active");
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
const [showMore, setShowMore] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="mb-6">
|
<Card className="mb-6">
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="flex flex-col items-start justify-between gap-4 md:flex-row md:items-center">
|
<div className="relative flex flex-col items-start justify-between gap-4 md:flex-row md:items-center">
|
||||||
<div className="flex flex-1 flex-col gap-4 md:flex-row md:items-center">
|
<div className="w-full flex flex-1 flex-col gap-4 md:flex-row md:items-center">
|
||||||
<Link href={"/"} className="hidden md:inline-flex">
|
<Link href={"/"} className="hidden md:inline-flex">
|
||||||
<TelegramIcon className="h-6 w-6" />
|
<TelegramIcon className="h-6 w-6" />
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="w-full md:w-auto">
|
||||||
<Select value={accountId} onValueChange={handleAccountChange}>
|
<Select value={accountId} onValueChange={handleAccountChange}>
|
||||||
<SelectTrigger className="w-full md:w-[200px]">
|
<SelectTrigger className="w-full md:w-[200px]">
|
||||||
<SelectValue placeholder="Select account ...">
|
<SelectValue placeholder="Select account ...">
|
||||||
|
|
@ -99,9 +104,14 @@ export function Header() {
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ChatSelect disabled={!accountId} />
|
{(!isMobile || showMore) && (
|
||||||
|
<>
|
||||||
|
<ChatSelect disabled={!accountId} />
|
||||||
|
|
||||||
|
<AutoDownloadDialog />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<AutoDownloadDialog />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
|
|
@ -147,6 +157,15 @@ export function Header() {
|
||||||
|
|
||||||
<SettingsDialog />
|
<SettingsDialog />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => setShowMore(!showMore)}
|
||||||
|
className="absolute bottom-0 right-0 md:hidden"
|
||||||
|
>
|
||||||
|
<Ellipsis className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
||||||
19
web/src/hooks/use-is-mobile.ts
Normal file
19
web/src/hooks/use-is-mobile.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { useLayoutEffect, useState } from "react";
|
||||||
|
|
||||||
|
const useIsMobile = (): boolean => {
|
||||||
|
const [isMobile, setIsMobile] = useState(false);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const checkMobile = () => {
|
||||||
|
setIsMobile(window.innerWidth < 768);
|
||||||
|
};
|
||||||
|
|
||||||
|
checkMobile();
|
||||||
|
window.addEventListener("resize", checkMobile);
|
||||||
|
return () => window.removeEventListener("resize", checkMobile);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return isMobile;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useIsMobile;
|
||||||
Loading…
Reference in a new issue