diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico index 39dc233a..fa4db2fc 100644 Binary files a/src-tauri/icons/icon.ico and b/src-tauri/icons/icon.ico differ diff --git a/src/components/connection/connection-table.tsx b/src/components/connection/connection-table.tsx index d7577c72..2cf1d2e1 100644 --- a/src/components/connection/connection-table.tsx +++ b/src/components/connection/connection-table.tsx @@ -1,15 +1,14 @@ import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; -import React, { useMemo, useState, useEffect, RefObject } from "react"; +import React, { useMemo, useState, useEffect, useRef } from "react"; import { ColumnDef, flexRender, getCoreRowModel, useReactTable, - Row, + Header, ColumnSizingState, } from "@tanstack/react-table"; -import { TableVirtuoso, TableComponents } from "react-virtuoso"; import { Table, @@ -27,7 +26,30 @@ import { cn } from "@root/lib/utils"; dayjs.extend(relativeTime); -// Интерфейс для строки данных, которую использует react-table +interface IConnectionsItem { + id: string; + metadata: { + host: string; + destinationIP: string; + destinationPort: string; + remoteDestination: string; + process?: string; + processPath?: string; + sourceIP: string; + sourcePort: string; + type: string; + network: string; + }; + rule: string; + rulePayload?: string; + chains: string[]; + download: number; + upload: number; + curDownload?: number; + curUpload?: number; + start: string; +} + interface ConnectionRow { id: string; host: string; @@ -45,29 +67,81 @@ interface ConnectionRow { connectionData: IConnectionsItem; } -// Интерфейс для пропсов, которые компонент получает от родителя interface Props { connections: IConnectionsItem[]; onShowDetail: (data: IConnectionsItem) => void; scrollerRef: (element: HTMLElement | Window | null) => void; } +const ColumnResizer = ({ header }: { header: Header }) => { + return ( +
+ ); +}; + export const ConnectionTable = (props: Props) => { const { connections, onShowDetail, scrollerRef } = props; + const tableContainerRef = useRef(null); + + useEffect(() => { + if (tableContainerRef.current && scrollerRef) { + scrollerRef(tableContainerRef.current); + } + }, [scrollerRef]); const [columnSizing, setColumnSizing] = useState(() => { try { const saved = localStorage.getItem("connection-table-widths"); - return saved ? JSON.parse(saved) : {}; + return saved + ? JSON.parse(saved) + : { + host: 220, + download: 88, + upload: 88, + dlSpeed: 88, + ulSpeed: 88, + chains: 340, + rule: 280, + process: 220, + time: 120, + source: 200, + remoteDestination: 200, + type: 160, + }; } catch { - return {}; + return { + host: 220, + download: 88, + upload: 88, + dlSpeed: 88, + ulSpeed: 88, + chains: 340, + rule: 280, + process: 220, + time: 120, + source: 200, + remoteDestination: 200, + type: 160, + }; } }); useEffect(() => { localStorage.setItem( - "connection-table-widths", - JSON.stringify(columnSizing), + "connection-table-widths", + JSON.stringify(columnSizing) ); }, [columnSizing]); @@ -77,13 +151,13 @@ export const ConnectionTable = (props: Props) => { const chains = [...each.chains].reverse().join(" / "); const rule = rulePayload ? `${each.rule}(${rulePayload})` : each.rule; const Destination = metadata.destinationIP - ? `${metadata.destinationIP}:${metadata.destinationPort}` - : `${metadata.remoteDestination}:${metadata.destinationPort}`; + ? `${metadata.destinationIP}:${metadata.destinationPort}` + : `${metadata.remoteDestination}:${metadata.destinationPort}`; return { id: each.id, host: metadata.host - ? `${metadata.host}:${metadata.destinationPort}` - : `${metadata.remoteDestination}:${metadata.destinationPort}`, + ? `${metadata.host}:${metadata.destinationPort}` + : `${metadata.remoteDestination}:${metadata.destinationPort}`, download: each.download, upload: each.upload, dlSpeed: each.curDownload ?? 0, @@ -101,102 +175,118 @@ export const ConnectionTable = (props: Props) => { }, [connections]); const columns = useMemo[]>( - () => [ - { - accessorKey: "host", - header: () => t("Host"), - size: columnSizing?.host || 220, - minSize: 180, - }, - { - accessorKey: "download", - header: () => t("Downloaded"), - size: columnSizing?.download || 88, - cell: ({ getValue }) => ( -
- {parseTraffic(getValue()).join(" ")} -
- ), - }, - { - accessorKey: "upload", - header: () => t("Uploaded"), - size: columnSizing?.upload || 88, - cell: ({ getValue }) => ( -
- {parseTraffic(getValue()).join(" ")} -
- ), - }, - { - accessorKey: "dlSpeed", - header: () => t("DL Speed"), - size: columnSizing?.dlSpeed || 88, - cell: ({ getValue }) => ( -
- {parseTraffic(getValue()).join(" ")}/s -
- ), - }, - { - accessorKey: "ulSpeed", - header: () => t("UL Speed"), - size: columnSizing?.ulSpeed || 88, - cell: ({ getValue }) => ( -
- {parseTraffic(getValue()).join(" ")}/s -
- ), - }, - { - accessorKey: "chains", - header: () => t("Chains"), - size: columnSizing?.chains || 340, - minSize: 180, - }, - { - accessorKey: "rule", - header: () => t("Rule"), - size: columnSizing?.rule || 280, - minSize: 180, - }, - { - accessorKey: "process", - header: () => t("Process"), - size: columnSizing?.process || 220, - minSize: 180, - }, - { - accessorKey: "time", - header: () => t("Time"), - size: columnSizing?.time || 120, - minSize: 100, - cell: ({ getValue }) => ( -
- {dayjs(getValue()).fromNow()} -
- ), - }, - { - accessorKey: "source", - header: () => t("Source"), - size: columnSizing?.source || 200, - minSize: 130, - }, - { - accessorKey: "remoteDestination", - header: () => t("Destination"), - size: columnSizing?.remoteDestination || 200, - minSize: 130, - }, - { - accessorKey: "type", - header: () => t("Type"), - size: columnSizing?.type || 160, - minSize: 100, - }, - ], - [columnSizing], + () => [ + { + accessorKey: "host", + header: () => t("Host"), + size: columnSizing?.host || 220, + minSize: 180, + maxSize: 400, + }, + { + accessorKey: "download", + header: () => t("Downloaded"), + size: columnSizing?.download || 88, + minSize: 80, + maxSize: 150, + cell: ({ getValue }) => ( +
+ {parseTraffic(getValue()).join(" ")} +
+ ), + }, + { + accessorKey: "upload", + header: () => t("Uploaded"), + size: columnSizing?.upload || 88, + minSize: 80, + maxSize: 150, + cell: ({ getValue }) => ( +
+ {parseTraffic(getValue()).join(" ")} +
+ ), + }, + { + accessorKey: "dlSpeed", + header: () => t("DL Speed"), + size: columnSizing?.dlSpeed || 88, + minSize: 80, + maxSize: 150, + cell: ({ getValue }) => ( +
+ {parseTraffic(getValue()).join(" ")}/s +
+ ), + }, + { + accessorKey: "ulSpeed", + header: () => t("UL Speed"), + size: columnSizing?.ulSpeed || 88, + minSize: 80, + maxSize: 150, + cell: ({ getValue }) => ( +
+ {parseTraffic(getValue()).join(" ")}/s +
+ ), + }, + { + accessorKey: "chains", + header: () => t("Chains"), + size: columnSizing?.chains || 340, + minSize: 180, + maxSize: 500, + }, + { + accessorKey: "rule", + header: () => t("Rule"), + size: columnSizing?.rule || 280, + minSize: 180, + maxSize: 400, + }, + { + accessorKey: "process", + header: () => t("Process"), + size: columnSizing?.process || 220, + minSize: 180, + maxSize: 350, + }, + { + accessorKey: "time", + header: () => t("Time"), + size: columnSizing?.time || 120, + minSize: 100, + maxSize: 180, + cell: ({ getValue }) => ( +
+ {dayjs(getValue()).fromNow()} +
+ ), + }, + { + accessorKey: "source", + header: () => t("Source"), + size: columnSizing?.source || 200, + minSize: 130, + maxSize: 300, + }, + { + accessorKey: "remoteDestination", + header: () => t("Destination"), + size: columnSizing?.remoteDestination || 200, + minSize: 130, + maxSize: 300, + }, + { + accessorKey: "type", + header: () => t("Type"), + size: columnSizing?.type || 160, + minSize: 100, + maxSize: 220, + }, + ], + [columnSizing] ); const table = useReactTable({ @@ -206,92 +296,91 @@ export const ConnectionTable = (props: Props) => { onColumnSizingChange: setColumnSizing, getCoreRowModel: getCoreRowModel(), columnResizeMode: "onChange", + enableColumnResizing: true, }); - const VirtuosoTableComponents = useMemo>>( - () => ({ - // Явно типизируем `ref` для каждого компонента - Scroller: React.forwardRef((props, ref) => ( -
- )), - Table: (props) => , - TableHead: React.forwardRef((props, ref) => ( - - )), - // Явно типизируем пропсы и `ref` для TableRow - TableRow: React.forwardRef< - HTMLTableRowElement, - { item: Row } & React.HTMLAttributes - >(({ item: row, ...props }, ref) => { - // `Virtuoso` передает нам готовую строку `row` в пропсе `item`. - // Больше не нужно искать ее по индексу! - return ( - onShowDetail(row.original.connectionData)} - /> - ); - }), - TableBody: React.forwardRef((props, ref) => ( - - )), - }), - [], - ); + const totalTableWidth = useMemo(() => { + return table.getCenterTotalSize(); + }, [table.getState().columnSizing]); + + if (connRows.length === 0) { + return ( +
+

{t("No connections")}

+
+ ); + } return ( -
- {connRows.length > 0 ? ( - - table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext(), - )} - - ))} - - )) - } - itemContent={(index, row) => ( - <> - {row.getVisibleCells().map((cell) => ( - onShowDetail(row.original.connectionData)} +
+
+ + {table.getHeaderGroups().map((headerGroup) => ( + - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - - )} - /> - ) : ( -
-

No results.

-
- )} - + {headerGroup.headers.map((header) => ( + +
+ {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} +
+ {header.column.getCanResize() && ( + + )} +
+ ))} +
+ ))} +
+ + + {table.getRowModel().rows.map((row) => ( + onShowDetail(row.original.connectionData)} + > + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ))} + +
+
); -}; +}; \ No newline at end of file diff --git a/src/components/profile/confirm-viewer.tsx b/src/components/profile/confirm-viewer.tsx index 6f6dbd59..96426f95 100644 --- a/src/components/profile/confirm-viewer.tsx +++ b/src/components/profile/confirm-viewer.tsx @@ -1,6 +1,5 @@ import { useTranslation } from "react-i18next"; -// Новые импорты из shadcn/ui import { AlertDialog, AlertDialogAction, @@ -18,7 +17,7 @@ interface Props { open: boolean; title: string; description: string; - onOpenChange: (open: boolean) => void; // shadcn использует этот коллбэк + onOpenChange: (open: boolean) => void; onConfirm: () => void; } @@ -30,7 +29,7 @@ export const ConfirmViewer = (props: Props) => { - {title} + {title} {description} diff --git a/src/components/profile/profile-item.tsx b/src/components/profile/profile-item.tsx index 19b7f6da..932034c8 100644 --- a/src/components/profile/profile-item.tsx +++ b/src/components/profile/profile-item.tsx @@ -23,7 +23,6 @@ import { open } from "@tauri-apps/plugin-shell"; import { ProxiesEditorViewer } from "./proxies-editor-viewer"; import { cn } from "@root/lib/utils"; -// --- Компоненты shadcn/ui --- import { Card } from "@/components/ui/card"; import { Progress } from "@/components/ui/progress"; import { Badge } from "@/components/ui/badge"; @@ -46,7 +45,6 @@ import { ContextMenuTrigger, } from "@/components/ui/context-menu"; -// --- Иконки --- import { GripVertical, File as FileIcon, @@ -71,10 +69,8 @@ import { } from "lucide-react"; import { t } from "i18next"; -// Активируем плагин для dayjs dayjs.extend(relativeTime); -// --- Вспомогательные функции --- const parseUrl = (url?: string): string | undefined => { if (!url) return undefined; try { @@ -302,6 +298,11 @@ export const ProfileItem = (props: Props) => { isDestructive: true, }; + const MAX_NAME_LENGTH = 25; + const truncatedName = name.length > MAX_NAME_LENGTH + ? `${name.slice(0, MAX_NAME_LENGTH)}...` + : name; + return (
@@ -459,7 +460,6 @@ export const ProfileItem = (props: Props) => { - {/* Модальные окна для редактирования */} {fileOpen && ( { setRulesOpen(false)} - profileUid={uid} // <-- Был 'uid', стал 'profileUid' + profileUid={uid} property={option?.rules ?? ""} - groupsUid={option?.groups ?? ""} // <-- Добавлен недостающий пропс - mergeUid={option?.merge ?? ""} // <-- Добавлен недостающий пропс + groupsUid={option?.groups ?? ""} + mergeUid={option?.merge ?? ""} onSave={onSave} /> )} @@ -491,7 +491,7 @@ export const ProfileItem = (props: Props) => { setProxiesOpen(false)} - profileUid={uid} // <-- Был 'uid', стал 'profileUid' + profileUid={uid} property={option?.proxies ?? ""} onSave={onSave} /> @@ -501,20 +501,20 @@ export const ProfileItem = (props: Props) => { setGroupsOpen(false)} - profileUid={uid} // <-- Был 'uid', стал 'profileUid' + profileUid={uid} property={option?.groups ?? ""} - proxiesUid={option?.proxies ?? ""} // <-- Добавлен недостающий пропс - mergeUid={option?.merge ?? ""} // <-- Добавлен недостающий пропс + proxiesUid={option?.proxies ?? ""} + mergeUid={option?.merge ?? ""} onSave={onSave} /> )}
); diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx index 1f188247..6aa3a726 100644 --- a/src/components/ui/dropdown-menu.tsx +++ b/src/components/ui/dropdown-menu.tsx @@ -5,234 +5,263 @@ import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"; import { cn } from "@root/lib/utils"; function DropdownMenu({ - ...props -}: React.ComponentProps) { + ...props + }: React.ComponentProps) { return ; } function DropdownMenuPortal({ - ...props -}: React.ComponentProps) { + ...props + }: React.ComponentProps) { return ( - + ); } function DropdownMenuTrigger({ - ...props -}: React.ComponentProps) { - return ( - - ); + ...props + }: React.ComponentProps) { + return ( + + ); } function DropdownMenuContent({ - className, - sideOffset = 4, - ...props -}: React.ComponentProps) { - return ( - - - + className, + sideOffset = 4, + ...props + }: React.ComponentProps) { + return ( + + + ); } function DropdownMenuGroup({ - ...props -}: React.ComponentProps) { - return ( - - ); + ...props + }: React.ComponentProps) { + return ( + + ); } function DropdownMenuItem({ - className, - inset, - variant = "default", - ...props -}: React.ComponentProps & { + className, + inset, + variant = "default", + ...props + }: React.ComponentProps & { inset?: boolean; variant?: "default" | "destructive"; }) { return ( - + ); } function DropdownMenuCheckboxItem({ - className, - children, - checked, - ...props -}: React.ComponentProps) { + className, + children, + checked, + ...props + }: React.ComponentProps) { return ( - + - {children} - + {children} + ); } function DropdownMenuRadioGroup({ - ...props -}: React.ComponentProps) { - return ( - - ); + ...props + }: React.ComponentProps) { + return ( + + ); } function DropdownMenuRadioItem({ - className, - children, - ...props -}: React.ComponentProps) { + className, + children, + ...props + }: React.ComponentProps) { return ( - + - {children} - + {children} + ); } function DropdownMenuLabel({ - className, - inset, - ...props -}: React.ComponentProps & { + className, + inset, + ...props + }: React.ComponentProps & { inset?: boolean; }) { return ( - + ); } function DropdownMenuSeparator({ - className, - ...props -}: React.ComponentProps) { + className, + ...props + }: React.ComponentProps) { return ( - + ); } function DropdownMenuShortcut({ - className, - ...props -}: React.ComponentProps<"span">) { + className, + ...props + }: React.ComponentProps<"span">) { return ( - + ); } function DropdownMenuSub({ - ...props -}: React.ComponentProps) { + ...props + }: React.ComponentProps) { return ; } function DropdownMenuSubTrigger({ - className, - inset, - children, - ...props -}: React.ComponentProps & { + className, + inset, + children, + ...props + }: React.ComponentProps & { inset?: boolean; }) { return ( - - {children} - - + + {children} + + ); } function DropdownMenuSubContent({ - className, - ...props -}: React.ComponentProps) { + className, + ...props + }: React.ComponentProps) { return ( - + ); } @@ -252,4 +281,4 @@ export { DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContent, -}; +}; \ No newline at end of file diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx index ec0b312a..52718692 100644 --- a/src/components/ui/select.tsx +++ b/src/components/ui/select.tsx @@ -5,179 +5,191 @@ import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"; import { cn } from "@root/lib/utils"; function Select({ - ...props -}: React.ComponentProps) { - return ; + ...props + }: React.ComponentProps) { + return ; } function SelectGroup({ - ...props -}: React.ComponentProps) { - return ; + ...props + }: React.ComponentProps) { + return ; } function SelectValue({ - ...props -}: React.ComponentProps) { - return ; + ...props + }: React.ComponentProps) { + return ; } function SelectTrigger({ - className, - size = "default", - children, - ...props -}: React.ComponentProps & { - size?: "sm" | "default"; + className, + size = "default", + children, + ...props + }: React.ComponentProps & { + size?: "sm" | "default"; }) { - return ( - - {children} - - - - - ); + return ( + + {children} + + + + + ); } function SelectContent({ - className, - children, - position = "popper", - ...props -}: React.ComponentProps) { - return ( - - - - - {children} - - - - - ); + className, + children, + position = "popper", + ...props + }: React.ComponentProps) { + return ( + + + + + {children} + + + + + ); } function SelectLabel({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); + className, + ...props + }: React.ComponentProps) { + return ( + + ); } function SelectItem({ - className, - children, - ...props -}: React.ComponentProps) { - return ( - + children, + ...props + }: React.ComponentProps) { + return ( + - {children} - - ); + {children} + + ); } function SelectSeparator({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); + className, + ...props + }: React.ComponentProps) { + return ( + + ); } function SelectScrollUpButton({ - className, - ...props -}: React.ComponentProps) { - return ( - - - - ); + className, + ...props + }: React.ComponentProps) { + return ( + + + + ); } function SelectScrollDownButton({ - className, - ...props -}: React.ComponentProps) { - return ( - - - - ); + ...props + }: React.ComponentProps) { + return ( + + + + ); } export { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectLabel, - SelectScrollDownButton, - SelectScrollUpButton, - SelectSeparator, - SelectTrigger, - SelectValue, -}; + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, +}; \ No newline at end of file diff --git a/src/pages/connections.tsx b/src/pages/connections.tsx index 2597ef00..19714aea 100644 --- a/src/pages/connections.tsx +++ b/src/pages/connections.tsx @@ -49,18 +49,37 @@ import { PauseCircle, ArrowDown, ArrowUp, - Menu, } from "lucide-react"; import {SidebarTrigger} from "@/components/ui/sidebar"; +interface IConnectionsItem { + id: string; + metadata: { + host: string; + destinationIP: string; + process?: string; + }; + start?: string; + curUpload?: number; + curDownload?: number; +} + +interface IConnections { + uploadTotal: number; + downloadTotal: number; + connections: IConnectionsItem[]; + data: IConnectionsItem[]; +} + +type OrderFunc = (list: IConnectionsItem[]) => IConnectionsItem[]; + const initConn: IConnections = { uploadTotal: 0, downloadTotal: 0, connections: [], + data: [], }; -type OrderFunc = (list: IConnectionsItem[]) => IConnectionsItem[]; - const ConnectionsPage = () => { const { t } = useTranslation(); const pageVisible = useVisibility(); @@ -72,14 +91,14 @@ const ConnectionsPage = () => { const orderOpts: Record = { Default: (list) => - list.sort( - (a, b) => - new Date(b.start || "0").getTime()! - - new Date(a.start || "0").getTime()!, - ), - "Upload Speed": (list) => list.sort((a, b) => b.curUpload! - a.curUpload!), + list.sort( + (a, b) => + new Date(b.start || "0").getTime()! - + new Date(a.start || "0").getTime()!, + ), + "Upload Speed": (list) => list.sort((a, b) => (b.curUpload ?? 0) - (a.curUpload ?? 0)), "Download Speed": (list) => - list.sort((a, b) => b.curDownload! - a.curDownload!), + list.sort((a, b) => (b.curDownload ?? 0) - (a.curDownload ?? 0)), }; const [isPaused, setIsPaused] = useState(false); @@ -91,6 +110,7 @@ const ConnectionsPage = () => { uploadTotal: connections.uploadTotal, downloadTotal: connections.downloadTotal, connections: connections.data, + data: connections.data, }; if (isPaused) return frozenData ?? currentData; return currentData; @@ -101,7 +121,7 @@ const ConnectionsPage = () => { let conns = displayData.connections.filter((conn) => { const { host, destinationIP, process } = conn.metadata; return ( - match(host || "") || match(destinationIP || "") || match(process || "") + match(host || "") || match(destinationIP || "") || match(process || "") ); }); if (orderFunc) conns = orderFunc(conns); @@ -109,24 +129,24 @@ const ConnectionsPage = () => { }, [displayData, match, curOrderOpt]); const [scrollingElement, setScrollingElement] = useState< - HTMLElement | Window | null + HTMLElement | Window | null >(null); const [isScrolled, setIsScrolled] = useState(false); const scrollerRefCallback = useCallback( - (node: HTMLElement | Window | null) => { - setScrollingElement(node); - }, - [], + (node: HTMLElement | Window | null) => { + setScrollingElement(node); + }, + [], ); useEffect(() => { if (!scrollingElement) return; const handleScroll = () => { const scrollTop = - scrollingElement instanceof Window - ? scrollingElement.scrollY - : scrollingElement.scrollTop; + scrollingElement instanceof Window + ? scrollingElement.scrollY + : scrollingElement.scrollTop; setIsScrolled(scrollTop > 5); }; @@ -137,8 +157,8 @@ const ConnectionsPage = () => { const onCloseAll = useLockFn(closeAllConnections); const detailRef = useRef(null!); const handleSearch = useCallback( - (m: (content: string) => boolean) => setMatch(() => m), - [], + (m: (content: string) => boolean) => setMatch(() => m), + [], ); const handlePauseToggle = useCallback(() => { setIsPaused((prev) => { @@ -147,6 +167,7 @@ const ConnectionsPage = () => { uploadTotal: connections.uploadTotal, downloadTotal: connections.downloadTotal, connections: connections.data, + data: connections.data, }); } else { setFrozenData(null); @@ -155,134 +176,138 @@ const ConnectionsPage = () => { }); }, [connections]); - return ( -
-
-
-
- -
-

- {t("Connections")} -

- -
-
-
- - {parseTraffic(displayData.downloadTotal)} -
-
- - {parseTraffic(displayData.uploadTotal)} -
-
- - - - - - -

{isTableLayout ? t("List View") : t("Table View")}

-
-
- - - - - -

{isPaused ? t("Resume") : t("Pause")}

-
-
- -
-
-
-
- {!isTableLayout && ( - - )} -
- -
-
-
+ const headerHeight = "7rem"; -
- {filterConn.length === 0 ? ( - - ) : isTableLayout ? ( -
- detailRef.current?.open(detail)} - scrollerRef={scrollerRefCallback} - /> + return ( +
+
+
+
+ +
+

+ {t("Connections")} +

+ +
+
+
+ + {parseTraffic(displayData.downloadTotal)} +
+
+ + {parseTraffic(displayData.uploadTotal)} +
+
+ + + + + + +

{isTableLayout ? t("List View") : t("Table View")}

+
+
+ + + + + +

{isPaused ? t("Resume") : t("Pause")}

+
+
+ +
+
- ) : ( - ( - detailRef.current?.open(item)} - /> +
+ {!isTableLayout && ( + )} - /> - )} - +
+ +
+
+
+ +
+ {filterConn.length === 0 ? ( + + ) : isTableLayout ? ( +
+ detailRef.current?.open(detail)} + scrollerRef={scrollerRefCallback} + /> +
+ ) : ( + ( + detailRef.current?.open(item)} + /> + )} + /> + )} + +
-
); }; -export default ConnectionsPage; +export default ConnectionsPage; \ No newline at end of file diff --git a/src/pages/home.tsx b/src/pages/home.tsx index a335894c..991757b9 100644 --- a/src/pages/home.tsx +++ b/src/pages/home.tsx @@ -48,14 +48,14 @@ const MinimalHomePage: React.FC = () => { const [isToggling, setIsToggling] = useState(false); const [isUpdating, setIsUpdating] = useState(false); const { profiles, patchProfiles, activateSelected, mutateProfiles } = - useProfiles(); + useProfiles(); const viewerRef = useRef(null); const [uidToActivate, setUidToActivate] = useState(null); const { connections } = useAppData(); const profileItems = useMemo(() => { const items = - profiles && Array.isArray(profiles.items) ? profiles.items : []; + profiles && Array.isArray(profiles.items) ? profiles.items : []; const allowedTypes = ["local", "remote"]; return items.filter((i: any) => i && allowedTypes.includes(i.type!)); }, [profiles]); @@ -66,20 +66,20 @@ const MinimalHomePage: React.FC = () => { const currentProfileName = currentProfile?.name || profiles?.current; const activateProfile = useCallback( - async (uid: string, notifySuccess: boolean) => { - try { - await patchProfiles({ current: uid }); - await closeAllConnections(); - await activateSelected(); - if (notifySuccess) { - toast.success(t("Profile Switched")); + async (uid: string, notifySuccess: boolean) => { + try { + await patchProfiles({ current: uid }); + await closeAllConnections(); + await activateSelected(); + if (notifySuccess) { + toast.success(t("Profile Switched")); + } + } catch (err: any) { + toast.error(err.message || err.toString()); + mutateProfiles(); } - } catch (err: any) { - toast.error(err.message || err.toString()); - mutateProfiles(); - } - }, - [patchProfiles, activateSelected, mutateProfiles, t], + }, + [patchProfiles, activateSelected, mutateProfiles, t], ); useEffect(() => { @@ -90,7 +90,6 @@ const MinimalHomePage: React.FC = () => { } }, [profileItems, activateProfile]); - const handleProfileChange = useLockFn(async (uid: string) => { if (profiles?.current === uid) return; await activateProfile(uid, true); @@ -102,7 +101,7 @@ const MinimalHomePage: React.FC = () => { const isTunAvailable = isServiceMode || isAdminMode; const isProxyEnabled = verge?.enable_system_proxy || verge?.enable_tun_mode; const showTunAlert = - (verge?.primary_action ?? "tun-mode") === "tun-mode" && !isTunAvailable; + (verge?.primary_action ?? "tun-mode") === "tun-mode" && !isTunAvailable; const handleToggleProxy = useLockFn(async () => { const turningOn = !isProxyEnabled; @@ -180,111 +179,188 @@ const MinimalHomePage: React.FC = () => { }, [isToggling, isProxyEnabled, t]); return ( -
-
- World map -
- - {isProxyEnabled && ( -
+
+ World map - )} - -
-
-
-
-
- {profileItems.length > 0 && ( -
- - - - - - {t("Profiles")} - - {profileItems.map((p) => ( - handleProfileChange(p.uid)} - > - {p.name} - {profiles?.current === p.uid && ( - - )} - - ))} - - viewerRef.current?.create()}> - - {t("Add Profile")} - - - -
- )} - {currentProfile?.type === 'remote' && ( -
- - - - - -

{t("Update Profile")}

-
-
-
- )} + + {isProxyEnabled && ( +
+ )} + +
+
+
-
-
-
-
- -
-
- {currentProfile?.announce && ( -
- {currentProfile.announce_url ? ( - - {currentProfile.announce.replace(/\\n/g, '\n')} - - +
+
+ {profileItems.length > 0 ? ( + <> +
+ + + + + + +

{t("Add Profile")}

+
+
+
+
+ + + + + + {t("Profiles")} + + {profileItems.map((p) => ( + handleProfileChange(p.uid)} + > + {p.name} + {profiles?.current === p.uid && ( + + )} + + ))} + + + {currentProfile?.type === 'remote' && ( +
+ + + + + +

{t("Update Profile")}

+
+
+
+ )} + ) : ( -

- {currentProfile.announce} -

+ <> +
+ + + + + + +

{t("Add Profile")}

+
+
+
+
+ + )}
- )} +
+
+
+ + +
+
+ {currentProfile?.announce && ( +
+ {currentProfile.announce_url ? ( + + {currentProfile.announce.replace(/\\n/g, '\n')} + + + ) : ( +

+ {currentProfile.announce} +

+ )} +
+ )}

{ > {statusInfo.text}

- {isProxyEnabled && ( -
-
- - {parseTraffic(connections.downloadTotal)} -
-
- - {parseTraffic(connections.uploadTotal)} -
-
- )} -
- -
- -
- - {showTunAlert && ( -
- - - {t("Attention Required")} - - {t("TUN requires Service Mode or Admin Mode")} - - {!isServiceMode && !isAdminMode && ( - - )} - + {isProxyEnabled && ( +
+
+ + {parseTraffic(connections.downloadTotal)} +
+
+ + {parseTraffic(connections.uploadTotal)} +
+
+ )}
- )} -
- {profileItems.length > 0 ? ( - - ) : ( - - - {t("Get Started")} - - {t( - "You don't have any profiles yet. Add your first one to begin.", - )} - - - +
+ +
+ + {showTunAlert && ( +
+ + + {t("Attention Required")} + + {t("TUN requires Service Mode or Admin Mode")} + + {!isServiceMode && !isAdminMode && ( + + )} + +
)} + +
+ {profileItems.length > 0 ? ( + + ) : ( + + + {t("Get Started")} + + {t( + "You don't have any profiles yet. Add your first one to begin.", + )} + + + + )} +
-
-
-
- {currentProfile?.support_url && ( -
+
+ - mutateProfiles()} /> -
+
+ )} + + mutateProfiles()} /> +
); }; -export default MinimalHomePage; +export default MinimalHomePage; \ No newline at end of file diff --git a/src/services/types.d.ts b/src/services/types.d.ts index e4878211..92181038 100644 --- a/src/services/types.d.ts +++ b/src/services/types.d.ts @@ -154,7 +154,7 @@ interface IConnectionsItem { start: string; chains: string[]; rule: string; - rulePayload: string; + rulePayload?: string; curUpload?: number; // upload speed, calculate at runtime curDownload?: number; // download speed, calculate at runtime }