diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index ee5d6934..aa8053b6 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1,5 @@ -custom: ['https://t.me/tribute/app?startapp=dtfk','https://t.me/tribute/app?startapp=dtLE'] +custom: + [ + "https://t.me/tribute/app?startapp=dtfk", + "https://t.me/tribute/app?startapp=dtLE", + ] diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a6dd9f14..954d6eef 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -91,17 +91,17 @@ jobs: > :warning: **Warning** If you get a notification that the application is corrupted when you run it on macOS, run this command:
sudo xattr -r -c /Applications/Koala\ Clash.app - + ### Linux
- +
- +
- + ### Windows (Win7 is no longer supported) #### Normal version (recommended)
@@ -217,11 +217,11 @@ jobs: shell: bash run: | TARGET_DIR="src-tauri/target/${{ matrix.target }}/release/bundle" - + if [ ! -d "$TARGET_DIR" ]; then exit 1 fi - + find "$TARGET_DIR" -type f \( -name "*.dmg" -o -name "*.deb" -o -name "*.rpm" \) -print0 | while IFS= read -r -d '' old_path; do dir_path=$(dirname "$old_path") old_filename=$(basename "$old_path") @@ -365,11 +365,11 @@ jobs: shell: bash run: | TARGET_DIR="src-tauri/target/${{ matrix.target }}/release/bundle" - + if [ ! -d "$TARGET_DIR" ]; then exit 1 fi - + find "$TARGET_DIR" -type f \( -name "*.dmg" -o -name "*.deb" -o -name "*.rpm" \) -print0 | while IFS= read -r -d '' old_path; do dir_path=$(dirname "$old_path") old_filename=$(basename "$old_path") @@ -578,14 +578,14 @@ jobs: echo "Using found update logs" UPDATE_LOGS=$(echo "$UPDATE_LOGS" | sed 's/^## \(v.*\)/\*\1\*/') fi - + cat > release.txt << EOF Вышло обновление! - + $UPDATE_LOGS - + [Ссылка на релиз](https://github.com/coolcoala/clash-verge-rev-lite/releases/latest) - + EOF - name: notify to channel @@ -603,4 +603,3 @@ jobs: token: ${{ secrets.TELEGRAM_TOKEN }} message_file: release.txt format: markdown - diff --git a/UPDATELOG.md b/UPDATELOG.md index c0f61405..df0d7fe9 100644 --- a/UPDATELOG.md +++ b/UPDATELOG.md @@ -1,7 +1,7 @@ ## v0.2.5 - new main page -- fixed issue with opening via shortcut +- fixed issue with opening via shortcut - fixed logo in sidebar - fixed issue with changing tray settings - name changed to koala clash @@ -27,7 +27,6 @@ - corrected side menu in compressed window - added check at the main toggle switch, now it cannot be enabled if there are no profiles. - ## v0.2.1 - added headers "announce-url", "update-always" diff --git a/hooks/use-mobile.ts b/hooks/use-mobile.ts index 2b0fe1df..a93d5839 100644 --- a/hooks/use-mobile.ts +++ b/hooks/use-mobile.ts @@ -1,19 +1,21 @@ -import * as React from "react" +import * as React from "react"; -const MOBILE_BREAKPOINT = 768 +const MOBILE_BREAKPOINT = 768; export function useIsMobile() { - const [isMobile, setIsMobile] = React.useState(undefined) + const [isMobile, setIsMobile] = React.useState( + undefined, + ); React.useEffect(() => { - const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) + const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); const onChange = () => { - setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) - } - mql.addEventListener("change", onChange) - setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) - return () => mql.removeEventListener("change", onChange) - }, []) + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); + }; + mql.addEventListener("change", onChange); + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); + return () => mql.removeEventListener("change", onChange); + }, []); - return !!isMobile + return !!isMobile; } diff --git a/src-tauri/webview2.arm64.json b/src-tauri/webview2.arm64.json index 18f87dcb..296703b5 100644 --- a/src-tauri/webview2.arm64.json +++ b/src-tauri/webview2.arm64.json @@ -31,4 +31,3 @@ } } } - diff --git a/src/App.tsx b/src/App.tsx index fec46f19..2d2f0663 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,9 +3,9 @@ import Layout from "./pages/_layout"; function App() { return ( - - - + + + ); } export default App; diff --git a/src/components/connection/connection-table.tsx b/src/components/connection/connection-table.tsx index 2cf1d2e1..3a906fbc 100644 --- a/src/components/connection/connection-table.tsx +++ b/src/components/connection/connection-table.tsx @@ -73,21 +73,25 @@ interface Props { scrollerRef: (element: HTMLElement | Window | null) => void; } -const ColumnResizer = ({ header }: { header: Header }) => { +const ColumnResizer = ({ + header, +}: { + header: Header; +}) => { return ( -
+
); }; @@ -105,8 +109,8 @@ export const ConnectionTable = (props: Props) => { try { const saved = localStorage.getItem("connection-table-widths"); return saved - ? JSON.parse(saved) - : { + ? JSON.parse(saved) + : { host: 220, download: 88, upload: 88, @@ -140,8 +144,8 @@ export const ConnectionTable = (props: Props) => { useEffect(() => { localStorage.setItem( - "connection-table-widths", - JSON.stringify(columnSizing) + "connection-table-widths", + JSON.stringify(columnSizing), ); }, [columnSizing]); @@ -151,13 +155,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, @@ -175,118 +179,118 @@ export const ConnectionTable = (props: Props) => { }, [connections]); const columns = useMemo[]>( - () => [ - { - 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] + () => [ + { + 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({ @@ -305,82 +309,82 @@ export const ConnectionTable = (props: Props) => { if (connRows.length === 0) { return ( -
-

{t("No connections")}

-
+
+

{t("No connections")}

+
); } return ( -
- - - {table.getHeaderGroups().map((headerGroup) => ( - +
+ + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + - {headerGroup.headers.map((header) => ( - -
- {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} -
- {header.column.getCanResize() && ( - +
+ {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), )} - - ))} - - ))} - +
+ {header.column.getCanResize() && ( + + )} +
+ ))} +
+ ))} +
- - {table.getRowModel().rows.map((row) => ( - onShowDetail(row.original.connectionData)} + + {table.getRowModel().rows.map((row) => ( + onShowDetail(row.original.connectionData)} + > + {row.getVisibleCells().map((cell) => ( + - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - - ))} - -
-
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ))} + + +
); -}; \ No newline at end of file +}; diff --git a/src/components/home/power-button.tsx b/src/components/home/power-button.tsx index eedd5823..456f7f90 100644 --- a/src/components/home/power-button.tsx +++ b/src/components/home/power-button.tsx @@ -1,64 +1,72 @@ -import React from 'react'; -import { cn } from '@root/lib/utils'; -import { Power } from 'lucide-react'; +import React from "react"; +import { cn } from "@root/lib/utils"; +import { Power } from "lucide-react"; -export interface PowerButtonProps extends React.ButtonHTMLAttributes { - checked?: boolean; - loading?: boolean; +export interface PowerButtonProps + extends React.ButtonHTMLAttributes { + checked?: boolean; + loading?: boolean; } -export const PowerButton = React.forwardRef( - ({ className, checked = false, loading = false, ...props }, ref) => { - const state = checked ? 'on' : 'off'; +export const PowerButton = React.forwardRef< + HTMLButtonElement, + PowerButtonProps +>(({ className, checked = false, loading = false, ...props }, ref) => { + const state = checked ? "on" : "off"; - return ( -
+ return ( +
+
-
+ - // Стили ТОЛЬКО для отключенного состояния (но не для загрузки) - (props.disabled && !loading) && 'grayscale opacity-50 shadow-none bg-slate-100/70 border-slate-300/80', - className - )} - {...props} - > - - - - {loading && ( -
-
-
- )} -
- ); - } -); \ No newline at end of file + {loading && ( +
+
+
+ )} +
+ ); +}); diff --git a/src/components/layout/sidebar.tsx b/src/components/layout/sidebar.tsx index b64a262a..77117206 100644 --- a/src/components/layout/sidebar.tsx +++ b/src/components/layout/sidebar.tsx @@ -1,16 +1,18 @@ -import { Link } from 'react-router-dom'; +import { Link } from "react-router-dom"; import { Sidebar, - SidebarContent, SidebarFooter, + SidebarContent, + SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarHeader, SidebarMenu, SidebarMenuButton, - SidebarMenuItem, useSidebar, -} from "@/components/ui/sidebar" -import { t } from 'i18next'; -import { cn } from '@root/lib/utils'; + SidebarMenuItem, + useSidebar, +} from "@/components/ui/sidebar"; +import { t } from "i18next"; +import { cn } from "@root/lib/utils"; import { Home, @@ -19,21 +21,22 @@ import { Cable, ListChecks, FileText, - Settings, EarthLock, -} from 'lucide-react'; + Settings, + EarthLock, +} from "lucide-react"; import { UpdateButton } from "@/components/layout/update-button"; import React from "react"; -import { SheetClose } from '@/components/ui/sheet'; -import logo from "@/assets/image/logo.png" +import { SheetClose } from "@/components/ui/sheet"; +import logo from "@/assets/image/logo.png"; const menuItems = [ - { title: 'Home', url: '/home', icon: Home }, - { title: 'Profiles', url: '/profile', icon: Users }, - { title: 'Proxies', url: '/proxies', icon: Server }, - { title: 'Connections', url: '/connections', icon: Cable }, - { title: 'Rules', url: '/rules', icon: ListChecks }, - { title: 'Logs', url: '/logs', icon: FileText }, - { title: 'Settings', url: '/settings', icon: Settings }, + { title: "Home", url: "/home", icon: Home }, + { title: "Profiles", url: "/profile", icon: Users }, + { title: "Proxies", url: "/proxies", icon: Server }, + { title: "Connections", url: "/connections", icon: Cable }, + { title: "Rules", url: "/rules", icon: ListChecks }, + { title: "Logs", url: "/logs", icon: FileText }, + { title: "Settings", url: "/settings", icon: Settings }, ]; export function AppSidebar() { @@ -41,18 +44,15 @@ export function AppSidebar() { return ( - - logo + logo Koala Clash @@ -69,30 +69,29 @@ export function AppSidebar() { key={item.title} to={item.url} className={cn( - 'flex items-center gap-3 rounded-lg px-3 py-2 text-muted-foreground transition-all hover:text-primary', - 'data-[active=true]:font-semibold data-[active=true]:border' + "flex items-center gap-3 rounded-lg px-3 py-2 text-muted-foreground transition-all hover:text-primary", + "data-[active=true]:font-semibold data-[active=true]:border", )} > {t(item.title)} - ) + ); return ( - - - {isMobile ? ( - - {linkElement} - + + + {isMobile ? ( + {linkElement} ) : ( linkElement )} - - - ) + + + ); })} @@ -104,5 +103,5 @@ export function AppSidebar() {
- ) + ); } diff --git a/src/components/layout/update-button.tsx b/src/components/layout/update-button.tsx index 414e75c3..f1d043e0 100644 --- a/src/components/layout/update-button.tsx +++ b/src/components/layout/update-button.tsx @@ -6,7 +6,7 @@ import { DialogRef } from "../base"; import { useVerge } from "@/hooks/use-verge"; import { Button } from "@/components/ui/button"; import { t } from "i18next"; -import {Download, RefreshCw} from "lucide-react"; +import { Download, RefreshCw } from "lucide-react"; import { useSidebar } from "../ui/sidebar"; interface Props { @@ -17,7 +17,7 @@ export const UpdateButton = (props: Props) => { const { className } = props; const { verge } = useVerge(); const { auto_check_update } = verge || {}; - const { state: sidebarState } = useSidebar(); + const { state: sidebarState } = useSidebar(); const viewerRef = useRef(null); @@ -36,7 +36,7 @@ export const UpdateButton = (props: Props) => { return ( <> - {sidebarState === 'collapsed' ? ( + {sidebarState === "collapsed" ? (
- + {type}
@@ -389,14 +388,13 @@ export const ProfileItem = (props: Props) => {
- - {parseTraffic(download)} - + {parseTraffic(download)} {total > 0 ? ( - {parseTraffic(total)} - ) : } - + {parseTraffic(total)} + ) : ( + + )}
@@ -510,11 +508,11 @@ export const ProfileItem = (props: Props) => { )}
); diff --git a/src/components/profile/profile-viewer.tsx b/src/components/profile/profile-viewer.tsx index c35c2d83..3c4ed007 100644 --- a/src/components/profile/profile-viewer.tsx +++ b/src/components/profile/profile-viewer.tsx @@ -12,7 +12,8 @@ import { createProfile, patchProfile, importProfile, - enhanceProfiles, createProfileFromShareLink, + enhanceProfiles, + createProfileFromShareLink, } from "@/services/cmds"; import { useProfiles } from "@/hooks/use-profiles"; import { showNotice } from "@/services/noticeService"; @@ -138,7 +139,9 @@ export const ProfileViewer = forwardRef( setIsCheckingUrl(true); const handler = setTimeout(() => { - const isValid = /^(https?|vmess|vless|ss|socks|trojan):\/\//.test(importUrl); + const isValid = /^(https?|vmess|vless|ss|socks|trojan):\/\//.test( + importUrl, + ); setIsUrlValid(isValid); setIsCheckingUrl(false); }, 500); @@ -165,23 +168,35 @@ export const ProfileViewer = forwardRef( await enhanceProfiles(); setOpen(false); } catch (err: any) { - const errorMessage = typeof err === 'string' ? err : (err.message || String(err)); + const errorMessage = + typeof err === "string" ? err : err.message || String(err); const lowerErrorMessage = errorMessage.toLowerCase(); - if (lowerErrorMessage.includes('device') || lowerErrorMessage.includes('устройств')) { - window.dispatchEvent(new CustomEvent('show-hwid-error', { detail: errorMessage })); + if ( + lowerErrorMessage.includes("device") || + lowerErrorMessage.includes("устройств") + ) { + window.dispatchEvent( + new CustomEvent("show-hwid-error", { detail: errorMessage }), + ); } else if (!isShareLink && errorMessage.includes("failed to fetch")) { - showNotice("info", t("Import failed, retrying with Clash proxy...")); - try { - await importProfile(importUrl, { with_proxy: false, self_proxy: true }); - showNotice("success", t("Profile Imported with Clash proxy")); - props.onChange(); - await enhanceProfiles(); - setOpen(false); - } catch (retryErr: any) { - showNotice("error", `${t("Import failed even with Clash proxy")}: ${retryErr?.message || retryErr.toString()}`); - } + showNotice("info", t("Import failed, retrying with Clash proxy...")); + try { + await importProfile(importUrl, { + with_proxy: false, + self_proxy: true, + }); + showNotice("success", t("Profile Imported with Clash proxy")); + props.onChange(); + await enhanceProfiles(); + setOpen(false); + } catch (retryErr: any) { + showNotice( + "error", + `${t("Import failed even with Clash proxy")}: ${retryErr?.message || retryErr.toString()}`, + ); + } } else { - showNotice("error", errorMessage); + showNotice("error", errorMessage); } } finally { setIsImporting(false); @@ -302,19 +317,26 @@ export const ProfileViewer = forwardRef(
{/^(vmess|vless|ss|socks|trojan):\/\//.test(importUrl) && ( -
+
- + + + + + + {t("Default Template")} + + + {t("Template without RU Rules")} + + -
- )} +
+ )} - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + ); -}); \ No newline at end of file +}); diff --git a/src/components/setting/setting-system.tsx b/src/components/setting/setting-system.tsx index 1cf0a678..a5516166 100644 --- a/src/components/setting/setting-system.tsx +++ b/src/components/setting/setting-system.tsx @@ -1,4 +1,4 @@ -import {useMemo, useRef, useState} from "react"; +import { useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { useLockFn } from "ahooks"; import { mutate } from "swr"; @@ -43,7 +43,7 @@ import { Power, BellOff, Repeat, - Fingerprint + Fingerprint, } from "lucide-react"; // Модальные окна @@ -56,7 +56,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import {useProfiles} from "@/hooks/use-profiles"; +import { useProfiles } from "@/hooks/use-profiles"; const isWIN = getSystem() === "windows"; interface Props { @@ -110,7 +110,7 @@ const SettingSystem = ({ onError }: Props) => { const { profiles } = useProfiles(); const hasProfiles = useMemo(() => { const items = profiles?.items ?? []; - return items.some(p => p.type === 'local' || p.type === 'remote'); + return items.some((p) => p.type === "local" || p.type === "remote"); }, [profiles]); const { @@ -261,7 +261,10 @@ const SettingSystem = ({ onError }: Props) => { ); } if (e) { - return patchVerge({ enable_tun_mode: true, enable_system_proxy: false }); + return patchVerge({ + enable_tun_mode: true, + enable_system_proxy: false, + }); } else { return patchVerge({ enable_tun_mode: false }); } diff --git a/src/components/setting/setting-verge-basic.tsx b/src/components/setting/setting-verge-basic.tsx index 3c08ea5c..bcc84bf9 100644 --- a/src/components/setting/setting-verge-basic.tsx +++ b/src/components/setting/setting-verge-basic.tsx @@ -188,20 +188,20 @@ const SettingVergeBasic = ({ onError }: Props) => { {OS !== "linux" && ( - } + label={ + + } > v} - onChange={(e) => onChangeData({ tray_event: e })} - onGuard={(e) => patchVerge({ tray_event: e })} - onChangeProps="onValueChange" + value={tray_event ?? "main_window"} + onCatch={onError} + onFormat={(v) => v} + onChange={(e) => onChangeData({ tray_event: e })} + onGuard={(e) => patchVerge({ tray_event: e })} + onChangeProps="onValueChange" > setOrderOpt(value)} - > - - - - - {Object.keys(orderOpts).map((opt) => ( - - {t(opt)} - - ))} - - - )} -
- + + + + + + +

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

+
+
+ + + + + +

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

+
+
+
- + - -
- {filterConn.length === 0 ? ( - - ) : isTableLayout ? ( -
- detailRef.current?.open(detail)} - scrollerRef={scrollerRefCallback} - /> -
- ) : ( - ( - detailRef.current?.open(item)} - /> - )} - /> +
+ {!isTableLayout && ( + )} - +
+ +
+ +
+ {filterConn.length === 0 ? ( + + ) : isTableLayout ? ( +
+ detailRef.current?.open(detail)} + scrollerRef={scrollerRefCallback} + /> +
+ ) : ( + ( + detailRef.current?.open(item)} + /> + )} + /> + )} + +
+ ); }; -export default ConnectionsPage; \ No newline at end of file +export default ConnectionsPage; diff --git a/src/pages/home.tsx b/src/pages/home.tsx index 991757b9..fe0baad3 100644 --- a/src/pages/home.tsx +++ b/src/pages/home.tsx @@ -1,4 +1,10 @@ -import React, {useRef, useMemo, useCallback, useState, useEffect} from "react"; +import React, { + useRef, + useMemo, + useCallback, + useState, + useEffect, +} from "react"; import { useLockFn } from "ahooks"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; @@ -25,7 +31,11 @@ import { AlertTriangle, Loader2, Globe, - Send, ExternalLink, RefreshCw, ArrowDown, ArrowUp, + Send, + ExternalLink, + RefreshCw, + ArrowDown, + ArrowUp, } from "lucide-react"; import { useVerge } from "@/hooks/use-verge"; import { useSystemState } from "@/hooks/use-system-state"; @@ -34,7 +44,12 @@ import { Switch } from "@/components/ui/switch"; import { ProxySelectors } from "@/components/home/proxy-selectors"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { closeAllConnections } from "@/services/api"; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; import { updateProfile } from "@/services/cmds"; import { SidebarTrigger } from "@/components/ui/sidebar"; import parseTraffic from "@/utils/parse-traffic"; @@ -48,45 +63,45 @@ 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]); const currentProfile = useMemo(() => { - return profileItems.find(p => p.uid === profiles?.current); + return profileItems.find((p) => p.uid === profiles?.current); }, [profileItems, profiles?.current]); 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")); - } - } catch (err: any) { - toast.error(err.message || err.toString()); - mutateProfiles(); + async (uid: string, notifySuccess: boolean) => { + try { + await patchProfiles({ current: uid }); + await closeAllConnections(); + await activateSelected(); + if (notifySuccess) { + toast.success(t("Profile Switched")); } - }, - [patchProfiles, activateSelected, mutateProfiles, t], + } catch (err: any) { + toast.error(err.message || err.toString()); + mutateProfiles(); + } + }, + [patchProfiles, activateSelected, mutateProfiles, t], ); useEffect(() => { - const uidToActivate = sessionStorage.getItem('activateProfile'); - if (uidToActivate && profileItems.some(p => p.uid === uidToActivate)) { + const uidToActivate = sessionStorage.getItem("activateProfile"); + if (uidToActivate && profileItems.some((p) => p.uid === uidToActivate)) { activateProfile(uidToActivate, false); - sessionStorage.removeItem('activateProfile'); + sessionStorage.removeItem("activateProfile"); } }, [profileItems, activateProfile]); @@ -101,7 +116,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; @@ -143,7 +158,7 @@ const MinimalHomePage: React.FC = () => { }); const handleUpdateProfile = useLockFn(async () => { - if (!currentProfile?.uid || currentProfile.type !== 'remote') return; + if (!currentProfile?.uid || currentProfile.type !== "remote") return; setIsUpdating(true); try { await updateProfile(currentProfile.uid); @@ -159,316 +174,325 @@ const MinimalHomePage: React.FC = () => { const statusInfo = useMemo(() => { if (isToggling) { return { - text: isProxyEnabled ? t('Disconnecting...') : t('Connecting...'), - color: isProxyEnabled ? '#f59e0b' : '#84cc16', + text: isProxyEnabled ? t("Disconnecting...") : t("Connecting..."), + color: isProxyEnabled ? "#f59e0b" : "#84cc16", isAnimating: true, }; } if (isProxyEnabled) { return { - text: t('Connected'), - color: '#22c55e', + text: t("Connected"), + color: "#22c55e", isAnimating: false, }; } return { - text: t('Disconnected'), - color: '#ef4444', + text: t("Disconnected"), + color: "#ef4444", isAnimating: false, }; }, [isToggling, isProxyEnabled, t]); return ( -
-
- World map -
- - {isProxyEnabled && ( -
- )} - -
-
- -
-
-
- {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")}

-
-
-
- )} - - ) : ( - <> -
- - - - - - -

{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 && ( - - )} - -
- )} - -
- {profileItems.length > 0 ? ( - - ) : ( - - - {t("Get Started")} - - {t( - "You don't have any profiles yet. Add your first one to begin.", - )} - - - - )} -
-
-
- - mutateProfiles()} /> +
+
+ World map
+ + {isProxyEnabled && ( +
+ )} + +
+
+ +
+
+
+ {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")}

+
+
+
+
+ )} + + ) : ( + <> +
+ + + + + + +

{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 && ( + + )} + +
+ )} + +
+ {profileItems.length > 0 ? ( + + ) : ( + + + {t("Get Started")} + + {t( + "You don't have any profiles yet. Add your first one to begin.", + )} + + + + )} +
+
+
+ + mutateProfiles()} /> +
); }; -export default MinimalHomePage; \ No newline at end of file +export default MinimalHomePage; diff --git a/src/pages/logs.tsx b/src/pages/logs.tsx index 2a3d1bbe..24e6154c 100644 --- a/src/pages/logs.tsx +++ b/src/pages/logs.tsx @@ -31,7 +31,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import {SidebarTrigger} from "@/components/ui/sidebar"; +import { SidebarTrigger } from "@/components/ui/sidebar"; const LogPage = () => { const { t } = useTranslation(); diff --git a/src/pages/profiles.tsx b/src/pages/profiles.tsx index 1c1a30c2..eff08f41 100644 --- a/src/pages/profiles.tsx +++ b/src/pages/profiles.tsx @@ -55,13 +55,7 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; -import { - PlusCircle, - RefreshCw, - Zap, - FileText, - Loader2, -} from "lucide-react"; +import { PlusCircle, RefreshCw, Zap, FileText, Loader2 } from "lucide-react"; import { SidebarTrigger } from "@/components/ui/sidebar"; const ProfilePage = () => { diff --git a/src/pages/rules.tsx b/src/pages/rules.tsx index 7df67cf5..e6e44c15 100644 --- a/src/pages/rules.tsx +++ b/src/pages/rules.tsx @@ -1,4 +1,10 @@ -import React, { useState, useMemo, useRef, useEffect, useCallback } from "react"; +import React, { + useState, + useMemo, + useRef, + useEffect, + useCallback, +} from "react"; import { useTranslation } from "react-i18next"; import { Virtuoso, VirtuosoHandle } from "react-virtuoso"; import { useAppData } from "@/providers/app-data-provider"; diff --git a/src/services/cmds.ts b/src/services/cmds.ts index 4b7ccb80..788557f4 100644 --- a/src/services/cmds.ts +++ b/src/services/cmds.ts @@ -401,6 +401,9 @@ export async function getNextUpdateTime(uid: string) { return invoke("get_next_update_time", { uid }); } -export async function createProfileFromShareLink(link: string, templateName: string) { +export async function createProfileFromShareLink( + link: string, + templateName: string, +) { return invoke("create_profile_from_share_link", { link, templateName }); } diff --git a/src/services/noticeService.ts b/src/services/noticeService.ts index 2e19d7bb..3f482a76 100644 --- a/src/services/noticeService.ts +++ b/src/services/noticeService.ts @@ -1,25 +1,29 @@ import { toast } from "sonner"; -type NoticeType = 'success' | 'error' | 'info' | 'warning'; +type NoticeType = "success" | "error" | "info" | "warning"; -export const showNotice = (type: NoticeType, message: string, duration?: number) => { +export const showNotice = ( + type: NoticeType, + message: string, + duration?: number, +) => { const options = duration ? { duration } : {}; switch (type) { - case 'success': + case "success": toast.success(message, options); break; - case 'error': + case "error": toast.error(message, options); break; - case 'info': + case "info": toast.info(message, options); break; - case 'warning': + case "warning": toast.warning(message, options); break; default: toast(message, options); break; } -}; \ No newline at end of file +};