From af4463fef1149471c32b51dd5a81627968bdff32 Mon Sep 17 00:00:00 2001 From: XiaoBuHaly <145245150+XiaoBuHaly@users.noreply.github.com> Date: Sun, 9 Nov 2025 00:15:01 +0800 Subject: [PATCH 01/49] feat(connections): enable persistent column visibility and ordering #5235 (#5249) * feat(connections): enable persistent column visibility and ordering fix(connections): smooth column manager access in table view fix(connections): integrate column manager into column menu - Move the Connections column manager trigger into the DataGrid column menu, reusing existing i18n strings - Clean up the page toolbar by removing the standalone button and lifting dialog state to the parent page fix(connections): unify column order handling and resolve lint warnings fix(connections): unify column order handling and enforce 0 ESLint warnings - extract reconcileColumnOrder helper and reuse it across: - initial normalization (useEffect) - manager drag order handler (handleManagerOrderChange) - derive columnOptions directly from the computed, sorted columns to keep a single source of truth and prevent dialog/grid drift - connections.tsx: remove direct setState in useEffect; gate dialog open with `isTableLayout && isColumnManagerOpen`; clean up unused imports - connection-column-manager.tsx: replace deprecated `primaryTypographyProps` with `slotProps` - run full lint; project now has 0 warnings on main configuration * feat(connection-table): safeguard column visibility --------- Co-authored-by: Slinetrac --- .../connection/connection-column-manager.tsx | 191 ++++++++++ .../connection/connection-table.tsx | 345 ++++++++++++++++-- src/locales/ar/connections.json | 4 + src/locales/de/connections.json | 4 + src/locales/en/connections.json | 4 + src/locales/es/connections.json | 4 + src/locales/fa/connections.json | 4 + src/locales/id/connections.json | 4 + src/locales/jp/connections.json | 4 + src/locales/ko/connections.json | 4 + src/locales/ru/connections.json | 4 + src/locales/tr/connections.json | 4 + src/locales/tt/connections.json | 4 + src/locales/zh/connections.json | 4 + src/locales/zhtw/connections.json | 4 + src/pages/connections.tsx | 43 ++- src/types/generated/i18n-keys.ts | 2 + src/types/generated/i18n-resources.ts | 4 + 18 files changed, 600 insertions(+), 37 deletions(-) create mode 100644 src/components/connection/connection-column-manager.tsx diff --git a/src/components/connection/connection-column-manager.tsx b/src/components/connection/connection-column-manager.tsx new file mode 100644 index 000000000..f2a5a6e42 --- /dev/null +++ b/src/components/connection/connection-column-manager.tsx @@ -0,0 +1,191 @@ +import { + closestCenter, + DndContext, + PointerSensor, + useSensor, + useSensors, + type DragEndEvent, +} from "@dnd-kit/core"; +import { arrayMove, SortableContext, useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; +import { DragIndicatorRounded } from "@mui/icons-material"; +import { + Button, + Checkbox, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + IconButton, + List, + ListItem, + ListItemText, +} from "@mui/material"; +import { useCallback, useMemo } from "react"; +import { useTranslation } from "react-i18next"; + +interface ColumnOption { + field: string; + label: string; + visible: boolean; +} + +interface Props { + open: boolean; + columns: ColumnOption[]; + onClose: () => void; + onToggle: (field: string, visible: boolean) => void; + onOrderChange: (order: string[]) => void; + onReset: () => void; +} + +export const ConnectionColumnManager = ({ + open, + columns, + onClose, + onToggle, + onOrderChange, + onReset, +}: Props) => { + const sensors = useSensors( + useSensor(PointerSensor, { + activationConstraint: { distance: 6 }, + }), + ); + const { t } = useTranslation(); + + const items = useMemo(() => columns.map((column) => column.field), [columns]); + const visibleCount = useMemo( + () => columns.filter((column) => column.visible).length, + [columns], + ); + + const handleDragEnd = useCallback( + (event: DragEndEvent) => { + const { active, over } = event; + if (!over || active.id === over.id) return; + + const order = columns.map((column) => column.field); + const oldIndex = order.indexOf(active.id as string); + const newIndex = order.indexOf(over.id as string); + if (oldIndex === -1 || newIndex === -1) return; + + onOrderChange(arrayMove(order, oldIndex, newIndex)); + }, + [columns, onOrderChange], + ); + + return ( + + + {t("connections.components.columnManager.title")} + + + + + + {columns.map((column) => ( + + ))} + + + + + + + + + + ); +}; + +interface SortableColumnItemProps { + column: ColumnOption; + onToggle: (field: string, visible: boolean) => void; + dragHandleLabel: string; + disableToggle?: boolean; +} + +const SortableColumnItem = ({ + column, + onToggle, + dragHandleLabel, + disableToggle = false, +}: SortableColumnItemProps) => { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id: column.field }); + + const style = useMemo( + () => ({ + transform: CSS.Transform.toString(transform), + transition, + }), + [transform, transition], + ); + + return ( + `1px solid ${theme.palette.divider}`, + backgroundColor: isDragging ? "action.hover" : "transparent", + display: "flex", + alignItems: "center", + gap: 1, + }} + style={style} + > + onToggle(column.field, event.target.checked)} + /> + + + + + + ); +}; diff --git a/src/components/connection/connection-table.tsx b/src/components/connection/connection-table.tsx index 8761a522a..f1eebb535 100644 --- a/src/components/connection/connection-table.tsx +++ b/src/components/connection/connection-table.tsx @@ -1,24 +1,63 @@ +import { Box } from "@mui/material"; import { DataGrid, GridColDef, + GridColumnOrderChangeParams, GridColumnResizeParams, + GridColumnVisibilityModel, useGridApiRef, + GridColumnMenuItemProps, + GridColumnMenuHideItem, + useGridRootProps, } from "@mui/x-data-grid"; import dayjs from "dayjs"; import { useLocalStorage } from "foxact/use-local-storage"; -import { useLayoutEffect, useMemo, useState } from "react"; +import { + useCallback, + useEffect, + useLayoutEffect, + useMemo, + createContext, + use, +} from "react"; +import type { MouseEvent } from "react"; import { useTranslation } from "react-i18next"; import parseTraffic from "@/utils/parse-traffic"; import { truncateStr } from "@/utils/truncate-str"; +import { ConnectionColumnManager } from "./connection-column-manager"; + +const ColumnManagerContext = createContext<() => void>(() => {}); + +/** + * Reconcile stored column order with base columns to handle added/removed fields + */ +const reconcileColumnOrder = ( + storedOrder: string[], + baseFields: string[], +): string[] => { + const filtered = storedOrder.filter((field) => baseFields.includes(field)); + const missing = baseFields.filter((field) => !filtered.includes(field)); + return [...filtered, ...missing]; +}; + interface Props { connections: IConnectionsItem[]; onShowDetail: (data: IConnectionsItem) => void; + columnManagerOpen: boolean; + onOpenColumnManager: () => void; + onCloseColumnManager: () => void; } export const ConnectionTable = (props: Props) => { - const { connections, onShowDetail } = props; + const { + connections, + onShowDetail, + columnManagerOpen, + onOpenColumnManager, + onCloseColumnManager, + } = props; const { t } = useTranslation(); const apiRef = useGridApiRef(); useLayoutEffect(() => { @@ -145,10 +184,6 @@ export const ConnectionTable = (props: Props) => { }; }, [apiRef]); - const [columnVisible, setColumnVisible] = useState< - Partial> - >({}); - const [columnWidths, setColumnWidths] = useLocalStorage< Record >( @@ -158,7 +193,43 @@ export const ConnectionTable = (props: Props) => { {}, ); - const columns = useMemo(() => { + const [columnVisibilityModel, setColumnVisibilityModel] = useLocalStorage< + Partial> + >( + "connection-table-visibility", + {}, + { + serializer: JSON.stringify, + deserializer: (value) => { + try { + const parsed = JSON.parse(value); + if (parsed && typeof parsed === "object") return parsed; + } catch (err) { + console.warn("Failed to parse connection-table-visibility", err); + } + return {}; + }, + }, + ); + + const [columnOrder, setColumnOrder] = useLocalStorage( + "connection-table-order", + [], + { + serializer: JSON.stringify, + deserializer: (value) => { + try { + const parsed = JSON.parse(value); + if (Array.isArray(parsed)) return parsed; + } catch (err) { + console.warn("Failed to parse connection-table-order", err); + } + return []; + }, + }, + ); + + const baseColumns = useMemo(() => { return [ { field: "host", @@ -248,6 +319,49 @@ export const ConnectionTable = (props: Props) => { ]; }, [columnWidths, t]); + useEffect(() => { + setColumnOrder((prevValue) => { + const baseFields = baseColumns.map((col) => col.field); + const prev = Array.isArray(prevValue) ? prevValue : []; + const reconciled = reconcileColumnOrder(prev, baseFields); + if ( + reconciled.length === prev.length && + reconciled.every((field, i) => field === prev[i]) + ) { + return prevValue; + } + return reconciled; + }); + }, [baseColumns, setColumnOrder]); + + const columns = useMemo(() => { + const order = Array.isArray(columnOrder) ? columnOrder : []; + const orderMap = new Map(order.map((field, index) => [field, index])); + + return [...baseColumns].sort((a, b) => { + const aIndex = orderMap.has(a.field) + ? (orderMap.get(a.field) as number) + : Number.MAX_SAFE_INTEGER; + const bIndex = orderMap.has(b.field) + ? (orderMap.get(b.field) as number) + : Number.MAX_SAFE_INTEGER; + + if (aIndex === bIndex) { + return order.indexOf(a.field) - order.indexOf(b.field); + } + + return aIndex - bIndex; + }); + }, [baseColumns, columnOrder]); + + const visibleColumnsCount = useMemo(() => { + return columns.reduce((count, column) => { + return (columnVisibilityModel?.[column.field] ?? true) !== false + ? count + 1 + : count; + }, 0); + }, [columns, columnVisibilityModel]); + const handleColumnResize = (params: GridColumnResizeParams) => { const { colDef, width } = params; setColumnWidths((prev) => ({ @@ -256,6 +370,111 @@ export const ConnectionTable = (props: Props) => { })); }; + const handleColumnVisibilityChange = useCallback( + (model: GridColumnVisibilityModel) => { + const hiddenFields = new Set(); + Object.entries(model).forEach(([field, value]) => { + if (value === false) { + hiddenFields.add(field); + } + }); + + const nextVisibleCount = columns.reduce((count, column) => { + return hiddenFields.has(column.field) ? count : count + 1; + }, 0); + + if (nextVisibleCount === 0) { + return; + } + + setColumnVisibilityModel(() => { + const sanitized: Partial> = {}; + hiddenFields.forEach((field) => { + sanitized[field] = false; + }); + return sanitized; + }); + }, + [columns, setColumnVisibilityModel], + ); + + const handleToggleColumn = useCallback( + (field: string, visible: boolean) => { + if (!visible && visibleColumnsCount <= 1) { + return; + } + + setColumnVisibilityModel((prev) => { + const next = { ...(prev ?? {}) }; + if (visible) { + delete next[field]; + } else { + next[field] = false; + } + return next; + }); + }, + [setColumnVisibilityModel, visibleColumnsCount], + ); + + const handleColumnOrderChange = useCallback( + (params: GridColumnOrderChangeParams) => { + setColumnOrder((prevValue) => { + const baseFields = baseColumns.map((col) => col.field); + const currentOrder = Array.isArray(prevValue) + ? [...prevValue] + : [...baseFields]; + const field = params.column.field; + const currentIndex = currentOrder.indexOf(field); + if (currentIndex === -1) return currentOrder; + + currentOrder.splice(currentIndex, 1); + const targetIndex = Math.min( + Math.max(params.targetIndex, 0), + currentOrder.length, + ); + currentOrder.splice(targetIndex, 0, field); + + return currentOrder; + }); + }, + [baseColumns, setColumnOrder], + ); + + const handleManagerOrderChange = useCallback( + (order: string[]) => { + setColumnOrder(() => { + const baseFields = baseColumns.map((col) => col.field); + return reconcileColumnOrder(order, baseFields); + }); + }, + [baseColumns, setColumnOrder], + ); + + const handleResetColumns = useCallback(() => { + setColumnVisibilityModel({}); + setColumnOrder(baseColumns.map((col) => col.field)); + }, [baseColumns, setColumnOrder, setColumnVisibilityModel]); + + const gridVisibilityModel = useMemo(() => { + const result: GridColumnVisibilityModel = {}; + if (!columnVisibilityModel) return result; + Object.entries(columnVisibilityModel).forEach(([field, value]) => { + if (typeof value === "boolean") { + result[field] = value; + } + }); + return result; + }, [columnVisibilityModel]); + + const columnOptions = useMemo(() => { + return columns.map((column) => ({ + field: column.field, + label: column.headerName ?? column.field, + visible: (columnVisibilityModel?.[column.field] ?? true) !== false, + })); + }, [columns, columnVisibilityModel]); + const connRows = useMemo(() => { return connections.map((each) => { const { metadata, rulePayload } = each; @@ -286,24 +505,98 @@ export const ConnectionTable = (props: Props) => { }, [connections]); return ( - onShowDetail(e.row.connectionData)} - density="compact" - sx={{ - border: "none", - "div:focus": { outline: "none !important" }, - "& .MuiDataGrid-columnHeader": { - userSelect: "none", - }, - }} - columnVisibilityModel={columnVisible} - onColumnVisibilityModelChange={(e) => setColumnVisible(e)} - onColumnResize={handleColumnResize} - disableColumnMenu={false} - /> + + + onShowDetail(e.row.connectionData)} + density="compact" + sx={{ + flex: 1, + border: "none", + minHeight: 0, + "div:focus": { outline: "none !important" }, + "& .MuiDataGrid-columnHeader": { + userSelect: "none", + }, + }} + columnVisibilityModel={gridVisibilityModel} + onColumnVisibilityModelChange={handleColumnVisibilityChange} + onColumnResize={handleColumnResize} + onColumnOrderChange={handleColumnOrderChange} + slotProps={{ + columnMenu: { + slots: { + columnMenuColumnsItem: ConnectionColumnMenuColumnsItem, + }, + }, + }} + /> + + + + ); +}; + +type ConnectionColumnMenuManageItemProps = GridColumnMenuItemProps & { + onOpenColumnManager: () => void; +}; + +const ConnectionColumnMenuManageItem = ( + props: ConnectionColumnMenuManageItemProps, +) => { + const { onClick, onOpenColumnManager } = props; + const rootProps = useGridRootProps(); + const { t } = useTranslation(); + const handleClick = useCallback( + (event: MouseEvent) => { + onClick(event); + onOpenColumnManager(); + }, + [onClick, onOpenColumnManager], + ); + + if (rootProps.disableColumnSelector) { + return null; + } + + const MenuItem = rootProps.slots.baseMenuItem; + const Icon = rootProps.slots.columnMenuManageColumnsIcon; + + return ( + }> + {t("connections.components.columnManager.title")} + + ); +}; + +const ConnectionColumnMenuColumnsItem = (props: GridColumnMenuItemProps) => { + const onOpenColumnManager = use(ColumnManagerContext); + + return ( + <> + + + ); }; diff --git a/src/locales/ar/connections.json b/src/locales/ar/connections.json index ad88bf120..be0db7d6e 100644 --- a/src/locales/ar/connections.json +++ b/src/locales/ar/connections.json @@ -23,6 +23,10 @@ }, "actions": { "closeConnection": "إغلاق الاتصال" + }, + "columnManager": { + "title": "الأعمدة", + "dragHandle": "Drag handle" } } } diff --git a/src/locales/de/connections.json b/src/locales/de/connections.json index 97af29ee3..65d032fcf 100644 --- a/src/locales/de/connections.json +++ b/src/locales/de/connections.json @@ -23,6 +23,10 @@ }, "actions": { "closeConnection": "Verbindung schließen" + }, + "columnManager": { + "title": "Spalten", + "dragHandle": "Drag handle" } } } diff --git a/src/locales/en/connections.json b/src/locales/en/connections.json index 4d5e53605..713a8d114 100644 --- a/src/locales/en/connections.json +++ b/src/locales/en/connections.json @@ -23,6 +23,10 @@ }, "actions": { "closeConnection": "Close Connection" + }, + "columnManager": { + "title": "Columns", + "dragHandle": "Drag handle" } } } diff --git a/src/locales/es/connections.json b/src/locales/es/connections.json index e40c6b526..d398d03fe 100644 --- a/src/locales/es/connections.json +++ b/src/locales/es/connections.json @@ -23,6 +23,10 @@ }, "actions": { "closeConnection": "Cerrar conexión" + }, + "columnManager": { + "title": "Columnas", + "dragHandle": "Drag handle" } } } diff --git a/src/locales/fa/connections.json b/src/locales/fa/connections.json index dfd891c2a..3f7d62f18 100644 --- a/src/locales/fa/connections.json +++ b/src/locales/fa/connections.json @@ -23,6 +23,10 @@ }, "actions": { "closeConnection": "بستن اتصال" + }, + "columnManager": { + "title": "ستون‌ها", + "dragHandle": "Drag handle" } } } diff --git a/src/locales/id/connections.json b/src/locales/id/connections.json index 1616eeeb0..6b515227a 100644 --- a/src/locales/id/connections.json +++ b/src/locales/id/connections.json @@ -23,6 +23,10 @@ }, "actions": { "closeConnection": "Tutup Koneksi" + }, + "columnManager": { + "title": "Kolom", + "dragHandle": "Drag handle" } } } diff --git a/src/locales/jp/connections.json b/src/locales/jp/connections.json index 806f4bb13..6cde1afbd 100644 --- a/src/locales/jp/connections.json +++ b/src/locales/jp/connections.json @@ -23,6 +23,10 @@ }, "actions": { "closeConnection": "接続を閉じる" + }, + "columnManager": { + "title": "列", + "dragHandle": "Drag handle" } } } diff --git a/src/locales/ko/connections.json b/src/locales/ko/connections.json index f277d252a..036fa5853 100644 --- a/src/locales/ko/connections.json +++ b/src/locales/ko/connections.json @@ -23,6 +23,10 @@ }, "actions": { "closeConnection": "연결 닫기" + }, + "columnManager": { + "title": "열", + "dragHandle": "Drag handle" } } } diff --git a/src/locales/ru/connections.json b/src/locales/ru/connections.json index 09d0caf92..24c706fbe 100644 --- a/src/locales/ru/connections.json +++ b/src/locales/ru/connections.json @@ -23,6 +23,10 @@ }, "actions": { "closeConnection": "Закрыть соединение" + }, + "columnManager": { + "title": "Столбцы", + "dragHandle": "Drag handle" } } } diff --git a/src/locales/tr/connections.json b/src/locales/tr/connections.json index e13e2bd18..040cae012 100644 --- a/src/locales/tr/connections.json +++ b/src/locales/tr/connections.json @@ -23,6 +23,10 @@ }, "actions": { "closeConnection": "Bağlantıyı Kapat" + }, + "columnManager": { + "title": "Sütunlar", + "dragHandle": "Drag handle" } } } diff --git a/src/locales/tt/connections.json b/src/locales/tt/connections.json index 847a07f1e..27a2476a9 100644 --- a/src/locales/tt/connections.json +++ b/src/locales/tt/connections.json @@ -23,6 +23,10 @@ }, "actions": { "closeConnection": "Тоташуны ябу" + }, + "columnManager": { + "title": "Баганалар", + "dragHandle": "Drag handle" } } } diff --git a/src/locales/zh/connections.json b/src/locales/zh/connections.json index 3b4a4ddd2..2e498493f 100644 --- a/src/locales/zh/connections.json +++ b/src/locales/zh/connections.json @@ -23,6 +23,10 @@ }, "actions": { "closeConnection": "关闭连接" + }, + "columnManager": { + "title": "列设置", + "dragHandle": "拖拽控件" } } } diff --git a/src/locales/zhtw/connections.json b/src/locales/zhtw/connections.json index 6da04e1a5..5acf73055 100644 --- a/src/locales/zhtw/connections.json +++ b/src/locales/zhtw/connections.json @@ -23,6 +23,10 @@ }, "actions": { "closeConnection": "關閉連線" + }, + "columnManager": { + "title": "欄位設定", + "dragHandle": "Drag handle" } } } diff --git a/src/pages/connections.tsx b/src/pages/connections.tsx index 5286b73e2..4d2dce6ae 100644 --- a/src/pages/connections.tsx +++ b/src/pages/connections.tsx @@ -86,6 +86,7 @@ const ConnectionsPage = () => { const [isPaused, setIsPaused] = useState(false); const [frozenData, setFrozenData] = useState(null); + const [isColumnManagerOpen, setIsColumnManagerOpen] = useState(false); // 使用全局连接数据 const displayData = useMemo(() => { @@ -110,14 +111,17 @@ const ConnectionsPage = () => { const [filterConn] = useMemo(() => { const orderFunc = orderFunctionMap[curOrderOpt]; - let conns = displayData.connections?.filter((conn) => { - const { host, destinationIP, process } = conn.metadata; - return ( - match(host || "") || match(destinationIP || "") || match(process || "") - ); - }); - - if (orderFunc) conns = orderFunc(conns ?? []); + let conns: IConnectionsItem[] = (displayData.connections ?? []).filter( + (conn) => { + const { host, destinationIP, process } = conn.metadata; + return ( + match(host || "") || + match(destinationIP || "") || + match(process || "") + ); + }, + ); + if (orderFunc) conns = orderFunc(conns); return [conns]; }, [displayData, match, curOrderOpt]); @@ -145,6 +149,8 @@ const ConnectionsPage = () => { }); }, [connections]); + const hasTableData = filterConn.length > 0; + return ( { pt: 1, mb: 0.5, mx: "10px", - height: "36px", + minHeight: "36px", display: "flex", alignItems: "center", + gap: 1, userSelect: "text", position: "sticky", top: 0, @@ -235,15 +242,29 @@ const ConnectionsPage = () => { ))} )} - + *": { + flex: 1, + }, + }} + > + + - {!filterConn || filterConn.length === 0 ? ( + {!hasTableData ? ( ) : isTableLayout ? ( detailRef.current?.open(detail)} + columnManagerOpen={isTableLayout && isColumnManagerOpen} + onOpenColumnManager={() => setIsColumnManagerOpen(true)} + onCloseColumnManager={() => setIsColumnManagerOpen(false)} /> ) : ( Date: Sun, 9 Nov 2025 00:21:03 +0800 Subject: [PATCH 02/49] docs: UPDATELOG.md --- UPDATELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/UPDATELOG.md b/UPDATELOG.md index c26123b1d..2a409c0e1 100644 --- a/UPDATELOG.md +++ b/UPDATELOG.md @@ -4,6 +4,9 @@
✨ 新增功能 + +- 支持连接页面各个项目的排序 +
From 8f080389feb08dee145b57c630f0503f69d2f16e Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Sun, 9 Nov 2025 01:09:10 +0800 Subject: [PATCH 03/49] feat: add Changelog.md and update scripts to reference it - Created a new Changelog.md file to document version updates and changes. - Updated the updatelog.mjs script to parse Changelog.md instead of UPDATELOG.md. - Modified updater-fixed-webview2.mjs to use the new Changelog.md for update notes. --- .github/workflows/alpha.yml | 8 +- .github/workflows/autobuild.yml | 16 +- .github/workflows/release.yml | 16 +- .prettierignore | 2 +- UPDATELOG.md => Changelog.histroy.md | 240 --------------------------- Changelog.md | 239 ++++++++++++++++++++++++++ scripts/updatelog.mjs | 12 +- scripts/updater-fixed-webview2.mjs | 4 +- 8 files changed, 268 insertions(+), 269 deletions(-) rename UPDATELOG.md => Changelog.histroy.md (85%) create mode 100644 Changelog.md diff --git a/.github/workflows/alpha.yml b/.github/workflows/alpha.yml index 8f9836989..a7221a367 100644 --- a/.github/workflows/alpha.yml +++ b/.github/workflows/alpha.yml @@ -185,18 +185,18 @@ jobs: - name: Fetch UPDATE logs id: fetch_update_logs run: | - if [ -f "UPDATELOG.md" ]; then - UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md) + if [ -f "Changelog.md" ]; then + UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' Changelog.md) if [ -n "$UPDATE_LOGS" ]; then echo "Found update logs" echo "UPDATE_LOGS<> $GITHUB_ENV echo "$UPDATE_LOGS" >> $GITHUB_ENV echo "EOF" >> $GITHUB_ENV else - echo "No update sections found in UPDATELOG.md" + echo "No update sections found in Changelog.md" fi else - echo "UPDATELOG.md file not found" + echo "Changelog.md file not found" fi shell: bash diff --git a/.github/workflows/autobuild.yml b/.github/workflows/autobuild.yml index 65178f415..e839dc855 100644 --- a/.github/workflows/autobuild.yml +++ b/.github/workflows/autobuild.yml @@ -36,18 +36,18 @@ jobs: - name: Fetch UPDATE logs id: fetch_update_logs run: | - if [ -f "UPDATELOG.md" ]; then - UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md) + if [ -f "Changelog.md" ]; then + UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' Changelog.md) if [ -n "$UPDATE_LOGS" ]; then echo "Found update logs" echo "UPDATE_LOGS<> $GITHUB_ENV echo "$UPDATE_LOGS" >> $GITHUB_ENV echo "EOF" >> $GITHUB_ENV else - echo "No update sections found in UPDATELOG.md" + echo "No update sections found in Changelog.md" fi else - echo "UPDATELOG.md file not found" + echo "Changelog.md file not found" fi shell: bash @@ -510,18 +510,18 @@ jobs: - name: Fetch UPDATE logs id: fetch_update_logs run: | - if [ -f "UPDATELOG.md" ]; then - UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md) + if [ -f "Changelog.md" ]; then + UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' Changelog.md) if [ -n "$UPDATE_LOGS" ]; then echo "Found update logs" echo "UPDATE_LOGS<> $GITHUB_ENV echo "$UPDATE_LOGS" >> $GITHUB_ENV echo "EOF" >> $GITHUB_ENV else - echo "No update sections found in UPDATELOG.md" + echo "No update sections found in Changelog.md" fi else - echo "UPDATELOG.md file not found" + echo "Changelog.md file not found" fi shell: bash diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0385c16c9..59504436b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -74,18 +74,18 @@ jobs: - name: Fetch UPDATE logs id: fetch_update_logs run: | - if [ -f "UPDATELOG.md" ]; then - UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md) + if [ -f "Changelog.md" ]; then + UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' Changelog.md) if [ -n "$UPDATE_LOGS" ]; then echo "Found update logs" echo "UPDATE_LOGS<> $GITHUB_ENV echo "$UPDATE_LOGS" >> $GITHUB_ENV echo "EOF" >> $GITHUB_ENV else - echo "No update sections found in UPDATELOG.md" + echo "No update sections found in Changelog.md" fi else - echo "UPDATELOG.md file not found" + echo "Changelog.md file not found" fi shell: bash @@ -553,18 +553,18 @@ jobs: - name: Fetch UPDATE logs id: fetch_update_logs run: | - if [ -f "UPDATELOG.md" ]; then - UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md) + if [ -f "Changelog.md" ]; then + UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' Changelog.md) if [ -n "$UPDATE_LOGS" ]; then echo "Found update logs" echo "UPDATE_LOGS<> $GITHUB_ENV echo "$UPDATE_LOGS" >> $GITHUB_ENV echo "EOF" >> $GITHUB_ENV else - echo "No update sections found in UPDATELOG.md" + echo "No update sections found in Changelog.md" fi else - echo "UPDATELOG.md file not found" + echo "Changelog.md file not found" fi shell: bash diff --git a/.prettierignore b/.prettierignore index 0a3b69148..bc817cd59 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,5 +1,5 @@ # README.md -# UPDATELOG.md +# Changelog.md # CONTRIBUTING.md pnpm-lock.yaml diff --git a/UPDATELOG.md b/Changelog.histroy.md similarity index 85% rename from UPDATELOG.md rename to Changelog.histroy.md index 2a409c0e1..115ab2d91 100644 --- a/UPDATELOG.md +++ b/Changelog.histroy.md @@ -1,243 +1,3 @@ -# v2.4.4 - -### 🐞 修复问题 - -
- ✨ 新增功能 - -- 支持连接页面各个项目的排序 - -
- -
- 🚀 优化改进 - -- 替换前端信息编辑组件,提供更好性能 - -
- -## v2.4.3 - -**发行代号:澜** -代号释义:澜象征平稳与融合,本次版本聚焦稳定性、兼容性、性能与体验优化,全面提升整体可靠性。 - -特别感谢 @Slinetrac, @oomeow, @Lythrilla, @Dragon1573 的出色贡献 - -### 🐞 修复问题 - -- 优化服务模式重装逻辑,避免不必要的重复检查 -- 修复轻量模式退出无响应的问题 -- 修复托盘轻量模式支持退出/进入 -- 修复静默启动和自动进入轻量模式时,托盘状态刷新不再依赖窗口创建流程 -- macOS Tun/系统代理 模式下图标大小不统一 -- 托盘节点切换不再显示隐藏组 -- 修复前端 IP 检测无法使用 ipapi, ipsb 提供商 -- 修复MacOS 下 Tun开启后 系统代理无法打开的问题 -- 修复服务模式启动时,修改、生成配置文件或重启内核可能导致页面卡死的问题 -- 修复 Webdav 恢复备份不重启 -- 修复 Linux 开机后无法正常代理需要手动设置 -- 修复增加订阅或导入订阅文件时订阅页面无更新 -- 修复系统代理守卫功能不工作 -- 修复 KDE + Wayland 下多屏显示 UI 异常 -- 修复 Windows 深色模式下首次启动客户端标题栏颜色异常 -- 修复静默启动不加载完整 WebView 的问题 -- 修复 Linux WebKit 网络进程的崩溃 -- 修复无法导入订阅 -- 修复实际导入成功但显示导入失败的问题 -- 修复服务不可用时,自动关闭 Tun 模式导致应用卡死问题 -- 修复删除订阅时未能实际删除相关文件 -- 修复 macOS 连接界面显示异常 -- 修复规则配置项在不同配置文件间全局共享导致切换被重置的问题 -- 修复 Linux Wayland 下部分 GPU 可能出现的 UI 渲染问题 -- 修复自动更新使版本回退的问题 -- 修复首页自定义卡片在切换轻量模式时失效 -- 修复悬浮跳转导航失效 -- 修复小键盘热键映射错误 -- 修复前端无法及时刷新操作状态 -- 修复 macOS 从 Dock 栏退出轻量模式状态不同步 -- 修复 Linux 系统主题切换不生效 -- 修复 `允许自动更新` 字段使手动订阅刷新失效 -- 修复轻量模式托盘状态不同步 -- 修复一键导入订阅导致应用卡死崩溃的问题 - -
- ✨ 新增功能 - -- **Mihomo(Meta) 内核升级至 v1.19.15** -- 支持前端修改日志(最大文件大小、最大保留数量) -- 新增链式代理图形化设置功能 -- 新增系统标题栏与程序标题栏切换 (设置-页面设置-倾向系统标题栏) -- 监听关机事件,自动关闭系统代理 -- 主界面“当前节点”卡片新增“延迟测试”按钮 -- 新增批量选择配置文件功能 -- Windows / Linux / MacOS 监听关机信号,优雅恢复网络设置 -- 新增本地备份功能 -- 主界面“当前节点”卡片新增自动延迟检测开关(默认关闭) -- 允许独立控制订阅自动更新 -- 托盘 `更多` 中新增 `关闭所有连接` 按钮 -- 新增左侧菜单栏的排序功能(右键点击左侧菜单栏) -- 托盘 `打开目录` 中新增 `应用日志` 和 `内核日志` -
- -
- 🚀 优化改进 - -- 重构并简化服务模式启动检测流程,消除重复检测 -- 重构并简化窗口创建流程 -- 重构日志系统,单个日志默认最大 10 MB -- 优化前端资源占用 -- 改进 macos 下系统代理设置的方法 -- 优化 TUN 模式可用性的判断 -- 移除流媒体检测的系统级提示(使用软件内通知) -- 优化后端 i18n 资源占用 -- 改进 Linux 托盘支持并添加 `--no-tray` 选项 -- Linux 现在在新生成的配置中默认将 TUN 栈恢复为 mixed 模式 -- 为代理延迟测试的 URL 设置增加了保护以及添加了安全的备用 URL -- 更新了 Wayland 合成器检测逻辑,从而在 Hyprland 会话中保留原生 Wayland 后端 -- 改进 Windows 和 Unix 的 服务连接方式以及权限,避免无法连接服务或内核 -- 修改内核默认日志级别为 Info -- 支持通过桌面快捷方式重新打开应用 -- 支持订阅界面输入链接后回车导入 -- 选择按延迟排序时每次延迟测试自动刷新节点顺序 -- 配置重载失败时自动重启核心 -- 启用 TUN 前等待服务就绪 -- 卸载 TUN 时会先关闭 -- 优化应用启动页 -- 优化首页当前节点对MATCH规则的支持 -- 允许在 `界面设置` 修改 `悬浮跳转导航延迟` -- 添加热键绑定错误的提示信息 -- 在 macOS 10.15 及更高版本默认包含 Mihomo-go122,以解决 Intel 架构 Mac 无法运行内核的问题 -- Tun 模式不可用时,禁用系统托盘的 Tun 模式菜单 -- 改进订阅更新方式,仍失败需打开订阅设置 `允许危险证书` -- 允许设置 Mihomo 端口范围 1000(含) - 65536(含) - -
- -## v2.4.2 - -### ✨ 新增功能 - -- 增加托盘节点选择 - -### 🚀 性能优化 - -- 优化前端首页加载速度 -- 优化前端未使用 i18n 文件缓存 -- 优化后端内存占用 -- 优化后端启动速度 - -### 🐞 修复问题 - -- 修复首页节点切换失效的问题 -- 修复和优化服务检查流程 -- 修复2.4.1引入的订阅地址重定向报错问题 -- 修复 rpm/deb 包名称问题 -- 修复托盘轻量模式状态检测异常 -- 修复通过 scheme 导入订阅崩溃 -- 修复单例检测实效 -- 修复启动阶段可能导致的无法连接内核 -- 修复导入订阅无法 Auth Basic - -### 👙 界面样式 - -- 简化和改进代理设置样式 - -## v2.4.1 - -### 🏆 重大改进 - -- **应用响应速度提升**:采用全新异步处理架构,大幅提升应用响应速度和稳定性 - -### ✨ 新增功能 - -- **Mihomo(Meta) 内核升级至 v1.19.13** - -### 🚀 性能优化 - -- 优化热键响应速度,提升快捷键操作体验 -- 改进服务管理响应性,减少系统服务操作等待时间 -- 提升文件和配置处理性能 -- 优化任务管理和日志记录效率 -- 优化异步内存管理,减少内存占用并提升多任务处理效率 -- 优化启动阶段初始化性能 - -### 🐞 修复问题 - -- 修复应用在某些操作中可能出现的响应延迟问题 -- 修复任务管理中的潜在并发问题 -- 修复通过托盘重启应用无法恢复 -- 修复订阅在某些情况下无法导入 -- 修复无法新建订阅时使用远程链接 -- 修复卸载服务后的 tun 开关状态问题 -- 修复页面快速切换订阅时导致崩溃 -- 修复丢失工作目录时无法恢复环境 -- 修复从轻量模式恢复导致崩溃 - -### 👙 界面样式 - -- 统一代理设置样式 - -### 🗑️ 移除内容 - -- 移除启动阶段自动清理过期订阅 - -## v2.4.0 - -**发行代号:融** -代号释义: 「融」象征融合与贯通,寓意新版本通过全新 IPC 通信机制 将系统各部分紧密衔接,打破壁垒,实现更高效的 数据流通与全面性能优化。 - -### 🏆 重大改进 - -- **核心通信架构升级**:采用全新通信机制,提升应用性能和稳定性 -- **流量监控系统重构**:全新的流量监控界面,支持更丰富的数据展示 -- **数据缓存优化**:改进配置和节点数据缓存,提升响应速度 - -### ✨ 新增功能 - -- **Mihomo(Meta) 内核升级至 v1.19.12** -- 新增版本信息复制按钮 -- 增强型流量监控,支持更详细的数据分析 -- 新增流量图表多种显示模式 -- 新增强制刷新配置和节点缓存功能 -- 首页流量统计支持查看刻度线详情 - -### 🚀 性能优化 - -- 全面提升数据传输和处理效率 -- 优化内存使用,减少系统资源消耗 -- 改进流量图表渲染性能 -- 优化配置和节点刷新策略,从5秒延长到60秒 -- 改进数据缓存机制,减少重复请求 -- 优化异步程序性能 - -### 🐞 修复问题 - -- 修复系统代理状态检测和显示不一致问题 -- 修复系统主题窗口颜色不一致问题 -- 修复特殊字符 URL 处理问题 -- 修复配置修改后缓存不同步问题 -- 修复 Windows 安装器自启设置问题 -- 修复 macOS 下 Dock 图标恢复窗口问题 -- 修复 linux 下 KDE/Plasma 异常标题栏按钮 -- 修复架构升级后节点测速功能异常 -- 修复架构升级后流量统计功能异常 -- 修复架构升级后日志功能异常 -- 修复外部控制器跨域配置保存问题 -- 修复首页端口显示不一致问题 -- 修复首页流量统计刻度线显示问题 -- 修复日志页面按钮功能混淆问题 -- 修复日志等级设置保存问题 -- 修复日志等级异常过滤 -- 修复清理日志天数功能异常 -- 修复偶发性启动卡死问题 -- 修复首页虚拟网卡开关在管理模式下的状态问题 - -### 🔧 技术改进 - -- 统一使用新的内核通信方式 -- 新增外部控制器配置界面 -- 改进跨平台兼容性支持 - ## v2.3.2 ### 🐞 修复问题 diff --git a/Changelog.md b/Changelog.md new file mode 100644 index 000000000..e42486059 --- /dev/null +++ b/Changelog.md @@ -0,0 +1,239 @@ +# v2.4.4 + +### 🐞 修复问题 + +
+ ✨ 新增功能 + +- 支持连接页面各个项目的排序 + +
+ +
+ 🚀 优化改进 + +- 替换前端信息编辑组件,提供更好性能 + +
+ +## v2.4.3 + +**发行代号:澜** +代号释义:澜象征平稳与融合,本次版本聚焦稳定性、兼容性、性能与体验优化,全面提升整体可靠性。 + +特别感谢 @Slinetrac, @oomeow, @Lythrilla, @Dragon1573 的出色贡献 + +### 🐞 修复问题 + +- 优化服务模式重装逻辑,避免不必要的重复检查 +- 修复轻量模式退出无响应的问题 +- 修复托盘轻量模式支持退出/进入 +- 修复静默启动和自动进入轻量模式时,托盘状态刷新不再依赖窗口创建流程 +- macOS Tun/系统代理 模式下图标大小不统一 +- 托盘节点切换不再显示隐藏组 +- 修复前端 IP 检测无法使用 ipapi, ipsb 提供商 +- 修复MacOS 下 Tun开启后 系统代理无法打开的问题 +- 修复服务模式启动时,修改、生成配置文件或重启内核可能导致页面卡死的问题 +- 修复 Webdav 恢复备份不重启 +- 修复 Linux 开机后无法正常代理需要手动设置 +- 修复增加订阅或导入订阅文件时订阅页面无更新 +- 修复系统代理守卫功能不工作 +- 修复 KDE + Wayland 下多屏显示 UI 异常 +- 修复 Windows 深色模式下首次启动客户端标题栏颜色异常 +- 修复静默启动不加载完整 WebView 的问题 +- 修复 Linux WebKit 网络进程的崩溃 +- 修复无法导入订阅 +- 修复实际导入成功但显示导入失败的问题 +- 修复服务不可用时,自动关闭 Tun 模式导致应用卡死问题 +- 修复删除订阅时未能实际删除相关文件 +- 修复 macOS 连接界面显示异常 +- 修复规则配置项在不同配置文件间全局共享导致切换被重置的问题 +- 修复 Linux Wayland 下部分 GPU 可能出现的 UI 渲染问题 +- 修复自动更新使版本回退的问题 +- 修复首页自定义卡片在切换轻量模式时失效 +- 修复悬浮跳转导航失效 +- 修复小键盘热键映射错误 +- 修复前端无法及时刷新操作状态 +- 修复 macOS 从 Dock 栏退出轻量模式状态不同步 +- 修复 Linux 系统主题切换不生效 +- 修复 `允许自动更新` 字段使手动订阅刷新失效 +- 修复轻量模式托盘状态不同步 +- 修复一键导入订阅导致应用卡死崩溃的问题 + +
+ ✨ 新增功能 + +- **Mihomo(Meta) 内核升级至 v1.19.15** +- 支持前端修改日志(最大文件大小、最大保留数量) +- 新增链式代理图形化设置功能 +- 新增系统标题栏与程序标题栏切换 (设置-页面设置-倾向系统标题栏) +- 监听关机事件,自动关闭系统代理 +- 主界面“当前节点”卡片新增“延迟测试”按钮 +- 新增批量选择配置文件功能 +- Windows / Linux / MacOS 监听关机信号,优雅恢复网络设置 +- 新增本地备份功能 +- 主界面“当前节点”卡片新增自动延迟检测开关(默认关闭) +- 允许独立控制订阅自动更新 +- 托盘 `更多` 中新增 `关闭所有连接` 按钮 +- 新增左侧菜单栏的排序功能(右键点击左侧菜单栏) +- 托盘 `打开目录` 中新增 `应用日志` 和 `内核日志` +
+ +
+ 🚀 优化改进 + +- 重构并简化服务模式启动检测流程,消除重复检测 +- 重构并简化窗口创建流程 +- 重构日志系统,单个日志默认最大 10 MB +- 优化前端资源占用 +- 改进 macos 下系统代理设置的方法 +- 优化 TUN 模式可用性的判断 +- 移除流媒体检测的系统级提示(使用软件内通知) +- 优化后端 i18n 资源占用 +- 改进 Linux 托盘支持并添加 `--no-tray` 选项 +- Linux 现在在新生成的配置中默认将 TUN 栈恢复为 mixed 模式 +- 为代理延迟测试的 URL 设置增加了保护以及添加了安全的备用 URL +- 更新了 Wayland 合成器检测逻辑,从而在 Hyprland 会话中保留原生 Wayland 后端 +- 改进 Windows 和 Unix 的 服务连接方式以及权限,避免无法连接服务或内核 +- 修改内核默认日志级别为 Info +- 支持通过桌面快捷方式重新打开应用 +- 支持订阅界面输入链接后回车导入 +- 选择按延迟排序时每次延迟测试自动刷新节点顺序 +- 配置重载失败时自动重启核心 +- 启用 TUN 前等待服务就绪 +- 卸载 TUN 时会先关闭 +- 优化应用启动页 +- 优化首页当前节点对MATCH规则的支持 +- 允许在 `界面设置` 修改 `悬浮跳转导航延迟` +- 添加热键绑定错误的提示信息 +- 在 macOS 10.15 及更高版本默认包含 Mihomo-go122,以解决 Intel 架构 Mac 无法运行内核的问题 +- Tun 模式不可用时,禁用系统托盘的 Tun 模式菜单 +- 改进订阅更新方式,仍失败需打开订阅设置 `允许危险证书` +- 允许设置 Mihomo 端口范围 1000(含) - 65536(含) + +
+ +## v2.4.2 + +### ✨ 新增功能 + +- 增加托盘节点选择 + +### 🚀 性能优化 + +- 优化前端首页加载速度 +- 优化前端未使用 i18n 文件缓存 +- 优化后端内存占用 +- 优化后端启动速度 + +### 🐞 修复问题 + +- 修复首页节点切换失效的问题 +- 修复和优化服务检查流程 +- 修复2.4.1引入的订阅地址重定向报错问题 +- 修复 rpm/deb 包名称问题 +- 修复托盘轻量模式状态检测异常 +- 修复通过 scheme 导入订阅崩溃 +- 修复单例检测实效 +- 修复启动阶段可能导致的无法连接内核 +- 修复导入订阅无法 Auth Basic + +### 👙 界面样式 + +- 简化和改进代理设置样式 + +## v2.4.1 + +### 🏆 重大改进 + +- **应用响应速度提升**:采用全新异步处理架构,大幅提升应用响应速度和稳定性 + +### ✨ 新增功能 + +- **Mihomo(Meta) 内核升级至 v1.19.13** + +### 🚀 性能优化 + +- 优化热键响应速度,提升快捷键操作体验 +- 改进服务管理响应性,减少系统服务操作等待时间 +- 提升文件和配置处理性能 +- 优化任务管理和日志记录效率 +- 优化异步内存管理,减少内存占用并提升多任务处理效率 +- 优化启动阶段初始化性能 + +### 🐞 修复问题 + +- 修复应用在某些操作中可能出现的响应延迟问题 +- 修复任务管理中的潜在并发问题 +- 修复通过托盘重启应用无法恢复 +- 修复订阅在某些情况下无法导入 +- 修复无法新建订阅时使用远程链接 +- 修复卸载服务后的 tun 开关状态问题 +- 修复页面快速切换订阅时导致崩溃 +- 修复丢失工作目录时无法恢复环境 +- 修复从轻量模式恢复导致崩溃 + +### 👙 界面样式 + +- 统一代理设置样式 + +### 🗑️ 移除内容 + +- 移除启动阶段自动清理过期订阅 + +## v2.4.0 + +**发行代号:融** +代号释义: 「融」象征融合与贯通,寓意新版本通过全新 IPC 通信机制 将系统各部分紧密衔接,打破壁垒,实现更高效的 数据流通与全面性能优化。 + +### 🏆 重大改进 + +- **核心通信架构升级**:采用全新通信机制,提升应用性能和稳定性 +- **流量监控系统重构**:全新的流量监控界面,支持更丰富的数据展示 +- **数据缓存优化**:改进配置和节点数据缓存,提升响应速度 + +### ✨ 新增功能 + +- **Mihomo(Meta) 内核升级至 v1.19.12** +- 新增版本信息复制按钮 +- 增强型流量监控,支持更详细的数据分析 +- 新增流量图表多种显示模式 +- 新增强制刷新配置和节点缓存功能 +- 首页流量统计支持查看刻度线详情 + +### 🚀 性能优化 + +- 全面提升数据传输和处理效率 +- 优化内存使用,减少系统资源消耗 +- 改进流量图表渲染性能 +- 优化配置和节点刷新策略,从5秒延长到60秒 +- 改进数据缓存机制,减少重复请求 +- 优化异步程序性能 + +### 🐞 修复问题 + +- 修复系统代理状态检测和显示不一致问题 +- 修复系统主题窗口颜色不一致问题 +- 修复特殊字符 URL 处理问题 +- 修复配置修改后缓存不同步问题 +- 修复 Windows 安装器自启设置问题 +- 修复 macOS 下 Dock 图标恢复窗口问题 +- 修复 linux 下 KDE/Plasma 异常标题栏按钮 +- 修复架构升级后节点测速功能异常 +- 修复架构升级后流量统计功能异常 +- 修复架构升级后日志功能异常 +- 修复外部控制器跨域配置保存问题 +- 修复首页端口显示不一致问题 +- 修复首页流量统计刻度线显示问题 +- 修复日志页面按钮功能混淆问题 +- 修复日志等级设置保存问题 +- 修复日志等级异常过滤 +- 修复清理日志天数功能异常 +- 修复偶发性启动卡死问题 +- 修复首页虚拟网卡开关在管理模式下的状态问题 + +### 🔧 技术改进 + +- 统一使用新的内核通信方式 +- 新增外部控制器配置界面 +- 改进跨平台兼容性支持 diff --git a/scripts/updatelog.mjs b/scripts/updatelog.mjs index b00d35087..89faeabd7 100644 --- a/scripts/updatelog.mjs +++ b/scripts/updatelog.mjs @@ -2,9 +2,9 @@ import fs from "fs"; import fsp from "fs/promises"; import path from "path"; -const UPDATE_LOG = "UPDATELOG.md"; +const UPDATE_LOG = "Changelog.md"; -// parse the UPDATELOG.md +// parse the Changelog.md export async function resolveUpdateLog(tag) { const cwd = process.cwd(); @@ -14,7 +14,7 @@ export async function resolveUpdateLog(tag) { const file = path.join(cwd, UPDATE_LOG); if (!fs.existsSync(file)) { - throw new Error("could not found UPDATELOG.md"); + throw new Error("could not found Changelog.md"); } const data = await fsp.readFile(file, "utf-8"); @@ -38,7 +38,7 @@ export async function resolveUpdateLog(tag) { }); if (!map[tag]) { - throw new Error(`could not found "${tag}" in UPDATELOG.md`); + throw new Error(`could not found "${tag}" in Changelog.md`); } return map[tag].join("\n").trim(); @@ -49,7 +49,7 @@ export async function resolveUpdateLogDefault() { const file = path.join(cwd, UPDATE_LOG); if (!fs.existsSync(file)) { - throw new Error("could not found UPDATELOG.md"); + throw new Error("could not found Changelog.md"); } const data = await fsp.readFile(file, "utf-8"); @@ -77,7 +77,7 @@ export async function resolveUpdateLogDefault() { } if (!firstTag) { - throw new Error("could not found any version tag in UPDATELOG.md"); + throw new Error("could not found any version tag in Changelog.md"); } return content.join("\n").trim(); diff --git a/scripts/updater-fixed-webview2.mjs b/scripts/updater-fixed-webview2.mjs index 083c9b21f..33389c634 100644 --- a/scripts/updater-fixed-webview2.mjs +++ b/scripts/updater-fixed-webview2.mjs @@ -1,4 +1,4 @@ -import { getOctokit, context } from "@actions/github"; +import { context, getOctokit } from "@actions/github"; import fetch from "node-fetch"; import { resolveUpdateLog } from "./updatelog.mjs"; @@ -36,7 +36,7 @@ async function resolveUpdater() { const updateData = { name: tag.name, - notes: await resolveUpdateLog(tag.name), // use updatelog.md + notes: await resolveUpdateLog(tag.name), // use Changelog.md pub_date: new Date().toISOString(), platforms: { "windows-x86_64": { signature: "", url: "" }, From a57ffba8ba46041fab1770764d8be26614f52e63 Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Sun, 9 Nov 2025 02:23:35 +0800 Subject: [PATCH 04/49] refactor: replace RwLock with AtomicBool for is_exiting and emergency_mode in Handle and NotificationSystem --- src-tauri/src/core/handle.rs | 26 ++++++++++++++++---------- src-tauri/src/core/notification.rs | 12 ++++++------ 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src-tauri/src/core/handle.rs b/src-tauri/src/core/handle.rs index 52d700e9d..894688f96 100644 --- a/src-tauri/src/core/handle.rs +++ b/src-tauri/src/core/handle.rs @@ -1,27 +1,33 @@ use crate::{APP_HANDLE, constants::timing, singleton}; use parking_lot::RwLock; use smartstring::alias::String; -use std::{sync::Arc, thread}; +use std::{ + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, + thread, +}; use tauri::{AppHandle, Manager, WebviewWindow}; use tauri_plugin_mihomo::{Mihomo, MihomoExt}; use tokio::sync::RwLockReadGuard; use super::notification::{ErrorMessage, FrontendEvent, NotificationSystem}; -#[derive(Debug, Clone)] +#[derive(Debug)] pub struct Handle { - is_exiting: Arc>, + is_exiting: AtomicBool, startup_errors: Arc>>, - startup_completed: Arc>, + startup_completed: AtomicBool, pub(crate) notification_system: Arc>>, } impl Default for Handle { fn default() -> Self { Self { - is_exiting: Arc::new(RwLock::new(false)), + is_exiting: AtomicBool::new(false), startup_errors: Arc::new(RwLock::new(Vec::new())), - startup_completed: Arc::new(RwLock::new(false)), + startup_completed: AtomicBool::new(false), notification_system: Arc::new(RwLock::new(Some(NotificationSystem::new()))), } } @@ -108,7 +114,7 @@ impl Handle { let status_str = status.into(); let msg_str = msg.into(); - if !*handle.startup_completed.read() { + if !handle.startup_completed.load(Ordering::Acquire) { handle.startup_errors.write().push(ErrorMessage { status: status_str, message: msg_str, @@ -139,7 +145,7 @@ impl Handle { } pub fn mark_startup_completed(&self) { - *self.startup_completed.write() = true; + self.startup_completed.store(true, Ordering::Release); self.send_startup_errors(); } @@ -182,7 +188,7 @@ impl Handle { } pub fn set_is_exiting(&self) { - *self.is_exiting.write() = true; + self.is_exiting.store(true, Ordering::Release); let mut system_opt = self.notification_system.write(); if let Some(system) = system_opt.as_mut() { @@ -191,7 +197,7 @@ impl Handle { } pub fn is_exiting(&self) -> bool { - *self.is_exiting.read() + self.is_exiting.load(Ordering::Acquire) } } diff --git a/src-tauri/src/core/notification.rs b/src-tauri/src/core/notification.rs index eaee59322..83680c249 100644 --- a/src-tauri/src/core/notification.rs +++ b/src-tauri/src/core/notification.rs @@ -8,7 +8,7 @@ use parking_lot::RwLock; use smartstring::alias::String; use std::{ sync::{ - atomic::{AtomicU64, Ordering}, + atomic::{AtomicBool, AtomicU64, Ordering}, mpsc, }, thread, @@ -47,7 +47,7 @@ pub struct NotificationSystem { worker_handle: Option>, pub(super) is_running: bool, stats: EventStats, - emergency_mode: RwLock, + emergency_mode: AtomicBool, } impl Default for NotificationSystem { @@ -63,7 +63,7 @@ impl NotificationSystem { worker_handle: None, is_running: false, stats: EventStats::default(), - emergency_mode: RwLock::new(false), + emergency_mode: AtomicBool::new(false), } } @@ -125,7 +125,7 @@ impl NotificationSystem { } fn should_skip_event(&self, event: &FrontendEvent) -> bool { - let is_emergency = *self.emergency_mode.read(); + let is_emergency = self.emergency_mode.load(Ordering::Acquire); matches!( (is_emergency, event), (true, FrontendEvent::NoticeMessage { status, .. }) if status == "info" @@ -184,14 +184,14 @@ impl NotificationSystem { *self.stats.last_error_time.write() = Some(Instant::now()); let errors = self.stats.total_errors.load(Ordering::Relaxed); - if errors > retry::EVENT_EMIT_THRESHOLD && !*self.emergency_mode.read() { + if errors > retry::EVENT_EMIT_THRESHOLD && !self.emergency_mode.load(Ordering::Acquire) { logging!( warn, Type::Frontend, "Entering emergency mode after {} errors", errors ); - *self.emergency_mode.write() = true; + self.emergency_mode.store(true, Ordering::Release); } } From 243f5b1b962f8e46035c5415d352c20b76b49a5c Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Sun, 9 Nov 2025 02:29:50 +0800 Subject: [PATCH 05/49] refactor: replace OnceCell with AtomicBool for UI readiness state --- src-tauri/src/utils/resolve/ui.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src-tauri/src/utils/resolve/ui.rs b/src-tauri/src/utils/resolve/ui.rs index 55d5d8528..a7658528a 100644 --- a/src-tauri/src/utils/resolve/ui.rs +++ b/src-tauri/src/utils/resolve/ui.rs @@ -8,8 +8,8 @@ use tokio::sync::Notify; use crate::{logging, utils::logging::Type}; -// 使用 AtomicBool 替代 RwLock,性能更好且无锁 -static UI_READY: OnceCell = OnceCell::new(); +// 获取 UI 是否准备就绪的全局状态 +static UI_READY: AtomicBool = AtomicBool::new(false); // 获取UI就绪状态细节 static UI_READY_STATE: OnceCell = OnceCell::new(); // 添加通知机制,用于事件驱动的 UI 就绪检测 @@ -39,8 +39,8 @@ impl Default for UiReadyState { } } -pub(super) fn get_ui_ready() -> &'static AtomicBool { - UI_READY.get_or_init(|| AtomicBool::new(false)) +pub fn get_ui_ready() -> &'static AtomicBool { + &UI_READY } fn get_ui_ready_state() -> &'static UiReadyState { From 9c5cda793d90df7f2a6b81f685bee34b12f6f232 Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Sun, 9 Nov 2025 02:47:48 +0800 Subject: [PATCH 06/49] refactor: streamline UI readiness handling by replacing RwLock with AtomicU8 and updating related functions --- src-tauri/src/cmd/app.rs | 29 +++++----------------------- src-tauri/src/utils/resolve/ui.rs | 32 +++++++++---------------------- 2 files changed, 14 insertions(+), 47 deletions(-) diff --git a/src-tauri/src/cmd/app.rs b/src-tauri/src/cmd/app.rs index 959277feb..5fcc5566a 100644 --- a/src-tauri/src/cmd/app.rs +++ b/src-tauri/src/cmd/app.rs @@ -1,5 +1,6 @@ use super::CmdResult; use crate::core::sysopt::Sysopt; +use crate::utils::resolve::ui::{self, UiReadyStage}; use crate::{ cmd::StringifyErr, feat, logging, @@ -242,34 +243,14 @@ pub async fn copy_icon_file(path: String, icon_info: IconInfo) -> CmdResult CmdResult<()> { logging!(info, Type::Cmd, "前端UI已准备就绪"); - crate::utils::resolve::ui::mark_ui_ready(); + ui::mark_ui_ready(); Ok(()) } /// UI加载阶段 #[tauri::command] -pub fn update_ui_stage(stage: String) -> CmdResult<()> { - logging!(info, Type::Cmd, "UI加载阶段更新: {}", stage.as_str()); - - use crate::utils::resolve::ui::UiReadyStage; - - let stage_enum = match stage.as_str() { - "NotStarted" => UiReadyStage::NotStarted, - "Loading" => UiReadyStage::Loading, - "DomReady" => UiReadyStage::DomReady, - "ResourcesLoaded" => UiReadyStage::ResourcesLoaded, - "Ready" => UiReadyStage::Ready, - _ => { - logging!( - warn, - Type::Cmd, - "Warning: 未知的UI加载阶段: {}", - stage.as_str() - ); - return Err(format!("未知的UI加载阶段: {}", stage.as_str()).into()); - } - }; - - crate::utils::resolve::ui::update_ui_ready_stage(stage_enum); +pub fn update_ui_stage(stage: UiReadyStage) -> CmdResult<()> { + logging!(info, Type::Cmd, "UI加载阶段更新: {:?}", &stage); + ui::update_ui_ready_stage(stage); Ok(()) } diff --git a/src-tauri/src/utils/resolve/ui.rs b/src-tauri/src/utils/resolve/ui.rs index a7658528a..d93fe9e6f 100644 --- a/src-tauri/src/utils/resolve/ui.rs +++ b/src-tauri/src/utils/resolve/ui.rs @@ -1,8 +1,8 @@ use once_cell::sync::OnceCell; -use parking_lot::RwLock; +use serde::{Deserialize, Serialize}; use std::sync::{ Arc, - atomic::{AtomicBool, Ordering}, + atomic::{AtomicBool, AtomicU8, Ordering}, }; use tokio::sync::Notify; @@ -11,40 +11,27 @@ use crate::{logging, utils::logging::Type}; // 获取 UI 是否准备就绪的全局状态 static UI_READY: AtomicBool = AtomicBool::new(false); // 获取UI就绪状态细节 -static UI_READY_STATE: OnceCell = OnceCell::new(); +static UI_READY_STATE: AtomicU8 = AtomicU8::new(0); // 添加通知机制,用于事件驱动的 UI 就绪检测 static UI_READY_NOTIFY: OnceCell> = OnceCell::new(); // UI就绪阶段状态枚举 -#[derive(Debug, Clone, Copy, PartialEq)] +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub enum UiReadyStage { - NotStarted, + NotStarted = 0, Loading, DomReady, ResourcesLoaded, Ready, } -// UI就绪详细状态 -#[derive(Debug)] -struct UiReadyState { - stage: RwLock, -} - -impl Default for UiReadyState { - fn default() -> Self { - Self { - stage: RwLock::new(UiReadyStage::NotStarted), - } - } -} - pub fn get_ui_ready() -> &'static AtomicBool { &UI_READY } -fn get_ui_ready_state() -> &'static UiReadyState { - UI_READY_STATE.get_or_init(UiReadyState::default) +fn get_ui_ready_state() -> &'static AtomicU8 { + &UI_READY_STATE } fn get_ui_ready_notify() -> &'static Arc { @@ -53,8 +40,7 @@ fn get_ui_ready_notify() -> &'static Arc { // 更新UI准备阶段 pub fn update_ui_ready_stage(stage: UiReadyStage) { - let state = get_ui_ready_state(); - *state.stage.write() = stage; + get_ui_ready_state().store(stage as u8, Ordering::Release); // 如果是最终阶段,标记UI完全就绪 if stage == UiReadyStage::Ready { mark_ui_ready(); From d3d47d448aedc1d82d61887de24380f2152d5b62 Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Sun, 9 Nov 2025 02:52:22 +0800 Subject: [PATCH 07/49] docs: update Changelog.md to include backend memory and performance optimizations --- Changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Changelog.md b/Changelog.md index e42486059..52886dace 100644 --- a/Changelog.md +++ b/Changelog.md @@ -13,6 +13,7 @@ 🚀 优化改进 - 替换前端信息编辑组件,提供更好性能 +- 优化后端内存和性能表现
From 5c323fc40f8cb50ab1572c911c31b03e31dcb42a Mon Sep 17 00:00:00 2001 From: Slinetrac Date: Sun, 9 Nov 2025 10:15:21 +0800 Subject: [PATCH 08/49] docs: move Changelog.history.md to docs --- Changelog.histroy.md => docs/Changelog.history.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Changelog.histroy.md => docs/Changelog.history.md (100%) diff --git a/Changelog.histroy.md b/docs/Changelog.history.md similarity index 100% rename from Changelog.histroy.md rename to docs/Changelog.history.md From 2f7b3bb9a037b4a5fb7e3fab3d5b4c016328a3c7 Mon Sep 17 00:00:00 2001 From: Slinetrac Date: Sun, 9 Nov 2025 10:51:21 +0800 Subject: [PATCH 09/49] chore(i18n): update zhtw translations Co-authored-by: LiMoon --- src-tauri/locales/zhtw.yml | 8 ++++---- src/locales/zhtw/connections.json | 4 ++-- src/locales/zhtw/home.json | 4 ++-- src/locales/zhtw/rules.json | 10 +++++----- src/locales/zhtw/settings.json | 26 +++++++++++++------------- 5 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src-tauri/locales/zhtw.yml b/src-tauri/locales/zhtw.yml index 040226b2e..8e2b0845b 100644 --- a/src-tauri/locales/zhtw.yml +++ b/src-tauri/locales/zhtw.yml @@ -10,8 +10,8 @@ notifications: title: 系統代理 body: 系統代理狀態已更新。 tunModeToggled: - title: TUN 模式 - body: TUN 模式狀態已更新。 + title: 虛擬網路介面卡模式 + body: 已更新虛擬網路介面卡模式狀態。 lightweightModeEntered: title: 輕量模式 body: 已進入輕量模式。 @@ -31,7 +31,7 @@ tray: profiles: 訂閱 proxies: 代理 systemProxy: 系統代理 - tunMode: TUN 模式 + tunMode: 虛擬網路介面卡模式 closeAllConnections: 關閉所有連線 lightweightMode: 輕量模式 copyEnv: 複製環境變數 @@ -48,5 +48,5 @@ tray: exit: 離開 tooltip: systemProxy: 系統代理 - tun: TUN + tun: 虛擬網路介面卡 profile: 訂閱 diff --git a/src/locales/zhtw/connections.json b/src/locales/zhtw/connections.json index 5acf73055..e2dca70c1 100644 --- a/src/locales/zhtw/connections.json +++ b/src/locales/zhtw/connections.json @@ -17,7 +17,7 @@ "type": "類型" }, "order": { - "default": "Default", + "default": "預設", "uploadSpeed": "上傳速度", "downloadSpeed": "下載速度" }, @@ -26,7 +26,7 @@ }, "columnManager": { "title": "欄位設定", - "dragHandle": "Drag handle" + "dragHandle": "拖曳以移動" } } } diff --git a/src/locales/zhtw/home.json b/src/locales/zhtw/home.json index 5853832ca..e4d82121e 100644 --- a/src/locales/zhtw/home.json +++ b/src/locales/zhtw/home.json @@ -85,7 +85,7 @@ "unknown": "未知" }, "errors": { - "load": "獲取 IP 資訊失敗" + "load": "取得 IP 資訊失敗" } }, "currentProxy": { @@ -116,7 +116,7 @@ "download": "下載" }, "patterns": { - "minutes": "{{time}} Minutes" + "minutes": "{{time}} 分" } }, "clashMode": { diff --git a/src/locales/zhtw/rules.json b/src/locales/zhtw/rules.json index 3eca5a4fd..c65bee98e 100644 --- a/src/locales/zhtw/rules.json +++ b/src/locales/zhtw/rules.json @@ -13,11 +13,11 @@ "feedback": { "notifications": { "provider": { - "updateSuccess": "{{name}} updated successfully", - "updateFailed": "Failed to update {{name}}: {{message}}", - "genericError": "Update failed: {{message}}", - "none": "No providers available to update", - "allUpdated": "All providers updated successfully" + "updateSuccess": "已成功更新 {{name}}", + "updateFailed": "更新 {{name}} 失敗:{{message}}", + "genericError": "更新失敗:{{message}}", + "none": "沒有可供更新的提供者", + "allUpdated": "所有提供者都已成功更新" } } }, diff --git a/src/locales/zhtw/settings.json b/src/locales/zhtw/settings.json index cfc6ab3fb..e5c1abcba 100644 --- a/src/locales/zhtw/settings.json +++ b/src/locales/zhtw/settings.json @@ -154,7 +154,7 @@ "tooltips": { "backupInfo": "支援本機或 WebDAV 方式備份配置檔案", "openConfDir": "如果軟體執行異常,!備份!並刪除此資料夾下的所有檔案,重新啟動軟體", - "liteMode": "關閉 GUI 介面,僅保留內核執行" + "liteMode": "關閉圖形介面,僅保留內核執行" }, "actions": { "copyVersion": "複製Verge版本號" @@ -192,10 +192,10 @@ "cssInjection": "CSS 注入" }, "actions": { - "editCss": "Edit CSS" + "editCss": "編輯 CSS" }, "dialogs": { - "editCssTitle": "Edit CSS" + "editCssTitle": "編輯 CSS" } }, "layout": { @@ -234,11 +234,11 @@ "clashPort": { "title": "連接埠設定", "fields": { - "mixed": "混合代理連接埠", - "socks": "SOCKS 代理連接埠", - "http": "HTTP(S) 代理連接埠", - "redir": "Redir 透明代理連接埠", - "tproxy": "Tproxy Port" + "mixed": "混合連接埠", + "socks": "SOCKS 連接埠", + "http": "HTTP(S) 連接埠", + "redir": "Redir 透明連接埠", + "tproxy": "Tproxy 連接埠" }, "actions": { "random": "隨機連接埠" @@ -298,7 +298,7 @@ "usernameRequired": "使用者名稱不能為空", "passwordRequired": "密碼不能為空", "webdavConfigSaved": "WebDAV 配置儲存成功", - "webdavConfigSaveFailed": "儲存 WebDAV 配置失敗: {{error}}", + "webdavConfigSaveFailed": "儲存 WebDAV 設定檔失敗: {{error}}", "backupCreated": "備份建立成功", "backupFailed": "備份失敗: {{error}}", "localBackupCreated": "本機備份建立成功", @@ -314,7 +314,7 @@ "backupTime": "備份時間", "actions": "動作", "noBackups": "暫無備份", - "rowsPerPage": "Rows per page" + "rowsPerPage": "每頁列數" } }, "misc": { @@ -349,7 +349,7 @@ } }, "update": { - "title": "New Version v{{version}}", + "title": "新版本 v{{version}}", "actions": { "goToRelease": "前往發佈頁面", "update": "更新" @@ -401,7 +401,7 @@ "mtu": "最大傳輸單位" }, "tooltips": { - "dnsHijack": "Please use , to separate multiple DNS servers" + "dnsHijack": "請用半形逗號來區隔多個 DNS 伺服器" }, "messages": { "applied": "設定已套用" @@ -498,7 +498,7 @@ "configError": "DNS 設定錯誤:" }, "errors": { - "invalid": "Invalid configuration", + "invalid": "無效的設定", "invalidYaml": "YAML 格式無效" } }, From ba1b9796e8b959ab332b6d320bd302dcf7326f9c Mon Sep 17 00:00:00 2001 From: oomeow Date: Sun, 9 Nov 2025 13:09:32 +0800 Subject: [PATCH 10/49] fix: Tun disable failed due to too short timeout --- src-tauri/src/feat/window.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src-tauri/src/feat/window.rs b/src-tauri/src/feat/window.rs index 8352fea1b..e17e7f29d 100644 --- a/src-tauri/src/feat/window.rs +++ b/src-tauri/src/feat/window.rs @@ -57,13 +57,8 @@ pub async fn clean_async() -> bool { let disable_tun = serde_json::json!({ "tun": { "enable": false } }); - #[cfg(target_os = "windows")] - let tun_timeout = Duration::from_millis(100); - #[cfg(not(target_os = "windows"))] - let tun_timeout = Duration::from_millis(100); - match timeout( - tun_timeout, + Duration::from_millis(1000), handle::Handle::mihomo() .await .patch_base_config(&disable_tun), From 72544ccb54e2ea9c4ef1970e945bed01891104fd Mon Sep 17 00:00:00 2001 From: oomeow Date: Sun, 9 Nov 2025 13:32:59 +0800 Subject: [PATCH 11/49] fix: missing `app_log_max_size` and `app_log_max_count` when saving misc settings --- src/components/setting/mods/misc-viewer.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/setting/mods/misc-viewer.tsx b/src/components/setting/mods/misc-viewer.tsx index dcdf924c6..62109e070 100644 --- a/src/components/setting/mods/misc-viewer.tsx +++ b/src/components/setting/mods/misc-viewer.tsx @@ -59,6 +59,8 @@ export const MiscViewer = forwardRef((props, ref) => { try { await patchVerge({ app_log_level: values.appLogLevel, + app_log_max_size: values.appLogMaxSize, + app_log_max_count: values.appLogMaxCount, auto_close_connection: values.autoCloseConnection, auto_check_update: values.autoCheckUpdate, enable_builtin_enhanced: values.enableBuiltinEnhanced, From 02ff2d3c2ef426b230cd5266b72d70aec0ab6c9a Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Sun, 9 Nov 2025 15:35:22 +0800 Subject: [PATCH 12/49] chore: update dependencies in Cargo.toml and Cargo.lock for improved performance and features --- src-tauri/Cargo.lock | 47 ++++++++++++++++++++++++++++++++++++++++++++ src-tauri/Cargo.toml | 8 ++++++-- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index b7bf8c381..596ffbfff 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + [[package]] name = "adler2" version = "2.0.1" @@ -572,6 +581,21 @@ dependencies = [ "tokio", ] +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link 0.2.1", +] + [[package]] name = "base62" version = "2.2.3" @@ -2802,6 +2826,12 @@ dependencies = [ "polyval", ] +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + [[package]] name = "gio" version = "0.18.4" @@ -4911,6 +4941,15 @@ dependencies = [ "objc2-security", ] +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -5119,8 +5158,10 @@ version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ + "backtrace", "cfg-if", "libc", + "petgraph", "redox_syscall 0.5.18", "smallvec", "windows-link 0.2.1", @@ -6368,6 +6409,12 @@ dependencies = [ "ordered-multimap", ] +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + [[package]] name = "rustc-hash" version = "2.1.1" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 1c252fdf3..13cc038b0 100755 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -27,10 +27,10 @@ sysinfo = { version = "0.37.2", features = ["network", "system"] } boa_engine = "0.21.0" serde_json = "1.0.145" serde_yaml_ng = "0.10.0" -once_cell = "1.21.3" +once_cell = { version = "1.21.3", features = ["parking_lot"] } port_scanner = "0.1.5" delay_timer = "0.11.6" -parking_lot = "0.12.5" +parking_lot = { version = "0.12.5", features = ["hardware-lock-elision"] } percent-encoding = "2.3.2" tokio = { version = "1.48.0", features = [ "rt-multi-thread", @@ -170,6 +170,10 @@ crate-type = ["staticlib", "cdylib", "rlib"] [dev-dependencies] criterion = { version = "0.7.0", features = ["async_tokio"] } +parking_lot = { version = "0.12.5", features = [ + "hardware-lock-elision", + "deadlock_detection", +] } [lints.clippy] # Core categories - most important for code safety and correctness From d1934606496b38e04f3b0a1e5149543e229f93a5 Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Sun, 9 Nov 2025 15:46:40 +0800 Subject: [PATCH 13/49] refactor: replace inline log fetching with dedicated script for better maintainability --- .github/workflows/autobuild.yml | 30 ++--------------------- .github/workflows/release.yml | 30 ++--------------------- Changelog.md | 2 +- scripts/extract_update_logs.sh | 43 +++++++++++++++++++++++++++++++++ 4 files changed, 48 insertions(+), 57 deletions(-) create mode 100755 scripts/extract_update_logs.sh diff --git a/.github/workflows/autobuild.yml b/.github/workflows/autobuild.yml index e839dc855..d8be39757 100644 --- a/.github/workflows/autobuild.yml +++ b/.github/workflows/autobuild.yml @@ -35,20 +35,7 @@ jobs: - name: Fetch UPDATE logs id: fetch_update_logs - run: | - if [ -f "Changelog.md" ]; then - UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' Changelog.md) - if [ -n "$UPDATE_LOGS" ]; then - echo "Found update logs" - echo "UPDATE_LOGS<> $GITHUB_ENV - echo "$UPDATE_LOGS" >> $GITHUB_ENV - echo "EOF" >> $GITHUB_ENV - else - echo "No update sections found in Changelog.md" - fi - else - echo "Changelog.md file not found" - fi + run: bash ./scripts/extract_update_logs.sh shell: bash - uses: pnpm/action-setup@v4 @@ -509,20 +496,7 @@ jobs: - name: Fetch UPDATE logs id: fetch_update_logs - run: | - if [ -f "Changelog.md" ]; then - UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' Changelog.md) - if [ -n "$UPDATE_LOGS" ]; then - echo "Found update logs" - echo "UPDATE_LOGS<> $GITHUB_ENV - echo "$UPDATE_LOGS" >> $GITHUB_ENV - echo "EOF" >> $GITHUB_ENV - else - echo "No update sections found in Changelog.md" - fi - else - echo "Changelog.md file not found" - fi + run: bash ./scripts/extract_update_logs.sh shell: bash - name: Install Node diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 59504436b..153a5f8c9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -73,20 +73,7 @@ jobs: - name: Fetch UPDATE logs id: fetch_update_logs - run: | - if [ -f "Changelog.md" ]; then - UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' Changelog.md) - if [ -n "$UPDATE_LOGS" ]; then - echo "Found update logs" - echo "UPDATE_LOGS<> $GITHUB_ENV - echo "$UPDATE_LOGS" >> $GITHUB_ENV - echo "EOF" >> $GITHUB_ENV - else - echo "No update sections found in Changelog.md" - fi - else - echo "Changelog.md file not found" - fi + run: bash ./scripts/extract_update_logs.sh shell: bash - name: Set Env @@ -552,20 +539,7 @@ jobs: - name: Fetch UPDATE logs id: fetch_update_logs - run: | - if [ -f "Changelog.md" ]; then - UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' Changelog.md) - if [ -n "$UPDATE_LOGS" ]; then - echo "Found update logs" - echo "UPDATE_LOGS<> $GITHUB_ENV - echo "$UPDATE_LOGS" >> $GITHUB_ENV - echo "EOF" >> $GITHUB_ENV - else - echo "No update sections found in Changelog.md" - fi - else - echo "Changelog.md file not found" - fi + run: bash ./scripts/extract_update_logs.sh shell: bash - name: Install Node diff --git a/Changelog.md b/Changelog.md index 52886dace..c03c99e33 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,4 +1,4 @@ -# v2.4.4 +## v2.4.4 ### 🐞 修复问题 diff --git a/scripts/extract_update_logs.sh b/scripts/extract_update_logs.sh new file mode 100755 index 000000000..1283cf3b0 --- /dev/null +++ b/scripts/extract_update_logs.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# +# extract_update_logs.sh +# 从 Changelog.md 提取最新版本 (## v...) 的更新内容 +# 并输出到屏幕或写入环境变量文件(如 GitHub Actions) + +set -euo pipefail + +CHANGELOG_FILE="Changelog.md" + +if [[ ! -f "$CHANGELOG_FILE" ]]; then + echo "❌ 文件不存在: $CHANGELOG_FILE" >&2 + exit 1 +fi + +# 提取从第一个 '## v' 开始到下一个 '## v' 前的内容 +UPDATE_LOGS=$(awk ' + /^## v/ { + if (found) exit; + found=1 + } + found +' "$CHANGELOG_FILE") + +if [[ -z "$UPDATE_LOGS" ]]; then + echo "⚠️ 未找到更新日志内容" + exit 0 +fi + +echo "✅ 提取到的最新版本日志内容如下:" +echo "----------------------------------------" +echo "$UPDATE_LOGS" +echo "----------------------------------------" + +# 如果在 GitHub Actions 环境中(GITHUB_ENV 已定义) +if [[ -n "${GITHUB_ENV:-}" ]]; then + { + echo "UPDATE_LOGS<> "$GITHUB_ENV" + echo "✅ 已写入 GitHub 环境变量 UPDATE_LOGS" +fi From 6026f432da4476b508a1d3373ca953db9bdf141e Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Sun, 9 Nov 2025 16:02:21 +0800 Subject: [PATCH 14/49] fix: update method call to fetch configuration data for global hotkey settings --- src-tauri/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 01573ef46..42758a4f9 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -322,7 +322,7 @@ pub fn run() { AsyncHandler::spawn(move || async move { let is_enable_global_hotkey = Config::verge() .await - .latest_arc() + .data_arc() .enable_global_hotkey .unwrap_or(true); @@ -362,7 +362,7 @@ pub fn run() { let _ = hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdW); let is_enable_global_hotkey = Config::verge() .await - .latest_arc() + .data_arc() .enable_global_hotkey .unwrap_or(true); if !is_enable_global_hotkey { From c763fdd23361a32dd09b773f380aae2a931ad8da Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Sun, 9 Nov 2025 16:05:32 +0800 Subject: [PATCH 15/49] fix: update configuration data access from latest_arc to data_arc for TUN and system proxy settings --- src-tauri/src/feat/window.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src-tauri/src/feat/window.rs b/src-tauri/src/feat/window.rs index e17e7f29d..876ba4432 100644 --- a/src-tauri/src/feat/window.rs +++ b/src-tauri/src/feat/window.rs @@ -47,7 +47,7 @@ pub async fn clean_async() -> bool { let tun_task = async { let tun_enabled = Config::verge() .await - .latest_arc() + .data_arc() .enable_tun_mode .unwrap_or(false); @@ -95,7 +95,7 @@ pub async fn clean_async() -> bool { // 检查系统代理是否开启 let sys_proxy_enabled = Config::verge() .await - .latest_arc() + .data_arc() .enable_system_proxy .unwrap_or(false); @@ -171,7 +171,7 @@ pub async fn clean_async() -> bool { { let sys_proxy_enabled = Config::verge() .await - .latest_arc() + .data_arc() .enable_system_proxy .unwrap_or(false); @@ -311,7 +311,7 @@ pub async fn hide() { let enable_auto_light_weight_mode = Config::verge() .await - .latest_arc() + .data_arc() .enable_auto_light_weight_mode .unwrap_or(false); From f644036e087fa25bf0e971c6ef7ee0fce9dc1f75 Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Sun, 9 Nov 2025 16:31:37 +0800 Subject: [PATCH 16/49] chore: update Rust toolchain to version 1.91 and adjust edition in rustfmt configuration --- src-tauri/Cargo.toml | 1 + src-tauri/rust-toolchain.toml | 3 +++ src-tauri/rustfmt.toml | 2 +- src-tauri/src/core/backup.rs | 3 ++- src-tauri/src/utils/linux.rs | 4 +++- 5 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 src-tauri/rust-toolchain.toml diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 13cc038b0..7385c7e6e 100755 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -8,6 +8,7 @@ repository = "https://github.com/clash-verge-rev/clash-verge-rev.git" default-run = "clash-verge" edition = "2024" build = "build.rs" +rust-version = "1.91" [package.metadata.bundle] identifier = "io.github.clash-verge-rev.clash-verge-rev" diff --git a/src-tauri/rust-toolchain.toml b/src-tauri/rust-toolchain.toml new file mode 100644 index 000000000..cdeba7a2b --- /dev/null +++ b/src-tauri/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "1.91.0" +components = ["rustfmt", "clippy"] diff --git a/src-tauri/rustfmt.toml b/src-tauri/rustfmt.toml index baaa750e3..7e141a0b8 100644 --- a/src-tauri/rustfmt.toml +++ b/src-tauri/rustfmt.toml @@ -6,7 +6,7 @@ use_small_heuristics = "Default" reorder_imports = true reorder_modules = true remove_nested_parens = true -edition = "2021" +edition = "2024" merge_derives = true use_try_shorthand = false use_field_init_shorthand = false diff --git a/src-tauri/src/core/backup.rs b/src-tauri/src/core/backup.rs index 023167580..11681ab06 100644 --- a/src-tauri/src/core/backup.rs +++ b/src-tauri/src/core/backup.rs @@ -92,7 +92,8 @@ impl WebDavClient { || verge.webdav_username.is_none() || verge.webdav_password.is_none() { - let msg: String = "Unable to create web dav client, please make sure the webdav config is correct".into(); + let msg: String = + "Unable to create web dav client, please make sure the webdav config is correct".into(); return Err(anyhow::Error::msg(msg)); } diff --git a/src-tauri/src/utils/linux.rs b/src-tauri/src/utils/linux.rs index 65e0ae49f..e3ced3871 100644 --- a/src-tauri/src/utils/linux.rs +++ b/src-tauri/src/utils/linux.rs @@ -262,7 +262,9 @@ impl DmabufDecision { if intel_gpu.inconclusive { decision.message = Some("Wayland 上检测到 Intel GPU,但缺少 boot_vga 信息:预防性禁用 WebKit DMABUF,若确认非主 GPU 可通过 CLASH_VERGE_DMABUF=1 覆盖。".into()); } else { - decision.message = Some("Wayland 上检测到 Intel 主 GPU (0x8086):禁用 WebKit DMABUF 以避免帧缓冲失败。".into()); + decision.message = Some( + "Wayland 上检测到 Intel 主 GPU (0x8086):禁用 WebKit DMABUF 以避免帧缓冲失败。".into(), + ); } } else if session.is_wayland { decision.message = Some( From 3e81dcd78fbc5a171753e09a85775fffcae745cb Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Sun, 9 Nov 2025 16:59:43 +0800 Subject: [PATCH 17/49] refactor: simplify icon file search logic in find_target_icons function Try with 1.91.0 new API --- src-tauri/src/utils/dirs.rs | 35 +++++++++++------------------------ 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/src-tauri/src/utils/dirs.rs b/src-tauri/src/utils/dirs.rs index 56c74f2bc..f83af90f4 100644 --- a/src-tauri/src/utils/dirs.rs +++ b/src-tauri/src/utils/dirs.rs @@ -103,31 +103,18 @@ pub fn app_icons_dir() -> Result { pub fn find_target_icons(target: &str) -> Result> { let icons_dir = app_icons_dir()?; - let mut matching_files = Vec::new(); + let icon_path = fs::read_dir(&icons_dir)? + .filter_map(|entry| entry.ok().map(|e| e.path())) + .find(|path| { + path.file_prefix().is_some_and(|prefix| prefix == target) + && path + .extension() + .is_some_and(|ext| ext == "ico" || ext == "png") + }); - for entry in fs::read_dir(icons_dir)? { - let entry = entry?; - let path = entry.path(); - - if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) - && file_name.starts_with(target) - && (file_name.ends_with(".ico") || file_name.ends_with(".png")) - { - matching_files.push(path); - } - } - - if matching_files.is_empty() { - Ok(None) - } else { - match matching_files.first() { - Some(first_path) => { - let first = path_to_str(first_path)?; - Ok(Some(first.into())) - } - None => Ok(None), - } - } + icon_path + .map(|path| path_to_str(&path).map(|s| s.into())) + .transpose() } /// logs dir From 5f2ac77923634d4b60e2a085a71d9dc86bbf078a Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Sun, 9 Nov 2025 18:38:59 +0800 Subject: [PATCH 18/49] chore: update Rust toolchain to version 1.91.0 in autobuild workflow fix: update Rust toolchain action to use master branch for consistency feat: add support for Windows ARM64 Gnullvm in development workflow refactor: remove Windows ARM64 Gnullvm support and ensure Rust target is added for the pinned toolchain feat: add Rust target installation step for pinned toolchain in autobuild workflow fix: remove unnecessary blank lines in autobuild workflow --- .github/workflows/autobuild.yml | 31 +++++++++++++++++++++++++++++-- .github/workflows/dev.yml | 14 +++++++++----- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/.github/workflows/autobuild.yml b/.github/workflows/autobuild.yml index d8be39757..0f978e8a3 100644 --- a/.github/workflows/autobuild.yml +++ b/.github/workflows/autobuild.yml @@ -145,7 +145,10 @@ jobs: uses: actions/checkout@v4 - name: Install Rust Stable - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@master + with: + toolchain: "1.91.0" + targets: ${{ matrix.target }} - name: Add Rust Target run: rustup target add ${{ matrix.target }} @@ -201,6 +204,13 @@ jobs: - name: Release ${{ env.TAG_CHANNEL }} Version run: pnpm release-version autobuild-latest + - name: Add Rust Target + run: | + # Ensure cross target is installed for the pinned toolchain; fallback without explicit toolchain if needed + rustup target add ${{ matrix.target }} --toolchain 1.91.0 || rustup target add ${{ matrix.target }} + rustup target list --installed + echo "Rust target ${{ matrix.target }} installed." + - name: Tauri build for Windows-macOS-Linux uses: tauri-apps/tauri-action@v0 env: @@ -244,7 +254,10 @@ jobs: uses: actions/checkout@v4 - name: Install Rust Stable - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@master + with: + toolchain: "1.91.0" + targets: ${{ matrix.target }} - name: Add Rust Target run: rustup target add ${{ matrix.target }} @@ -334,6 +347,13 @@ jobs: gcc-arm-linux-gnueabihf \ g++-arm-linux-gnueabihf + - name: Add Rust Target + run: | + # Ensure cross target is installed for the pinned toolchain; fallback without explicit toolchain if needed + rustup target add ${{ matrix.target }} --toolchain 1.91.0 || rustup target add ${{ matrix.target }} + rustup target list --installed + echo "Rust target ${{ matrix.target }} installed." + - name: Tauri Build for Linux run: | export PKG_CONFIG_ALLOW_CROSS=1 @@ -433,6 +453,13 @@ jobs: Remove-Item .\src-tauri\tauri.windows.conf.json Rename-Item .\src-tauri\webview2.${{ matrix.arch }}.json tauri.windows.conf.json + - name: Add Rust Target + run: | + # Ensure cross target is installed for the pinned toolchain; fallback without explicit toolchain if needed + rustup target add ${{ matrix.target }} --toolchain 1.91.0 || rustup target add ${{ matrix.target }} + rustup target list --installed + echo "Rust target ${{ matrix.target }} installed." + - name: Tauri build for Windows id: build uses: tauri-apps/tauri-action@v0 diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 336c5e422..e5ae26c32 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -74,11 +74,7 @@ jobs: - name: Install Rust Stable if: github.event.inputs[matrix.input] == 'true' - uses: dtolnay/rust-toolchain@stable - - - name: Add Rust Target - if: github.event.inputs[matrix.input] == 'true' - run: rustup target add ${{ matrix.target }} + uses: dtolnay/rust-toolchain@1.91.0 - name: Rust Cache if: github.event.inputs[matrix.input] == 'true' @@ -118,6 +114,14 @@ jobs: if: github.event.inputs[matrix.input] == 'true' run: pnpm release-version ${{ env.TAG_NAME }} + - name: Add Rust Target + if: github.event.inputs[matrix.input] == 'true' + run: | + # Ensure cross target is installed for the pinned toolchain; fallback without explicit toolchain if needed + rustup target add ${{ matrix.target }} --toolchain 1.91.0 || rustup target add ${{ matrix.target }} + rustup target list --installed + echo "Rust target ${{ matrix.target }} installed." + - name: Tauri build if: github.event.inputs[matrix.input] == 'true' uses: tauri-apps/tauri-action@v0 From f5160ef2771249a141a0671c08bd52e8e25d3201 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 9 Nov 2025 21:01:49 +0800 Subject: [PATCH 19/49] chore(deps): update dependency @tauri-apps/cli to v2.9.4 (#5375) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- pnpm-lock.yaml | 107 +++++++++++++++++++++++-------------------------- 2 files changed, 51 insertions(+), 58 deletions(-) diff --git a/package.json b/package.json index fadc52056..59c681a92 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "@actions/github": "^6.0.1", "@eslint-react/eslint-plugin": "^2.3.1", "@eslint/js": "^9.39.1", - "@tauri-apps/cli": "2.9.3", + "@tauri-apps/cli": "2.9.4", "@types/js-yaml": "^4.0.9", "@types/lodash-es": "^4.17.12", "@types/node": "^24.10.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 63720b241..4f081b099 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -145,8 +145,8 @@ importers: specifier: ^9.39.1 version: 9.39.1 '@tauri-apps/cli': - specifier: 2.9.3 - version: 2.9.3 + specifier: 2.9.4 + version: 2.9.4 '@types/js-yaml': specifier: ^4.0.9 version: 4.0.9 @@ -1694,74 +1694,74 @@ packages: '@tauri-apps/api@2.9.0': resolution: {integrity: sha512-qD5tMjh7utwBk9/5PrTA/aGr3i5QaJ/Mlt7p8NilQ45WgbifUNPyKWsA63iQ8YfQq6R8ajMapU+/Q8nMcPRLNw==} - '@tauri-apps/cli-darwin-arm64@2.9.3': - resolution: {integrity: sha512-W8FQXZXQmQ0Fmj9UJXNrm2mLdIaLLriKVY7o/FzmizyIKTPIvHjfZALTNybbpTQRbJvKoGHLrW1DNzAWVDWJYg==} + '@tauri-apps/cli-darwin-arm64@2.9.4': + resolution: {integrity: sha512-9rHkMVtbMhe0AliVbrGpzMahOBg3rwV46JYRELxR9SN6iu1dvPOaMaiC4cP6M/aD1424ziXnnMdYU06RAH8oIw==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@tauri-apps/cli-darwin-x64@2.9.3': - resolution: {integrity: sha512-zDwu40rlshijt3TU6aRvzPUyVpapsx1sNfOlreDMTaMelQLHl6YoQzSRpLHYwrHrhimxyX2uDqnKIiuGel0Lhg==} + '@tauri-apps/cli-darwin-x64@2.9.4': + resolution: {integrity: sha512-VT9ymNuT06f5TLjCZW2hfSxbVtZDhORk7CDUDYiq5TiSYQdxkl8MVBy0CCFFcOk4QAkUmqmVUA9r3YZ/N/vPRQ==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@tauri-apps/cli-linux-arm-gnueabihf@2.9.3': - resolution: {integrity: sha512-+Oc2OfcTRwYtW93VJqd/HOk77buORwC9IToj/qsEvM7bTMq6Kda4alpZprzwrCHYANSw+zD8PgjJdljTpe4p+g==} + '@tauri-apps/cli-linux-arm-gnueabihf@2.9.4': + resolution: {integrity: sha512-tTWkEPig+2z3Rk0zqZYfjUYcgD+aSm72wdrIhdYobxbQZOBw0zfn50YtWv+av7bm0SHvv75f0l7JuwgZM1HFow==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - '@tauri-apps/cli-linux-arm64-gnu@2.9.3': - resolution: {integrity: sha512-59GqU/J1n9wFyAtleoQOaU0oVIo+kwQynEw4meFDoKRXszKGor6lTsbsS3r0QKLSPbc0o/yYGJhqqCtkYjb/eg==} + '@tauri-apps/cli-linux-arm64-gnu@2.9.4': + resolution: {integrity: sha512-ql6vJ611qoqRYHxkKPnb2vHa27U+YRKRmIpLMMBeZnfFtZ938eao7402AQCH1mO2+/8ioUhbpy9R/ZcLTXVmkg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tauri-apps/cli-linux-arm64-musl@2.9.3': - resolution: {integrity: sha512-fzvG+jEn5/iYGNH6Z2IRMheYFC4pJdXa19BR9fFm6Bdn2cuajRLDKdUcEME/DCtwqclphXtFZTrT4oezY5vI/A==} + '@tauri-apps/cli-linux-arm64-musl@2.9.4': + resolution: {integrity: sha512-vg7yNn7ICTi6hRrcA/6ff2UpZQP7un3xe3SEld5QM0prgridbKAiXGaCKr3BnUBx/rGXegQlD/wiLcWdiiraSw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tauri-apps/cli-linux-riscv64-gnu@2.9.3': - resolution: {integrity: sha512-qV8DZXI/fZwawk6T3Th1g6smiNC2KeQTk7XFgKvqZ6btC01z3UTsQmNGvI602zwm3Ld1TBZb4+rEWu2QmQimmw==} + '@tauri-apps/cli-linux-riscv64-gnu@2.9.4': + resolution: {integrity: sha512-l8L+3VxNk6yv5T/Z/gv5ysngmIpsai40B9p6NQQyqYqxImqYX37pqREoEBl1YwG7szGnDibpWhidPrWKR59OJA==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] - '@tauri-apps/cli-linux-x64-gnu@2.9.3': - resolution: {integrity: sha512-tquyEONCNRfqEBWEe4eAHnxFN5yY5lFkCuD4w79XLIovUxVftQ684+xLp7zkhntkt4y20SMj2AgJa/+MOlx4Kg==} + '@tauri-apps/cli-linux-x64-gnu@2.9.4': + resolution: {integrity: sha512-PepPhCXc/xVvE3foykNho46OmCyx47E/aG676vKTVp+mqin5d+IBqDL6wDKiGNT5OTTxKEyNlCQ81Xs2BQhhqA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tauri-apps/cli-linux-x64-musl@2.9.3': - resolution: {integrity: sha512-v2cBIB/6ji8DL+aiL5QUykU3ZO8OoJGyx50/qv2HQVzkf85KdaYSis3D/oVRemN/pcDz+vyCnnL3XnzFnDl4JQ==} + '@tauri-apps/cli-linux-x64-musl@2.9.4': + resolution: {integrity: sha512-zcd1QVffh5tZs1u1SCKUV/V7RRynebgYUNWHuV0FsIF1MjnULUChEXhAhug7usCDq4GZReMJOoXa6rukEozWIw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tauri-apps/cli-win32-arm64-msvc@2.9.3': - resolution: {integrity: sha512-ZGvBy7nvrHPbE0HeKp/ioaiw8bNgAHxWnb7JRZ4/G0A+oFj0SeSFxl9k5uU6FKnM7bHM23Gd1oeaDex9g5Fceg==} + '@tauri-apps/cli-win32-arm64-msvc@2.9.4': + resolution: {integrity: sha512-/7ZhnP6PY04bEob23q8MH/EoDISdmR1wuNm0k9d5HV7TDMd2GGCDa8dPXA4vJuglJKXIfXqxFmZ4L+J+MO42+w==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@tauri-apps/cli-win32-ia32-msvc@2.9.3': - resolution: {integrity: sha512-UsgIwOnpCoY9NK9/65QiwgmWVIE80LE7SwRYVblGtmlY9RYfsYvpbItwsovA/AcHMTiO+OCvS/q9yLeqS3m6Sg==} + '@tauri-apps/cli-win32-ia32-msvc@2.9.4': + resolution: {integrity: sha512-1LmAfaC4Cq+3O1Ir1ksdhczhdtFSTIV51tbAGtbV/mr348O+M52A/xwCCXQank0OcdBxy5BctqkMtuZnQvA8uQ==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] - '@tauri-apps/cli-win32-x64-msvc@2.9.3': - resolution: {integrity: sha512-fmw7NrrHE5m49idCvJAx9T9bsupjdJ0a3p3DPCNCZRGANU6R1tA1L+KTlVuUtdAldX2NqU/9UPo2SCslYKgJHQ==} + '@tauri-apps/cli-win32-x64-msvc@2.9.4': + resolution: {integrity: sha512-EdYd4c9wGvtPB95kqtEyY+bUR+k4kRw3IA30mAQ1jPH6z57AftT8q84qwv0RDp6kkEqOBKxeInKfqi4BESYuqg==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@tauri-apps/cli@2.9.3': - resolution: {integrity: sha512-BQ7iLUXTQcyG1PpzLWeVSmBCedYDpnA/6Cm/kRFGtqjTf/eVUlyYO5S2ee07tLum3nWwDBWTGFZeruO8yEukfA==} + '@tauri-apps/cli@2.9.4': + resolution: {integrity: sha512-pvylWC9QckrOS9ATWXIXcgu7g2hKK5xTL5ZQyZU/U0n9l88SEFGcWgLQNa8WZmd+wWIOWhkxOFcOl3i6ubDNNw==} engines: {node: '>= 10'} hasBin: true @@ -3251,9 +3251,6 @@ packages: lru-queue@0.1.0: resolution: {integrity: sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==} - magic-string@0.30.19: - resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} - magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -5881,52 +5878,52 @@ snapshots: '@tauri-apps/api@2.9.0': {} - '@tauri-apps/cli-darwin-arm64@2.9.3': + '@tauri-apps/cli-darwin-arm64@2.9.4': optional: true - '@tauri-apps/cli-darwin-x64@2.9.3': + '@tauri-apps/cli-darwin-x64@2.9.4': optional: true - '@tauri-apps/cli-linux-arm-gnueabihf@2.9.3': + '@tauri-apps/cli-linux-arm-gnueabihf@2.9.4': optional: true - '@tauri-apps/cli-linux-arm64-gnu@2.9.3': + '@tauri-apps/cli-linux-arm64-gnu@2.9.4': optional: true - '@tauri-apps/cli-linux-arm64-musl@2.9.3': + '@tauri-apps/cli-linux-arm64-musl@2.9.4': optional: true - '@tauri-apps/cli-linux-riscv64-gnu@2.9.3': + '@tauri-apps/cli-linux-riscv64-gnu@2.9.4': optional: true - '@tauri-apps/cli-linux-x64-gnu@2.9.3': + '@tauri-apps/cli-linux-x64-gnu@2.9.4': optional: true - '@tauri-apps/cli-linux-x64-musl@2.9.3': + '@tauri-apps/cli-linux-x64-musl@2.9.4': optional: true - '@tauri-apps/cli-win32-arm64-msvc@2.9.3': + '@tauri-apps/cli-win32-arm64-msvc@2.9.4': optional: true - '@tauri-apps/cli-win32-ia32-msvc@2.9.3': + '@tauri-apps/cli-win32-ia32-msvc@2.9.4': optional: true - '@tauri-apps/cli-win32-x64-msvc@2.9.3': + '@tauri-apps/cli-win32-x64-msvc@2.9.4': optional: true - '@tauri-apps/cli@2.9.3': + '@tauri-apps/cli@2.9.4': optionalDependencies: - '@tauri-apps/cli-darwin-arm64': 2.9.3 - '@tauri-apps/cli-darwin-x64': 2.9.3 - '@tauri-apps/cli-linux-arm-gnueabihf': 2.9.3 - '@tauri-apps/cli-linux-arm64-gnu': 2.9.3 - '@tauri-apps/cli-linux-arm64-musl': 2.9.3 - '@tauri-apps/cli-linux-riscv64-gnu': 2.9.3 - '@tauri-apps/cli-linux-x64-gnu': 2.9.3 - '@tauri-apps/cli-linux-x64-musl': 2.9.3 - '@tauri-apps/cli-win32-arm64-msvc': 2.9.3 - '@tauri-apps/cli-win32-ia32-msvc': 2.9.3 - '@tauri-apps/cli-win32-x64-msvc': 2.9.3 + '@tauri-apps/cli-darwin-arm64': 2.9.4 + '@tauri-apps/cli-darwin-x64': 2.9.4 + '@tauri-apps/cli-linux-arm-gnueabihf': 2.9.4 + '@tauri-apps/cli-linux-arm64-gnu': 2.9.4 + '@tauri-apps/cli-linux-arm64-musl': 2.9.4 + '@tauri-apps/cli-linux-riscv64-gnu': 2.9.4 + '@tauri-apps/cli-linux-x64-gnu': 2.9.4 + '@tauri-apps/cli-linux-x64-musl': 2.9.4 + '@tauri-apps/cli-win32-arm64-msvc': 2.9.4 + '@tauri-apps/cli-win32-ia32-msvc': 2.9.4 + '@tauri-apps/cli-win32-x64-msvc': 2.9.4 '@tauri-apps/plugin-clipboard-manager@2.3.2': dependencies: @@ -6191,7 +6188,7 @@ snapshots: browserslist: 4.25.1 browserslist-to-esbuild: 2.1.1(browserslist@4.25.1) core-js: 3.45.0 - magic-string: 0.30.19 + magic-string: 0.30.21 regenerator-runtime: 0.14.1 systemjs: 6.15.1 terser: 5.44.1 @@ -7686,10 +7683,6 @@ snapshots: dependencies: es5-ext: 0.10.64 - magic-string@0.30.19: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 From 7f267fa727aa897346b6f1d8212a829ee36a8cdb Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Sun, 9 Nov 2025 21:25:36 +0800 Subject: [PATCH 20/49] fix: update configuration data access from latest_arc to data_arc in Clash and WebDAV commands and init windows --- src-tauri/src/cmd/clash.rs | 2 +- src-tauri/src/cmd/webdav.rs | 2 +- src-tauri/src/core/hotkey.rs | 6 +++--- src-tauri/src/utils/resolve/mod.rs | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src-tauri/src/cmd/clash.rs b/src-tauri/src/cmd/clash.rs index 84855bd5e..7ca0bb454 100644 --- a/src-tauri/src/cmd/clash.rs +++ b/src-tauri/src/cmd/clash.rs @@ -22,7 +22,7 @@ pub async fn copy_clash_env() -> CmdResult { /// 获取Clash信息 #[tauri::command] pub async fn get_clash_info() -> CmdResult { - Ok(Config::clash().await.latest_arc().get_client_info()) + Ok(Config::clash().await.data_arc().get_client_info()) } /// 修改Clash配置 diff --git a/src-tauri/src/cmd/webdav.rs b/src-tauri/src/cmd/webdav.rs index 209f39e7a..20ae9d048 100644 --- a/src-tauri/src/cmd/webdav.rs +++ b/src-tauri/src/cmd/webdav.rs @@ -19,7 +19,7 @@ pub async fn save_webdav_config(url: String, username: String, password: String) Config::verge().await.edit_draft(|e| e.patch_config(&patch)); Config::verge().await.apply(); - let verge_data = Config::verge().await.latest_arc(); + let verge_data = Config::verge().await.data_arc(); verge_data.save_file().await.stringify_err()?; core::backup::WebDavClient::global().reset(); Ok(()) diff --git a/src-tauri/src/core/hotkey.rs b/src-tauri/src/core/hotkey.rs index f75e7e922..6a06756c2 100755 --- a/src-tauri/src/core/hotkey.rs +++ b/src-tauri/src/core/hotkey.rs @@ -227,7 +227,7 @@ impl Hotkey { let is_enable_global_hotkey = Config::verge() .await - .latest_arc() + .data_arc() .enable_global_hotkey .unwrap_or(true); @@ -264,7 +264,7 @@ singleton_with_logging!(Hotkey, INSTANCE, "Hotkey"); impl Hotkey { pub async fn init(&self, skip: bool) -> Result<()> { let verge = Config::verge().await; - let enable_global_hotkey = !skip && verge.latest_arc().enable_global_hotkey.unwrap_or(true); + let enable_global_hotkey = !skip && verge.data_arc().enable_global_hotkey.unwrap_or(true); logging!( debug, @@ -274,7 +274,7 @@ impl Hotkey { ); // Extract hotkeys data before async operations - let hotkeys = verge.latest_arc().hotkeys.as_ref().cloned(); + let hotkeys = verge.data_arc().hotkeys.as_ref().cloned(); if let Some(hotkeys) = hotkeys { logging!( diff --git a/src-tauri/src/utils/resolve/mod.rs b/src-tauri/src/utils/resolve/mod.rs index 5a3556161..542cac466 100644 --- a/src-tauri/src/utils/resolve/mod.rs +++ b/src-tauri/src/utils/resolve/mod.rs @@ -179,7 +179,7 @@ pub(super) async fn refresh_tray_menu() { pub(super) async fn init_window() { let is_silent_start = Config::verge() .await - .latest_arc() + .data_arc() .enable_silent_start .unwrap_or(false); #[cfg(target_os = "macos")] From e6c8f762dbef60212f782d3c0fc9d780a113ea8f Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Sun, 9 Nov 2025 22:07:12 +0800 Subject: [PATCH 21/49] refactor: replace cloned() with clone() for improved performance in multiple files --- src-tauri/Cargo.toml | 13 +++++++++++-- src-tauri/src/core/backup.rs | 7 +++---- src-tauri/src/core/hotkey.rs | 2 +- src-tauri/src/utils/draft.rs | 6 +----- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 7385c7e6e..32a5c2709 100755 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -190,8 +190,10 @@ unimplemented = "deny" # Development quality lints todo = "warn" dbg_macro = "warn" -#print_stdout = "warn" -#print_stderr = "warn" + +# 我们期望所有输出方式通过 logging 模块进行统一管理 +# print_stdout = "deny" +# print_stderr = "deny" # Performance lints for proxy application clone_on_ref_ptr = "warn" @@ -249,3 +251,10 @@ iter_on_empty_collections = "deny" equatable_if_let = "deny" collection_is_never_read = "deny" branches_sharing_code = "deny" +pathbuf_init_then_push = "deny" +option_as_ref_cloned = "deny" +large_types_passed_by_value = "deny" +# implicit_clone = "deny" // 可能会造成额外开销,暂时不开启 +expl_impl_clone_on_copy = "deny" +copy_iterator = "deny" +cloned_instead_of_copied = "deny" diff --git a/src-tauri/src/core/backup.rs b/src-tauri/src/core/backup.rs index 11681ab06..229139801 100644 --- a/src-tauri/src/core/backup.rs +++ b/src-tauri/src/core/backup.rs @@ -100,13 +100,12 @@ impl WebDavClient { let config = WebDavConfig { url: verge .webdav_url - .as_ref() - .cloned() + .clone() .unwrap_or_default() .trim_end_matches('/') .into(), - username: verge.webdav_username.as_ref().cloned().unwrap_or_default(), - password: verge.webdav_password.as_ref().cloned().unwrap_or_default(), + username: verge.webdav_username.clone().unwrap_or_default(), + password: verge.webdav_password.clone().unwrap_or_default(), }; // 存储配置到 ArcSwapOption diff --git a/src-tauri/src/core/hotkey.rs b/src-tauri/src/core/hotkey.rs index 6a06756c2..f2b2d0be0 100755 --- a/src-tauri/src/core/hotkey.rs +++ b/src-tauri/src/core/hotkey.rs @@ -274,7 +274,7 @@ impl Hotkey { ); // Extract hotkeys data before async operations - let hotkeys = verge.data_arc().hotkeys.as_ref().cloned(); + let hotkeys = verge.data_arc().hotkeys.clone(); if let Some(hotkeys) = hotkeys { logging!( diff --git a/src-tauri/src/utils/draft.rs b/src-tauri/src/utils/draft.rs index cc81b1f2d..8eab1f302 100644 --- a/src-tauri/src/utils/draft.rs +++ b/src-tauri/src/utils/draft.rs @@ -27,11 +27,7 @@ impl Draft { /// 这也是零拷贝:只 clone Arc,不 clone T pub fn latest_arc(&self) -> SharedBox { let guard = self.inner.read(); - guard - .1 - .as_ref() - .cloned() - .unwrap_or_else(|| Arc::clone(&guard.0)) + guard.1.clone().unwrap_or_else(|| Arc::clone(&guard.0)) } /// 通过闭包以可变方式编辑草稿(在闭包中我们给出 &mut T) From 4eeb8834640b79f846eaedebf1869349b49842af Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Sun, 9 Nov 2025 22:15:37 +0800 Subject: [PATCH 22/49] refactor: update imports to use as _ for unused identifiers across multiple files --- src-tauri/Cargo.toml | 4 ++++ src-tauri/src/cmd/app.rs | 8 ++++---- src-tauri/src/cmd/backup.rs | 2 +- src-tauri/src/cmd/clash.rs | 2 +- src-tauri/src/cmd/network.rs | 4 ++-- src-tauri/src/cmd/profile.rs | 2 +- src-tauri/src/cmd/runtime.rs | 9 +++++++-- src-tauri/src/cmd/save_profile.rs | 6 +++--- src-tauri/src/cmd/service.rs | 2 +- src-tauri/src/cmd/system.rs | 2 +- src-tauri/src/cmd/validate.rs | 2 +- src-tauri/src/cmd/verge.rs | 2 +- src-tauri/src/cmd/webdav.rs | 2 +- src-tauri/src/config/clash.rs | 2 +- src-tauri/src/config/encrypt.rs | 4 ++-- src-tauri/src/config/prfitem.rs | 2 +- src-tauri/src/config/profiles.rs | 4 ++-- src-tauri/src/core/backup.rs | 2 +- src-tauri/src/core/event_driven_proxy.rs | 2 +- src-tauri/src/core/handle.rs | 4 ++-- src-tauri/src/core/hotkey.rs | 2 +- src-tauri/src/core/manager/config.rs | 2 +- src-tauri/src/core/manager/state.rs | 2 +- src-tauri/src/core/notification.rs | 2 +- src-tauri/src/core/service.rs | 2 +- src-tauri/src/core/sysopt.rs | 2 +- src-tauri/src/core/timer.rs | 2 +- src-tauri/src/core/validate.rs | 2 +- src-tauri/src/enhance/mod.rs | 9 ++++++++- src-tauri/src/feat/backup.rs | 2 +- src-tauri/src/feat/profile.rs | 2 +- src-tauri/src/feat/proxy.rs | 2 +- src-tauri/src/lib.rs | 4 ++-- src-tauri/src/module/lightweight.rs | 4 ++-- src-tauri/src/utils/dirs.rs | 2 +- src-tauri/src/utils/help.rs | 2 +- src-tauri/src/utils/init.rs | 10 +++++----- src-tauri/src/utils/logging.rs | 2 +- src-tauri/src/utils/notification.rs | 2 +- src-tauri/src/utils/resolve/dns.rs | 4 ++-- src-tauri/src/utils/server.rs | 2 +- src-tauri/src/utils/window_manager.rs | 2 +- 42 files changed, 74 insertions(+), 58 deletions(-) diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 32a5c2709..9f57f1eab 100755 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -258,3 +258,7 @@ large_types_passed_by_value = "deny" expl_impl_clone_on_copy = "deny" copy_iterator = "deny" cloned_instead_of_copied = "deny" +# self_only_used_in_recursion = "deny" // Since 1.92.0 +unnecessary_self_imports = "deny" +unused_trait_names = "deny" +wildcard_imports = "deny" diff --git a/src-tauri/src/cmd/app.rs b/src-tauri/src/cmd/app.rs index 5fcc5566a..4a806acb7 100644 --- a/src-tauri/src/cmd/app.rs +++ b/src-tauri/src/cmd/app.rs @@ -2,18 +2,18 @@ use super::CmdResult; use crate::core::sysopt::Sysopt; use crate::utils::resolve::ui::{self, UiReadyStage}; use crate::{ - cmd::StringifyErr, + cmd::StringifyErr as _, feat, logging, utils::{ - dirs::{self, PathBufExec}, + dirs::{self, PathBufExec as _}, logging::Type, }, }; use smartstring::alias::String; use std::path::Path; -use tauri::{AppHandle, Manager}; +use tauri::{AppHandle, Manager as _}; use tokio::fs; -use tokio::io::AsyncWriteExt; +use tokio::io::AsyncWriteExt as _; /// 打开应用程序所在目录 #[tauri::command] diff --git a/src-tauri/src/cmd/backup.rs b/src-tauri/src/cmd/backup.rs index a105f4d8a..238e81bdc 100644 --- a/src-tauri/src/cmd/backup.rs +++ b/src-tauri/src/cmd/backup.rs @@ -1,5 +1,5 @@ use super::CmdResult; -use crate::{cmd::StringifyErr, feat}; +use crate::{cmd::StringifyErr as _, feat}; use feat::LocalBackupFile; use smartstring::alias::String; diff --git a/src-tauri/src/cmd/clash.rs b/src-tauri/src/cmd/clash.rs index 7ca0bb454..ab37a4b3d 100644 --- a/src-tauri/src/cmd/clash.rs +++ b/src-tauri/src/cmd/clash.rs @@ -1,7 +1,7 @@ use super::CmdResult; use crate::utils::dirs; use crate::{ - cmd::StringifyErr, + cmd::StringifyErr as _, config::{ClashInfo, Config}, constants, core::{CoreManager, handle, validate::CoreConfigValidator}, diff --git a/src-tauri/src/cmd/network.rs b/src-tauri/src/cmd/network.rs index c0dedfde6..be2fccff8 100644 --- a/src-tauri/src/cmd/network.rs +++ b/src-tauri/src/cmd/network.rs @@ -1,5 +1,5 @@ use super::CmdResult; -use crate::cmd::StringifyErr; +use crate::cmd::StringifyErr as _; use crate::core::{EventDrivenProxyManager, async_proxy_query::AsyncProxyQuery}; use crate::process::AsyncHandler; use crate::{logging, utils::logging::Type}; @@ -93,7 +93,7 @@ pub fn get_network_interfaces() -> Vec { /// 获取网络接口详细信息 #[tauri::command] pub fn get_network_interfaces_info() -> CmdResult> { - use network_interface::{NetworkInterface, NetworkInterfaceConfig}; + use network_interface::{NetworkInterface, NetworkInterfaceConfig as _}; let names = get_network_interfaces(); let interfaces = NetworkInterface::show().stringify_err()?; diff --git a/src-tauri/src/cmd/profile.rs b/src-tauri/src/cmd/profile.rs index 0368d520e..8f2a0f207 100644 --- a/src-tauri/src/cmd/profile.rs +++ b/src-tauri/src/cmd/profile.rs @@ -1,5 +1,5 @@ use super::CmdResult; -use super::StringifyErr; +use super::StringifyErr as _; use crate::{ config::{ Config, IProfiles, PrfItem, PrfOption, diff --git a/src-tauri/src/cmd/runtime.rs b/src-tauri/src/cmd/runtime.rs index 5ecfe954f..d75e470d0 100644 --- a/src-tauri/src/cmd/runtime.rs +++ b/src-tauri/src/cmd/runtime.rs @@ -1,6 +1,11 @@ use super::CmdResult; -use crate::{cmd::StringifyErr, config::*, core::CoreManager, log_err}; -use anyhow::{Context, anyhow}; +use crate::{ + cmd::StringifyErr as _, + config::{Config, ConfigType}, + core::CoreManager, + log_err, +}; +use anyhow::{Context as _, anyhow}; use serde_yaml_ng::Mapping; use smartstring::alias::String; use std::collections::HashMap; diff --git a/src-tauri/src/cmd/save_profile.rs b/src-tauri/src/cmd/save_profile.rs index a52462950..21f2c4040 100644 --- a/src-tauri/src/cmd/save_profile.rs +++ b/src-tauri/src/cmd/save_profile.rs @@ -1,8 +1,8 @@ use super::CmdResult; use crate::{ - cmd::StringifyErr, - config::*, - core::{validate::CoreConfigValidator, *}, + cmd::StringifyErr as _, + config::{Config, PrfItem}, + core::{CoreManager, handle, validate::CoreConfigValidator}, logging, utils::{dirs, logging::Type}, }; diff --git a/src-tauri/src/cmd/service.rs b/src-tauri/src/cmd/service.rs index 7e5b7d2c0..3f7b92273 100644 --- a/src-tauri/src/cmd/service.rs +++ b/src-tauri/src/cmd/service.rs @@ -1,4 +1,4 @@ -use super::{CmdResult, StringifyErr}; +use super::{CmdResult, StringifyErr as _}; use crate::core::service::{self, SERVICE_MANAGER, ServiceStatus}; use smartstring::SmartString; diff --git a/src-tauri/src/cmd/system.rs b/src-tauri/src/cmd/system.rs index 8ba1e6c67..27a4f5f6c 100644 --- a/src-tauri/src/cmd/system.rs +++ b/src-tauri/src/cmd/system.rs @@ -10,7 +10,7 @@ use crate::{ #[cfg(target_os = "windows")] use deelevate::{PrivilegeLevel, Token}; use once_cell::sync::Lazy; -use tauri_plugin_clipboard_manager::ClipboardExt; +use tauri_plugin_clipboard_manager::ClipboardExt as _; use tokio::time::Instant; // 存储应用启动时间的全局变量 diff --git a/src-tauri/src/cmd/validate.rs b/src-tauri/src/cmd/validate.rs index 33536e3b7..a0134f340 100644 --- a/src-tauri/src/cmd/validate.rs +++ b/src-tauri/src/cmd/validate.rs @@ -1,6 +1,6 @@ use super::CmdResult; use crate::{ - core::{validate::CoreConfigValidator, *}, + core::{handle, validate::CoreConfigValidator}, logging, utils::logging::Type, }; diff --git a/src-tauri/src/cmd/verge.rs b/src-tauri/src/cmd/verge.rs index c7b05fe5d..943eb9db6 100644 --- a/src-tauri/src/cmd/verge.rs +++ b/src-tauri/src/cmd/verge.rs @@ -1,5 +1,5 @@ use super::CmdResult; -use crate::{cmd::StringifyErr, config::IVerge, feat, utils::draft::SharedBox}; +use crate::{cmd::StringifyErr as _, config::IVerge, feat, utils::draft::SharedBox}; /// 获取Verge配置 #[tauri::command] diff --git a/src-tauri/src/cmd/webdav.rs b/src-tauri/src/cmd/webdav.rs index 20ae9d048..30e246540 100644 --- a/src-tauri/src/cmd/webdav.rs +++ b/src-tauri/src/cmd/webdav.rs @@ -1,6 +1,6 @@ use super::CmdResult; use crate::{ - cmd::StringifyErr, + cmd::StringifyErr as _, config::{Config, IVerge}, core, feat, }; diff --git a/src-tauri/src/config/clash.rs b/src-tauri/src/config/clash.rs index 195ec6c93..095a0b812 100644 --- a/src-tauri/src/config/clash.rs +++ b/src-tauri/src/config/clash.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; use serde_yaml_ng::{Mapping, Value}; use std::{ net::{IpAddr, Ipv4Addr, SocketAddr}, - str::FromStr, + str::FromStr as _, }; #[derive(Default, Debug, Clone)] diff --git a/src-tauri/src/config/encrypt.rs b/src-tauri/src/config/encrypt.rs index 44ac85b15..71743fae4 100644 --- a/src-tauri/src/config/encrypt.rs +++ b/src-tauri/src/config/encrypt.rs @@ -1,9 +1,9 @@ use crate::utils::dirs::get_encryption_key; use aes_gcm::{ Aes256Gcm, Key, - aead::{Aead, KeyInit}, + aead::{Aead as _, KeyInit as _}, }; -use base64::{Engine, engine::general_purpose::STANDARD}; +use base64::{Engine as _, engine::general_purpose::STANDARD}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::cell::Cell; use std::future::Future; diff --git a/src-tauri/src/config/prfitem.rs b/src-tauri/src/config/prfitem.rs index e817d7108..bdbac0183 100644 --- a/src-tauri/src/config/prfitem.rs +++ b/src-tauri/src/config/prfitem.rs @@ -6,7 +6,7 @@ use crate::{ tmpl, }, }; -use anyhow::{Context, Result, bail}; +use anyhow::{Context as _, Result, bail}; use serde::{Deserialize, Serialize}; use serde_yaml_ng::Mapping; use smartstring::alias::String; diff --git a/src-tauri/src/config/profiles.rs b/src-tauri/src/config/profiles.rs index 0ee3557ef..0f8a83671 100644 --- a/src-tauri/src/config/profiles.rs +++ b/src-tauri/src/config/profiles.rs @@ -1,10 +1,10 @@ use super::{PrfOption, prfitem::PrfItem}; use crate::utils::{ - dirs::{self, PathBufExec}, + dirs::{self, PathBufExec as _}, help, }; use crate::{logging, utils::logging::Type}; -use anyhow::{Context, Result, bail}; +use anyhow::{Context as _, Result, bail}; use serde::{Deserialize, Serialize}; use serde_yaml_ng::Mapping; use smartstring::alias::String; diff --git a/src-tauri/src/core/backup.rs b/src-tauri/src/core/backup.rs index 229139801..ad3168546 100644 --- a/src-tauri/src/core/backup.rs +++ b/src-tauri/src/core/backup.rs @@ -13,7 +13,7 @@ use smartstring::alias::String; use std::{ collections::HashMap, env::{consts::OS, temp_dir}, - io::Write, + io::Write as _, path::PathBuf, sync::Arc, time::Duration, diff --git a/src-tauri/src/core/event_driven_proxy.rs b/src-tauri/src/core/event_driven_proxy.rs index e6a711490..b4257be26 100644 --- a/src-tauri/src/core/event_driven_proxy.rs +++ b/src-tauri/src/core/event_driven_proxy.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use tokio::sync::RwLock; use tokio::sync::{mpsc, oneshot}; use tokio::time::{Duration, sleep, timeout}; -use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream}; +use tokio_stream::{StreamExt as _, wrappers::UnboundedReceiverStream}; use crate::config::{Config, IVerge}; use crate::core::{async_proxy_query::AsyncProxyQuery, handle}; diff --git a/src-tauri/src/core/handle.rs b/src-tauri/src/core/handle.rs index 894688f96..a5c973a61 100644 --- a/src-tauri/src/core/handle.rs +++ b/src-tauri/src/core/handle.rs @@ -8,8 +8,8 @@ use std::{ }, thread, }; -use tauri::{AppHandle, Manager, WebviewWindow}; -use tauri_plugin_mihomo::{Mihomo, MihomoExt}; +use tauri::{AppHandle, Manager as _, WebviewWindow}; +use tauri_plugin_mihomo::{Mihomo, MihomoExt as _}; use tokio::sync::RwLockReadGuard; use super::notification::{ErrorMessage, FrontendEvent, NotificationSystem}; diff --git a/src-tauri/src/core/hotkey.rs b/src-tauri/src/core/hotkey.rs index f2b2d0be0..44db1f019 100755 --- a/src-tauri/src/core/hotkey.rs +++ b/src-tauri/src/core/hotkey.rs @@ -8,7 +8,7 @@ use anyhow::{Result, bail}; use arc_swap::ArcSwap; use smartstring::alias::String; use std::{collections::HashMap, fmt, str::FromStr, sync::Arc}; -use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, ShortcutState}; +use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt as _, ShortcutState}; /// Enum representing all available hotkey functions #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] diff --git a/src-tauri/src/core/manager/config.rs b/src-tauri/src/core/manager/config.rs index 295230ab0..64283a590 100644 --- a/src-tauri/src/core/manager/config.rs +++ b/src-tauri/src/core/manager/config.rs @@ -1,6 +1,6 @@ use super::CoreManager; use crate::{ - config::*, + config::{Config, ConfigType, IRuntime}, constants::timing, core::{handle, validate::CoreConfigValidator}, logging, diff --git a/src-tauri/src/core/manager/state.rs b/src-tauri/src/core/manager/state.rs index 2be3be1ba..520c1f2a1 100644 --- a/src-tauri/src/core/manager/state.rs +++ b/src-tauri/src/core/manager/state.rs @@ -16,7 +16,7 @@ use compact_str::CompactString; use flexi_logger::DeferredNow; use log::Level; use scopeguard::defer; -use tauri_plugin_shell::ShellExt; +use tauri_plugin_shell::ShellExt as _; impl CoreManager { pub async fn get_clash_logs(&self) -> Result> { diff --git a/src-tauri/src/core/notification.rs b/src-tauri/src/core/notification.rs index 83680c249..fba5ecaa1 100644 --- a/src-tauri/src/core/notification.rs +++ b/src-tauri/src/core/notification.rs @@ -14,7 +14,7 @@ use std::{ thread, time::{Duration, Instant}, }; -use tauri::{Emitter, WebviewWindow}; +use tauri::{Emitter as _, WebviewWindow}; #[derive(Debug, Clone)] pub enum FrontendEvent { diff --git a/src-tauri/src/core/service.rs b/src-tauri/src/core/service.rs index 1737b9666..4fa48dba2 100644 --- a/src-tauri/src/core/service.rs +++ b/src-tauri/src/core/service.rs @@ -4,7 +4,7 @@ use crate::{ logging, logging_error, utils::{dirs, init::service_writer_config, logging::Type}, }; -use anyhow::{Context, Result, bail}; +use anyhow::{Context as _, Result, bail}; use clash_verge_service_ipc::CoreConfig; use compact_str::CompactString; use once_cell::sync::Lazy; diff --git a/src-tauri/src/core/sysopt.rs b/src-tauri/src/core/sysopt.rs index 964afa068..67a75b620 100644 --- a/src-tauri/src/core/sysopt.rs +++ b/src-tauri/src/core/sysopt.rs @@ -12,7 +12,7 @@ use smartstring::alias::String; use std::sync::atomic::{AtomicBool, Ordering}; #[cfg(not(target_os = "windows"))] use sysproxy::{Autoproxy, Sysproxy}; -use tauri_plugin_autostart::ManagerExt; +use tauri_plugin_autostart::ManagerExt as _; pub struct Sysopt { initialed: AtomicBool, diff --git a/src-tauri/src/core/timer.rs b/src-tauri/src/core/timer.rs index c19c43dd2..20c0bb6c0 100644 --- a/src-tauri/src/core/timer.rs +++ b/src-tauri/src/core/timer.rs @@ -2,7 +2,7 @@ use crate::{ config::Config, core::sysopt::Sysopt, feat, logging, logging_error, singleton, utils::logging::Type, }; -use anyhow::{Context, Result}; +use anyhow::{Context as _, Result}; use delay_timer::prelude::{DelayTimer, DelayTimerBuilder, TaskBuilder}; use parking_lot::RwLock; use smartstring::alias::String; diff --git a/src-tauri/src/core/validate.rs b/src-tauri/src/core/validate.rs index bfb31b628..330c96f3c 100644 --- a/src-tauri/src/core/validate.rs +++ b/src-tauri/src/core/validate.rs @@ -2,7 +2,7 @@ use anyhow::Result; use scopeguard::defer; use smartstring::alias::String; use std::sync::atomic::{AtomicBool, Ordering}; -use tauri_plugin_shell::ShellExt; +use tauri_plugin_shell::ShellExt as _; use tokio::fs; use crate::config::{Config, ConfigType}; diff --git a/src-tauri/src/enhance/mod.rs b/src-tauri/src/enhance/mod.rs index 7699ca133..6b74b74a0 100644 --- a/src-tauri/src/enhance/mod.rs +++ b/src-tauri/src/enhance/mod.rs @@ -5,7 +5,14 @@ mod script; pub mod seq; mod tun; -use self::{chain::*, field::*, merge::*, script::*, seq::*, tun::*}; +use self::{ + chain::{AsyncChainItemFrom as _, ChainItem, ChainType}, + field::{use_keys, use_lowercase, use_sort}, + merge::use_merge, + script::use_script, + seq::{SeqMap, use_seq}, + tun::use_tun, +}; use crate::constants; use crate::utils::dirs; use crate::{config::Config, utils::tmpl}; diff --git a/src-tauri/src/feat/backup.rs b/src-tauri/src/feat/backup.rs index 2aac55056..d55b1eaf1 100644 --- a/src-tauri/src/feat/backup.rs +++ b/src-tauri/src/feat/backup.rs @@ -4,7 +4,7 @@ use crate::{ logging, logging_error, process::AsyncHandler, utils::{ - dirs::{PathBufExec, app_home_dir, local_backup_dir}, + dirs::{PathBufExec as _, app_home_dir, local_backup_dir}, logging::Type, }, }; diff --git a/src-tauri/src/feat/profile.rs b/src-tauri/src/feat/profile.rs index ff4e42928..83446aeb9 100644 --- a/src-tauri/src/feat/profile.rs +++ b/src-tauri/src/feat/profile.rs @@ -7,7 +7,7 @@ use crate::{ }; use anyhow::{Result, bail}; use smartstring::alias::String; -use tauri::Emitter; +use tauri::Emitter as _; /// Toggle proxy profile pub async fn toggle_proxy_profile(profile_index: String) { diff --git a/src-tauri/src/feat/proxy.rs b/src-tauri/src/feat/proxy.rs index 9f7d513e8..69d4e0339 100644 --- a/src-tauri/src/feat/proxy.rs +++ b/src-tauri/src/feat/proxy.rs @@ -5,7 +5,7 @@ use crate::{ utils::logging::Type, }; use std::env; -use tauri_plugin_clipboard_manager::ClipboardExt; +use tauri_plugin_clipboard_manager::ClipboardExt as _; /// Toggle system proxy on/off pub async fn toggle_system_proxy() { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 42758a4f9..a702bc571 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -26,10 +26,10 @@ use anyhow::Result; use config::Config; use once_cell::sync::OnceCell; use rust_i18n::i18n; -use tauri::{AppHandle, Manager}; +use tauri::{AppHandle, Manager as _}; #[cfg(target_os = "macos")] use tauri_plugin_autostart::MacosLauncher; -use tauri_plugin_deep_link::DeepLinkExt; +use tauri_plugin_deep_link::DeepLinkExt as _; use utils::logging::Type; i18n!("locales", fallback = "zh"); diff --git a/src-tauri/src/module/lightweight.rs b/src-tauri/src/module/lightweight.rs index 30dd141cb..25978b163 100644 --- a/src-tauri/src/module/lightweight.rs +++ b/src-tauri/src/module/lightweight.rs @@ -10,10 +10,10 @@ use crate::{ use crate::logging_error; use crate::utils::window_manager::WindowManager; -use anyhow::{Context, Result}; +use anyhow::{Context as _, Result}; use delay_timer::prelude::TaskBuilder; use std::sync::atomic::{AtomicU8, AtomicU32, Ordering}; -use tauri::Listener; +use tauri::Listener as _; const LIGHT_WEIGHT_TASK_UID: &str = "light_weight_task"; diff --git a/src-tauri/src/utils/dirs.rs b/src-tauri/src/utils/dirs.rs index f83af90f4..6f678696e 100644 --- a/src-tauri/src/utils/dirs.rs +++ b/src-tauri/src/utils/dirs.rs @@ -9,7 +9,7 @@ use once_cell::sync::OnceCell; #[cfg(unix)] use std::iter; use std::{fs, path::PathBuf}; -use tauri::Manager; +use tauri::Manager as _; #[cfg(not(feature = "verge-dev"))] pub static APP_ID: &str = "io.github.clash-verge-rev.clash-verge-rev"; diff --git a/src-tauri/src/utils/help.rs b/src-tauri/src/utils/help.rs index 1fc58d036..a33b365a4 100644 --- a/src-tauri/src/utils/help.rs +++ b/src-tauri/src/utils/help.rs @@ -1,5 +1,5 @@ use crate::{config::with_encryption, enhance::seq::SeqMap, logging, utils::logging::Type}; -use anyhow::{Context, Result, anyhow, bail}; +use anyhow::{Context as _, Result, anyhow, bail}; use nanoid::nanoid; use serde::{Serialize, de::DeserializeOwned}; use serde_yaml_ng::Mapping; diff --git a/src-tauri/src/utils/init.rs b/src-tauri/src/utils/init.rs index cb8d3e6c8..5a3853a46 100644 --- a/src-tauri/src/utils/init.rs +++ b/src-tauri/src/utils/init.rs @@ -2,26 +2,26 @@ #[cfg(not(feature = "tauri-dev"))] use crate::utils::logging::NoModuleFilter; use crate::{ - config::*, + config::{Config, IClashTemp, IProfiles, IVerge}, constants, core::handle, logging, process::AsyncHandler, utils::{ - dirs::{self, PathBufExec, service_log_dir, sidecar_log_dir}, + dirs::{self, PathBufExec as _, service_log_dir, sidecar_log_dir}, help, logging::Type, }, }; use anyhow::Result; -use chrono::{Local, TimeZone}; +use chrono::{Local, TimeZone as _}; use clash_verge_service_ipc::WriterConfig; use flexi_logger::writers::FileLogWriter; use flexi_logger::{Cleanup, Criterion, FileSpec}; #[cfg(not(feature = "tauri-dev"))] use flexi_logger::{Duplicate, LogSpecBuilder, Logger}; -use std::{path::PathBuf, str::FromStr}; -use tauri_plugin_shell::ShellExt; +use std::{path::PathBuf, str::FromStr as _}; +use tauri_plugin_shell::ShellExt as _; use tokio::fs; use tokio::fs::DirEntry; diff --git a/src-tauri/src/utils/logging.rs b/src-tauri/src/utils/logging.rs index 35353c81e..7b5de419b 100644 --- a/src-tauri/src/utils/logging.rs +++ b/src-tauri/src/utils/logging.rs @@ -3,7 +3,7 @@ use flexi_logger::DeferredNow; #[cfg(not(feature = "tauri-dev"))] use flexi_logger::filter::LogLineFilter; use flexi_logger::writers::FileLogWriter; -use flexi_logger::writers::LogWriter; +use flexi_logger::writers::LogWriter as _; use log::Level; use log::Record; use std::{fmt, sync::Arc}; diff --git a/src-tauri/src/utils/notification.rs b/src-tauri/src/utils/notification.rs index 5370132d3..d7ff371a5 100644 --- a/src-tauri/src/utils/notification.rs +++ b/src-tauri/src/utils/notification.rs @@ -1,5 +1,5 @@ use crate::{core::handle, utils::i18n}; -use tauri_plugin_notification::NotificationExt; +use tauri_plugin_notification::NotificationExt as _; pub enum NotificationEvent<'a> { DashboardToggled, diff --git a/src-tauri/src/utils/resolve/dns.rs b/src-tauri/src/utils/resolve/dns.rs index e225f0c80..0896bccaf 100644 --- a/src-tauri/src/utils/resolve/dns.rs +++ b/src-tauri/src/utils/resolve/dns.rs @@ -3,7 +3,7 @@ use crate::{logging, utils::logging::Type}; pub async fn set_public_dns(dns_server: String) { use crate::utils::logging::Type; use crate::{core::handle, logging, utils::dirs}; - use tauri_plugin_shell::ShellExt; + use tauri_plugin_shell::ShellExt as _; let app_handle = handle::Handle::app_handle(); logging!(info, Type::Config, "try to set system dns"); @@ -50,7 +50,7 @@ pub async fn set_public_dns(dns_server: String) { #[cfg(target_os = "macos")] pub async fn restore_public_dns() { use crate::{core::handle, utils::dirs}; - use tauri_plugin_shell::ShellExt; + use tauri_plugin_shell::ShellExt as _; let app_handle = handle::Handle::app_handle(); logging!(info, Type::Config, "try to unset system dns"); let resource_dir = match dirs::app_resources_dir() { diff --git a/src-tauri/src/utils/server.rs b/src-tauri/src/utils/server.rs index 9e8e12296..f4d7ea6ad 100644 --- a/src-tauri/src/utils/server.rs +++ b/src-tauri/src/utils/server.rs @@ -14,7 +14,7 @@ use reqwest::ClientBuilder; use smartstring::alias::String; use std::time::Duration; use tokio::sync::oneshot; -use warp::Filter; +use warp::Filter as _; #[derive(serde::Deserialize, Debug)] struct QueryParam { diff --git a/src-tauri/src/utils/window_manager.rs b/src-tauri/src/utils/window_manager.rs index 801b38942..c6a535696 100644 --- a/src-tauri/src/utils/window_manager.rs +++ b/src-tauri/src/utils/window_manager.rs @@ -5,7 +5,7 @@ use crate::{ }; use std::future::Future; use std::pin::Pin; -use tauri::{Manager, WebviewWindow, Wry}; +use tauri::{Manager as _, WebviewWindow, Wry}; use once_cell::sync::OnceCell; use parking_lot::Mutex; From 221a1e89ebe9bc2d380b5e5d0627edc461116e6f Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Sun, 9 Nov 2025 22:37:52 +0800 Subject: [PATCH 23/49] chore: update Changelog for v2.4.4 with Mihomo(Meta) kernel upgrade and additional improvements --- Changelog.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Changelog.md b/Changelog.md index c03c99e33..6abbdba65 100644 --- a/Changelog.md +++ b/Changelog.md @@ -5,6 +5,7 @@
✨ 新增功能 +- **Mihomo(Meta) 内核升级至 v1.19.16** - 支持连接页面各个项目的排序
@@ -14,6 +15,8 @@ - 替换前端信息编辑组件,提供更好性能 - 优化后端内存和性能表现 +- 防止退出时可能的禁用 TUN 失败 +- i18n 支持 From 0dcdd7fed60d5aa32c3fbec4eabc63208e615b33 Mon Sep 17 00:00:00 2001 From: Slinetrac Date: Sun, 9 Nov 2025 23:23:03 +0800 Subject: [PATCH 24/49] fix: clippy lint --- src-tauri/src/cmd/uwp.rs | 2 +- src-tauri/src/core/event_driven_proxy.rs | 2 +- src-tauri/src/core/service.rs | 4 ++-- src-tauri/src/core/sysopt.rs | 2 +- src-tauri/src/lib.rs | 21 +++++++++++++-------- src-tauri/src/module/signal/windows.rs | 2 +- src-tauri/src/utils/autostart.rs | 6 +++--- src-tauri/src/utils/init.rs | 2 +- 8 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src-tauri/src/cmd/uwp.rs b/src-tauri/src/cmd/uwp.rs index 96db7eed0..779029583 100644 --- a/src-tauri/src/cmd/uwp.rs +++ b/src-tauri/src/cmd/uwp.rs @@ -4,7 +4,7 @@ use crate::cmd::CmdResult; #[cfg(windows)] mod platform { use crate::cmd::CmdResult; - use crate::cmd::StringifyErr; + use crate::cmd::StringifyErr as _; use crate::core::win_uwp; pub fn invoke_uwp_tool() -> CmdResult { diff --git a/src-tauri/src/core/event_driven_proxy.rs b/src-tauri/src/core/event_driven_proxy.rs index b4257be26..e03e9eeca 100644 --- a/src-tauri/src/core/event_driven_proxy.rs +++ b/src-tauri/src/core/event_driven_proxy.rs @@ -520,7 +520,7 @@ impl EventDrivenProxyManager { use crate::utils::dirs; #[allow(unused_imports)] // creation_flags必须 - use std::os::windows::process::CommandExt; + use std::os::windows::process::CommandExt as _; use tokio::process::Command; let binary_path = match dirs::service_path() { diff --git a/src-tauri/src/core/service.rs b/src-tauri/src/core/service.rs index 4fa48dba2..892294d95 100644 --- a/src-tauri/src/core/service.rs +++ b/src-tauri/src/core/service.rs @@ -37,7 +37,7 @@ async fn uninstall_service() -> Result<()> { use deelevate::{PrivilegeLevel, Token}; use runas::Command as RunasCommand; - use std::os::windows::process::CommandExt; + use std::os::windows::process::CommandExt as _; let binary_path = dirs::service_path()?; let uninstall_path = binary_path.with_file_name("clash-verge-service-uninstall.exe"); @@ -72,7 +72,7 @@ async fn install_service() -> Result<()> { use deelevate::{PrivilegeLevel, Token}; use runas::Command as RunasCommand; - use std::os::windows::process::CommandExt; + use std::os::windows::process::CommandExt as _; let binary_path = dirs::service_path()?; let install_path = binary_path.with_file_name("clash-verge-service-install.exe"); diff --git a/src-tauri/src/core/sysopt.rs b/src-tauri/src/core/sysopt.rs index 67a75b620..bd5383d97 100644 --- a/src-tauri/src/core/sysopt.rs +++ b/src-tauri/src/core/sysopt.rs @@ -59,7 +59,7 @@ async fn execute_sysproxy_command(args: Vec) -> Result<()> use crate::utils::dirs; use anyhow::bail; #[allow(unused_imports)] // Required for .creation_flags() method - use std::os::windows::process::CommandExt; + use std::os::windows::process::CommandExt as _; use tokio::process::Command; let binary_path = dirs::service_path()?; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index a702bc571..6858379ac 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -11,19 +11,14 @@ mod module; mod process; pub mod utils; use crate::constants::files; -#[cfg(target_os = "macos")] -use crate::module::lightweight; #[cfg(target_os = "linux")] use crate::utils::linux; -#[cfg(target_os = "macos")] -use crate::utils::window_manager::WindowManager; use crate::{ - core::{EventDrivenProxyManager, handle, hotkey}, + core::{EventDrivenProxyManager, handle}, process::AsyncHandler, utils::{resolve, server}, }; use anyhow::Result; -use config::Config; use once_cell::sync::OnceCell; use rust_i18n::i18n; use tauri::{AppHandle, Manager as _}; @@ -269,8 +264,18 @@ pub fn run() { .invoke_handler(app_init::generate_handlers()); mod event_handlers { - use super::*; - use crate::core::handle; + #[cfg(target_os = "macos")] + use crate::module::lightweight; + #[cfg(target_os = "macos")] + use crate::utils::window_manager::WindowManager; + use crate::{ + config::Config, + core::{self, handle, hotkey}, + logging, + process::AsyncHandler, + utils::logging::Type, + }; + use tauri::AppHandle; pub fn handle_ready_resumed(_app_handle: &AppHandle) { if handle::Handle::global().is_exiting() { diff --git a/src-tauri/src/module/signal/windows.rs b/src-tauri/src/module/signal/windows.rs index f9c13b67b..f10b415cc 100644 --- a/src-tauri/src/module/signal/windows.rs +++ b/src-tauri/src/module/signal/windows.rs @@ -1,4 +1,4 @@ -use tauri::Manager; +use tauri::Manager as _; use windows_sys::Win32::{ Foundation::{HWND, LPARAM, LRESULT, WPARAM}, UI::WindowsAndMessaging::{ diff --git a/src-tauri/src/utils/autostart.rs b/src-tauri/src/utils/autostart.rs index b4dc4965a..d898a6d09 100644 --- a/src-tauri/src/utils/autostart.rs +++ b/src-tauri/src/utils/autostart.rs @@ -4,7 +4,7 @@ use crate::{logging, utils::logging::Type}; use anyhow::{Result, anyhow}; #[cfg(target_os = "windows")] -use std::{os::windows::process::CommandExt, path::Path, path::PathBuf}; +use std::{os::windows::process::CommandExt as _, path::Path, path::PathBuf}; /// Windows 下的开机启动文件夹路径 #[cfg(target_os = "windows")] @@ -37,7 +37,7 @@ pub fn get_exe_path() -> Result { /// 创建快捷方式 #[cfg(target_os = "windows")] pub async fn create_shortcut() -> Result<()> { - use crate::utils::dirs::PathBufExec; + use crate::utils::dirs::PathBufExec as _; let exe_path = get_exe_path()?; let startup_dir = get_startup_dir()?; @@ -90,7 +90,7 @@ pub async fn create_shortcut() -> Result<()> { /// 删除快捷方式 #[cfg(target_os = "windows")] pub async fn remove_shortcut() -> Result<()> { - use crate::utils::dirs::PathBufExec; + use crate::utils::dirs::PathBufExec as _; let startup_dir = get_startup_dir()?; let old_shortcut_path = startup_dir.join("Clash-Verge.lnk"); diff --git a/src-tauri/src/utils/init.rs b/src-tauri/src/utils/init.rs index 5a3853a46..cbeea286a 100644 --- a/src-tauri/src/utils/init.rs +++ b/src-tauri/src/utils/init.rs @@ -466,7 +466,7 @@ pub async fn init_resources() -> Result<()> { #[cfg(target_os = "windows")] pub fn init_scheme() -> Result<()> { use tauri::utils::platform::current_exe; - use winreg::{RegKey, enums::*}; + use winreg::{RegKey, enums::HKEY_CURRENT_USER}; let app_exe = current_exe()?; let app_exe = dunce::canonicalize(app_exe)?; From c03e10b5dc521340ae1af47244c50f6b494218f8 Mon Sep 17 00:00:00 2001 From: Slinetrac Date: Sun, 9 Nov 2025 23:36:39 +0800 Subject: [PATCH 25/49] fix: macOS clippy lint --- src-tauri/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 6858379ac..c11ba74e9 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -276,6 +276,8 @@ pub fn run() { utils::logging::Type, }; use tauri::AppHandle; + #[cfg(target_os = "macos")] + use tauri::Manager; pub fn handle_ready_resumed(_app_handle: &AppHandle) { if handle::Handle::global().is_exiting() { From a8fe5a71f7b2dfe6d5ff5a25c9d8afa5b73e0329 Mon Sep 17 00:00:00 2001 From: Slinetrac Date: Sun, 9 Nov 2025 23:46:23 +0800 Subject: [PATCH 26/49] fix: use Manager as _ --- src-tauri/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index c11ba74e9..88e1ffecf 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -277,7 +277,7 @@ pub fn run() { }; use tauri::AppHandle; #[cfg(target_os = "macos")] - use tauri::Manager; + use tauri::Manager as _; pub fn handle_ready_resumed(_app_handle: &AppHandle) { if handle::Handle::global().is_exiting() { From 204bfa36e53a5df8d5064a1391132d4b31efe35d Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Mon, 10 Nov 2025 02:12:16 +0800 Subject: [PATCH 27/49] refactor: reorganize imports and remove unused axios instance code --- src/components/home/ip-info-card.tsx | 6 ++-- src/pages/_layout/useLayoutEvents.ts | 2 -- src/services/api.ts | 41 +--------------------------- 3 files changed, 4 insertions(+), 45 deletions(-) diff --git a/src/components/home/ip-info-card.tsx b/src/components/home/ip-info-card.tsx index a8f81d381..18360e550 100644 --- a/src/components/home/ip-info-card.tsx +++ b/src/components/home/ip-info-card.tsx @@ -1,11 +1,11 @@ import { LocationOnOutlined, RefreshOutlined, - VisibilityOutlined, VisibilityOffOutlined, + VisibilityOutlined, } from "@mui/icons-material"; -import { Box, Typography, Button, Skeleton, IconButton } from "@mui/material"; -import { useState, useEffect, useCallback, memo } from "react"; +import { Box, Button, IconButton, Skeleton, Typography } from "@mui/material"; +import { memo, useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { getIpInfo } from "@/services/api"; diff --git a/src/pages/_layout/useLayoutEvents.ts b/src/pages/_layout/useLayoutEvents.ts index c26084a3f..0b7955b30 100644 --- a/src/pages/_layout/useLayoutEvents.ts +++ b/src/pages/_layout/useLayoutEvents.ts @@ -4,7 +4,6 @@ import { useEffect } from "react"; import { mutate } from "swr"; import { useListen } from "@/hooks/use-listen"; -import { getAxios } from "@/services/api"; export const useLayoutEvents = ( handleNotice: (payload: [string, string]) => void, @@ -39,7 +38,6 @@ export const useLayoutEvents = ( register( addListener("verge://refresh-clash-config", async () => { - await getAxios(true); mutate("getProxies"); mutate("getVersion"); mutate("getClashConfig"); diff --git a/src/services/api.ts b/src/services/api.ts index a5508509f..2ad41c272 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -1,44 +1,4 @@ import { fetch } from "@tauri-apps/plugin-http"; -import axios, { AxiosInstance } from "axios"; - -import { getClashInfo } from "./cmds"; - -let instancePromise: Promise = null!; - -async function getInstancePromise() { - let server = ""; - let secret = ""; - - try { - const info = await getClashInfo(); - - if (info?.server) { - server = info.server; - - // compatible width `external-controller` - if (server.startsWith(":")) server = `127.0.0.1${server}`; - else if (/^\d+$/.test(server)) server = `127.0.0.1:${server}`; - } - if (info?.secret) secret = info?.secret; - } catch {} - - const axiosIns = axios.create({ - baseURL: `http://${server}`, - headers: secret ? { Authorization: `Bearer ${secret}` } : {}, - timeout: 15000, - }); - axiosIns.interceptors.response.use((r) => r.data); - return axiosIns; -} - -/// initialize some information -/// enable force update axiosIns -export const getAxios = async (force: boolean = false) => { - if (!instancePromise || force) { - instancePromise = getInstancePromise(); - } - return instancePromise; -}; // Get current IP and geolocation information (refactored IP detection with service-specific mappings) interface IpInfo { @@ -213,6 +173,7 @@ export const getIpInfo = async (): Promise => { timeoutId = setTimeout(() => { timeoutController.abort(); }, service.timeout || serviceTimeout); + console.debug("Fetching IP information..."); const response = await fetch(service.url, { method: "GET", From e66a4f6acad98752a11902cb1feaae0586819835 Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Mon, 10 Nov 2025 07:15:33 +0800 Subject: [PATCH 28/49] perf: some inline magic for Draft This would improve 8-14% CPU for all API --- src-tauri/Cargo.lock | 47 ------------------------------------ src-tauri/Cargo.toml | 4 --- src-tauri/src/utils/draft.rs | 7 ++++++ 3 files changed, 7 insertions(+), 51 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 596ffbfff..b7bf8c381 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2,15 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "addr2line" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" -dependencies = [ - "gimli", -] - [[package]] name = "adler2" version = "2.0.1" @@ -581,21 +572,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "backtrace" -version = "0.3.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-link 0.2.1", -] - [[package]] name = "base62" version = "2.2.3" @@ -2826,12 +2802,6 @@ dependencies = [ "polyval", ] -[[package]] -name = "gimli" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" - [[package]] name = "gio" version = "0.18.4" @@ -4941,15 +4911,6 @@ dependencies = [ "objc2-security", ] -[[package]] -name = "object" -version = "0.37.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" -dependencies = [ - "memchr", -] - [[package]] name = "once_cell" version = "1.21.3" @@ -5158,10 +5119,8 @@ version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ - "backtrace", "cfg-if", "libc", - "petgraph", "redox_syscall 0.5.18", "smallvec", "windows-link 0.2.1", @@ -6409,12 +6368,6 @@ dependencies = [ "ordered-multimap", ] -[[package]] -name = "rustc-demangle" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" - [[package]] name = "rustc-hash" version = "2.1.1" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 9f57f1eab..6efd1d19a 100755 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -171,10 +171,6 @@ crate-type = ["staticlib", "cdylib", "rlib"] [dev-dependencies] criterion = { version = "0.7.0", features = ["async_tokio"] } -parking_lot = { version = "0.12.5", features = [ - "hardware-lock-elision", - "deadlock_detection", -] } [lints.clippy] # Core categories - most important for code safety and correctness diff --git a/src-tauri/src/utils/draft.rs b/src-tauri/src/utils/draft.rs index 8eab1f302..07782f4bd 100644 --- a/src-tauri/src/utils/draft.rs +++ b/src-tauri/src/utils/draft.rs @@ -12,12 +12,14 @@ pub struct Draft { } impl Draft { + #[inline] pub fn new(data: T) -> Self { Self { inner: Arc::new(RwLock::new((Arc::new(Box::new(data)), None))), } } /// 以 Arc> 的形式获取当前“已提交(正式)”数据的快照(零拷贝,仅 clone Arc) + #[inline] pub fn data_arc(&self) -> SharedBox { let guard = self.inner.read(); Arc::clone(&guard.0) @@ -25,6 +27,7 @@ impl Draft { /// 获取当前(草稿若存在则返回草稿,否则返回已提交)的快照 /// 这也是零拷贝:只 clone Arc,不 clone T + #[inline] pub fn latest_arc(&self) -> SharedBox { let guard = self.inner.read(); guard.1.clone().unwrap_or_else(|| Arc::clone(&guard.0)) @@ -33,6 +36,7 @@ impl Draft { /// 通过闭包以可变方式编辑草稿(在闭包中我们给出 &mut T) /// - 延迟拷贝:如果只有这一个 Arc 引用,则直接修改,不会克隆 T; /// - 若草稿被其他读者共享,Arc::make_mut 会做一次 T.clone(最小必要拷贝)。 + #[inline] pub fn edit_draft(&self, f: F) -> R where F: FnOnce(&mut T) -> R, @@ -56,6 +60,7 @@ impl Draft { } /// 将草稿提交到已提交位置(替换),并清除草稿 + #[inline] pub fn apply(&self) { let mut guard = self.inner.write(); if let Some(d) = guard.1.take() { @@ -64,6 +69,7 @@ impl Draft { } /// 丢弃草稿(如果存在) + #[inline] pub fn discard(&self) { let mut guard = self.inner.write(); guard.1 = None; @@ -71,6 +77,7 @@ impl Draft { /// 异步地以拥有 Box 的方式修改已提交数据:将克隆一次已提交数据到本地, /// 异步闭包返回新的 Box(替换已提交数据)和业务返回值 R。 + #[inline] pub async fn with_data_modify(&self, f: F) -> Result where T: Send + Sync + 'static, From 49e5a557caa0753e312419237cc9651ac7136324 Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Mon, 10 Nov 2025 09:23:30 +0800 Subject: [PATCH 29/49] refactor: add inline annotations to CommandChildGuard methods --- src-tauri/src/process/guard.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src-tauri/src/process/guard.rs b/src-tauri/src/process/guard.rs index 0ffb89990..d1a41ab50 100644 --- a/src-tauri/src/process/guard.rs +++ b/src-tauri/src/process/guard.rs @@ -7,6 +7,7 @@ use crate::{logging, utils::logging::Type}; pub struct CommandChildGuard(Option); impl Drop for CommandChildGuard { + #[inline] fn drop(&mut self) { if let Err(err) = self.kill() { logging!( @@ -20,10 +21,12 @@ impl Drop for CommandChildGuard { } impl CommandChildGuard { + #[inline] pub const fn new(child: CommandChild) -> Self { Self(Some(child)) } + #[inline] pub fn kill(&mut self) -> Result<()> { if let Some(child) = self.0.take() { let _ = child.kill(); @@ -31,6 +34,7 @@ impl CommandChildGuard { Ok(()) } + #[inline] pub fn pid(&self) -> Option { self.0.as_ref().map(|c| c.pid()) } From b30825102212d02a134570746e56b6aa9df9975c Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Mon, 10 Nov 2025 09:30:55 +0800 Subject: [PATCH 30/49] refactor: replace async_runtime::block_on with AsyncHandler::block_on and add inline annotations --- src-tauri/src/lib.rs | 4 ++-- src-tauri/src/process/async_handler.rs | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 88e1ffecf..c3b374ca0 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -36,7 +36,7 @@ mod app_init { /// Initialize singleton monitoring for other instances pub fn init_singleton_check() -> Result<()> { - tauri::async_runtime::block_on(async move { + AsyncHandler::block_on(async move { logging!(info, Type::Setup, "开始检查单例实例..."); server::check_singleton().await?; Ok(()) @@ -425,7 +425,7 @@ pub fn run() { }); } tauri::RunEvent::ExitRequested { api, code, .. } => { - tauri::async_runtime::block_on(async { + AsyncHandler::block_on(async { let _ = handle::Handle::mihomo() .await .clear_all_ws_connections() diff --git a/src-tauri/src/process/async_handler.rs b/src-tauri/src/process/async_handler.rs index eeb60cf57..8c7966207 100644 --- a/src-tauri/src/process/async_handler.rs +++ b/src-tauri/src/process/async_handler.rs @@ -12,6 +12,7 @@ impl AsyncHandler { // async_runtime::handle() // } + #[inline] #[track_caller] pub fn spawn(f: F) -> JoinHandle<()> where @@ -23,6 +24,7 @@ impl AsyncHandler { async_runtime::spawn(f()) } + #[inline] #[track_caller] pub fn spawn_blocking(f: F) -> JoinHandle where @@ -34,7 +36,7 @@ impl AsyncHandler { async_runtime::spawn_blocking(f) } - #[allow(dead_code)] + #[inline] #[track_caller] pub fn block_on(fut: Fut) -> Fut::Output where From 78d5cb5ecaf84e9e18e2d0cb801a0f04e4fb05ee Mon Sep 17 00:00:00 2001 From: Cactus <79493862+erbanku@users.noreply.github.com> Date: Mon, 10 Nov 2025 11:54:41 +0800 Subject: [PATCH 31/49] i18n: remove proxy chain icon due to design inconsistency (#5377) style: prettier --- src/locales/ar/proxies.json | 2 +- src/locales/de/proxies.json | 2 +- src/locales/en/proxies.json | 2 +- src/locales/es/proxies.json | 2 +- src/locales/fa/proxies.json | 2 +- src/locales/id/proxies.json | 2 +- src/locales/jp/proxies.json | 2 +- src/locales/ko/proxies.json | 2 +- src/locales/ru/proxies.json | 2 +- src/locales/tr/proxies.json | 2 +- src/locales/tt/proxies.json | 2 +- src/locales/zh/proxies.json | 2 +- src/locales/zhtw/proxies.json | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/locales/ar/proxies.json b/src/locales/ar/proxies.json index 995a4d39f..071069f0c 100644 --- a/src/locales/ar/proxies.json +++ b/src/locales/ar/proxies.json @@ -6,7 +6,7 @@ "direct": "Direct" }, "actions": { - "toggleChain": "🔗 بروكسي السلسلة", + "toggleChain": "بروكسي السلسلة", "connect": "Connect", "disconnect": "Disconnect", "connecting": "Connecting...", diff --git a/src/locales/de/proxies.json b/src/locales/de/proxies.json index 022f60302..92e11588a 100644 --- a/src/locales/de/proxies.json +++ b/src/locales/de/proxies.json @@ -6,7 +6,7 @@ "direct": "Direct" }, "actions": { - "toggleChain": "🔗 Ketten-Proxy", + "toggleChain": "Ketten-Proxy", "connect": "Connect", "disconnect": "Disconnect", "connecting": "Connecting...", diff --git a/src/locales/en/proxies.json b/src/locales/en/proxies.json index a359ee324..f7cba5d77 100644 --- a/src/locales/en/proxies.json +++ b/src/locales/en/proxies.json @@ -6,7 +6,7 @@ "direct": "Direct" }, "actions": { - "toggleChain": "🔗 Chain Proxy", + "toggleChain": "Chain Proxy", "connect": "Connect", "disconnect": "Disconnect", "connecting": "Connecting...", diff --git a/src/locales/es/proxies.json b/src/locales/es/proxies.json index 1699dd5ae..719d4e5a3 100644 --- a/src/locales/es/proxies.json +++ b/src/locales/es/proxies.json @@ -6,7 +6,7 @@ "direct": "Direct" }, "actions": { - "toggleChain": "🔗 Proxy en cadena", + "toggleChain": "Proxy en cadena", "connect": "Connect", "disconnect": "Disconnect", "connecting": "Connecting...", diff --git a/src/locales/fa/proxies.json b/src/locales/fa/proxies.json index 20bebdfa8..d2e7c0b6d 100644 --- a/src/locales/fa/proxies.json +++ b/src/locales/fa/proxies.json @@ -6,7 +6,7 @@ "direct": "Direct" }, "actions": { - "toggleChain": "🔗 پراکسی زنجیره‌ای", + "toggleChain": "پراکسی زنجیره‌ای", "connect": "Connect", "disconnect": "Disconnect", "connecting": "Connecting...", diff --git a/src/locales/id/proxies.json b/src/locales/id/proxies.json index cc1d01776..26945b1d6 100644 --- a/src/locales/id/proxies.json +++ b/src/locales/id/proxies.json @@ -6,7 +6,7 @@ "direct": "Direct" }, "actions": { - "toggleChain": "🔗 Proxy Rantai", + "toggleChain": "Proxy Rantai", "connect": "Connect", "disconnect": "Disconnect", "connecting": "Connecting...", diff --git a/src/locales/jp/proxies.json b/src/locales/jp/proxies.json index 7667b6943..e46215b70 100644 --- a/src/locales/jp/proxies.json +++ b/src/locales/jp/proxies.json @@ -6,7 +6,7 @@ "direct": "Direct" }, "actions": { - "toggleChain": "🔗 チェーンプロキシ", + "toggleChain": "チェーンプロキシ", "connect": "Connect", "disconnect": "Disconnect", "connecting": "Connecting...", diff --git a/src/locales/ko/proxies.json b/src/locales/ko/proxies.json index 09ce8674d..00e6b383e 100644 --- a/src/locales/ko/proxies.json +++ b/src/locales/ko/proxies.json @@ -6,7 +6,7 @@ "direct": "직접" }, "actions": { - "toggleChain": "🔗 체인 프록시", + "toggleChain": "체인 프록시", "connect": "연결", "disconnect": "연결 해제", "connecting": "연결 중...", diff --git a/src/locales/ru/proxies.json b/src/locales/ru/proxies.json index fca9ccf26..32224adff 100644 --- a/src/locales/ru/proxies.json +++ b/src/locales/ru/proxies.json @@ -6,7 +6,7 @@ "direct": "Direct" }, "actions": { - "toggleChain": "🔗 Цепной прокси", + "toggleChain": "Цепной прокси", "connect": "Connect", "disconnect": "Disconnect", "connecting": "Connecting...", diff --git a/src/locales/tr/proxies.json b/src/locales/tr/proxies.json index ac098259b..89e71d09f 100644 --- a/src/locales/tr/proxies.json +++ b/src/locales/tr/proxies.json @@ -6,7 +6,7 @@ "direct": "Direct" }, "actions": { - "toggleChain": "🔗 Zincir Proxy", + "toggleChain": "Zincir Proxy", "connect": "Connect", "disconnect": "Disconnect", "connecting": "Connecting...", diff --git a/src/locales/tt/proxies.json b/src/locales/tt/proxies.json index 202044318..b35b89172 100644 --- a/src/locales/tt/proxies.json +++ b/src/locales/tt/proxies.json @@ -6,7 +6,7 @@ "direct": "Direct" }, "actions": { - "toggleChain": "🔗 Чылбыр прокси", + "toggleChain": "Чылбыр прокси", "connect": "Connect", "disconnect": "Disconnect", "connecting": "Connecting...", diff --git a/src/locales/zh/proxies.json b/src/locales/zh/proxies.json index dbcfdc607..e95db1d0f 100644 --- a/src/locales/zh/proxies.json +++ b/src/locales/zh/proxies.json @@ -6,7 +6,7 @@ "direct": "直连" }, "actions": { - "toggleChain": "🔗 链式代理", + "toggleChain": "链式代理", "connect": "连接", "disconnect": "断开", "connecting": "连接中...", diff --git a/src/locales/zhtw/proxies.json b/src/locales/zhtw/proxies.json index d6a010377..f4a1e32c4 100644 --- a/src/locales/zhtw/proxies.json +++ b/src/locales/zhtw/proxies.json @@ -6,7 +6,7 @@ "direct": "直連" }, "actions": { - "toggleChain": "🔗 鏈式代理", + "toggleChain": "鏈式代理", "connect": "連線", "disconnect": "中斷", "connecting": "連線中...", From 838e40179640e71efae194459ce81e21cd05595d Mon Sep 17 00:00:00 2001 From: Sline Date: Mon, 10 Nov 2025 13:49:14 +0800 Subject: [PATCH 32/49] feat(auto-backup): implement centralized auto-backup manager and UI (#5374) * feat(auto-backup): implement centralized auto-backup manager and UI - Introduced AutoBackupManager to handle verge settings, run a background scheduler, debounce change-driven backups, and trim auto-labeled archives (keeps 20); wired into startup and config refresh hooks (src-tauri/src/module/auto_backup.rs:28-209, src-tauri/src/utils/resolve/mod.rs:64-136, src-tauri/src/feat/config.rs:102-238) - Extended verge schema and backup helpers so scheduled/change-based settings persist, create_local_backup can rename archives, and profile/global-extend mutations now trigger backups (src-tauri/src/config/verge.rs:162-536, src/types/types.d.ts:857-859, src-tauri/src/feat/backup.rs:125-189, src-tauri/src/cmd/profile.rs:66-476, src-tauri/src/cmd/save_profile.rs:21-82) - Added Auto Backup settings panel in backup dialog with dual toggles + interval selector; localized new strings across all locales (src/components/setting/mods/auto-backup-settings.tsx:1-138, src/components/setting/mods/backup-viewer.tsx:28-309, src/locales/en/settings.json:312-326 and mirrored entries) - Regenerated typed i18n resources for strong typing in React (src/types/generated/i18n-keys.ts, src/types/generated/i18n-resources.ts) * refactor(setting/backup): restructure backup dialog for consistent layout * refactor(ui): unify settings dialog style * fix(backup): only trigger auto-backup on valid saves & restore restarts app safely * fix(backup): scrub console.log leak and rewire WebDAV dialog to actually probe server * refactor: rename SubscriptionChange to ProfileChange * chore: update i18n * chore: WebDAV i18n improvements * refactor(backup): error handling * refactor(auto-backup): wrap scheduler startup with maybe_start_runner * refactor: remove the redundant throw in handleExport * feat(backup-history-viewer): improve WebDAV handling and UI fallback * feat(auto-backup): trigger backups on all profile edits & improve interval input UX * refactor: use InputAdornment * docs: Changelog.md --- Changelog.md | 2 + src-tauri/src/cmd/profile.rs | 5 + src-tauri/src/cmd/save_profile.rs | 31 +- src-tauri/src/config/verge.rs | 15 + src-tauri/src/feat/backup.rs | 14 +- src-tauri/src/feat/config.rs | 6 +- src-tauri/src/module/auto_backup.rs | 332 ++++++++++++ src-tauri/src/module/mod.rs | 1 + src-tauri/src/utils/resolve/mod.rs | 7 +- .../setting/mods/auto-backup-settings.tsx | 205 +++++++ .../setting/mods/backup-config-viewer.tsx | 8 - .../setting/mods/backup-history-viewer.tsx | 344 ++++++++++++ .../setting/mods/backup-table-viewer.tsx | 347 ------------ src/components/setting/mods/backup-viewer.tsx | 499 +++++++----------- .../setting/mods/backup-webdav-dialog.tsx | 87 +++ .../setting/mods/local-backup-actions.tsx | 80 --- src/locales/ar/settings.json | 31 +- src/locales/ar/shared.json | 5 +- src/locales/de/settings.json | 31 +- src/locales/de/shared.json | 5 +- src/locales/en/settings.json | 31 +- src/locales/en/shared.json | 5 +- src/locales/es/settings.json | 31 +- src/locales/es/shared.json | 5 +- src/locales/fa/settings.json | 31 +- src/locales/fa/shared.json | 5 +- src/locales/id/settings.json | 31 +- src/locales/id/shared.json | 5 +- src/locales/jp/settings.json | 31 +- src/locales/jp/shared.json | 5 +- src/locales/ko/settings.json | 31 +- src/locales/ko/shared.json | 5 +- src/locales/ru/settings.json | 31 +- src/locales/ru/shared.json | 5 +- src/locales/tr/settings.json | 31 +- src/locales/tr/shared.json | 5 +- src/locales/tt/settings.json | 31 +- src/locales/tt/shared.json | 5 +- src/locales/zh/settings.json | 31 +- src/locales/zh/shared.json | 5 +- src/locales/zhtw/settings.json | 31 +- src/locales/zhtw/shared.json | 5 +- src/types/generated/i18n-keys.ts | 22 + src/types/generated/i18n-resources.ts | 32 ++ src/types/types.d.ts | 3 + 45 files changed, 1714 insertions(+), 794 deletions(-) create mode 100644 src-tauri/src/module/auto_backup.rs create mode 100644 src/components/setting/mods/auto-backup-settings.tsx create mode 100644 src/components/setting/mods/backup-history-viewer.tsx delete mode 100644 src/components/setting/mods/backup-table-viewer.tsx create mode 100644 src/components/setting/mods/backup-webdav-dialog.tsx delete mode 100644 src/components/setting/mods/local-backup-actions.tsx diff --git a/Changelog.md b/Changelog.md index 6abbdba65..fa1ab4072 100644 --- a/Changelog.md +++ b/Changelog.md @@ -7,6 +7,7 @@ - **Mihomo(Meta) 内核升级至 v1.19.16** - 支持连接页面各个项目的排序 +- 实现可选的自动备份 @@ -17,6 +18,7 @@ - 优化后端内存和性能表现 - 防止退出时可能的禁用 TUN 失败 - i18n 支持 +- 优化备份设置布局 diff --git a/src-tauri/src/cmd/profile.rs b/src-tauri/src/cmd/profile.rs index 8f2a0f207..edb94795a 100644 --- a/src-tauri/src/cmd/profile.rs +++ b/src-tauri/src/cmd/profile.rs @@ -11,6 +11,7 @@ use crate::{ }, core::{CoreManager, handle, timer::Timer, tray::Tray}, feat, logging, + module::auto_backup::{AutoBackupManager, AutoBackupTrigger}, process::AsyncHandler, ret_err, utils::{dirs, help, logging::Type}, @@ -90,6 +91,7 @@ pub async fn import_profile(url: std::string::String, option: Option) } logging!(info, Type::Cmd, "[导入订阅] 导入完成: {}", url); + AutoBackupManager::trigger_backup(AutoBackupTrigger::ProfileChange); Ok(()) } @@ -122,6 +124,7 @@ pub async fn create_profile(item: PrfItem, file_data: Option) -> CmdResu handle::Handle::notify_profile_changed(uid.clone()); } Config::profiles().await.apply(); + AutoBackupManager::trigger_backup(AutoBackupTrigger::ProfileChange); Ok(()) } Err(err) => { @@ -164,6 +167,7 @@ pub async fn delete_profile(index: String) -> CmdResult { // 发送配置变更通知 logging!(info, Type::Cmd, "[删除订阅] 发送配置变更通知: {}", index); handle::Handle::notify_profile_changed(index); + AutoBackupManager::trigger_backup(AutoBackupTrigger::ProfileChange); } Err(e) => { logging!(error, Type::Cmd, "{}", e); @@ -460,6 +464,7 @@ pub async fn patch_profile(index: String, profile: PrfItem) -> CmdResult { }); } + AutoBackupManager::trigger_backup(AutoBackupTrigger::ProfileChange); Ok(()) } diff --git a/src-tauri/src/cmd/save_profile.rs b/src-tauri/src/cmd/save_profile.rs index 21f2c4040..ed271504b 100644 --- a/src-tauri/src/cmd/save_profile.rs +++ b/src-tauri/src/cmd/save_profile.rs @@ -4,6 +4,7 @@ use crate::{ config::{Config, PrfItem}, core::{CoreManager, handle, validate::CoreConfigValidator}, logging, + module::auto_backup::{AutoBackupManager, AutoBackupTrigger}, utils::{dirs, logging::Type}, }; use smartstring::alias::String; @@ -17,6 +18,12 @@ pub async fn save_profile_file(index: String, file_data: Option) -> CmdR None => return Ok(()), }; + let backup_trigger = match index.as_str() { + "Merge" => Some(AutoBackupTrigger::GlobalMerge), + "Script" => Some(AutoBackupTrigger::GlobalScript), + _ => Some(AutoBackupTrigger::ProfileChange), + }; + // 在异步操作前获取必要元数据并释放锁 let (rel_path, is_merge_file) = { let profiles = Config::profiles().await; @@ -51,11 +58,17 @@ pub async fn save_profile_file(index: String, file_data: Option) -> CmdR is_merge_file ); - if is_merge_file { - return handle_merge_file(&file_path_str, &file_path, &original_content).await; + let changes_applied = if is_merge_file { + handle_merge_file(&file_path_str, &file_path, &original_content).await? + } else { + handle_full_validation(&file_path_str, &file_path, &original_content).await? + }; + + if changes_applied && let Some(trigger) = backup_trigger { + AutoBackupManager::trigger_backup(trigger); } - handle_full_validation(&file_path_str, &file_path, &original_content).await + Ok(()) } async fn restore_original( @@ -76,7 +89,7 @@ async fn handle_merge_file( file_path_str: &str, file_path: &std::path::Path, original_content: &str, -) -> CmdResult { +) -> CmdResult { logging!( info, Type::Config, @@ -96,7 +109,7 @@ async fn handle_merge_file( } else { handle::Handle::refresh_clash(); } - Ok(()) + Ok(true) } Ok((false, error_msg)) => { logging!( @@ -108,7 +121,7 @@ async fn handle_merge_file( restore_original(file_path, original_content).await?; let result = (false, error_msg.clone()); crate::cmd::validate::handle_yaml_validation_notice(&result, "合并配置文件"); - Ok(()) + Ok(false) } Err(e) => { logging!(error, Type::Config, "[cmd配置save] 验证过程发生错误: {}", e); @@ -122,11 +135,11 @@ async fn handle_full_validation( file_path_str: &str, file_path: &std::path::Path, original_content: &str, -) -> CmdResult { +) -> CmdResult { match CoreConfigValidator::validate_config_file(file_path_str, None).await { Ok((true, _)) => { logging!(info, Type::Config, "[cmd配置save] 验证成功"); - Ok(()) + Ok(true) } Ok((false, error_msg)) => { logging!(warn, Type::Config, "[cmd配置save] 验证失败: {}", error_msg); @@ -160,7 +173,7 @@ async fn handle_full_validation( handle::Handle::notice_message("config_validate::error", error_msg.to_owned()); } - Ok(()) + Ok(false) } Err(e) => { logging!(error, Type::Config, "[cmd配置save] 验证过程发生错误: {}", e); diff --git a/src-tauri/src/config/verge.rs b/src-tauri/src/config/verge.rs index 3a19e451e..9a25cf34c 100644 --- a/src-tauri/src/config/verge.rs +++ b/src-tauri/src/config/verge.rs @@ -158,6 +158,15 @@ pub struct IVerge { /// 0: 不清理; 1: 1天;2: 7天; 3: 30天; 4: 90天 pub auto_log_clean: Option, + /// Enable scheduled automatic backups + pub enable_auto_backup_schedule: Option, + + /// Automatic backup interval in hours + pub auto_backup_interval_hours: Option, + + /// Create backups automatically when critical configs change + pub auto_backup_on_change: Option, + /// verge 的各种 port 用于覆盖 clash 的各种 port #[cfg(not(target_os = "windows"))] pub verge_redir_port: Option, @@ -422,6 +431,9 @@ impl IVerge { auto_check_update: Some(true), enable_builtin_enhanced: Some(true), auto_log_clean: Some(2), // 1: 1天, 2: 7天, 3: 30天, 4: 90天 + enable_auto_backup_schedule: Some(false), + auto_backup_interval_hours: Some(24), + auto_backup_on_change: Some(true), webdav_url: None, webdav_username: None, webdav_password: None, @@ -517,6 +529,9 @@ impl IVerge { patch!(proxy_layout_column); patch!(test_list); patch!(auto_log_clean); + patch!(enable_auto_backup_schedule); + patch!(auto_backup_interval_hours); + patch!(auto_backup_on_change); patch!(webdav_url); patch!(webdav_username); diff --git a/src-tauri/src/feat/backup.rs b/src-tauri/src/feat/backup.rs index d55b1eaf1..055b1d680 100644 --- a/src-tauri/src/feat/backup.rs +++ b/src-tauri/src/feat/backup.rs @@ -123,6 +123,15 @@ pub async fn restore_webdav_backup(filename: String) -> Result<()> { /// Create a backup and save to local storage pub async fn create_local_backup() -> Result<()> { + create_local_backup_with_namer(|name| name.to_string().into()) + .await + .map(|_| ()) +} + +pub async fn create_local_backup_with_namer(namer: F) -> Result +where + F: FnOnce(&str) -> String, +{ let (file_name, temp_file_path) = backup::create_backup().await.map_err(|err| { logging!( error, @@ -133,7 +142,8 @@ pub async fn create_local_backup() -> Result<()> { })?; let backup_dir = local_backup_dir()?; - let target_path = backup_dir.join(file_name.as_str()); + let final_name = namer(file_name.as_str()); + let target_path = backup_dir.join(final_name.as_str()); if let Err(err) = move_file(temp_file_path.clone(), target_path.clone()).await { logging!( @@ -152,7 +162,7 @@ pub async fn create_local_backup() -> Result<()> { return Err(err); } - Ok(()) + Ok(final_name) } async fn move_file(from: PathBuf, to: PathBuf) -> Result<()> { diff --git a/src-tauri/src/feat/config.rs b/src-tauri/src/feat/config.rs index 2bd2cfa87..e7aec2a1f 100644 --- a/src-tauri/src/feat/config.rs +++ b/src-tauri/src/feat/config.rs @@ -2,7 +2,7 @@ use crate::{ config::{Config, IVerge}, core::{CoreManager, handle, hotkey, sysopt, tray}, logging_error, - module::lightweight, + module::{auto_backup::AutoBackupManager, lightweight}, utils::{draft::SharedBox, logging::Type}, }; use anyhow::Result; @@ -243,6 +243,10 @@ pub async fn patch_verge(patch: &IVerge, not_save_file: bool) -> Result<()> { return Err(err); } Config::verge().await.apply(); + logging_error!( + Type::Backup, + AutoBackupManager::global().refresh_settings().await + ); if !not_save_file { // 分离数据获取和异步调用 let verge_data = Config::verge().await.data_arc(); diff --git a/src-tauri/src/module/auto_backup.rs b/src-tauri/src/module/auto_backup.rs new file mode 100644 index 000000000..2cb9c89bb --- /dev/null +++ b/src-tauri/src/module/auto_backup.rs @@ -0,0 +1,332 @@ +use crate::{ + config::{Config, IVerge}, + feat::create_local_backup_with_namer, + logging, + process::AsyncHandler, + utils::{dirs::local_backup_dir, logging::Type}, +}; +use anyhow::Result; +use chrono::Local; +use once_cell::sync::OnceCell; +use parking_lot::RwLock; +use std::{ + path::PathBuf, + sync::{ + Arc, + atomic::{AtomicBool, AtomicI64, Ordering}, + }, + time::{Duration, UNIX_EPOCH}, +}; +use tokio::{ + fs, + sync::{Mutex, watch}, +}; + +const DEFAULT_INTERVAL_HOURS: u64 = 24; +const MIN_INTERVAL_HOURS: u64 = 1; +const MAX_INTERVAL_HOURS: u64 = 168; +const MIN_BACKUP_INTERVAL_SECS: i64 = 60; +const AUTO_BACKUP_KEEP: usize = 20; +const AUTO_MARKER: &str = "-auto-"; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum AutoBackupTrigger { + Scheduled, + GlobalMerge, + GlobalScript, + ProfileChange, +} + +impl AutoBackupTrigger { + const fn slug(self) -> &'static str { + match self { + Self::Scheduled => "scheduled", + Self::GlobalMerge => "merge", + Self::GlobalScript => "script", + Self::ProfileChange => "profile", + } + } + + const fn is_schedule(self) -> bool { + matches!(self, Self::Scheduled) + } +} + +#[derive(Clone, Copy, Debug)] +struct AutoBackupSettings { + schedule_enabled: bool, + interval_hours: u64, + change_enabled: bool, +} + +impl AutoBackupSettings { + fn from_verge(verge: &IVerge) -> Self { + let interval = verge + .auto_backup_interval_hours + .unwrap_or(DEFAULT_INTERVAL_HOURS) + .clamp(MIN_INTERVAL_HOURS, MAX_INTERVAL_HOURS); + + Self { + schedule_enabled: verge.enable_auto_backup_schedule.unwrap_or(false), + interval_hours: interval, + change_enabled: verge.auto_backup_on_change.unwrap_or(true), + } + } +} + +impl Default for AutoBackupSettings { + fn default() -> Self { + Self { + schedule_enabled: false, + interval_hours: DEFAULT_INTERVAL_HOURS, + change_enabled: true, + } + } +} + +pub struct AutoBackupManager { + settings: Arc>, + settings_tx: watch::Sender, + runner_started: AtomicBool, + exec_lock: Mutex<()>, + last_backup: AtomicI64, +} + +impl AutoBackupManager { + pub fn global() -> &'static Self { + static INSTANCE: OnceCell = OnceCell::new(); + INSTANCE.get_or_init(|| { + let (tx, _rx) = watch::channel(AutoBackupSettings::default()); + Self { + settings: Arc::new(RwLock::new(AutoBackupSettings::default())), + settings_tx: tx, + runner_started: AtomicBool::new(false), + exec_lock: Mutex::new(()), + last_backup: AtomicI64::new(0), + } + }) + } + + pub async fn init(&self) -> Result<()> { + let settings = Self::load_settings().await; + { + *self.settings.write() = settings; + } + let _ = self.settings_tx.send(settings); + self.maybe_start_runner(settings); + Ok(()) + } + + pub async fn refresh_settings(&self) -> Result<()> { + let settings = Self::load_settings().await; + { + *self.settings.write() = settings; + } + let _ = self.settings_tx.send(settings); + self.maybe_start_runner(settings); + Ok(()) + } + + pub fn trigger_backup(trigger: AutoBackupTrigger) { + AsyncHandler::spawn(move || async move { + if let Err(err) = Self::global().execute_trigger(trigger).await { + logging!( + warn, + Type::Backup, + "Auto backup execution failed ({:?}): {err:#?}", + trigger + ); + } + }); + } + + fn maybe_start_runner(&self, settings: AutoBackupSettings) { + if settings.schedule_enabled { + self.ensure_runner(); + } + } + + fn ensure_runner(&self) { + if self.runner_started.swap(true, Ordering::SeqCst) { + return; + } + + let mut rx = self.settings_tx.subscribe(); + AsyncHandler::spawn(move || async move { + Self::run_scheduler(&mut rx).await; + }); + } + + async fn run_scheduler(rx: &mut watch::Receiver) { + let mut current = *rx.borrow(); + loop { + if !current.schedule_enabled { + if rx.changed().await.is_err() { + break; + } + current = *rx.borrow(); + continue; + } + + let duration = Duration::from_secs(current.interval_hours.saturating_mul(3600)); + let sleeper = tokio::time::sleep(duration); + tokio::pin!(sleeper); + + tokio::select! { + _ = &mut sleeper => { + if let Err(err) = Self::global() + .execute_trigger(AutoBackupTrigger::Scheduled) + .await + { + logging!( + warn, + Type::Backup, + "Scheduled auto backup failed: {err:#?}" + ); + } + } + changed = rx.changed() => { + if changed.is_err() { + break; + } + current = *rx.borrow(); + } + } + } + } + + async fn execute_trigger(&self, trigger: AutoBackupTrigger) -> Result<()> { + let snapshot = *self.settings.read(); + + if trigger.is_schedule() && !snapshot.schedule_enabled { + return Ok(()); + } + if !trigger.is_schedule() && !snapshot.change_enabled { + return Ok(()); + } + + if !self.should_run_now() { + return Ok(()); + } + + let _guard = self.exec_lock.lock().await; + if !self.should_run_now() { + return Ok(()); + } + + let file_name = + create_local_backup_with_namer(|name| append_auto_suffix(name, trigger.slug()).into()) + .await?; + self.last_backup + .store(Local::now().timestamp(), Ordering::Release); + + if let Err(err) = cleanup_auto_backups().await { + logging!( + warn, + Type::Backup, + "Failed to cleanup old auto backups: {err:#?}" + ); + } + + logging!( + info, + Type::Backup, + "Auto backup created ({:?}): {}", + trigger, + file_name + ); + Ok(()) + } + + fn should_run_now(&self) -> bool { + let last = self.last_backup.load(Ordering::Acquire); + if last == 0 { + return true; + } + let now = Local::now().timestamp(); + now.saturating_sub(last) >= MIN_BACKUP_INTERVAL_SECS + } + + async fn load_settings() -> AutoBackupSettings { + let verge = Config::verge().await; + AutoBackupSettings::from_verge(&verge.latest_arc()) + } +} + +fn append_auto_suffix(file_name: &str, slug: &str) -> String { + match file_name.rsplit_once('.') { + Some((stem, ext)) => format!("{stem}{AUTO_MARKER}{slug}.{ext}"), + None => format!("{file_name}{AUTO_MARKER}{slug}"), + } +} + +async fn cleanup_auto_backups() -> Result<()> { + if AUTO_BACKUP_KEEP == 0 { + return Ok(()); + } + + let backup_dir = local_backup_dir()?; + if !backup_dir.exists() { + return Ok(()); + } + + let mut entries = match fs::read_dir(&backup_dir).await { + Ok(dir) => dir, + Err(err) => { + logging!( + warn, + Type::Backup, + "Failed to read backup directory: {err:#?}" + ); + return Ok(()); + } + }; + + let mut files: Vec<(PathBuf, u64)> = Vec::new(); + + while let Some(entry) = entries.next_entry().await? { + let path = entry.path(); + if !path.is_file() { + continue; + } + + let file_name = match entry.file_name().into_string() { + Ok(name) => name, + Err(_) => continue, + }; + + if !file_name.contains(AUTO_MARKER) { + continue; + } + + let modified = entry + .metadata() + .await + .and_then(|meta| meta.modified()) + .ok() + .and_then(|time| time.duration_since(UNIX_EPOCH).ok()) + .map(|dur| dur.as_secs()) + .unwrap_or(0); + + files.push((path, modified)); + } + + if files.len() <= AUTO_BACKUP_KEEP { + return Ok(()); + } + + files.sort_by_key(|(_, ts)| *ts); + let remove_count = files.len() - AUTO_BACKUP_KEEP; + for (path, _) in files.into_iter().take(remove_count) { + if let Err(err) = fs::remove_file(&path).await { + logging!( + warn, + Type::Backup, + "Failed to remove auto backup {}: {err:#?}", + path.display() + ); + } + } + + Ok(()) +} diff --git a/src-tauri/src/module/mod.rs b/src-tauri/src/module/mod.rs index 372ad5ba0..87055c6ca 100644 --- a/src-tauri/src/module/mod.rs +++ b/src-tauri/src/module/mod.rs @@ -1,3 +1,4 @@ +pub mod auto_backup; pub mod lightweight; pub mod signal; pub mod sysinfo; diff --git a/src-tauri/src/utils/resolve/mod.rs b/src-tauri/src/utils/resolve/mod.rs index 542cac466..b855b35c4 100644 --- a/src-tauri/src/utils/resolve/mod.rs +++ b/src-tauri/src/utils/resolve/mod.rs @@ -10,7 +10,7 @@ use crate::{ tray::Tray, }, logging, logging_error, - module::{lightweight::auto_lightweight_boot, signal}, + module::{auto_backup::AutoBackupManager, lightweight::auto_lightweight_boot, signal}, process::AsyncHandler, utils::{init, logging::Type, server, window_manager::WindowManager}, }; @@ -68,6 +68,7 @@ pub fn resolve_setup_async() { init_timer(), init_hotkey(), init_auto_lightweight_boot(), + init_auto_backup(), ); }); } @@ -127,6 +128,10 @@ pub(super) async fn init_auto_lightweight_boot() { logging_error!(Type::Setup, auto_lightweight_boot().await); } +pub(super) async fn init_auto_backup() { + logging_error!(Type::Setup, AutoBackupManager::global().init().await); +} + pub(super) fn init_signal() { logging!(info, Type::Setup, "Initializing signal handlers..."); signal::register(); diff --git a/src/components/setting/mods/auto-backup-settings.tsx b/src/components/setting/mods/auto-backup-settings.tsx new file mode 100644 index 000000000..c9736ec7a --- /dev/null +++ b/src/components/setting/mods/auto-backup-settings.tsx @@ -0,0 +1,205 @@ +import { + InputAdornment, + ListItem, + ListItemText, + Stack, + TextField, +} from "@mui/material"; +import { useLockFn } from "ahooks"; +import { Fragment, useMemo, useState, type ChangeEvent } from "react"; +import { useTranslation } from "react-i18next"; + +import { Switch } from "@/components/base"; +import { useVerge } from "@/hooks/use-verge"; +import { showNotice } from "@/services/noticeService"; + +const MIN_INTERVAL_HOURS = 1; +const MAX_INTERVAL_HOURS = 168; + +interface AutoBackupState { + scheduleEnabled: boolean; + intervalHours: number; + changeEnabled: boolean; +} + +export function AutoBackupSettings() { + const { t } = useTranslation(); + const { verge, patchVerge } = useVerge(); + const derivedValues = useMemo(() => { + return { + scheduleEnabled: verge?.enable_auto_backup_schedule ?? false, + intervalHours: verge?.auto_backup_interval_hours ?? 24, + changeEnabled: verge?.auto_backup_on_change ?? true, + }; + }, [ + verge?.enable_auto_backup_schedule, + verge?.auto_backup_interval_hours, + verge?.auto_backup_on_change, + ]); + const [pendingValues, setPendingValues] = useState( + null, + ); + const values = useMemo(() => { + if (!pendingValues) { + return derivedValues; + } + if ( + pendingValues.scheduleEnabled === derivedValues.scheduleEnabled && + pendingValues.intervalHours === derivedValues.intervalHours && + pendingValues.changeEnabled === derivedValues.changeEnabled + ) { + return derivedValues; + } + return pendingValues; + }, [pendingValues, derivedValues]); + const [intervalInputDraft, setIntervalInputDraft] = useState( + null, + ); + + const applyPatch = useLockFn( + async ( + partial: Partial, + payload: Partial, + ) => { + const nextValues = { ...values, ...partial }; + setPendingValues(nextValues); + try { + await patchVerge(payload); + } catch (error) { + showNotice.error(error); + setPendingValues(null); + } + }, + ); + + const disabled = !verge; + + const handleScheduleToggle = ( + _: ChangeEvent, + checked: boolean, + ) => { + applyPatch( + { scheduleEnabled: checked }, + { + enable_auto_backup_schedule: checked, + auto_backup_interval_hours: values.intervalHours, + }, + ); + }; + + const handleChangeToggle = ( + _: ChangeEvent, + checked: boolean, + ) => { + applyPatch({ changeEnabled: checked }, { auto_backup_on_change: checked }); + }; + + const handleIntervalInputChange = (event: ChangeEvent) => { + setIntervalInputDraft(event.target.value); + }; + + const commitIntervalInput = () => { + const rawValue = intervalInputDraft ?? values.intervalHours.toString(); + const trimmed = rawValue.trim(); + if (trimmed === "") { + setIntervalInputDraft(null); + return; + } + + const parsed = Number(trimmed); + if (!Number.isFinite(parsed)) { + setIntervalInputDraft(null); + return; + } + + const clamped = Math.min( + MAX_INTERVAL_HOURS, + Math.max(MIN_INTERVAL_HOURS, Math.round(parsed)), + ); + + if (clamped === values.intervalHours) { + setIntervalInputDraft(null); + return; + } + + applyPatch( + { intervalHours: clamped }, + { auto_backup_interval_hours: clamped }, + ); + setIntervalInputDraft(null); + }; + + const scheduleDisabled = disabled || !values.scheduleEnabled; + + return ( + + + + + + + + + + + + { + if (event.key === "Enter") { + event.preventDefault(); + commitIntervalInput(); + } + }} + sx={{ minWidth: 160 }} + slotProps={{ + input: { + endAdornment: ( + + {t("shared.units.hours")} + + ), + }, + htmlInput: { + min: MIN_INTERVAL_HOURS, + max: MAX_INTERVAL_HOURS, + inputMode: "numeric", + }, + }} + /> + + + + + + + + + + + ); +} diff --git a/src/components/setting/mods/backup-config-viewer.tsx b/src/components/setting/mods/backup-config-viewer.tsx index b1323db6f..21ce89c14 100644 --- a/src/components/setting/mods/backup-config-viewer.tsx +++ b/src/components/setting/mods/backup-config-viewer.tsx @@ -58,14 +58,6 @@ export const BackupConfigViewer = memo( webdav_username !== username || webdav_password !== password; - console.log( - "webdavChanged", - webdavChanged, - webdav_url, - webdav_username, - webdav_password, - ); - const handleClickShowPassword = () => { setShowPassword((prev) => !prev); }; diff --git a/src/components/setting/mods/backup-history-viewer.tsx b/src/components/setting/mods/backup-history-viewer.tsx new file mode 100644 index 000000000..4a63fc743 --- /dev/null +++ b/src/components/setting/mods/backup-history-viewer.tsx @@ -0,0 +1,344 @@ +import DeleteOutline from "@mui/icons-material/DeleteOutline"; +import DownloadRounded from "@mui/icons-material/DownloadRounded"; +import RefreshRounded from "@mui/icons-material/RefreshRounded"; +import RestoreRounded from "@mui/icons-material/RestoreRounded"; +import { + Box, + Button, + IconButton, + List, + ListItem, + ListItemText, + ListSubheader, + Stack, + Tab, + Tabs, + Typography, +} from "@mui/material"; +import { save } from "@tauri-apps/plugin-dialog"; +import { useLockFn } from "ahooks"; +import dayjs from "dayjs"; +import customParseFormat from "dayjs/plugin/customParseFormat"; +import relativeTime from "dayjs/plugin/relativeTime"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; + +import { BaseDialog, BaseLoadingOverlay } from "@/components/base"; +import { useVerge } from "@/hooks/use-verge"; +import { + deleteLocalBackup, + deleteWebdavBackup, + exportLocalBackup, + listLocalBackup, + listWebDavBackup, + restartApp, + restoreLocalBackup, + restoreWebDavBackup, +} from "@/services/cmds"; +import { showNotice } from "@/services/noticeService"; + +dayjs.extend(customParseFormat); +dayjs.extend(relativeTime); + +const DATE_FORMAT = "YYYY-MM-DD_HH-mm-ss"; +const FILENAME_PATTERN = /\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}/; + +type BackupSource = "local" | "webdav"; + +interface BackupHistoryViewerProps { + open: boolean; + source: BackupSource; + page: number; + onSourceChange: (source: BackupSource) => void; + onPageChange: (page: number) => void; + onClose: () => void; +} + +interface BackupRow { + filename: string; + platform: string; + backup_time: dayjs.Dayjs; +} + +const confirmAsync = async (message: string) => { + const fn = window.confirm as (msg?: string) => boolean; + return fn(message); +}; + +export const BackupHistoryViewer = ({ + open, + source, + page, + onSourceChange, + onPageChange, + onClose, +}: BackupHistoryViewerProps) => { + const { t } = useTranslation(); + const { verge } = useVerge(); + const [rows, setRows] = useState([]); + const [loading, setLoading] = useState(false); + const [isRestarting, setIsRestarting] = useState(false); + const isLocal = source === "local"; + const isWebDavConfigured = Boolean( + verge?.webdav_url && verge?.webdav_username && verge?.webdav_password, + ); + const shouldSkipWebDav = !isLocal && !isWebDavConfigured; + const pageSize = 8; + const isBusy = loading || isRestarting; + + const buildRow = useCallback((filename: string): BackupRow | null => { + const platform = filename.split("-")[0]; + const match = filename.match(FILENAME_PATTERN); + if (!match) return null; + return { + filename, + platform, + backup_time: dayjs(match[0], DATE_FORMAT), + }; + }, []); + + const fetchRows = useCallback(async () => { + if (!open) return; + if (shouldSkipWebDav) { + setRows([]); + return; + } + setLoading(true); + try { + const list = isLocal ? await listLocalBackup() : await listWebDavBackup(); + setRows( + list + .map((item) => buildRow(item.filename)) + .filter((item): item is BackupRow => item !== null) + .sort((a, b) => (a.backup_time.isAfter(b.backup_time) ? -1 : 1)), + ); + } catch (error) { + console.error(error); + setRows([]); + showNotice.error(error); + } finally { + setLoading(false); + } + }, [buildRow, isLocal, open, shouldSkipWebDav]); + + useEffect(() => { + void fetchRows(); + }, [fetchRows]); + + const total = rows.length; + const pageCount = Math.max(1, Math.ceil(total / pageSize)); + const currentPage = Math.min(page, pageCount - 1); + const pagedRows = rows.slice( + currentPage * pageSize, + currentPage * pageSize + pageSize, + ); + + const summary = useMemo(() => { + if (shouldSkipWebDav) { + return t("settings.modals.backup.manual.webdav"); + } + if (!total) return t("settings.modals.backup.history.empty"); + const recent = rows[0]?.backup_time.fromNow(); + return t("settings.modals.backup.history.summary", { + count: total, + recent, + }); + }, [rows, shouldSkipWebDav, t, total]); + + const handleDelete = useLockFn(async (filename: string) => { + if (isRestarting) return; + if ( + !(await confirmAsync(t("settings.modals.backup.messages.confirmDelete"))) + ) + return; + if (isLocal) { + await deleteLocalBackup(filename); + } else { + await deleteWebdavBackup(filename); + } + await fetchRows(); + }); + + const handleRestore = useLockFn(async (filename: string) => { + if (isRestarting) return; + if ( + !(await confirmAsync(t("settings.modals.backup.messages.confirmRestore"))) + ) + return; + if (isLocal) { + await restoreLocalBackup(filename); + } else { + await restoreWebDavBackup(filename); + } + showNotice.success("settings.modals.backup.messages.restoreSuccess"); + setIsRestarting(true); + window.setTimeout(() => { + void restartApp().catch((err: unknown) => { + setIsRestarting(false); + showNotice.error(err); + }); + }, 1000); + }); + + const handleExport = useLockFn(async (filename: string) => { + if (isRestarting) return; + if (!isLocal) return; + const savePath = await save({ defaultPath: filename }); + if (!savePath || Array.isArray(savePath)) return; + try { + await exportLocalBackup(filename, savePath); + showNotice.success("settings.modals.backup.messages.localBackupExported"); + } catch (ignoreError: unknown) { + showNotice.error( + "settings.modals.backup.messages.localBackupExportFailed", + ); + } + }); + + const handleRefresh = () => { + if (isRestarting) return; + void fetchRows(); + }; + + return ( + + + + + + { + if (isBusy) return; + onSourceChange(val as BackupSource); + onPageChange(0); + }} + textColor="primary" + indicatorColor="primary" + > + + + + + + + + + {summary} + + + + {t("settings.modals.backup.history.title")} + + } + > + {pagedRows.length === 0 ? ( + + + + ) : ( + pagedRows.map((row) => ( + + {isLocal && ( + handleExport(row.filename)} + > + + + )} + handleDelete(row.filename)} + > + + + handleRestore(row.filename)} + > + + + + } + > + + + )) + )} + + + {pageCount > 1 && ( + + + {currentPage + 1} / {pageCount} + + + + + + + )} + + + + ); +}; diff --git a/src/components/setting/mods/backup-table-viewer.tsx b/src/components/setting/mods/backup-table-viewer.tsx deleted file mode 100644 index 13c9f9824..000000000 --- a/src/components/setting/mods/backup-table-viewer.tsx +++ /dev/null @@ -1,347 +0,0 @@ -import DeleteIcon from "@mui/icons-material/Delete"; -import DownloadIcon from "@mui/icons-material/Download"; -import RestoreIcon from "@mui/icons-material/Restore"; -import { - Box, - Paper, - IconButton, - Divider, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - TablePagination, -} from "@mui/material"; -import { Typography } from "@mui/material"; -import { save } from "@tauri-apps/plugin-dialog"; -import { useLockFn } from "ahooks"; -import { Dayjs } from "dayjs"; -import { SVGProps, memo } from "react"; -import { useTranslation } from "react-i18next"; - -import { restartApp } from "@/services/cmds"; -import { showNotice } from "@/services/noticeService"; - -export type BackupFile = { - platform: string; - backup_time: Dayjs; - allow_apply: boolean; - filename: string; -}; - -export const DEFAULT_ROWS_PER_PAGE = 5; - -type ConfirmFn = (message?: string) => boolean | Promise; - -// Normalizes synchronous and async confirm implementations. -const confirmAsync = async (message: string): Promise => { - const confirmFn = window.confirm as unknown as ConfirmFn; - return await confirmFn.call(window, message); -}; - -interface BackupTableViewerProps { - datasource: BackupFile[]; - page: number; - onPageChange: ( - event: React.MouseEvent | null, - page: number, - ) => void; - total: number; - onRefresh: () => Promise; - onDelete: (filename: string) => Promise; - onRestore: (filename: string) => Promise; - onExport?: (filename: string, destination: string) => Promise; -} - -export const BackupTableViewer = memo( - ({ - datasource, - page, - onPageChange, - total, - onRefresh, - onDelete, - onRestore, - onExport, - }: BackupTableViewerProps) => { - const { t } = useTranslation(); - - const handleDelete = useLockFn(async (filename: string) => { - await onDelete(filename); - await onRefresh(); - }); - - const handleRestore = useLockFn(async (filename: string) => { - await onRestore(filename).then(() => { - showNotice.success("settings.modals.backup.messages.restoreSuccess"); - }); - await restartApp(); - }); - - const handleExport = useLockFn(async (filename: string) => { - if (!onExport) { - return; - } - try { - const savePath = await save({ - defaultPath: filename, - }); - if (!savePath || Array.isArray(savePath)) { - return; - } - await onExport(filename, savePath); - showNotice.success( - "settings.modals.backup.messages.localBackupExported", - ); - } catch (error) { - console.error(error); - showNotice.error( - "settings.modals.backup.messages.localBackupExportFailed", - ); - } - }); - - return ( - - - - - - {t("settings.modals.backup.table.filename")} - - - {t("settings.modals.backup.table.backupTime")} - - - {t("settings.modals.backup.table.actions")} - - - - - {datasource.length > 0 ? ( - datasource.map((file) => { - const rowKey = `${file.platform}-${file.filename}-${file.backup_time.valueOf()}`; - return ( - - - {file.platform === "windows" ? ( - - ) : file.platform === "linux" ? ( - - ) : ( - - )} - {file.filename} - - - {file.backup_time.fromNow()} - - - - {onExport && ( - <> - { - e.preventDefault(); - await handleExport(file.filename); - }} - > - - - - - )} - { - e.preventDefault(); - const confirmed = await confirmAsync( - t( - "settings.modals.backup.messages.confirmDelete", - ), - ); - if (confirmed) { - await handleDelete(file.filename); - } - }} - > - - - - { - e.preventDefault(); - const confirmed = await confirmAsync( - t( - "settings.modals.backup.messages.confirmRestore", - ), - ); - if (confirmed) { - await handleRestore(file.filename); - } - }} - > - - - - - - ); - }) - ) : ( - - - - - {t("settings.modals.backup.table.noBackups")} - - - - - )} - -
- -
- ); - }, -); - -function LinuxIcon(props: SVGProps) { - return ( - - - - - - - - - - - - - - - - ); -} - -function WindowsIcon(props: SVGProps) { - return ( - - - - ); -} - -function MacIcon(props: SVGProps) { - return ( - - - - ); -} diff --git a/src/components/setting/mods/backup-viewer.tsx b/src/components/setting/mods/backup-viewer.tsx index 3a1c1ecca..b9b62b76c 100644 --- a/src/components/setting/mods/backup-viewer.tsx +++ b/src/components/setting/mods/backup-viewer.tsx @@ -1,354 +1,213 @@ -import { Box, Button, Divider, Paper, Tab, Tabs } from "@mui/material"; -import dayjs from "dayjs"; -import customParseFormat from "dayjs/plugin/customParseFormat"; -import type { Ref } from "react"; +import { LoadingButton } from "@mui/lab"; import { - useCallback, - useEffect, - useImperativeHandle, - useMemo, - useReducer, - useRef, - useState, -} from "react"; -import { createPortal } from "react-dom"; + Button, + List, + ListItem, + ListItemText, + Stack, + Typography, +} from "@mui/material"; +import { useLockFn } from "ahooks"; +import type { ReactNode, Ref } from "react"; +import { useImperativeHandle, useState } from "react"; import { useTranslation } from "react-i18next"; -import { BaseDialog, BaseLoadingOverlay, DialogRef } from "@/components/base"; -import { - deleteLocalBackup, - deleteWebdavBackup, - listLocalBackup, - listWebDavBackup, - exportLocalBackup, - restoreLocalBackup, - restoreWebDavBackup, -} from "@/services/cmds"; +import { BaseDialog, DialogRef } from "@/components/base"; +import { createLocalBackup, createWebdavBackup } from "@/services/cmds"; +import { showNotice } from "@/services/noticeService"; -import { BackupConfigViewer } from "./backup-config-viewer"; -import { - BackupFile, - BackupTableViewer, - DEFAULT_ROWS_PER_PAGE, -} from "./backup-table-viewer"; -import { LocalBackupActions } from "./local-backup-actions"; -dayjs.extend(customParseFormat); +import { AutoBackupSettings } from "./auto-backup-settings"; +import { BackupHistoryViewer } from "./backup-history-viewer"; +import { BackupWebdavDialog } from "./backup-webdav-dialog"; -const DATE_FORMAT = "YYYY-MM-DD_HH-mm-ss"; -const FILENAME_PATTERN = /\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}/; type BackupSource = "local" | "webdav"; -type CloseButtonPosition = { top: number; left: number } | null; export function BackupViewer({ ref }: { ref?: Ref }) { const { t } = useTranslation(); const [open, setOpen] = useState(false); - const contentRef = useRef(null); - const [dialogPaper, setDialogPaper] = useReducer( - (_: HTMLElement | null, next: HTMLElement | null) => next, - null as HTMLElement | null, - ); - const [closeButtonPosition, setCloseButtonPosition] = useReducer( - (_: CloseButtonPosition, next: CloseButtonPosition) => next, - null as CloseButtonPosition, - ); - - const [isLoading, setIsLoading] = useState(false); - const [backupFiles, setBackupFiles] = useState([]); - const [total, setTotal] = useState(0); - const [page, setPage] = useState(0); - const [source, setSource] = useState("local"); + const [busyAction, setBusyAction] = useState(null); + const [historyOpen, setHistoryOpen] = useState(false); + const [historySource, setHistorySource] = useState("local"); + const [historyPage, setHistoryPage] = useState(0); + const [webdavDialogOpen, setWebdavDialogOpen] = useState(false); useImperativeHandle(ref, () => ({ - open: () => { - setOpen(true); - }, + open: () => setOpen(true), close: () => setOpen(false), })); - // Handle page change - const handleChangePage = useCallback( - (_: React.MouseEvent | null, page: number) => { - setPage(page); - }, - [], - ); + const openHistory = (target: BackupSource) => { + setHistorySource(target); + setHistoryPage(0); + setHistoryOpen(true); + }; - const handleChangeSource = useCallback( - (_event: React.SyntheticEvent, newSource: string) => { - setSource(newSource as BackupSource); - setPage(0); - }, - [], - ); - - const buildBackupFile = useCallback((filename: string) => { - const platform = filename.split("-")[0]; - const fileBackupTimeStr = filename.match(FILENAME_PATTERN); - - if (fileBackupTimeStr === null) { - return null; - } - - const backupTime = dayjs(fileBackupTimeStr[0], DATE_FORMAT); - const allowApply = true; - return { - filename, - platform, - backup_time: backupTime, - allow_apply: allowApply, - } as BackupFile; - }, []); - - const getAllBackupFiles = useCallback(async (): Promise => { - if (source === "local") { - const files = await listLocalBackup(); - return files - .map((file) => buildBackupFile(file.filename)) - .filter((item): item is BackupFile => item !== null) - .sort((a, b) => (a.backup_time.isAfter(b.backup_time) ? -1 : 1)); - } - - const files = await listWebDavBackup(); - return files - .map((file) => { - return buildBackupFile(file.filename); - }) - .filter((item): item is BackupFile => item !== null) - .sort((a, b) => (a.backup_time.isAfter(b.backup_time) ? -1 : 1)); - }, [buildBackupFile, source]); - - const fetchAndSetBackupFiles = useCallback(async () => { + const handleBackup = useLockFn(async (target: BackupSource) => { try { - setIsLoading(true); - const files = await getAllBackupFiles(); - setBackupFiles(files); - setTotal(files.length); + setBusyAction(target); + if (target === "local") { + await createLocalBackup(); + showNotice.success( + "settings.modals.backup.messages.localBackupCreated", + ); + } else { + await createWebdavBackup(); + showNotice.success("settings.modals.backup.messages.backupCreated"); + } } catch (error) { - setBackupFiles([]); - setTotal(0); console.error(error); + showNotice.error( + target === "local" + ? "settings.modals.backup.messages.localBackupFailed" + : "settings.modals.backup.messages.backupFailed", + target === "local" ? undefined : { error }, + ); } finally { - setIsLoading(false); + setBusyAction(null); } - }, [getAllBackupFiles]); - - useEffect(() => { - if (open) { - fetchAndSetBackupFiles(); - const paper = contentRef.current?.closest(".MuiPaper-root"); - setDialogPaper((paper as HTMLElement) ?? null); - } else { - setDialogPaper(null); - } - }, [open, fetchAndSetBackupFiles]); - - useEffect(() => { - if (!open || dialogPaper) { - return; - } - const frame = requestAnimationFrame(() => { - const paper = contentRef.current?.closest(".MuiPaper-root"); - setDialogPaper((paper as HTMLElement) ?? null); - }); - return () => cancelAnimationFrame(frame); - }, [open, dialogPaper]); - - useEffect(() => { - if (!dialogPaper) { - setCloseButtonPosition(null); - return; - } - if (typeof window === "undefined") { - return; - } - - const updatePosition = () => { - const rect = dialogPaper.getBoundingClientRect(); - setCloseButtonPosition({ - top: rect.bottom - 16, - left: rect.right - 24, - }); - }; - - updatePosition(); - - let resizeObserver: ResizeObserver | null = null; - if (typeof ResizeObserver !== "undefined") { - resizeObserver = new ResizeObserver(() => { - updatePosition(); - }); - resizeObserver.observe(dialogPaper); - } - - const scrollTargets: EventTarget[] = []; - const addScrollListener = (target: EventTarget | null) => { - if (!target) { - return; - } - target.addEventListener("scroll", updatePosition, true); - scrollTargets.push(target); - }; - - addScrollListener(window); - addScrollListener(dialogPaper); - const dialogContent = dialogPaper.querySelector(".MuiDialogContent-root"); - addScrollListener(dialogContent); - - window.addEventListener("resize", updatePosition); - - return () => { - resizeObserver?.disconnect(); - scrollTargets.forEach((target) => { - target.removeEventListener("scroll", updatePosition, true); - }); - window.removeEventListener("resize", updatePosition); - }; - }, [dialogPaper]); - - const handleDelete = useCallback( - async (filename: string) => { - if (source === "local") { - await deleteLocalBackup(filename); - } else { - await deleteWebdavBackup(filename); - } - }, - [source], - ); - - const handleRestore = useCallback( - async (filename: string) => { - if (source === "local") { - await restoreLocalBackup(filename); - } else { - await restoreWebDavBackup(filename); - } - }, - [source], - ); - - const handleExport = useCallback( - async (filename: string, destination: string) => { - await exportLocalBackup(filename, destination); - }, - [], - ); - - const dataSource = useMemo( - () => - backupFiles.slice( - page * DEFAULT_ROWS_PER_PAGE, - page * DEFAULT_ROWS_PER_PAGE + DEFAULT_ROWS_PER_PAGE, - ), - [backupFiles, page], - ); + }); return ( setOpen(false)} onClose={() => setOpen(false)} - disableFooter > - - - + `1px solid ${theme.palette.divider}`, + borderRadius: 2, + p: 2, }} > - - - - - {source === "local" ? ( - - ) : ( - - )} - - - - - - - {dialogPaper && - closeButtonPosition && - createPortal( - theme.zIndex.modal + 1, - }} - > - - , - dialogPaper, - )} + + {t("settings.modals.backup.auto.title")} + + + + + + + `1px solid ${theme.palette.divider}`, + borderRadius: 2, + p: 2, + }} + > + + {t("settings.modals.backup.manual.title")} + + + {( + [ + { + key: "local" as BackupSource, + title: t("settings.modals.backup.tabs.local"), + description: t("settings.modals.backup.manual.local"), + actions: [ + handleBackup("local")} + > + {t("settings.modals.backup.actions.backup")} + , + , + ], + }, + { + key: "webdav" as BackupSource, + title: t("settings.modals.backup.tabs.webdav"), + description: t("settings.modals.backup.manual.webdav"), + actions: [ + handleBackup("webdav")} + > + {t("settings.modals.backup.actions.backup")} + , + , + , + ], + }, + ] satisfies Array<{ + key: BackupSource; + title: string; + description: string; + actions: ReactNode[]; + }> + ).map((item, idx) => ( + + + + + {item.actions} + + + + ))} + + + + + setHistoryOpen(false)} + /> + setWebdavDialogOpen(false)} + onBackupSuccess={() => openHistory("webdav")} + setBusy={(loading) => setBusyAction(loading ? "webdav" : null)} + /> ); } diff --git a/src/components/setting/mods/backup-webdav-dialog.tsx b/src/components/setting/mods/backup-webdav-dialog.tsx new file mode 100644 index 000000000..8ad0e17a0 --- /dev/null +++ b/src/components/setting/mods/backup-webdav-dialog.tsx @@ -0,0 +1,87 @@ +import { Box } from "@mui/material"; +import { useCallback, useState } from "react"; +import { useTranslation } from "react-i18next"; + +import { BaseDialog, BaseLoadingOverlay } from "@/components/base"; +import { listWebDavBackup } from "@/services/cmds"; +import { showNotice } from "@/services/noticeService"; + +import { BackupConfigViewer } from "./backup-config-viewer"; + +interface BackupWebdavDialogProps { + open: boolean; + onClose: () => void; + onBackupSuccess?: () => void; + setBusy?: (loading: boolean) => void; +} + +export const BackupWebdavDialog = ({ + open, + onClose, + onBackupSuccess, + setBusy, +}: BackupWebdavDialogProps) => { + const { t } = useTranslation(); + const [loading, setLoading] = useState(false); + + const handleLoading = useCallback( + (value: boolean) => { + setLoading(value); + setBusy?.(value); + }, + [setBusy], + ); + + const refreshWebdav = useCallback( + async (options?: { silent?: boolean }) => { + handleLoading(true); + try { + await listWebDavBackup(); + if (!options?.silent) { + showNotice.success( + "settings.modals.backup.messages.webdavRefreshSuccess", + ); + } + } catch (error) { + showNotice.error( + "settings.modals.backup.messages.webdavRefreshFailed", + { error }, + ); + } finally { + handleLoading(false); + } + }, + [handleLoading], + ); + + const refreshSilently = useCallback( + () => refreshWebdav({ silent: true }), + [refreshWebdav], + ); + + return ( + + + + { + await refreshSilently(); + onBackupSuccess?.(); + }} + onSaveSuccess={refreshSilently} + onRefresh={refreshWebdav} + onInit={refreshSilently} + /> + + + ); +}; diff --git a/src/components/setting/mods/local-backup-actions.tsx b/src/components/setting/mods/local-backup-actions.tsx deleted file mode 100644 index e2dd5bd50..000000000 --- a/src/components/setting/mods/local-backup-actions.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { Button, Grid, Stack, Typography } from "@mui/material"; -import { useLockFn } from "ahooks"; -import { memo } from "react"; -import { useTranslation } from "react-i18next"; - -import { createLocalBackup } from "@/services/cmds"; -import { showNotice } from "@/services/noticeService"; - -interface LocalBackupActionsProps { - onBackupSuccess: () => Promise; - onRefresh: () => Promise; - setLoading: (loading: boolean) => void; -} - -export const LocalBackupActions = memo( - ({ onBackupSuccess, onRefresh, setLoading }: LocalBackupActionsProps) => { - const { t } = useTranslation(); - - const handleBackup = useLockFn(async () => { - try { - setLoading(true); - await createLocalBackup(); - showNotice.success( - "settings.modals.backup.messages.localBackupCreated", - ); - await onBackupSuccess(); - } catch (error) { - console.error(error); - showNotice.error("settings.modals.backup.messages.localBackupFailed"); - } finally { - setLoading(false); - } - }); - - const handleRefresh = useLockFn(async () => { - setLoading(true); - try { - await onRefresh(); - } finally { - setLoading(false); - } - }); - - return ( - - - - {t("settings.modals.backup.fields.info")} - - - - - - - - - - ); - }, -); diff --git a/src/locales/ar/settings.json b/src/locales/ar/settings.json index 4c01bff35..6a9ffcac3 100644 --- a/src/locales/ar/settings.json +++ b/src/locales/ar/settings.json @@ -285,7 +285,8 @@ "exportBackup": "Export Backup", "deleteBackup": "حذف النسخة الاحتياطية", "restore": "استعادة", - "restoreBackup": "استعادة النسخة الاحتياطية" + "restoreBackup": "استعادة النسخة الاحتياطية", + "viewHistory": "View history" }, "fields": { "webdavUrl": "عنوان خادم WebDAV", @@ -306,9 +307,37 @@ "restoreSuccess": "تمت الاستعادة بنجاح، سيعاد تشغيل التطبيق خلال ثانية واحدة", "localBackupExported": "Local backup exported successfully", "localBackupExportFailed": "Failed to export local backup", + "webdavRefreshSuccess": "WebDAV refresh succeeded", + "webdavRefreshFailed": "WebDAV refresh failed: {{error}}", "confirmDelete": "هل تريد بالتأكيد حذف ملف النسخة الاحتياطية هذا؟", "confirmRestore": "هل تريد بالتأكيد استعادة ملف النسخة الاحتياطية هذا؟" }, + "auto": { + "title": "Automatic backup", + "scheduleLabel": "Enable scheduled backup", + "scheduleHelper": "Create local backups in the background at the configured interval.", + "intervalLabel": "Backup frequency", + "changeLabel": "Backup on critical changes", + "changeHelper": "Automatically back up when the global extend config/script or subscriptions are added, removed, or edited.", + "options": { + "hours": "Every {{n}} hours", + "days": "Every {{n}} days" + } + }, + "manual": { + "title": "Manual backup", + "local": "Creates a snapshot on this device, stored under the app data directory.", + "webdav": "Upload a snapshot to your WebDAV server once credentials are set.", + "configureWebdav": "Configure WebDAV" + }, + "history": { + "title": "Backup history", + "summary": "{{count}} backups • latest {{recent}}", + "empty": "No backups available" + }, + "webdav": { + "title": "WebDAV settings" + }, "table": { "filename": "اسم الملف", "backupTime": "وقت النسخ الاحتياطي", diff --git a/src/locales/ar/shared.json b/src/locales/ar/shared.json index 604109371..9c4d4489b 100644 --- a/src/locales/ar/shared.json +++ b/src/locales/ar/shared.json @@ -21,7 +21,9 @@ "pause": "إيقاف مؤقت", "resume": "استأنف", "closeAll": "إغلاق الكل", - "clear": "مسح" + "clear": "مسح", + "previous": "Previous", + "next": "Next" }, "labels": { "updateAt": "التحديث عند", @@ -48,6 +50,7 @@ "milliseconds": "ميلي ثانية", "seconds": "ثواني", "minutes": "دقائق", + "hours": "ساعات", "kilobytes": "KB", "files": "Files" }, diff --git a/src/locales/de/settings.json b/src/locales/de/settings.json index 29a3cc39b..1cfa4c01f 100644 --- a/src/locales/de/settings.json +++ b/src/locales/de/settings.json @@ -285,7 +285,8 @@ "exportBackup": "Export Backup", "deleteBackup": "Sicherung löschen", "restore": "Wiederherstellen", - "restoreBackup": "Sicherung wiederherstellen" + "restoreBackup": "Sicherung wiederherstellen", + "viewHistory": "View history" }, "fields": { "webdavUrl": "WebDAV-Serveradresse http(s)://", @@ -306,9 +307,37 @@ "restoreSuccess": "Wiederherstellung erfolgreich. Die App wird in 1 Sekunde neu starten.", "localBackupExported": "Local backup exported successfully", "localBackupExportFailed": "Failed to export local backup", + "webdavRefreshSuccess": "WebDAV refresh succeeded", + "webdavRefreshFailed": "WebDAV refresh failed: {{error}}", "confirmDelete": "Confirm to delete this backup file?", "confirmRestore": "Confirm to restore this backup file?" }, + "auto": { + "title": "Automatic backup", + "scheduleLabel": "Enable scheduled backup", + "scheduleHelper": "Create local backups in the background at the configured interval.", + "intervalLabel": "Backup frequency", + "changeLabel": "Backup on critical changes", + "changeHelper": "Automatically back up when the global extend config/script or subscriptions are added, removed, or edited.", + "options": { + "hours": "Every {{n}} hours", + "days": "Every {{n}} days" + } + }, + "manual": { + "title": "Manual backup", + "local": "Creates a snapshot on this device, stored under the app data directory.", + "webdav": "Upload a snapshot to your WebDAV server once credentials are set.", + "configureWebdav": "Configure WebDAV" + }, + "history": { + "title": "Backup history", + "summary": "{{count}} backups • latest {{recent}}", + "empty": "No backups available" + }, + "webdav": { + "title": "WebDAV settings" + }, "table": { "filename": "Dateiname", "backupTime": "Sicherungszeit", diff --git a/src/locales/de/shared.json b/src/locales/de/shared.json index 22684cae0..eb2e34d7d 100644 --- a/src/locales/de/shared.json +++ b/src/locales/de/shared.json @@ -21,7 +21,9 @@ "pause": "Pausieren", "resume": "Fortsetzen", "closeAll": "Alle schließen", - "clear": "Löschen" + "clear": "Löschen", + "previous": "Previous", + "next": "Next" }, "labels": { "updateAt": "Aktualisiert am", @@ -48,6 +50,7 @@ "milliseconds": "Millisekunden", "seconds": "Sekunden", "minutes": "Minuten", + "hours": "Stunden", "kilobytes": "KB", "files": "Files" }, diff --git a/src/locales/en/settings.json b/src/locales/en/settings.json index 661ab2f72..08cce1d21 100644 --- a/src/locales/en/settings.json +++ b/src/locales/en/settings.json @@ -285,7 +285,8 @@ "exportBackup": "Export Backup", "deleteBackup": "Delete Backup", "restore": "Restore", - "restoreBackup": "Restore Backup" + "restoreBackup": "Restore Backup", + "viewHistory": "View history" }, "fields": { "webdavUrl": "WebDAV Server URL", @@ -306,9 +307,37 @@ "restoreSuccess": "Restore Success, App will restart in 1s", "localBackupExported": "Local backup exported successfully", "localBackupExportFailed": "Failed to export local backup", + "webdavRefreshSuccess": "WebDAV refresh succeeded", + "webdavRefreshFailed": "WebDAV refresh failed: {{error}}", "confirmDelete": "Confirm to delete this backup file?", "confirmRestore": "Confirm to restore this backup file?" }, + "auto": { + "title": "Automatic backup", + "scheduleLabel": "Enable scheduled backup", + "scheduleHelper": "Create local backups in the background at the configured interval.", + "intervalLabel": "Backup frequency", + "changeLabel": "Backup on critical changes", + "changeHelper": "Automatically back up when the global extend config/script or subscriptions are added, removed, or edited.", + "options": { + "hours": "Every {{n}} hours", + "days": "Every {{n}} days" + } + }, + "manual": { + "title": "Manual backup", + "local": "Creates a snapshot on this device, stored under the app data directory.", + "webdav": "Upload a snapshot to your WebDAV server once credentials are set.", + "configureWebdav": "Configure WebDAV" + }, + "history": { + "title": "Backup history", + "summary": "{{count}} backups • latest {{recent}}", + "empty": "No backups available" + }, + "webdav": { + "title": "WebDAV settings" + }, "table": { "filename": "Filename", "backupTime": "Backup Time", diff --git a/src/locales/en/shared.json b/src/locales/en/shared.json index 436164218..1e2f61317 100644 --- a/src/locales/en/shared.json +++ b/src/locales/en/shared.json @@ -21,7 +21,9 @@ "pause": "Pause", "resume": "Resume", "closeAll": "Close All", - "clear": "Clear" + "clear": "Clear", + "previous": "Previous", + "next": "Next" }, "labels": { "updateAt": "Update At", @@ -48,6 +50,7 @@ "milliseconds": "ms", "seconds": "seconds", "minutes": "mins", + "hours": "hrs", "kilobytes": "KB", "files": "Files" }, diff --git a/src/locales/es/settings.json b/src/locales/es/settings.json index cbc1d05ab..a687ed588 100644 --- a/src/locales/es/settings.json +++ b/src/locales/es/settings.json @@ -285,7 +285,8 @@ "exportBackup": "Export Backup", "deleteBackup": "Eliminar copia de seguridad", "restore": "Restaurar", - "restoreBackup": "Restaurar copia de seguridad" + "restoreBackup": "Restaurar copia de seguridad", + "viewHistory": "View history" }, "fields": { "webdavUrl": "Dirección del servidor WebDAV http(s)://", @@ -306,9 +307,37 @@ "restoreSuccess": "Restauración exitosa. La aplicación se reiniciará en 1 segundo", "localBackupExported": "Local backup exported successfully", "localBackupExportFailed": "Failed to export local backup", + "webdavRefreshSuccess": "WebDAV refresh succeeded", + "webdavRefreshFailed": "WebDAV refresh failed: {{error}}", "confirmDelete": "Confirm to delete this backup file?", "confirmRestore": "Confirm to restore this backup file?" }, + "auto": { + "title": "Automatic backup", + "scheduleLabel": "Enable scheduled backup", + "scheduleHelper": "Create local backups in the background at the configured interval.", + "intervalLabel": "Backup frequency", + "changeLabel": "Backup on critical changes", + "changeHelper": "Automatically back up when the global extend config/script or subscriptions are added, removed, or edited.", + "options": { + "hours": "Every {{n}} hours", + "days": "Every {{n}} days" + } + }, + "manual": { + "title": "Manual backup", + "local": "Creates a snapshot on this device, stored under the app data directory.", + "webdav": "Upload a snapshot to your WebDAV server once credentials are set.", + "configureWebdav": "Configure WebDAV" + }, + "history": { + "title": "Backup history", + "summary": "{{count}} backups • latest {{recent}}", + "empty": "No backups available" + }, + "webdav": { + "title": "WebDAV settings" + }, "table": { "filename": "Nombre del archivo", "backupTime": "Tiempo de copia de seguridad", diff --git a/src/locales/es/shared.json b/src/locales/es/shared.json index cb06326a8..7ed7494c6 100644 --- a/src/locales/es/shared.json +++ b/src/locales/es/shared.json @@ -21,7 +21,9 @@ "pause": "Pausar", "resume": "Reanudar", "closeAll": "Cerrar todas", - "clear": "Limpiar" + "clear": "Limpiar", + "previous": "Previous", + "next": "Next" }, "labels": { "updateAt": "Actualizado el", @@ -48,6 +50,7 @@ "milliseconds": "Milisegundos", "seconds": "Segundos", "minutes": "Minutos", + "hours": "Horas", "kilobytes": "KB", "files": "Files" }, diff --git a/src/locales/fa/settings.json b/src/locales/fa/settings.json index 36a41769d..b99ec661e 100644 --- a/src/locales/fa/settings.json +++ b/src/locales/fa/settings.json @@ -285,7 +285,8 @@ "exportBackup": "Export Backup", "deleteBackup": "حذف پشتیبان", "restore": "بازیابی", - "restoreBackup": "بازیابی پشتیبان" + "restoreBackup": "بازیابی پشتیبان", + "viewHistory": "View history" }, "fields": { "webdavUrl": "http(s):// URL سرور WebDAV", @@ -306,9 +307,37 @@ "restoreSuccess": "بازیابی با موفقیت انجام شد، برنامه در 1 ثانیه راه‌اندازی مجدد می‌شود", "localBackupExported": "Local backup exported successfully", "localBackupExportFailed": "Failed to export local backup", + "webdavRefreshSuccess": "WebDAV refresh succeeded", + "webdavRefreshFailed": "WebDAV refresh failed: {{error}}", "confirmDelete": "آیا از حذف این فایل پشتیبان اطمینان دارید؟", "confirmRestore": "آیا از بازیابی این فایل پشتیبان اطمینان دارید؟" }, + "auto": { + "title": "Automatic backup", + "scheduleLabel": "Enable scheduled backup", + "scheduleHelper": "Create local backups in the background at the configured interval.", + "intervalLabel": "Backup frequency", + "changeLabel": "Backup on critical changes", + "changeHelper": "Automatically back up when the global extend config/script or subscriptions are added, removed, or edited.", + "options": { + "hours": "Every {{n}} hours", + "days": "Every {{n}} days" + } + }, + "manual": { + "title": "Manual backup", + "local": "Creates a snapshot on this device, stored under the app data directory.", + "webdav": "Upload a snapshot to your WebDAV server once credentials are set.", + "configureWebdav": "Configure WebDAV" + }, + "history": { + "title": "Backup history", + "summary": "{{count}} backups • latest {{recent}}", + "empty": "No backups available" + }, + "webdav": { + "title": "WebDAV settings" + }, "table": { "filename": "نام فایل", "backupTime": "زمان پشتیبان‌گیری", diff --git a/src/locales/fa/shared.json b/src/locales/fa/shared.json index 374707ac0..6ab4e72d0 100644 --- a/src/locales/fa/shared.json +++ b/src/locales/fa/shared.json @@ -21,7 +21,9 @@ "pause": "توقف", "resume": "از سرگیری", "closeAll": "بستن همه", - "clear": "پاک کردن" + "clear": "پاک کردن", + "previous": "Previous", + "next": "Next" }, "labels": { "updateAt": "به‌روزرسانی در", @@ -48,6 +50,7 @@ "milliseconds": "میلی‌ثانیه", "seconds": "ثانیه‌ها", "minutes": "دقیقه", + "hours": "ساعت", "kilobytes": "KB", "files": "Files" }, diff --git a/src/locales/id/settings.json b/src/locales/id/settings.json index 4196f7fa4..abdf1bea5 100644 --- a/src/locales/id/settings.json +++ b/src/locales/id/settings.json @@ -285,7 +285,8 @@ "exportBackup": "Export Backup", "deleteBackup": "Hapus Cadangan", "restore": "Pulihkan", - "restoreBackup": "Pulihkan Cadangan" + "restoreBackup": "Pulihkan Cadangan", + "viewHistory": "View history" }, "fields": { "webdavUrl": "URL Server WebDAV", @@ -306,9 +307,37 @@ "restoreSuccess": "Pemulihan Berhasil, Aplikasi akan dimulai ulang dalam 1 detik", "localBackupExported": "Local backup exported successfully", "localBackupExportFailed": "Failed to export local backup", + "webdavRefreshSuccess": "WebDAV refresh succeeded", + "webdavRefreshFailed": "WebDAV refresh failed: {{error}}", "confirmDelete": "Konfirmasi untuk menghapus file cadangan ini?", "confirmRestore": "Konfirmasi untuk memulihkan file cadangan ini?" }, + "auto": { + "title": "Automatic backup", + "scheduleLabel": "Enable scheduled backup", + "scheduleHelper": "Create local backups in the background at the configured interval.", + "intervalLabel": "Backup frequency", + "changeLabel": "Backup on critical changes", + "changeHelper": "Automatically back up when the global extend config/script or subscriptions are added, removed, or edited.", + "options": { + "hours": "Every {{n}} hours", + "days": "Every {{n}} days" + } + }, + "manual": { + "title": "Manual backup", + "local": "Creates a snapshot on this device, stored under the app data directory.", + "webdav": "Upload a snapshot to your WebDAV server once credentials are set.", + "configureWebdav": "Configure WebDAV" + }, + "history": { + "title": "Backup history", + "summary": "{{count}} backups • latest {{recent}}", + "empty": "No backups available" + }, + "webdav": { + "title": "WebDAV settings" + }, "table": { "filename": "Nama Berkas", "backupTime": "Waktu Cadangan", diff --git a/src/locales/id/shared.json b/src/locales/id/shared.json index 14eeec27a..057e9f74b 100644 --- a/src/locales/id/shared.json +++ b/src/locales/id/shared.json @@ -21,7 +21,9 @@ "pause": "Jeda", "resume": "Lanjut", "closeAll": "Tutup Semua", - "clear": "Bersihkan" + "clear": "Bersihkan", + "previous": "Previous", + "next": "Next" }, "labels": { "updateAt": "Diperbarui Pada", @@ -48,6 +50,7 @@ "milliseconds": "milidetik", "seconds": "detik", "minutes": "menit", + "hours": "jam", "kilobytes": "KB", "files": "Files" }, diff --git a/src/locales/jp/settings.json b/src/locales/jp/settings.json index 51635416e..f02ec9b55 100644 --- a/src/locales/jp/settings.json +++ b/src/locales/jp/settings.json @@ -285,7 +285,8 @@ "exportBackup": "Export Backup", "deleteBackup": "バックアップを削除", "restore": "復元", - "restoreBackup": "バックアップを復元" + "restoreBackup": "バックアップを復元", + "viewHistory": "View history" }, "fields": { "webdavUrl": "WebDAVサーバーのURL http(s)://", @@ -306,9 +307,37 @@ "restoreSuccess": "復元に成功しました。アプリケーションは1秒後に再起動します。", "localBackupExported": "Local backup exported successfully", "localBackupExportFailed": "Failed to export local backup", + "webdavRefreshSuccess": "WebDAV refresh succeeded", + "webdavRefreshFailed": "WebDAV refresh failed: {{error}}", "confirmDelete": "Confirm to delete this backup file?", "confirmRestore": "Confirm to restore this backup file?" }, + "auto": { + "title": "Automatic backup", + "scheduleLabel": "Enable scheduled backup", + "scheduleHelper": "Create local backups in the background at the configured interval.", + "intervalLabel": "Backup frequency", + "changeLabel": "Backup on critical changes", + "changeHelper": "Automatically back up when the global extend config/script or subscriptions are added, removed, or edited.", + "options": { + "hours": "Every {{n}} hours", + "days": "Every {{n}} days" + } + }, + "manual": { + "title": "Manual backup", + "local": "Creates a snapshot on this device, stored under the app data directory.", + "webdav": "Upload a snapshot to your WebDAV server once credentials are set.", + "configureWebdav": "Configure WebDAV" + }, + "history": { + "title": "Backup history", + "summary": "{{count}} backups • latest {{recent}}", + "empty": "No backups available" + }, + "webdav": { + "title": "WebDAV settings" + }, "table": { "filename": "ファイル名", "backupTime": "バックアップ時間", diff --git a/src/locales/jp/shared.json b/src/locales/jp/shared.json index ac83ba54d..292e7c1b2 100644 --- a/src/locales/jp/shared.json +++ b/src/locales/jp/shared.json @@ -21,7 +21,9 @@ "pause": "一時停止", "resume": "再開", "closeAll": "すべて閉じる", - "clear": "クリア" + "clear": "クリア", + "previous": "Previous", + "next": "Next" }, "labels": { "updateAt": "更新日時", @@ -48,6 +50,7 @@ "milliseconds": "ミリ秒", "seconds": "秒", "minutes": "分", + "hours": "時間", "kilobytes": "KB", "files": "Files" }, diff --git a/src/locales/ko/settings.json b/src/locales/ko/settings.json index 1fe918129..faa61a57c 100644 --- a/src/locales/ko/settings.json +++ b/src/locales/ko/settings.json @@ -285,7 +285,8 @@ "exportBackup": "백업 내보내기", "deleteBackup": "백업 삭제", "restore": "복원", - "restoreBackup": "백업 복원" + "restoreBackup": "백업 복원", + "viewHistory": "View history" }, "fields": { "webdavUrl": "WebDAV 서버 URL", @@ -306,9 +307,37 @@ "restoreSuccess": "복원 성공, 1초 후 앱이 재시작됩니다", "localBackupExported": "로컬 백업이 내보내졌습니다", "localBackupExportFailed": "로컬 백업 내보내기 실패", + "webdavRefreshSuccess": "WebDAV refresh succeeded", + "webdavRefreshFailed": "WebDAV refresh failed: {{error}}", "confirmDelete": "이 백업 파일을 삭제하시겠습니까?", "confirmRestore": "이 백업 파일을 복원하시겠습니까?" }, + "auto": { + "title": "Automatic backup", + "scheduleLabel": "Enable scheduled backup", + "scheduleHelper": "Create local backups in the background at the configured interval.", + "intervalLabel": "Backup frequency", + "changeLabel": "Backup on critical changes", + "changeHelper": "Automatically back up when the global extend config/script or subscriptions are added, removed, or edited.", + "options": { + "hours": "Every {{n}} hours", + "days": "Every {{n}} days" + } + }, + "manual": { + "title": "Manual backup", + "local": "Creates a snapshot on this device, stored under the app data directory.", + "webdav": "Upload a snapshot to your WebDAV server once credentials are set.", + "configureWebdav": "Configure WebDAV" + }, + "history": { + "title": "Backup history", + "summary": "{{count}} backups • latest {{recent}}", + "empty": "No backups available" + }, + "webdav": { + "title": "WebDAV settings" + }, "table": { "filename": "파일명", "backupTime": "백업 시간", diff --git a/src/locales/ko/shared.json b/src/locales/ko/shared.json index ca8e69842..4309e5f9c 100644 --- a/src/locales/ko/shared.json +++ b/src/locales/ko/shared.json @@ -21,7 +21,9 @@ "pause": "일시 정지", "resume": "재개", "closeAll": "모두 닫기", - "clear": "지우기" + "clear": "지우기", + "previous": "Previous", + "next": "Next" }, "labels": { "updateAt": "업데이트 시간", @@ -48,6 +50,7 @@ "milliseconds": "밀리초", "seconds": "초", "minutes": "분", + "hours": "시간", "kilobytes": "KB", "files": "파일" }, diff --git a/src/locales/ru/settings.json b/src/locales/ru/settings.json index 3b6551ea2..2a73cbf61 100644 --- a/src/locales/ru/settings.json +++ b/src/locales/ru/settings.json @@ -285,7 +285,8 @@ "exportBackup": "Export Backup", "deleteBackup": "Удалить резервную копию", "restore": "Восстановить", - "restoreBackup": "Восстановить резервную копию" + "restoreBackup": "Восстановить резервную копию", + "viewHistory": "View history" }, "fields": { "webdavUrl": "URL-адрес сервера WebDAV http(s)://", @@ -306,9 +307,37 @@ "restoreSuccess": "Восстановление успешно выполнено, приложение перезапустится через 1 секунду", "localBackupExported": "Local backup exported successfully", "localBackupExportFailed": "Failed to export local backup", + "webdavRefreshSuccess": "WebDAV refresh succeeded", + "webdavRefreshFailed": "WebDAV refresh failed: {{error}}", "confirmDelete": "Вы уверены, что хотите удалить этот файл резервной копии?", "confirmRestore": "Вы уверены, что хотите восстановить этот файл резервной копии?" }, + "auto": { + "title": "Automatic backup", + "scheduleLabel": "Enable scheduled backup", + "scheduleHelper": "Create local backups in the background at the configured interval.", + "intervalLabel": "Backup frequency", + "changeLabel": "Backup on critical changes", + "changeHelper": "Automatically back up when the global extend config/script or subscriptions are added, removed, or edited.", + "options": { + "hours": "Every {{n}} hours", + "days": "Every {{n}} days" + } + }, + "manual": { + "title": "Manual backup", + "local": "Creates a snapshot on this device, stored under the app data directory.", + "webdav": "Upload a snapshot to your WebDAV server once credentials are set.", + "configureWebdav": "Configure WebDAV" + }, + "history": { + "title": "Backup history", + "summary": "{{count}} backups • latest {{recent}}", + "empty": "No backups available" + }, + "webdav": { + "title": "WebDAV settings" + }, "table": { "filename": "Имя файла", "backupTime": "Время резервного копирования", diff --git a/src/locales/ru/shared.json b/src/locales/ru/shared.json index e7c5c8617..be79ca589 100644 --- a/src/locales/ru/shared.json +++ b/src/locales/ru/shared.json @@ -21,7 +21,9 @@ "pause": "Пауза", "resume": "Возобновить", "closeAll": "Закрыть всё", - "clear": "Очистить" + "clear": "Очистить", + "previous": "Previous", + "next": "Next" }, "labels": { "updateAt": "Обновлено в", @@ -48,6 +50,7 @@ "milliseconds": "миллисекунды", "seconds": "секунды", "minutes": "минуты", + "hours": "часы", "kilobytes": "KB", "files": "Files" }, diff --git a/src/locales/tr/settings.json b/src/locales/tr/settings.json index 6c0adc844..2dee69253 100644 --- a/src/locales/tr/settings.json +++ b/src/locales/tr/settings.json @@ -285,7 +285,8 @@ "exportBackup": "Export Backup", "deleteBackup": "Yedeği Sil", "restore": "Geri Yükle", - "restoreBackup": "Yedeği Geri Yükle" + "restoreBackup": "Yedeği Geri Yükle", + "viewHistory": "View history" }, "fields": { "webdavUrl": "WebDAV Sunucu URL'si", @@ -306,9 +307,37 @@ "restoreSuccess": "Geri Yükleme Başarılı, Uygulama 1 saniye içinde yeniden başlatılacak", "localBackupExported": "Local backup exported successfully", "localBackupExportFailed": "Failed to export local backup", + "webdavRefreshSuccess": "WebDAV refresh succeeded", + "webdavRefreshFailed": "WebDAV refresh failed: {{error}}", "confirmDelete": "Bu yedek dosyasını silmeyi onaylıyor musunuz?", "confirmRestore": "Bu yedek dosyasını geri yüklemeyi onaylıyor musunuz?" }, + "auto": { + "title": "Automatic backup", + "scheduleLabel": "Enable scheduled backup", + "scheduleHelper": "Create local backups in the background at the configured interval.", + "intervalLabel": "Backup frequency", + "changeLabel": "Backup on critical changes", + "changeHelper": "Automatically back up when the global extend config/script or subscriptions are added, removed, or edited.", + "options": { + "hours": "Every {{n}} hours", + "days": "Every {{n}} days" + } + }, + "manual": { + "title": "Manual backup", + "local": "Creates a snapshot on this device, stored under the app data directory.", + "webdav": "Upload a snapshot to your WebDAV server once credentials are set.", + "configureWebdav": "Configure WebDAV" + }, + "history": { + "title": "Backup history", + "summary": "{{count}} backups • latest {{recent}}", + "empty": "No backups available" + }, + "webdav": { + "title": "WebDAV settings" + }, "table": { "filename": "Dosya Adı", "backupTime": "Yedekleme Zamanı", diff --git a/src/locales/tr/shared.json b/src/locales/tr/shared.json index f1ce80268..e094bdbc2 100644 --- a/src/locales/tr/shared.json +++ b/src/locales/tr/shared.json @@ -21,7 +21,9 @@ "pause": "Duraklat", "resume": "Sürdür", "closeAll": "Tümünü Kapat", - "clear": "Temizle" + "clear": "Temizle", + "previous": "Previous", + "next": "Next" }, "labels": { "updateAt": "Güncelleme Zamanı", @@ -48,6 +50,7 @@ "milliseconds": "ms", "seconds": "saniye", "minutes": "dakika", + "hours": "saat", "kilobytes": "KB", "files": "Files" }, diff --git a/src/locales/tt/settings.json b/src/locales/tt/settings.json index 692c0b550..231779998 100644 --- a/src/locales/tt/settings.json +++ b/src/locales/tt/settings.json @@ -285,7 +285,8 @@ "exportBackup": "Export Backup", "deleteBackup": "Резерв копияне бетерү", "restore": "Кайтару", - "restoreBackup": "Резерв копияне кайтару" + "restoreBackup": "Резерв копияне кайтару", + "viewHistory": "View history" }, "fields": { "webdavUrl": "WebDAV сервер URL-ы (http(s)://)", @@ -306,9 +307,37 @@ "restoreSuccess": "Уңышлы кайтарылды, кушымта 1 секундтан яңадан башланачак", "localBackupExported": "Local backup exported successfully", "localBackupExportFailed": "Failed to export local backup", + "webdavRefreshSuccess": "WebDAV refresh succeeded", + "webdavRefreshFailed": "WebDAV refresh failed: {{error}}", "confirmDelete": "Бу резерв копия файлын бетерергә телисезме?", "confirmRestore": "Бу резерв копия файлын кире кайтарырга телисезме?" }, + "auto": { + "title": "Automatic backup", + "scheduleLabel": "Enable scheduled backup", + "scheduleHelper": "Create local backups in the background at the configured interval.", + "intervalLabel": "Backup frequency", + "changeLabel": "Backup on critical changes", + "changeHelper": "Automatically back up when the global extend config/script or subscriptions are added, removed, or edited.", + "options": { + "hours": "Every {{n}} hours", + "days": "Every {{n}} days" + } + }, + "manual": { + "title": "Manual backup", + "local": "Creates a snapshot on this device, stored under the app data directory.", + "webdav": "Upload a snapshot to your WebDAV server once credentials are set.", + "configureWebdav": "Configure WebDAV" + }, + "history": { + "title": "Backup history", + "summary": "{{count}} backups • latest {{recent}}", + "empty": "No backups available" + }, + "webdav": { + "title": "WebDAV settings" + }, "table": { "filename": "Файл исеме", "backupTime": "Резерв копия вакыты", diff --git a/src/locales/tt/shared.json b/src/locales/tt/shared.json index 88199bd5f..9405b43ac 100644 --- a/src/locales/tt/shared.json +++ b/src/locales/tt/shared.json @@ -21,7 +21,9 @@ "pause": "Туктау", "resume": "Дәвам", "closeAll": "Барысын да ябу", - "clear": "Чистарту" + "clear": "Чистарту", + "previous": "Previous", + "next": "Next" }, "labels": { "updateAt": "Яңартылган вакыт", @@ -48,6 +50,7 @@ "milliseconds": "Миллисекундлар", "seconds": "Секундлар", "minutes": "Минутлар", + "hours": "Сәгатьләр", "kilobytes": "KB", "files": "Files" }, diff --git a/src/locales/zh/settings.json b/src/locales/zh/settings.json index 368cdc4e4..3677fe1cf 100644 --- a/src/locales/zh/settings.json +++ b/src/locales/zh/settings.json @@ -285,7 +285,8 @@ "exportBackup": "导出备份", "deleteBackup": "删除备份", "restore": "恢复", - "restoreBackup": "恢复备份" + "restoreBackup": "恢复备份", + "viewHistory": "查看记录" }, "fields": { "webdavUrl": "WebDAV 服务器地址 http(s)://", @@ -306,9 +307,37 @@ "restoreSuccess": "恢复成功,应用将在 1 秒后重启", "localBackupExported": "本地备份导出成功", "localBackupExportFailed": "本地备份导出失败", + "webdavRefreshSuccess": "WebDAV 刷新成功", + "webdavRefreshFailed": "WebDAV 刷新失败: {{error}}", "confirmDelete": "确认删除此备份文件吗?", "confirmRestore": "确认恢复此份文件吗?" }, + "auto": { + "title": "自动备份", + "scheduleLabel": "启用定时备份", + "scheduleHelper": "按设定频率在后台创建本地备份文件。", + "intervalLabel": "备份频率", + "changeLabel": "关键变更时自动备份", + "changeHelper": "全局扩展配置/脚本或订阅增删改后会自动备份。", + "options": { + "hours": "每 {{n}} 小时", + "days": "每 {{n}} 天" + } + }, + "manual": { + "title": "手动备份", + "local": "在本设备的应用数据目录中创建备份。", + "webdav": "配置 WebDAV 后,可直接上传备份到服务器。", + "configureWebdav": "配置 WebDAV" + }, + "history": { + "title": "备份记录", + "summary": "共 {{count}} 份备份 · 最近 {{recent}}", + "empty": "暂无备份记录" + }, + "webdav": { + "title": "WebDAV 设置" + }, "table": { "filename": "文件名称", "backupTime": "备份时间", diff --git a/src/locales/zh/shared.json b/src/locales/zh/shared.json index 788e1d79e..7db791f85 100644 --- a/src/locales/zh/shared.json +++ b/src/locales/zh/shared.json @@ -21,7 +21,9 @@ "pause": "暂停", "resume": "继续", "closeAll": "关闭全部", - "clear": "清除" + "clear": "清除", + "previous": "上一页", + "next": "下一页" }, "labels": { "updateAt": "更新于", @@ -48,6 +50,7 @@ "milliseconds": "毫秒", "seconds": "秒", "minutes": "分钟", + "hours": "小时", "kilobytes": "KB", "files": "文件" }, diff --git a/src/locales/zhtw/settings.json b/src/locales/zhtw/settings.json index e5c1abcba..d78440f29 100644 --- a/src/locales/zhtw/settings.json +++ b/src/locales/zhtw/settings.json @@ -285,7 +285,8 @@ "exportBackup": "匯出備份", "deleteBackup": "刪除備份", "restore": "還原", - "restoreBackup": "還原備份" + "restoreBackup": "還原備份", + "viewHistory": "檢視紀錄" }, "fields": { "webdavUrl": "WebDAV 伺服器位址 http(s)://", @@ -306,9 +307,37 @@ "restoreSuccess": "還原成功,應用程式將在 1 秒後重啟", "localBackupExported": "本機備份匯出成功", "localBackupExportFailed": "本機備份匯出失敗", + "webdavRefreshSuccess": "WebDAV 更新成功", + "webdavRefreshFailed": "WebDAV 更新失敗: {{error}}", "confirmDelete": "確認是否刪除此備份檔案嗎?", "confirmRestore": "確認還原此份檔案嗎?" }, + "auto": { + "title": "自動備份", + "scheduleLabel": "啟用定時備份", + "scheduleHelper": "依設定頻率在背景建立本機備份檔案。", + "intervalLabel": "備份頻率", + "changeLabel": "關鍵變更時自動備份", + "changeHelper": "全域擴充配置/腳本或訂閱新增、刪除、修改時自動備份。", + "options": { + "hours": "每 {{n}} 小時", + "days": "每 {{n}} 天" + } + }, + "manual": { + "title": "手動備份", + "local": "在本機應用資料資料夾建立備份檔。", + "webdav": "設定 WebDAV 後,可直接上傳備份至伺服器。", + "configureWebdav": "設定 WebDAV" + }, + "history": { + "title": "備份紀錄", + "summary": "共 {{count}} 份備份 · 最近 {{recent}}", + "empty": "尚無備份紀錄" + }, + "webdav": { + "title": "WebDAV 設定" + }, "table": { "filename": "檔案名稱", "backupTime": "備份時間", diff --git a/src/locales/zhtw/shared.json b/src/locales/zhtw/shared.json index 48cb83c2f..c7d5fe72c 100644 --- a/src/locales/zhtw/shared.json +++ b/src/locales/zhtw/shared.json @@ -21,7 +21,9 @@ "pause": "暫停", "resume": "繼續", "closeAll": "關閉全部", - "clear": "清除" + "clear": "清除", + "previous": "上一頁", + "next": "下一頁" }, "labels": { "updateAt": "更新於", @@ -48,6 +50,7 @@ "milliseconds": "毫秒", "seconds": "秒", "minutes": "分鐘", + "hours": "小時", "kilobytes": "KB", "files": "檔案" }, diff --git a/src/types/generated/i18n-keys.ts b/src/types/generated/i18n-keys.ts index 78ebff265..99abd3fe6 100644 --- a/src/types/generated/i18n-keys.ts +++ b/src/types/generated/i18n-keys.ts @@ -462,6 +462,7 @@ export const translationKeys = [ "settings.modals.backup.actions.deleteBackup", "settings.modals.backup.actions.restore", "settings.modals.backup.actions.restoreBackup", + "settings.modals.backup.actions.viewHistory", "settings.modals.backup.fields.webdavUrl", "settings.modals.backup.fields.username", "settings.modals.backup.fields.info", @@ -478,8 +479,26 @@ export const translationKeys = [ "settings.modals.backup.messages.restoreSuccess", "settings.modals.backup.messages.localBackupExported", "settings.modals.backup.messages.localBackupExportFailed", + "settings.modals.backup.messages.webdavRefreshSuccess", + "settings.modals.backup.messages.webdavRefreshFailed", "settings.modals.backup.messages.confirmDelete", "settings.modals.backup.messages.confirmRestore", + "settings.modals.backup.auto.title", + "settings.modals.backup.auto.scheduleLabel", + "settings.modals.backup.auto.scheduleHelper", + "settings.modals.backup.auto.intervalLabel", + "settings.modals.backup.auto.changeLabel", + "settings.modals.backup.auto.changeHelper", + "settings.modals.backup.auto.options.hours", + "settings.modals.backup.auto.options.days", + "settings.modals.backup.manual.title", + "settings.modals.backup.manual.local", + "settings.modals.backup.manual.webdav", + "settings.modals.backup.manual.configureWebdav", + "settings.modals.backup.history.title", + "settings.modals.backup.history.summary", + "settings.modals.backup.history.empty", + "settings.modals.backup.webdav.title", "settings.modals.backup.table.filename", "settings.modals.backup.table.backupTime", "settings.modals.backup.table.actions", @@ -639,6 +658,8 @@ export const translationKeys = [ "shared.actions.resume", "shared.actions.closeAll", "shared.actions.clear", + "shared.actions.previous", + "shared.actions.next", "shared.labels.updateAt", "shared.labels.timeout", "shared.labels.icon", @@ -659,6 +680,7 @@ export const translationKeys = [ "shared.units.milliseconds", "shared.units.seconds", "shared.units.minutes", + "shared.units.hours", "shared.units.kilobytes", "shared.units.files", "shared.placeholders.filter", diff --git a/src/types/generated/i18n-resources.ts b/src/types/generated/i18n-resources.ts index 497ec0331..3c23476eb 100644 --- a/src/types/generated/i18n-resources.ts +++ b/src/types/generated/i18n-resources.ts @@ -690,12 +690,36 @@ export interface TranslationResources { restore: string; restoreBackup: string; selectTarget: string; + viewHistory: string; + }; + auto: { + changeHelper: string; + changeLabel: string; + intervalLabel: string; + options: { + days: string; + hours: string; + }; + scheduleHelper: string; + scheduleLabel: string; + title: string; }; fields: { info: string; username: string; webdavUrl: string; }; + history: { + empty: string; + summary: string; + title: string; + }; + manual: { + configureWebdav: string; + local: string; + title: string; + webdav: string; + }; messages: { backupCreated: string; backupFailed: string; @@ -711,6 +735,8 @@ export interface TranslationResources { usernameRequired: string; webdavConfigSaved: string; webdavConfigSaveFailed: string; + webdavRefreshFailed: string; + webdavRefreshSuccess: string; webdavUrlRequired: string; }; table: { @@ -725,6 +751,9 @@ export interface TranslationResources { webdav: string; }; title: string; + webdav: { + title: string; + }; }; clashCore: { variants: { @@ -1139,7 +1168,9 @@ export interface TranslationResources { hideDetails: string; listView: string; new: string; + next: string; pause: string; + previous: string; refresh: string; refreshPage: string; resetToDefault: string; @@ -1242,6 +1273,7 @@ export interface TranslationResources { }; units: { files: string; + hours: string; kilobytes: string; milliseconds: string; minutes: string; diff --git a/src/types/types.d.ts b/src/types/types.d.ts index cc9eaac37..317eaf5bf 100644 --- a/src/types/types.d.ts +++ b/src/types/types.d.ts @@ -854,6 +854,9 @@ interface IVergeConfig { enable_auto_delay_detection?: boolean; enable_builtin_enhanced?: boolean; auto_log_clean?: 0 | 1 | 2 | 3 | 4; + enable_auto_backup_schedule?: boolean; + auto_backup_interval_hours?: number; + auto_backup_on_change?: boolean; proxy_layout_column?: number; test_list?: IVergeTestItem[]; webdav_url?: string; From 7d42d5cb0f81f059bdde18e200244bbbedfd5e7f Mon Sep 17 00:00:00 2001 From: Slinetrac Date: Mon, 10 Nov 2025 14:30:47 +0800 Subject: [PATCH 33/49] feat: add proxy chain icon --- src/pages/proxies.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/pages/proxies.tsx b/src/pages/proxies.tsx index 29b630f68..2f38b5ff9 100644 --- a/src/pages/proxies.tsx +++ b/src/pages/proxies.tsx @@ -1,3 +1,4 @@ +import { LanOutlined, LanRounded } from "@mui/icons-material"; import { Box, Button, ButtonGroup } from "@mui/material"; import { useLockFn } from "ahooks"; import { useCallback, useEffect, useMemo, useReducer, useState } from "react"; @@ -167,6 +168,13 @@ const ProxyPage = () => { variant={isChainMode ? "contained" : "outlined"} onClick={onToggleChainMode} sx={{ ml: 1 }} + startIcon={ + isChainMode ? ( + + ) : ( + + ) + } > {t("proxies.page.actions.toggleChain")} From 33f575ed4d10b8b6161477eca4c0ef21775e1856 Mon Sep 17 00:00:00 2001 From: Tunglies Date: Mon, 10 Nov 2025 19:06:37 +0800 Subject: [PATCH 34/49] refactor: update icon handling in tray methods to accept IVerge reference and improve logging #5379 (#5387) * refactor: update icon handling in tray methods to accept IVerge reference and improve logging #5379 * refactor: simplify icon retrieval logic in Tray implementation --- src-tauri/src/core/tray/mod.rs | 52 +++++++++++-------- src-tauri/src/feat/clash.rs | 2 +- src-tauri/src/feat/config.rs | 7 ++- src-tauri/src/utils/dirs.rs | 15 ++++-- src/components/setting/mods/layout-viewer.tsx | 1 - 5 files changed, 47 insertions(+), 30 deletions(-) diff --git a/src-tauri/src/core/tray/mod.rs b/src-tauri/src/core/tray/mod.rs index 574e9294f..5595d15a2 100644 --- a/src-tauri/src/core/tray/mod.rs +++ b/src-tauri/src/core/tray/mod.rs @@ -4,7 +4,7 @@ use tauri_plugin_mihomo::models::Proxies; use tokio::fs; #[cfg(target_os = "macos")] pub mod speed_rate; -use crate::config::PrfSelected; +use crate::config::{IVerge, PrfSelected}; use crate::core::service; use crate::module::lightweight; use crate::process::AsyncHandler; @@ -83,8 +83,7 @@ pub struct Tray { } impl TrayState { - pub async fn get_common_tray_icon() -> (bool, Vec) { - let verge = Config::verge().await.latest_arc(); + async fn get_common_tray_icon(verge: &IVerge) -> (bool, Vec) { let is_common_tray_icon = verge.common_tray_icon.unwrap_or(false); if is_common_tray_icon && let Ok(Some(common_icon_path)) = find_target_icons("common") @@ -120,8 +119,7 @@ impl TrayState { } } - pub async fn get_sysproxy_tray_icon() -> (bool, Vec) { - let verge = Config::verge().await.latest_arc(); + async fn get_sysproxy_tray_icon(verge: &IVerge) -> (bool, Vec) { let is_sysproxy_tray_icon = verge.sysproxy_tray_icon.unwrap_or(false); if is_sysproxy_tray_icon && let Ok(Some(sysproxy_icon_path)) = find_target_icons("sysproxy") @@ -157,8 +155,7 @@ impl TrayState { } } - pub async fn get_tun_tray_icon() -> (bool, Vec) { - let verge = Config::verge().await.latest_arc(); + async fn get_tun_tray_icon(verge: &IVerge) -> (bool, Vec) { let is_tun_tray_icon = verge.tun_tray_icon.unwrap_or(false); if is_tun_tray_icon && let Ok(Some(tun_icon_path)) = find_target_icons("tun") @@ -351,7 +348,7 @@ impl Tray { /// 更新托盘图标 #[cfg(target_os = "macos")] - pub async fn update_icon(&self) -> Result<()> { + pub async fn update_icon(&self, verge: Option<&IVerge>) -> Result<()> { if handle::Handle::global().is_exiting() { logging!(debug, Type::Tray, "应用正在退出,跳过托盘图标更新"); return Ok(()); @@ -371,15 +368,19 @@ impl Tray { } }; - let verge = Config::verge().await.latest_arc(); + let verge = match verge { + Some(v) => v, + None => &Config::verge().await.data_arc(), + }; + let system_mode = verge.enable_system_proxy.as_ref().unwrap_or(&false); let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false); let (_is_custom_icon, icon_bytes) = match (*system_mode, *tun_mode) { - (true, true) => TrayState::get_tun_tray_icon().await, - (true, false) => TrayState::get_sysproxy_tray_icon().await, - (false, true) => TrayState::get_tun_tray_icon().await, - (false, false) => TrayState::get_common_tray_icon().await, + (true, true) => TrayState::get_tun_tray_icon(verge).await, + (true, false) => TrayState::get_sysproxy_tray_icon(verge).await, + (false, true) => TrayState::get_tun_tray_icon(verge).await, + (false, false) => TrayState::get_common_tray_icon(verge).await, }; let colorful = verge @@ -394,7 +395,7 @@ impl Tray { } #[cfg(not(target_os = "macos"))] - pub async fn update_icon(&self) -> Result<()> { + pub async fn update_icon(&self, verge: Option<&IVerge>) -> Result<()> { if handle::Handle::global().is_exiting() { logging!(debug, Type::Tray, "应用正在退出,跳过托盘图标更新"); return Ok(()); @@ -414,15 +415,19 @@ impl Tray { } }; - let verge = Config::verge().await.latest_arc(); + let verge = match verge { + Some(v) => v, + None => &Config::verge().await.data_arc(), + }; + let system_mode = verge.enable_system_proxy.as_ref().unwrap_or(&false); let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false); let (_is_custom_icon, icon_bytes) = match (*system_mode, *tun_mode) { - (true, true) => TrayState::get_tun_tray_icon().await, - (true, false) => TrayState::get_sysproxy_tray_icon().await, - (false, true) => TrayState::get_tun_tray_icon().await, - (false, false) => TrayState::get_common_tray_icon().await, + (true, true) => TrayState::get_tun_tray_icon(verge).await, + (true, false) => TrayState::get_sysproxy_tray_icon(verge).await, + (false, true) => TrayState::get_tun_tray_icon(verge).await, + (false, false) => TrayState::get_common_tray_icon(verge).await, }; let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?)); @@ -506,12 +511,12 @@ impl Tray { return Ok(()); } self.update_menu().await?; - self.update_icon().await?; + self.update_icon(None).await?; self.update_tooltip().await?; Ok(()) } - pub async fn create_tray_from_handle(&self, app_handle: &AppHandle) -> Result<()> { + async fn create_tray_from_handle(&self, app_handle: &AppHandle) -> Result<()> { if handle::Handle::global().is_exiting() { logging!(debug, Type::Tray, "应用正在退出,跳过托盘创建"); return Ok(()); @@ -519,8 +524,10 @@ impl Tray { logging!(info, Type::Tray, "正在从AppHandle创建系统托盘"); + let verge = Config::verge().await.data_arc(); + // 获取图标 - let icon_bytes = TrayState::get_common_tray_icon().await.1; + let icon_bytes = TrayState::get_common_tray_icon(&verge).await.1; let icon = tauri::image::Image::from_bytes(&icon_bytes)?; #[cfg(target_os = "linux")] @@ -530,6 +537,7 @@ impl Tray { #[cfg(any(target_os = "macos", target_os = "windows"))] let show_menu_on_left_click = { + // TODO 优化这里 复用 verge let tray_event = { Config::verge().await.latest_arc().tray_event.clone() }; let tray_event: String = tray_event.unwrap_or_else(|| "main_window".into()); tray_event.as_str() == "tray_menu" diff --git a/src-tauri/src/feat/clash.rs b/src-tauri/src/feat/clash.rs index 5fe26b2b3..ce178c15e 100644 --- a/src-tauri/src/feat/clash.rs +++ b/src-tauri/src/feat/clash.rs @@ -82,7 +82,7 @@ pub async fn change_clash_mode(mode: String) { if clash_data.save_config().await.is_ok() { handle::Handle::refresh_clash(); logging_error!(Type::Tray, tray::Tray::global().update_menu().await); - logging_error!(Type::Tray, tray::Tray::global().update_icon().await); + logging_error!(Type::Tray, tray::Tray::global().update_icon(None).await); } let is_auto_close_connection = Config::verge() diff --git a/src-tauri/src/feat/config.rs b/src-tauri/src/feat/config.rs index e7aec2a1f..33c11ea07 100644 --- a/src-tauri/src/feat/config.rs +++ b/src-tauri/src/feat/config.rs @@ -22,7 +22,7 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> { } else { if patch.get("mode").is_some() { logging_error!(Type::Tray, tray::Tray::global().update_menu().await); - logging_error!(Type::Tray, tray::Tray::global().update_icon().await); + logging_error!(Type::Tray, tray::Tray::global().update_icon(None).await); } Config::runtime() .await @@ -180,6 +180,7 @@ fn determine_update_flags(patch: &IVerge) -> i32 { update_flags } +#[allow(clippy::cognitive_complexity)] async fn process_terminated_flags(update_flags: i32, patch: &IVerge) -> Result<()> { // Process updates based on flags if (update_flags & (UpdateFlags::RestartCore as i32)) != 0 { @@ -211,7 +212,9 @@ async fn process_terminated_flags(update_flags: i32, patch: &IVerge) -> Result<( tray::Tray::global().update_menu().await?; } if (update_flags & (UpdateFlags::SystrayIcon as i32)) != 0 { - tray::Tray::global().update_icon().await?; + tray::Tray::global() + .update_icon(Some(&Config::verge().await.latest_arc())) + .await?; } if (update_flags & (UpdateFlags::SystrayTooltip as i32)) != 0 { tray::Tray::global().update_tooltip().await?; diff --git a/src-tauri/src/utils/dirs.rs b/src-tauri/src/utils/dirs.rs index 6f678696e..f00e33f33 100644 --- a/src-tauri/src/utils/dirs.rs +++ b/src-tauri/src/utils/dirs.rs @@ -106,10 +106,17 @@ pub fn find_target_icons(target: &str) -> Result> { let icon_path = fs::read_dir(&icons_dir)? .filter_map(|entry| entry.ok().map(|e| e.path())) .find(|path| { - path.file_prefix().is_some_and(|prefix| prefix == target) - && path - .extension() - .is_some_and(|ext| ext == "ico" || ext == "png") + let prefix_matches = path + .file_prefix() + .and_then(|p| p.to_str()) + .is_some_and(|prefix| prefix.starts_with(target)); + let ext_matches = path + .extension() + .and_then(|e| e.to_str()) + .is_some_and(|ext| { + ext.eq_ignore_ascii_case("ico") || ext.eq_ignore_ascii_case("png") + }); + prefix_matches && ext_matches }); icon_path diff --git a/src/components/setting/mods/layout-viewer.tsx b/src/components/setting/mods/layout-viewer.tsx index 6f1482f4d..3f367a3fa 100644 --- a/src/components/setting/mods/layout-viewer.tsx +++ b/src/components/setting/mods/layout-viewer.tsx @@ -425,7 +425,6 @@ export const LayoutViewer = forwardRef((_, ref) => { await initIconPath(); onChangeData({ common_tray_icon: true }); patchVerge({ common_tray_icon: true }); - console.log(); } } }} From 2d9eef834b1eb00cb2e206d658efe1cfd1ab2ce7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 22:38:29 +0800 Subject: [PATCH 35/49] chore(deps): update dependency @eslint-react/eslint-plugin to ^2.3.4 (#5394) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- pnpm-lock.yaml | 150 ++++++++++++++++++++++++------------------------- 2 files changed, 76 insertions(+), 76 deletions(-) diff --git a/package.json b/package.json index 59c681a92..76e9f922f 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ }, "devDependencies": { "@actions/github": "^6.0.1", - "@eslint-react/eslint-plugin": "^2.3.1", + "@eslint-react/eslint-plugin": "^2.3.4", "@eslint/js": "^9.39.1", "@tauri-apps/cli": "2.9.4", "@types/js-yaml": "^4.0.9", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4f081b099..8aeb07ad3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -139,8 +139,8 @@ importers: specifier: ^6.0.1 version: 6.0.1 '@eslint-react/eslint-plugin': - specifier: ^2.3.1 - version: 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + specifier: ^2.3.4 + version: 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) '@eslint/js': specifier: ^9.39.1 version: 9.39.1 @@ -1010,31 +1010,31 @@ packages: resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint-react/ast@2.3.1': - resolution: {integrity: sha512-jB/P72HVbZcC7DtUvjna8tjPSageAS6L9x5muMsBRQxEXkfv2J6CPX47sSpaPu1mMJn1Zzpn9m5z4aTPbfV6Ug==} + '@eslint-react/ast@2.3.4': + resolution: {integrity: sha512-wueu2vBRwKqnrgOuz1JmEefhn5dgfa1NgvFtDMfefePfICTYFtwkTuOJNGJI77TUnqV9inp9ZNDB1nyAMmlZMg==} engines: {node: '>=20.19.0'} - '@eslint-react/core@2.3.1': - resolution: {integrity: sha512-R0gXjIqHqqYSeHxNMbXblnlwzmZ2gD32aVPmrJB+SrLP0rItzo/WgVSvstjOK5+N5KExdM87hopFcqnlZS3ONg==} + '@eslint-react/core@2.3.4': + resolution: {integrity: sha512-1ve3pSjAhd4PKcTpZd0bQwkAgWOAJjfgAixTfOvJ7QhWi3gPKq0Wn0vi78XcFO8K0IL3EcOfX2g2JhZJzSRnWQ==} engines: {node: '>=20.19.0'} - '@eslint-react/eff@2.3.1': - resolution: {integrity: sha512-k58lxHmhzatRZXVFzWdLZwfRwPgI5Thhf7BNVJ9F+NI2G1fFypclSVFRPqjGmI5jQ8bqB+5UVt9Rh49rZGZPzw==} + '@eslint-react/eff@2.3.4': + resolution: {integrity: sha512-/SXI23DpQCutSXPdAPdSvErfsBFYwaqbgQy5dLmnde4wHivk/eD9O/rA/Xu1c4j9tLJG/sZBQ9DzN/UXHizjag==} engines: {node: '>=20.19.0'} - '@eslint-react/eslint-plugin@2.3.1': - resolution: {integrity: sha512-ThWx+AWI3Tl/6g+L1Cq/kTQrrZ4NXWMxRN92iBswYMW7bPaolh/8WBdiLAVZldqnlm+l6LZriia89jyr0CeTHA==} + '@eslint-react/eslint-plugin@2.3.4': + resolution: {integrity: sha512-fcLZNQRbzjBK510a2oqLcsvdCSMK169x3CGfi1E3JvdMPyLICM+KZ8wNnhULZPey7BnEFVWC7dQfz5Dct60C7g==} engines: {node: '>=20.19.0'} peerDependencies: eslint: ^9.38.0 typescript: ^5.9.3 - '@eslint-react/shared@2.3.1': - resolution: {integrity: sha512-UiTbPi1i7UPdsIT2Z7mKZ3zzrgAm1GLeexkKe4QwvZJ1LLeEJmgMwHUw852+VzlDeV8stcQmZ9zWqFX2L0CmGg==} + '@eslint-react/shared@2.3.4': + resolution: {integrity: sha512-FN3X4jA0kksFO9cjXwRaFHA6wbc7AgHjYkczdfwm0dOr/uUVv3Sc/F3kUNJ6KFOSoDCfNGybRCICu9DZbanhWA==} engines: {node: '>=20.19.0'} - '@eslint-react/var@2.3.1': - resolution: {integrity: sha512-1rC9dbuKKMq77pPoODGT91VTA3ReivIAfdFJePEjscPSRAUhCy7QPA/yK8MPe9nTsG89IDV+hilCGKiLZW8vNQ==} + '@eslint-react/var@2.3.4': + resolution: {integrity: sha512-hT2TvusBcEfFLFGfwZRstkQv9lp+079DEba5F6Dr53g3f+gBvLNDcayb5zDgwNP5+hQRd8lY8goEOfA8PWyaJQ==} engines: {node: '>=20.19.0'} '@eslint/config-array@0.21.1': @@ -2613,15 +2613,15 @@ packages: eslint-config-prettier: optional: true - eslint-plugin-react-dom@2.3.1: - resolution: {integrity: sha512-Zuvb8iDYRbi8s7mYzvjHKD+i+loHjF6TKJiLGYM/t9F42OWU7V7b4sjIM7pXueukl0o8BSJXDVrQ+9sHOOmxBA==} + eslint-plugin-react-dom@2.3.4: + resolution: {integrity: sha512-xAetOWwrfYnqU0Vd6LjEQi1paAwU7Tgoa+jHUb7QCKxdILDV618yPrMigWpY+438cg6oXTYE/awFI/Rv+dOFCw==} engines: {node: '>=20.19.0'} peerDependencies: eslint: ^9.38.0 typescript: ^5.9.3 - eslint-plugin-react-hooks-extra@2.3.1: - resolution: {integrity: sha512-2t4xQYhUEgPNq1SDQJEXuH3doT+h5spVmerX4rPnBFx0zG2sYfaJV1Gz6z40pI1L3CtBrZag5nFJ44AF/BEg0w==} + eslint-plugin-react-hooks-extra@2.3.4: + resolution: {integrity: sha512-widwgbMP3cC3sjmHidryg2McYqnzoIWcpsQ4JYnaXmRMMTem+Sp7a3+fkWvAUfWQBQIbigPT9O2+VhlvC+xlPg==} engines: {node: '>=20.0.0'} peerDependencies: eslint: ^9.38.0 @@ -2633,8 +2633,8 @@ packages: peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 - eslint-plugin-react-naming-convention@2.3.1: - resolution: {integrity: sha512-Ghh1o++3XDk3zNKF7DXy3kIN1kJYFiH7wvl4aJF5m9LytQGFrJKTA5kygAaWgR7iL8o4mjk5Ty6Be3OKskpHwA==} + eslint-plugin-react-naming-convention@2.3.4: + resolution: {integrity: sha512-KP6d3WxiLmTnm245VK1lLI4Pzu/tkJLNOsmAetDm0kPfamD71msZMerp4VYFsFtqk0km9rbRKE7Cdd27tL43Ow==} engines: {node: '>=20.19.0'} peerDependencies: eslint: ^9.38.0 @@ -2645,15 +2645,15 @@ packages: peerDependencies: eslint: '>=8.40' - eslint-plugin-react-web-api@2.3.1: - resolution: {integrity: sha512-rb7AYR9SCJkCDkFdqnD6JHNLKF1o29o6tZLSaPdzA1Ssxh7/VKgJ8GpTrgl3Rv+Gnyn+w+3w4XE14d7T1Db9nA==} + eslint-plugin-react-web-api@2.3.4: + resolution: {integrity: sha512-NQMVDVIOiU+vJ98hkrIZeNdIwmO54UOjB8EfiHkgDWk69QBJrBtrn5AguFBuUlBUdZR/9vp4yKdQFtzBbRcDTw==} engines: {node: '>=20.19.0'} peerDependencies: eslint: ^9.38.0 typescript: ^5.9.3 - eslint-plugin-react-x@2.3.1: - resolution: {integrity: sha512-7zfi297NfkoEtqaz2W953gdK4J9nJD5okVhJVxgrcrP+9FVertkGqpbWtMZLpQuWJ216FncY8P6t1U+af8KNOA==} + eslint-plugin-react-x@2.3.4: + resolution: {integrity: sha512-6rW/CALxGCO9M2wuIUMNKuepSf5uW+Sg+AaQtLpsfs+B6fmDVHFrxIgg9wYk2aRrI6MMQcjDUMEYPUX9eOg2wQ==} engines: {node: '>=20.19.0'} peerDependencies: eslint: ^9.38.0 @@ -5212,9 +5212,9 @@ snapshots: '@eslint-community/regexpp@4.12.1': {} - '@eslint-react/ast@2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@eslint-react/ast@2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-react/eff': 2.3.1 + '@eslint-react/eff': 2.3.4 '@typescript-eslint/types': 8.46.3 '@typescript-eslint/typescript-estree': 8.46.3(typescript@5.9.3) '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) @@ -5224,12 +5224,12 @@ snapshots: - supports-color - typescript - '@eslint-react/core@2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@eslint-react/core@2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-react/ast': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/eff': 2.3.1 - '@eslint-react/shared': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/var': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/ast': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.3.4 + '@eslint-react/shared': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.46.3 '@typescript-eslint/types': 8.46.3 '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) @@ -5240,30 +5240,30 @@ snapshots: - supports-color - typescript - '@eslint-react/eff@2.3.1': {} + '@eslint-react/eff@2.3.4': {} - '@eslint-react/eslint-plugin@2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@eslint-react/eslint-plugin@2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-react/eff': 2.3.1 - '@eslint-react/shared': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.3.4 + '@eslint-react/shared': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.46.3 '@typescript-eslint/type-utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/types': 8.46.3 '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) - eslint-plugin-react-dom: 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-react-hooks-extra: 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-react-naming-convention: 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-react-web-api: 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-react-x: 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-react-dom: 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-react-hooks-extra: 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-react-naming-convention: 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-react-web-api: 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-react-x: 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) ts-api-utils: 2.1.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@eslint-react/shared@2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@eslint-react/shared@2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-react/eff': 2.3.1 + '@eslint-react/eff': 2.3.4 '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) ts-pattern: 5.9.0 zod: 4.1.12 @@ -5272,10 +5272,10 @@ snapshots: - supports-color - typescript - '@eslint-react/var@2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@eslint-react/var@2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-react/ast': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/eff': 2.3.1 + '@eslint-react/ast': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.3.4 '@typescript-eslint/scope-manager': 8.46.3 '@typescript-eslint/types': 8.46.3 '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) @@ -6927,13 +6927,13 @@ snapshots: optionalDependencies: eslint-config-prettier: 10.1.8(eslint@9.39.1(jiti@2.6.1)) - eslint-plugin-react-dom@2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): + eslint-plugin-react-dom@2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@eslint-react/ast': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/core': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/eff': 2.3.1 - '@eslint-react/shared': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/var': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/ast': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/core': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.3.4 + '@eslint-react/shared': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.46.3 '@typescript-eslint/types': 8.46.3 '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) @@ -6945,13 +6945,13 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-react-hooks-extra@2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): + eslint-plugin-react-hooks-extra@2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@eslint-react/ast': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/core': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/eff': 2.3.1 - '@eslint-react/shared': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/var': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/ast': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/core': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.3.4 + '@eslint-react/shared': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.46.3 '@typescript-eslint/type-utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/types': 8.46.3 @@ -6974,13 +6974,13 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-react-naming-convention@2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): + eslint-plugin-react-naming-convention@2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@eslint-react/ast': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/core': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/eff': 2.3.1 - '@eslint-react/shared': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/var': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/ast': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/core': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.3.4 + '@eslint-react/shared': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.46.3 '@typescript-eslint/type-utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/types': 8.46.3 @@ -6996,13 +6996,13 @@ snapshots: dependencies: eslint: 9.39.1(jiti@2.6.1) - eslint-plugin-react-web-api@2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): + eslint-plugin-react-web-api@2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@eslint-react/ast': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/core': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/eff': 2.3.1 - '@eslint-react/shared': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/var': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/ast': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/core': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.3.4 + '@eslint-react/shared': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.46.3 '@typescript-eslint/types': 8.46.3 '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) @@ -7013,13 +7013,13 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-react-x@2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): + eslint-plugin-react-x@2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@eslint-react/ast': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/core': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/eff': 2.3.1 - '@eslint-react/shared': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/var': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/ast': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/core': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.3.4 + '@eslint-react/shared': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.46.3 '@typescript-eslint/type-utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/types': 8.46.3 From 600019f9801d6f8d6df1ff20bc2da8dcd5934cf9 Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Tue, 11 Nov 2025 08:24:23 +0800 Subject: [PATCH 36/49] chore: update tauri-plugin-mihomo --- src-tauri/Cargo.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index b7bf8c381..a2965c4c4 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1257,7 +1257,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" dependencies = [ "lazy_static", - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] @@ -3395,7 +3395,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.1", + "socket2 0.5.10", "system-configuration", "tokio", "tower-service", @@ -5030,7 +5030,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] @@ -5830,7 +5830,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.6.1", + "socket2 0.5.10", "thiserror 2.0.17", "tokio", "tracing", @@ -5867,7 +5867,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.1", + "socket2 0.5.10", "tracing", "windows-sys 0.60.2", ] @@ -7703,7 +7703,7 @@ dependencies = [ [[package]] name = "tauri-plugin-mihomo" version = "0.1.1" -source = "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo#dcb6b5a6753233422e7cea23042239c7994c605c" +source = "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo#d0f00b33cea294cc693e177441fc897426ecbc39" dependencies = [ "base64 0.22.1", "futures-util", @@ -9331,7 +9331,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] From 6e9c6e60bb8c3358a6b7b2a7624b5336a0deba94 Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Tue, 11 Nov 2025 08:53:14 +0800 Subject: [PATCH 37/49] chore: update sysproxy-rs --- src-tauri/Cargo.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index a2965c4c4..ced4d66ba 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3415,7 +3415,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.2", + "windows-core 0.61.2", ] [[package]] @@ -6416,7 +6416,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -7284,7 +7284,7 @@ dependencies = [ [[package]] name = "sysproxy" version = "0.3.1" -source = "git+https://github.com/clash-verge-rev/sysproxy-rs#50100ab03eb802056c381f3c5009e903c67e3bac" +source = "git+https://github.com/clash-verge-rev/sysproxy-rs#ea6e5b5bcef32025e1df914d663eea8558afacb2" dependencies = [ "interfaces", "iptools", From e02f23e0aaaec621226a3387ca65247ddcfb126f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 11 Nov 2025 16:38:08 +0800 Subject: [PATCH 38/49] chore(deps): update npm dependencies (#5400) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 6 +- pnpm-lock.yaml | 298 ++++++++++++++++++++++++------------------------- 2 files changed, 152 insertions(+), 152 deletions(-) diff --git a/package.json b/package.json index 76e9f922f..1941048f7 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "axios": "^1.13.2", "dayjs": "1.11.19", "foxact": "^0.2.49", - "i18next": "^25.6.1", + "i18next": "^25.6.2", "js-yaml": "^4.1.0", "json-schema": "^0.4.0", "lodash-es": "^4.17.21", @@ -112,11 +112,11 @@ "meta-json-schema": "^1.19.14", "node-fetch": "^3.3.2", "prettier": "^3.6.2", - "sass": "^1.93.3", + "sass": "^1.94.0", "tar": "^7.5.2", "terser": "^5.44.1", "typescript": "^5.9.3", - "typescript-eslint": "^8.46.3", + "typescript-eslint": "^8.46.4", "vite": "^7.2.2", "vite-plugin-monaco-editor-esm": "^2.0.2", "vite-plugin-svgr": "^4.5.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8aeb07ad3..5e44f718a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -81,8 +81,8 @@ importers: specifier: ^0.2.49 version: 0.2.49(react@19.2.0) i18next: - specifier: ^25.6.1 - version: 25.6.1(typescript@5.9.3) + specifier: ^25.6.2 + version: 25.6.2(typescript@5.9.3) js-yaml: specifier: ^4.1.0 version: 4.1.0 @@ -115,7 +115,7 @@ importers: version: 7.66.0(react@19.2.0) react-i18next: specifier: 16.2.4 - version: 16.2.4(i18next@25.6.1(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3) + version: 16.2.4(i18next@25.6.2(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3) react-markdown: specifier: 10.1.0 version: 10.1.0(@types/react@19.2.2)(react@19.2.0) @@ -130,7 +130,7 @@ importers: version: 2.3.6(react@19.2.0) tauri-plugin-mihomo-api: specifier: git+https://github.com/clash-verge-rev/tauri-plugin-mihomo - version: https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/dcb6b5a6753233422e7cea23042239c7994c605c + version: https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/d0f00b33cea294cc693e177441fc897426ecbc39 types-pac: specifier: ^1.0.3 version: 1.0.3 @@ -164,10 +164,10 @@ importers: version: 19.2.2(@types/react@19.2.2) '@vitejs/plugin-legacy': specifier: ^7.2.1 - version: 7.2.1(terser@5.44.1)(vite@7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.1)(yaml@2.8.1)) + version: 7.2.1(terser@5.44.1)(vite@7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)) '@vitejs/plugin-react-swc': specifier: ^4.2.1 - version: 4.2.1(vite@7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.1)(yaml@2.8.1)) + version: 4.2.1(vite@7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)) adm-zip: specifier: ^0.5.16 version: 0.5.16 @@ -188,10 +188,10 @@ importers: version: 10.1.8(eslint@9.39.1(jiti@2.6.1)) eslint-import-resolver-typescript: specifier: ^4.4.4 - version: 4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) + version: 4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-import-x: specifier: ^4.16.1 - version: 4.16.1(@typescript-eslint/utils@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)) + version: 4.16.1(@typescript-eslint/utils@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-prettier: specifier: ^5.5.4 version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.6.2) @@ -203,7 +203,7 @@ importers: version: 0.4.24(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-unused-imports: specifier: ^4.3.0 - version: 4.3.0(@typescript-eslint/eslint-plugin@8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)) + version: 4.3.0(@typescript-eslint/eslint-plugin@8.46.4(@typescript-eslint/parser@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)) glob: specifier: ^11.0.3 version: 11.0.3 @@ -232,8 +232,8 @@ importers: specifier: ^3.6.2 version: 3.6.2 sass: - specifier: ^1.93.3 - version: 1.93.3 + specifier: ^1.94.0 + version: 1.94.0 tar: specifier: ^7.5.2 version: 7.5.2 @@ -244,20 +244,20 @@ importers: specifier: ^5.9.3 version: 5.9.3 typescript-eslint: - specifier: ^8.46.3 - version: 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + specifier: ^8.46.4 + version: 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) vite: specifier: ^7.2.2 - version: 7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.1)(yaml@2.8.1) + version: 7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1) vite-plugin-monaco-editor-esm: specifier: ^2.0.2 version: 2.0.2(monaco-editor@0.54.0) vite-plugin-svgr: specifier: ^4.5.0 - version: 4.5.0(rollup@4.46.2)(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.1)(yaml@2.8.1)) + version: 4.5.0(rollup@4.46.2)(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)) vitest: specifier: ^4.0.8 - version: 4.0.8(@types/debug@4.1.12)(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.1)(yaml@2.8.1) + version: 4.0.8(@types/debug@4.1.12)(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1) packages: @@ -1859,63 +1859,63 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} - '@typescript-eslint/eslint-plugin@8.46.3': - resolution: {integrity: sha512-sbaQ27XBUopBkRiuY/P9sWGOWUW4rl8fDoHIUmLpZd8uldsTyB4/Zg6bWTegPoTLnKj9Hqgn3QD6cjPNB32Odw==} + '@typescript-eslint/eslint-plugin@8.46.4': + resolution: {integrity: sha512-R48VhmTJqplNyDxCyqqVkFSZIx1qX6PzwqgcXn1olLrzxcSBDlOsbtcnQuQhNtnNiJ4Xe5gREI1foajYaYU2Vg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.46.3 + '@typescript-eslint/parser': ^8.46.4 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.46.3': - resolution: {integrity: sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==} + '@typescript-eslint/parser@8.46.4': + resolution: {integrity: sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.46.3': - resolution: {integrity: sha512-Fz8yFXsp2wDFeUElO88S9n4w1I4CWDTXDqDr9gYvZgUpwXQqmZBr9+NTTql5R3J7+hrJZPdpiWaB9VNhAKYLuQ==} + '@typescript-eslint/project-service@8.46.4': + resolution: {integrity: sha512-nPiRSKuvtTN+no/2N1kt2tUh/HoFzeEgOm9fQ6XQk4/ApGqjx0zFIIaLJ6wooR1HIoozvj2j6vTi/1fgAz7UYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@8.46.3': - resolution: {integrity: sha512-FCi7Y1zgrmxp3DfWfr+3m9ansUUFoy8dkEdeQSgA9gbm8DaHYvZCdkFRQrtKiedFf3Ha6VmoqoAaP68+i+22kg==} + '@typescript-eslint/scope-manager@8.46.4': + resolution: {integrity: sha512-tMDbLGXb1wC+McN1M6QeDx7P7c0UWO5z9CXqp7J8E+xGcJuUuevWKxuG8j41FoweS3+L41SkyKKkia16jpX7CA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.46.3': - resolution: {integrity: sha512-GLupljMniHNIROP0zE7nCcybptolcH8QZfXOpCfhQDAdwJ/ZTlcaBOYebSOZotpti/3HrHSw7D3PZm75gYFsOA==} + '@typescript-eslint/tsconfig-utils@8.46.4': + resolution: {integrity: sha512-+/XqaZPIAk6Cjg7NWgSGe27X4zMGqrFqZ8atJsX3CWxH/jACqWnrWI68h7nHQld0y+k9eTTjb9r+KU4twLoo9A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.46.3': - resolution: {integrity: sha512-ZPCADbr+qfz3aiTTYNNkCbUt+cjNwI/5McyANNrFBpVxPt7GqpEYz5ZfdwuFyGUnJ9FdDXbGODUu6iRCI6XRXw==} + '@typescript-eslint/type-utils@8.46.4': + resolution: {integrity: sha512-V4QC8h3fdT5Wro6vANk6eojqfbv5bpwHuMsBcJUJkqs2z5XnYhJzyz9Y02eUmF9u3PgXEUiOt4w4KHR3P+z0PQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@8.46.3': - resolution: {integrity: sha512-G7Ok9WN/ggW7e/tOf8TQYMaxgID3Iujn231hfi0Pc7ZheztIJVpO44ekY00b7akqc6nZcvregk0Jpah3kep6hA==} + '@typescript-eslint/types@8.46.4': + resolution: {integrity: sha512-USjyxm3gQEePdUwJBFjjGNG18xY9A2grDVGuk7/9AkjIF1L+ZrVnwR5VAU5JXtUnBL/Nwt3H31KlRDaksnM7/w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.46.3': - resolution: {integrity: sha512-f/NvtRjOm80BtNM5OQtlaBdM5BRFUv7gf381j9wygDNL+qOYSNOgtQ/DCndiYi80iIOv76QqaTmp4fa9hwI0OA==} + '@typescript-eslint/typescript-estree@8.46.4': + resolution: {integrity: sha512-7oV2qEOr1d4NWNmpXLR35LvCfOkTNymY9oyW+lUHkmCno7aOmIf/hMaydnJBUTBMRCOGZh8YjkFOc8dadEoNGA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.46.3': - resolution: {integrity: sha512-VXw7qmdkucEx9WkmR3ld/u6VhRyKeiF1uxWwCy/iuNfokjJ7VhsgLSOTjsol8BunSw190zABzpwdNsze2Kpo4g==} + '@typescript-eslint/utils@8.46.4': + resolution: {integrity: sha512-AbSv11fklGXV6T28dp2Me04Uw90R2iJ30g2bgLz529Koehrmkbs1r7paFqr1vPCZi7hHwYxYtxfyQMRC8QaVSg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@8.46.3': - resolution: {integrity: sha512-uk574k8IU0rOF/AjniX8qbLSGURJVUCeM5e4MIMKBFFi8weeiLrG1fyQejyLXQpRZbU/1BuQasleV/RfHC3hHg==} + '@typescript-eslint/visitor-keys@8.46.4': + resolution: {integrity: sha512-/++5CYLQqsO9HFGLI7APrxBJYo+5OCMpViuhV8q5/Qa3o5mMrF//eQHks+PXcsAVaLdn817fMuS7zqoXNNZGaw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@ungap/structured-clone@1.3.0': @@ -2953,8 +2953,8 @@ packages: engines: {node: '>=18'} hasBin: true - i18next@25.6.1: - resolution: {integrity: sha512-yUWvdXtalZztmKrKw3yz/AvSP3yKyqIkVPx/wyvoYy9lkLmwzItLxp0iHZLG5hfVQ539Jor4XLO+U+NHIXg7pw==} + i18next@25.6.2: + resolution: {integrity: sha512-0GawNyVUw0yvJoOEBq1VHMAsqdM23XrHkMtl2gKEjviJQSLVXsrPqsoYAxBEugW5AB96I2pZkwRxyl8WZVoWdw==} peerDependencies: typescript: ^5 peerDependenciesMeta: @@ -3776,8 +3776,8 @@ packages: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} - sass@1.93.3: - resolution: {integrity: sha512-elOcIZRTM76dvxNAjqYrucTSI0teAF/L2Lv0s6f6b7FOwcwIuA357bIE871580AjHJuSvLIRUosgV+lIWx6Rgg==} + sass@1.94.0: + resolution: {integrity: sha512-Dqh7SiYcaFtdv5Wvku6QgS5IGPm281L+ZtVD1U2FJa7Q0EFRlq8Z3sjYtz6gYObsYThUOz9ArwFqPZx+1azILQ==} engines: {node: '>=14.0.0'} hasBin: true @@ -3978,8 +3978,8 @@ packages: resolution: {integrity: sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==} engines: {node: '>=18'} - tauri-plugin-mihomo-api@https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/dcb6b5a6753233422e7cea23042239c7994c605c: - resolution: {tarball: https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/dcb6b5a6753233422e7cea23042239c7994c605c} + tauri-plugin-mihomo-api@https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/d0f00b33cea294cc693e177441fc897426ecbc39: + resolution: {tarball: https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/d0f00b33cea294cc693e177441fc897426ecbc39} version: 0.1.0 terser@5.44.1: @@ -4065,8 +4065,8 @@ packages: types-pac@1.0.3: resolution: {integrity: sha512-MF2UAZGvGMOM+vHi9Zj/LvQqdNN1m1xSB+PjAW9B/GvFqaB4GwR18YaIbGIGDRTW/J8iqFXQHLZd5eJVtho46w==} - typescript-eslint@8.46.3: - resolution: {integrity: sha512-bAfgMavTuGo+8n6/QQDVQz4tZ4f7Soqg53RbrlZQEoAltYop/XR4RAts/I0BrO3TTClTSTFJ0wYbla+P8cEWJA==} + typescript-eslint@8.46.4: + resolution: {integrity: sha512-KALyxkpYV5Ix7UhvjTwJXZv76VWsHG+NjNlt/z+a17SOQSiOcBdUXdbJdyXi7RPxrBFECtFOiPwUJQusJuCqrg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -5215,9 +5215,9 @@ snapshots: '@eslint-react/ast@2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-react/eff': 2.3.4 - '@typescript-eslint/types': 8.46.3 - '@typescript-eslint/typescript-estree': 8.46.3(typescript@5.9.3) - '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) string-ts: 2.2.1 transitivePeerDependencies: - eslint @@ -5230,9 +5230,9 @@ snapshots: '@eslint-react/eff': 2.3.4 '@eslint-react/shared': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) '@eslint-react/var': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.46.3 - '@typescript-eslint/types': 8.46.3 - '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.46.4 + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) birecord: 0.1.1 ts-pattern: 5.9.0 transitivePeerDependencies: @@ -5246,10 +5246,10 @@ snapshots: dependencies: '@eslint-react/eff': 2.3.4 '@eslint-react/shared': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.46.3 - '@typescript-eslint/type-utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/types': 8.46.3 - '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.46.4 + '@typescript-eslint/type-utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) eslint-plugin-react-dom: 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint-plugin-react-hooks-extra: 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) @@ -5264,7 +5264,7 @@ snapshots: '@eslint-react/shared@2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-react/eff': 2.3.4 - '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) ts-pattern: 5.9.0 zod: 4.1.12 transitivePeerDependencies: @@ -5276,9 +5276,9 @@ snapshots: dependencies: '@eslint-react/ast': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) '@eslint-react/eff': 2.3.4 - '@typescript-eslint/scope-manager': 8.46.3 - '@typescript-eslint/types': 8.46.3 - '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.46.4 + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) ts-pattern: 5.9.0 transitivePeerDependencies: - eslint @@ -6023,14 +6023,14 @@ snapshots: '@types/unist@3.0.3': {} - '@typescript-eslint/eslint-plugin@8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.46.4(@typescript-eslint/parser@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.46.3 - '@typescript-eslint/type-utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.46.3 + '@typescript-eslint/parser': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.46.4 + '@typescript-eslint/type-utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.46.4 eslint: 9.39.1(jiti@2.6.1) graphemer: 1.4.0 ignore: 7.0.5 @@ -6040,41 +6040,41 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/parser@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.46.3 - '@typescript-eslint/types': 8.46.3 - '@typescript-eslint/typescript-estree': 8.46.3(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.46.3 + '@typescript-eslint/scope-manager': 8.46.4 + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.46.4 debug: 4.4.3 eslint: 9.39.1(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.46.3(typescript@5.9.3)': + '@typescript-eslint/project-service@8.46.4(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.46.3(typescript@5.9.3) - '@typescript-eslint/types': 8.46.3 + '@typescript-eslint/tsconfig-utils': 8.46.4(typescript@5.9.3) + '@typescript-eslint/types': 8.46.4 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.46.3': + '@typescript-eslint/scope-manager@8.46.4': dependencies: - '@typescript-eslint/types': 8.46.3 - '@typescript-eslint/visitor-keys': 8.46.3 + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/visitor-keys': 8.46.4 - '@typescript-eslint/tsconfig-utils@8.46.3(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.46.4(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.46.3 - '@typescript-eslint/typescript-estree': 8.46.3(typescript@5.9.3) - '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) debug: 4.4.3 eslint: 9.39.1(jiti@2.6.1) ts-api-utils: 2.1.0(typescript@5.9.3) @@ -6082,14 +6082,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.46.3': {} + '@typescript-eslint/types@8.46.4': {} - '@typescript-eslint/typescript-estree@8.46.3(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.46.4(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.46.3(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.46.3(typescript@5.9.3) - '@typescript-eslint/types': 8.46.3 - '@typescript-eslint/visitor-keys': 8.46.3 + '@typescript-eslint/project-service': 8.46.4(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.46.4(typescript@5.9.3) + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/visitor-keys': 8.46.4 debug: 4.4.3 fast-glob: 3.3.3 is-glob: 4.0.3 @@ -6100,20 +6100,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/utils@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.8.0(eslint@9.39.1(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.46.3 - '@typescript-eslint/types': 8.46.3 - '@typescript-eslint/typescript-estree': 8.46.3(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.46.4 + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.46.3': + '@typescript-eslint/visitor-keys@8.46.4': dependencies: - '@typescript-eslint/types': 8.46.3 + '@typescript-eslint/types': 8.46.4 eslint-visitor-keys: 4.2.1 '@ungap/structured-clone@1.3.0': {} @@ -6177,7 +6177,7 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - '@vitejs/plugin-legacy@7.2.1(terser@5.44.1)(vite@7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.1)(yaml@2.8.1))': + '@vitejs/plugin-legacy@7.2.1(terser@5.44.1)(vite@7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))': dependencies: '@babel/core': 7.28.4 '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.28.4) @@ -6192,15 +6192,15 @@ snapshots: regenerator-runtime: 0.14.1 systemjs: 6.15.1 terser: 5.44.1 - vite: 7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.1)(yaml@2.8.1) + vite: 7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1) transitivePeerDependencies: - supports-color - '@vitejs/plugin-react-swc@4.2.1(vite@7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.1)(yaml@2.8.1))': + '@vitejs/plugin-react-swc@4.2.1(vite@7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))': dependencies: '@rolldown/pluginutils': 1.0.0-beta.46 '@swc/core': 1.14.0 - vite: 7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.1)(yaml@2.8.1) + vite: 7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1) transitivePeerDependencies: - '@swc/helpers' @@ -6213,13 +6213,13 @@ snapshots: chai: 6.2.0 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.8(vite@7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.1)(yaml@2.8.1))': + '@vitest/mocker@4.0.8(vite@7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))': dependencies: '@vitest/spy': 4.0.8 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.1)(yaml@2.8.1) + vite: 7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1) '@vitest/pretty-format@4.0.8': dependencies: @@ -6842,7 +6842,7 @@ snapshots: - supports-color optional: true - eslint-import-resolver-typescript@4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)): + eslint-import-resolver-typescript@4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)): dependencies: debug: 4.4.3 eslint: 9.39.1(jiti@2.6.1) @@ -6853,26 +6853,26 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1(jiti@2.6.1)) - eslint-plugin-import-x: 4.16.1(@typescript-eslint/utils@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-import-x: 4.16.1(@typescript-eslint/utils@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) transitivePeerDependencies: - supports-color optional: true - eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)): + eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)): dependencies: - '@typescript-eslint/types': 8.46.3 + '@typescript-eslint/types': 8.46.4 comment-parser: 1.4.1 debug: 4.4.3 eslint: 9.39.1(jiti@2.6.1) @@ -6883,12 +6883,12 @@ snapshots: stable-hash-x: 0.2.0 unrs-resolver: 1.11.1 optionalDependencies: - '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -6899,7 +6899,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -6911,7 +6911,7 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -6934,9 +6934,9 @@ snapshots: '@eslint-react/eff': 2.3.4 '@eslint-react/shared': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) '@eslint-react/var': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.46.3 - '@typescript-eslint/types': 8.46.3 - '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.46.4 + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) compare-versions: 6.1.1 eslint: 9.39.1(jiti@2.6.1) string-ts: 2.2.1 @@ -6952,10 +6952,10 @@ snapshots: '@eslint-react/eff': 2.3.4 '@eslint-react/shared': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) '@eslint-react/var': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.46.3 - '@typescript-eslint/type-utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/types': 8.46.3 - '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.46.4 + '@typescript-eslint/type-utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) string-ts: 2.2.1 ts-pattern: 5.9.0 @@ -6981,10 +6981,10 @@ snapshots: '@eslint-react/eff': 2.3.4 '@eslint-react/shared': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) '@eslint-react/var': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.46.3 - '@typescript-eslint/type-utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/types': 8.46.3 - '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.46.4 + '@typescript-eslint/type-utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) string-ts: 2.2.1 ts-pattern: 5.9.0 @@ -7003,9 +7003,9 @@ snapshots: '@eslint-react/eff': 2.3.4 '@eslint-react/shared': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) '@eslint-react/var': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.46.3 - '@typescript-eslint/types': 8.46.3 - '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.46.4 + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) string-ts: 2.2.1 ts-pattern: 5.9.0 @@ -7020,10 +7020,10 @@ snapshots: '@eslint-react/eff': 2.3.4 '@eslint-react/shared': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) '@eslint-react/var': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.46.3 - '@typescript-eslint/type-utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/types': 8.46.3 - '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.46.4 + '@typescript-eslint/type-utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) compare-versions: 6.1.1 eslint: 9.39.1(jiti@2.6.1) is-immutable-type: 5.0.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) @@ -7034,11 +7034,11 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-unused-imports@4.3.0(@typescript-eslint/eslint-plugin@8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)): + eslint-plugin-unused-imports@4.3.0(@typescript-eslint/eslint-plugin@8.46.4(@typescript-eslint/parser@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)): dependencies: eslint: 9.39.1(jiti@2.6.1) optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.46.4(@typescript-eslint/parser@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint-scope@8.4.0: dependencies: @@ -7376,7 +7376,7 @@ snapshots: husky@9.1.7: {} - i18next@25.6.1(typescript@5.9.3): + i18next@25.6.2(typescript@5.9.3): dependencies: '@babel/runtime': 7.28.4 optionalDependencies: @@ -7497,7 +7497,7 @@ snapshots: is-immutable-type@5.0.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/type-utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) ts-api-utils: 2.1.0(typescript@5.9.3) ts-declaration-location: 1.0.7(typescript@5.9.3) @@ -8200,11 +8200,11 @@ snapshots: dependencies: react: 19.2.0 - react-i18next@16.2.4(i18next@25.6.1(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3): + react-i18next@16.2.4(i18next@25.6.2(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3): dependencies: '@babel/runtime': 7.28.4 html-parse-stringify: 3.0.1 - i18next: 25.6.1(typescript@5.9.3) + i18next: 25.6.2(typescript@5.9.3) react: 19.2.0 use-sync-external-store: 1.6.0(react@19.2.0) optionalDependencies: @@ -8396,7 +8396,7 @@ snapshots: is-regex: 1.2.1 optional: true - sass@1.93.3: + sass@1.94.0: dependencies: chokidar: 4.0.3 immutable: 5.1.2 @@ -8629,7 +8629,7 @@ snapshots: minizlib: 3.1.0 yallist: 5.0.0 - tauri-plugin-mihomo-api@https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/dcb6b5a6753233422e7cea23042239c7994c605c: + tauri-plugin-mihomo-api@https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/d0f00b33cea294cc693e177441fc897426ecbc39: dependencies: '@tauri-apps/api': 2.9.0 @@ -8732,12 +8732,12 @@ snapshots: types-pac@1.0.3: {} - typescript-eslint@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): + typescript-eslint@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.46.3(typescript@5.9.3) - '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.46.4(@typescript-eslint/parser@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: @@ -8857,18 +8857,18 @@ snapshots: dependencies: monaco-editor: 0.54.0 - vite-plugin-svgr@4.5.0(rollup@4.46.2)(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.1)(yaml@2.8.1)): + vite-plugin-svgr@4.5.0(rollup@4.46.2)(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)): dependencies: '@rollup/pluginutils': 5.2.0(rollup@4.46.2) '@svgr/core': 8.1.0(typescript@5.9.3) '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3)) - vite: 7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.1)(yaml@2.8.1) + vite: 7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1) transitivePeerDependencies: - rollup - supports-color - typescript - vite@7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.1)(yaml@2.8.1): + vite@7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1): dependencies: esbuild: 0.25.4 fdir: 6.5.0(picomatch@4.0.3) @@ -8880,14 +8880,14 @@ snapshots: '@types/node': 24.10.0 fsevents: 2.3.3 jiti: 2.6.1 - sass: 1.93.3 + sass: 1.94.0 terser: 5.44.1 yaml: 2.8.1 - vitest@4.0.8(@types/debug@4.1.12)(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.1)(yaml@2.8.1): + vitest@4.0.8(@types/debug@4.1.12)(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1): dependencies: '@vitest/expect': 4.0.8 - '@vitest/mocker': 4.0.8(vite@7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.1)(yaml@2.8.1)) + '@vitest/mocker': 4.0.8(vite@7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)) '@vitest/pretty-format': 4.0.8 '@vitest/runner': 4.0.8 '@vitest/snapshot': 4.0.8 @@ -8904,7 +8904,7 @@ snapshots: tinyexec: 0.3.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.1)(yaml@2.8.1) + vite: 7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 From afb38427764f120ec26ebe42f1bbd393d528af25 Mon Sep 17 00:00:00 2001 From: Sline Date: Tue, 11 Nov 2025 20:15:07 +0800 Subject: [PATCH 39/49] feat(canvas): add adaptive FPS and pause rendering on window blur (#5409) --- .../home/enhanced-canvas-traffic-graph.tsx | 228 ++++++++++++++++-- 1 file changed, 214 insertions(+), 14 deletions(-) diff --git a/src/components/home/enhanced-canvas-traffic-graph.tsx b/src/components/home/enhanced-canvas-traffic-graph.tsx index a77f4db85..0cb5d43cf 100644 --- a/src/components/home/enhanced-canvas-traffic-graph.tsx +++ b/src/components/home/enhanced-canvas-traffic-graph.tsx @@ -77,6 +77,17 @@ const GRAPH_CONFIG = { }, }; +const MIN_FPS = 8; +const MAX_FPS = 20; +const FPS_ADJUST_INTERVAL = 3000; // ms +const FPS_SAMPLE_WINDOW = 12; +const STALE_DATA_THRESHOLD = 2500; // ms without fresh data => drop FPS +const RESUME_FPS_TARGET = 12; +const RESUME_COOLDOWN_MS = 2000; + +const getNow = () => + typeof performance !== "undefined" ? performance.now() : Date.now(); + interface EnhancedCanvasTrafficGraphProps { ref?: Ref; } @@ -105,6 +116,10 @@ export const EnhancedCanvasTrafficGraph = memo( const [timeRange, setTimeRange] = useState(10); const [chartStyle, setChartStyle] = useState<"bezier" | "line">("bezier"); + const initialFocusState = + typeof document !== "undefined" ? !document.hidden : true; + const [isWindowFocused, setIsWindowFocused] = useState(initialFocusState); + // 悬浮提示状态 const [tooltipData, setTooltipData] = useState({ x: 0, @@ -122,6 +137,18 @@ export const EnhancedCanvasTrafficGraph = memo( const animationFrameRef = useRef(undefined); const lastRenderTimeRef = useRef(0); const isInitializedRef = useRef(false); + const isWindowFocusedRef = useRef(initialFocusState); + const fpsControllerRef = useRef<{ + target: number; + samples: number[]; + lastAdjustTime: number; + }>({ + target: GRAPH_CONFIG.targetFPS, + samples: [], + lastAdjustTime: 0, + }); + const lastDataTimestampRef = useRef(0); + const resumeCooldownRef = useRef(0); // 当前显示的数据缓存 const [displayData, dispatchDisplayData] = useReducer( @@ -129,6 +156,7 @@ export const EnhancedCanvasTrafficGraph = memo( [], ); const debounceTimeoutRef = useRef(null); + const [currentFPS, setCurrentFPS] = useState(GRAPH_CONFIG.targetFPS); // 主题颜色配置 const colors = useMemo( @@ -165,6 +193,74 @@ export const EnhancedCanvasTrafficGraph = memo( }; }, [dataPoints, timeRange, getDataForTimeRange, updateDisplayData]); + useEffect(() => { + if (displayData.length === 0) { + lastDataTimestampRef.current = 0; + fpsControllerRef.current.target = GRAPH_CONFIG.targetFPS; + fpsControllerRef.current.samples = []; + fpsControllerRef.current.lastAdjustTime = 0; + // eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect + setCurrentFPS(GRAPH_CONFIG.targetFPS); + return; + } + + const latestTimestamp = + displayData[displayData.length - 1]?.timestamp ?? null; + if (latestTimestamp) { + lastDataTimestampRef.current = latestTimestamp; + } + }, [displayData]); + + const handleFocusStateChange = useCallback( + (focused: boolean) => { + isWindowFocusedRef.current = focused; + setIsWindowFocused(focused); + + const highResNow = getNow(); + lastRenderTimeRef.current = highResNow; + + if (focused) { + resumeCooldownRef.current = Date.now(); + const controller = fpsControllerRef.current; + const resumeTarget = Math.max( + MIN_FPS, + Math.min(controller.target, RESUME_FPS_TARGET), + ); + controller.target = resumeTarget; + controller.samples = []; + controller.lastAdjustTime = 0; + setCurrentFPS(resumeTarget); + } else { + resumeCooldownRef.current = 0; + } + }, + [setIsWindowFocused, setCurrentFPS], + ); + + useEffect(() => { + if (typeof window === "undefined" || typeof document === "undefined") { + return; + } + + const handleFocus = () => handleFocusStateChange(true); + const handleBlur = () => handleFocusStateChange(false); + const handleVisibilityChange = () => + handleFocusStateChange(!document.hidden); + + window.addEventListener("focus", handleFocus); + window.addEventListener("blur", handleBlur); + document.addEventListener("visibilitychange", handleVisibilityChange); + + return () => { + window.removeEventListener("focus", handleFocus); + window.removeEventListener("blur", handleBlur); + document.removeEventListener( + "visibilitychange", + handleVisibilityChange, + ); + }; + }, [handleFocusStateChange]); + // Y轴坐标计算 - 基于刻度范围的线性映射 const calculateY = useCallback( (value: number, height: number, data: ITrafficDataPoint[]): number => { @@ -792,31 +888,135 @@ export const EnhancedCanvasTrafficGraph = memo( tooltipData, ]); + const collectFrameSample = useCallback( + (renderDuration: number, frameBudget: number) => { + const controller = fpsControllerRef.current; + controller.samples.push(renderDuration); + if (controller.samples.length > FPS_SAMPLE_WINDOW) { + controller.samples.shift(); + } + + const perfNow = getNow(); + const lastDataAge = + lastDataTimestampRef.current > 0 + ? Date.now() - lastDataTimestampRef.current + : null; + const isDataStale = + typeof lastDataAge === "number" && lastDataAge > STALE_DATA_THRESHOLD; + + let inResumeCooldown = false; + if (resumeCooldownRef.current) { + const elapsedSinceResume = Date.now() - resumeCooldownRef.current; + if (elapsedSinceResume < RESUME_COOLDOWN_MS) { + inResumeCooldown = true; + } else { + resumeCooldownRef.current = 0; + } + } + + if (isDataStale && controller.target !== MIN_FPS) { + controller.target = MIN_FPS; + controller.samples = []; + controller.lastAdjustTime = perfNow; + setCurrentFPS(controller.target); + return; + } + + if ( + !isDataStale && + !inResumeCooldown && + controller.target < GRAPH_CONFIG.targetFPS + ) { + controller.target = Math.min( + GRAPH_CONFIG.targetFPS, + controller.target + 2, + ); + controller.samples = []; + controller.lastAdjustTime = perfNow; + setCurrentFPS(controller.target); + } + + if ( + controller.lastAdjustTime !== 0 && + perfNow - controller.lastAdjustTime < FPS_ADJUST_INTERVAL + ) { + return; + } + + if (controller.samples.length === 0) return; + + const avgRender = + controller.samples.reduce((sum, value) => sum + value, 0) / + controller.samples.length; + + let nextTarget = controller.target; + + if (avgRender > frameBudget * 0.75 && controller.target > MIN_FPS) { + nextTarget = Math.max(MIN_FPS, controller.target - 2); + } else if ( + avgRender < Math.max(4, frameBudget * 0.4) && + controller.target < MAX_FPS && + !inResumeCooldown + ) { + nextTarget = Math.min(MAX_FPS, controller.target + 2); + } + + controller.samples = []; + controller.lastAdjustTime = perfNow; + + if (nextTarget !== controller.target) { + controller.target = nextTarget; + setCurrentFPS(nextTarget); + } + }, + [setCurrentFPS], + ); + // 受控的动画循环 useEffect(() => { - const animate = (currentTime: number) => { - // 控制帧率,减少不必要的重绘 - if ( - currentTime - lastRenderTimeRef.current >= - 1000 / GRAPH_CONFIG.targetFPS - ) { - drawGraph(); - lastRenderTimeRef.current = currentTime; + if (!isWindowFocused || displayData.length === 0) { + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current); + animationFrameRef.current = undefined; } + lastRenderTimeRef.current = getNow(); + return; + } + + const animate = (currentTime: number) => { + if (!isWindowFocusedRef.current) { + lastRenderTimeRef.current = getNow(); + animationFrameRef.current = undefined; + return; + } + + const targetFPS = fpsControllerRef.current.target; + const frameBudget = 1000 / targetFPS; + + if ( + currentTime - lastRenderTimeRef.current >= frameBudget || + !isInitializedRef.current + ) { + const drawStart = getNow(); + drawGraph(); + const drawEnd = getNow(); + + lastRenderTimeRef.current = currentTime; + collectFrameSample(drawEnd - drawStart, frameBudget); + } + animationFrameRef.current = requestAnimationFrame(animate); }; - // 只有在有数据时才开始动画 - if (displayData.length > 0) { - animationFrameRef.current = requestAnimationFrame(animate); - } + animationFrameRef.current = requestAnimationFrame(animate); return () => { if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); + animationFrameRef.current = undefined; } }; - }, [drawGraph, displayData.length]); + }, [drawGraph, displayData.length, isWindowFocused, collectFrameSample]); // 切换时间范围 const handleTimeRangeClick = useCallback((event: React.MouseEvent) => { @@ -977,7 +1177,7 @@ export const EnhancedCanvasTrafficGraph = memo( }} > Points: {displayData.length} | Compressed:{" "} - {samplerStats.compressedBufferSize} + {samplerStats.compressedBufferSize} | FPS: {currentFPS} {/* 悬浮提示框 */} From 645b92bc2815fe55bbc827907bff0edbfee48674 Mon Sep 17 00:00:00 2001 From: Slinetrac Date: Wed, 12 Nov 2025 00:08:17 +0800 Subject: [PATCH 40/49] fix(tun): remove Linux-only block that forced tun.stack to mixed --- src-tauri/src/enhance/tun.rs | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/src-tauri/src/enhance/tun.rs b/src-tauri/src/enhance/tun.rs index a09051fac..dc78dc268 100644 --- a/src-tauri/src/enhance/tun.rs +++ b/src-tauri/src/enhance/tun.rs @@ -2,8 +2,6 @@ use serde_yaml_ng::{Mapping, Value}; #[cfg(target_os = "macos")] use crate::process::AsyncHandler; -#[cfg(target_os = "linux")] -use crate::{logging, utils::logging::Type}; macro_rules! revise { ($map: expr, $key: expr, $val: expr) => { @@ -31,27 +29,6 @@ pub fn use_tun(mut config: Mapping, enable: bool) -> Mapping { }); if enable { - #[cfg(target_os = "linux")] - { - let stack_key = Value::from("stack"); - let should_override = match tun_val.get(&stack_key) { - Some(value) => value - .as_str() - .map(|stack| stack.eq_ignore_ascii_case("gvisor")) - .unwrap_or(false), - None => true, - }; - - if should_override { - revise!(tun_val, "stack", "mixed"); - logging!( - warn, - Type::Network, - "Warning: gVisor TUN stack detected on Linux; falling back to 'mixed' for compatibility" - ); - } - } - // 读取DNS配置 let dns_key = Value::from("dns"); let dns_val = config.get(&dns_key); From b40136a790d314da1fbeff78acc471ba54106e50 Mon Sep 17 00:00:00 2001 From: Slinetrac Date: Wed, 12 Nov 2025 00:19:50 +0800 Subject: [PATCH 41/49] chore: Changelog.md --- Changelog.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Changelog.md b/Changelog.md index fa1ab4072..91ec1ed5e 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,8 @@ ### 🐞 修复问题 +- Linux 无法切换 TUN 堆栈 +
✨ 新增功能 @@ -19,6 +21,7 @@ - 防止退出时可能的禁用 TUN 失败 - i18n 支持 - 优化备份设置布局 +- 优化流量图性能表现,实现动态 FPS 和窗口失焦自动暂停
From d18c303e121f7f87ae41b59e2346d1a885653b04 Mon Sep 17 00:00:00 2001 From: Slinetrac Date: Wed, 12 Nov 2025 00:39:46 +0800 Subject: [PATCH 42/49] fix(tun): set linux DEFAULT_STACK to gvisor --- src-tauri/src/constants.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src-tauri/src/constants.rs b/src-tauri/src/constants.rs index 3195ea404..a07da7c15 100644 --- a/src-tauri/src/constants.rs +++ b/src-tauri/src/constants.rs @@ -68,10 +68,6 @@ pub mod error_patterns { } pub mod tun { - #[cfg(target_os = "linux")] - pub const DEFAULT_STACK: &str = "mixed"; - - #[cfg(not(target_os = "linux"))] pub const DEFAULT_STACK: &str = "gvisor"; pub const DNS_HIJACK: &[&str] = &["any:53"]; From 92a3f0cae3a897a005d24abf752e373a5aa8b18c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 12 Nov 2025 01:26:04 +0800 Subject: [PATCH 43/49] chore(deps): update dependency @types/react to v19.2.3 (#5416) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- pnpm-lock.yaml | 150 ++++++++++++++++++++++++------------------------- 2 files changed, 76 insertions(+), 76 deletions(-) diff --git a/package.json b/package.json index 1941048f7..ebdd85ec1 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "@types/js-yaml": "^4.0.9", "@types/lodash-es": "^4.17.12", "@types/node": "^24.10.0", - "@types/react": "19.2.2", + "@types/react": "19.2.3", "@types/react-dom": "19.2.2", "@vitejs/plugin-legacy": "^7.2.1", "@vitejs/plugin-react-swc": "^4.2.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5e44f718a..8a425bca7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,10 +19,10 @@ importers: version: 3.2.2(react@19.2.0) '@emotion/react': specifier: ^11.14.0 - version: 11.14.0(@types/react@19.2.2)(react@19.2.0) + version: 11.14.0(@types/react@19.2.3)(react@19.2.0) '@emotion/styled': specifier: ^11.14.1 - version: 11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0) + version: 11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0) '@juggle/resize-observer': specifier: ^3.4.0 version: 3.4.0 @@ -31,16 +31,16 @@ importers: version: 4.7.0(monaco-editor@0.54.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@mui/icons-material': specifier: ^7.3.5 - version: 7.3.5(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.2)(react@19.2.0) + version: 7.3.5(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.3)(react@19.2.0) '@mui/lab': specifier: 7.0.0-beta.17 - version: 7.0.0-beta.17(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 7.0.0-beta.17(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@mui/material': specifier: ^7.3.5 - version: 7.3.5(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 7.3.5(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@mui/x-data-grid': specifier: ^8.17.0 - version: 8.17.0(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@mui/system@7.3.5(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 8.17.0(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@mui/system@7.3.5(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@tauri-apps/api': specifier: 2.9.0 version: 2.9.0 @@ -118,7 +118,7 @@ importers: version: 16.2.4(i18next@25.6.2(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3) react-markdown: specifier: 10.1.0 - version: 10.1.0(@types/react@19.2.2)(react@19.2.0) + version: 10.1.0(@types/react@19.2.3)(react@19.2.0) react-router: specifier: ^7.9.5 version: 7.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -157,11 +157,11 @@ importers: specifier: ^24.10.0 version: 24.10.0 '@types/react': - specifier: 19.2.2 - version: 19.2.2 + specifier: 19.2.3 + version: 19.2.3 '@types/react-dom': specifier: 19.2.2 - version: 19.2.2(@types/react@19.2.2) + version: 19.2.2(@types/react@19.2.3) '@vitejs/plugin-legacy': specifier: ^7.2.1 version: 7.2.1(terser@5.44.1)(vite@7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)) @@ -1850,8 +1850,8 @@ packages: peerDependencies: '@types/react': '*' - '@types/react@19.2.2': - resolution: {integrity: sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==} + '@types/react@19.2.3': + resolution: {integrity: sha512-k5dJVszUiNr1DSe8Cs+knKR6IrqhqdhpUwzqhkS8ecQTSf3THNtbfIp/umqHMpX2bv+9dkx3fwDv/86LcSfvSg==} '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -5077,7 +5077,7 @@ snapshots: '@emotion/memoize@0.9.0': {} - '@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0)': + '@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 '@emotion/babel-plugin': 11.13.5 @@ -5089,7 +5089,7 @@ snapshots: hoist-non-react-statics: 3.3.2 react: 19.2.0 optionalDependencies: - '@types/react': 19.2.2 + '@types/react': 19.2.3 transitivePeerDependencies: - supports-color @@ -5103,18 +5103,18 @@ snapshots: '@emotion/sheet@1.4.0': {} - '@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0)': + '@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 '@emotion/babel-plugin': 11.13.5 '@emotion/is-prop-valid': 1.3.1 - '@emotion/react': 11.14.0(@types/react@19.2.2)(react@19.2.0) + '@emotion/react': 11.14.0(@types/react@19.2.3)(react@19.2.0) '@emotion/serialize': 1.3.3 '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.0) '@emotion/utils': 1.4.2 react: 19.2.0 optionalDependencies: - '@types/react': 19.2.2 + '@types/react': 19.2.3 transitivePeerDependencies: - supports-color @@ -5397,39 +5397,39 @@ snapshots: '@mui/core-downloads-tracker@7.3.5': {} - '@mui/icons-material@7.3.5(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.2)(react@19.2.0)': + '@mui/icons-material@7.3.5(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.3)(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 - '@mui/material': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@mui/material': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) react: 19.2.0 optionalDependencies: - '@types/react': 19.2.2 + '@types/react': 19.2.3 - '@mui/lab@7.0.0-beta.17(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@mui/lab@7.0.0-beta.17(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 - '@mui/material': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@mui/system': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0) - '@mui/types': 7.4.8(@types/react@19.2.2) - '@mui/utils': 7.3.5(@types/react@19.2.2)(react@19.2.0) + '@mui/material': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@mui/system': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0) + '@mui/types': 7.4.8(@types/react@19.2.3) + '@mui/utils': 7.3.5(@types/react@19.2.3)(react@19.2.0) clsx: 2.1.1 prop-types: 15.8.1 react: 19.2.0 react-dom: 19.2.0(react@19.2.0) optionalDependencies: - '@emotion/react': 11.14.0(@types/react@19.2.2)(react@19.2.0) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0) - '@types/react': 19.2.2 + '@emotion/react': 11.14.0(@types/react@19.2.3)(react@19.2.0) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0) + '@types/react': 19.2.3 - '@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 '@mui/core-downloads-tracker': 7.3.5 - '@mui/system': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0) - '@mui/types': 7.4.8(@types/react@19.2.2) - '@mui/utils': 7.3.5(@types/react@19.2.2)(react@19.2.0) + '@mui/system': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0) + '@mui/types': 7.4.8(@types/react@19.2.3) + '@mui/utils': 7.3.5(@types/react@19.2.3)(react@19.2.0) '@popperjs/core': 2.11.8 - '@types/react-transition-group': 4.4.12(@types/react@19.2.2) + '@types/react-transition-group': 4.4.12(@types/react@19.2.3) clsx: 2.1.1 csstype: 3.1.3 prop-types: 15.8.1 @@ -5438,20 +5438,20 @@ snapshots: react-is: 19.2.0 react-transition-group: 4.4.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0) optionalDependencies: - '@emotion/react': 11.14.0(@types/react@19.2.2)(react@19.2.0) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0) - '@types/react': 19.2.2 + '@emotion/react': 11.14.0(@types/react@19.2.3)(react@19.2.0) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0) + '@types/react': 19.2.3 - '@mui/private-theming@7.3.5(@types/react@19.2.2)(react@19.2.0)': + '@mui/private-theming@7.3.5(@types/react@19.2.3)(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 - '@mui/utils': 7.3.5(@types/react@19.2.2)(react@19.2.0) + '@mui/utils': 7.3.5(@types/react@19.2.3)(react@19.2.0) prop-types: 15.8.1 react: 19.2.0 optionalDependencies: - '@types/react': 19.2.2 + '@types/react': 19.2.3 - '@mui/styled-engine@7.3.5(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(react@19.2.0)': + '@mui/styled-engine@7.3.5(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 '@emotion/cache': 11.14.0 @@ -5461,77 +5461,77 @@ snapshots: prop-types: 15.8.1 react: 19.2.0 optionalDependencies: - '@emotion/react': 11.14.0(@types/react@19.2.2)(react@19.2.0) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0) + '@emotion/react': 11.14.0(@types/react@19.2.3)(react@19.2.0) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0) - '@mui/system@7.3.5(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0)': + '@mui/system@7.3.5(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 - '@mui/private-theming': 7.3.5(@types/react@19.2.2)(react@19.2.0) - '@mui/styled-engine': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(react@19.2.0) - '@mui/types': 7.4.8(@types/react@19.2.2) - '@mui/utils': 7.3.5(@types/react@19.2.2)(react@19.2.0) + '@mui/private-theming': 7.3.5(@types/react@19.2.3)(react@19.2.0) + '@mui/styled-engine': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(react@19.2.0) + '@mui/types': 7.4.8(@types/react@19.2.3) + '@mui/utils': 7.3.5(@types/react@19.2.3)(react@19.2.0) clsx: 2.1.1 csstype: 3.1.3 prop-types: 15.8.1 react: 19.2.0 optionalDependencies: - '@emotion/react': 11.14.0(@types/react@19.2.2)(react@19.2.0) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0) - '@types/react': 19.2.2 + '@emotion/react': 11.14.0(@types/react@19.2.3)(react@19.2.0) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0) + '@types/react': 19.2.3 - '@mui/types@7.4.8(@types/react@19.2.2)': + '@mui/types@7.4.8(@types/react@19.2.3)': dependencies: '@babel/runtime': 7.28.4 optionalDependencies: - '@types/react': 19.2.2 + '@types/react': 19.2.3 - '@mui/utils@7.3.5(@types/react@19.2.2)(react@19.2.0)': + '@mui/utils@7.3.5(@types/react@19.2.3)(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 - '@mui/types': 7.4.8(@types/react@19.2.2) + '@mui/types': 7.4.8(@types/react@19.2.3) '@types/prop-types': 15.7.15 clsx: 2.1.1 prop-types: 15.8.1 react: 19.2.0 react-is: 19.2.0 optionalDependencies: - '@types/react': 19.2.2 + '@types/react': 19.2.3 - '@mui/x-data-grid@8.17.0(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@mui/system@7.3.5(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@mui/x-data-grid@8.17.0(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@mui/system@7.3.5(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 - '@mui/material': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@mui/system': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0) - '@mui/utils': 7.3.5(@types/react@19.2.2)(react@19.2.0) - '@mui/x-internals': 8.17.0(@types/react@19.2.2)(react@19.2.0) - '@mui/x-virtualizer': 0.2.7(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@mui/material': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@mui/system': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0) + '@mui/utils': 7.3.5(@types/react@19.2.3)(react@19.2.0) + '@mui/x-internals': 8.17.0(@types/react@19.2.3)(react@19.2.0) + '@mui/x-virtualizer': 0.2.7(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) clsx: 2.1.1 prop-types: 15.8.1 react: 19.2.0 react-dom: 19.2.0(react@19.2.0) use-sync-external-store: 1.6.0(react@19.2.0) optionalDependencies: - '@emotion/react': 11.14.0(@types/react@19.2.2)(react@19.2.0) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0) + '@emotion/react': 11.14.0(@types/react@19.2.3)(react@19.2.0) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0) transitivePeerDependencies: - '@types/react' - '@mui/x-internals@8.17.0(@types/react@19.2.2)(react@19.2.0)': + '@mui/x-internals@8.17.0(@types/react@19.2.3)(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 - '@mui/utils': 7.3.5(@types/react@19.2.2)(react@19.2.0) + '@mui/utils': 7.3.5(@types/react@19.2.3)(react@19.2.0) react: 19.2.0 reselect: 5.1.1 use-sync-external-store: 1.6.0(react@19.2.0) transitivePeerDependencies: - '@types/react' - '@mui/x-virtualizer@0.2.7(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@mui/x-virtualizer@0.2.7(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 - '@mui/utils': 7.3.5(@types/react@19.2.2)(react@19.2.0) - '@mui/x-internals': 8.17.0(@types/react@19.2.2)(react@19.2.0) + '@mui/utils': 7.3.5(@types/react@19.2.3)(react@19.2.0) + '@mui/x-internals': 8.17.0(@types/react@19.2.3)(react@19.2.0) react: 19.2.0 react-dom: 19.2.0(react@19.2.0) transitivePeerDependencies: @@ -6007,15 +6007,15 @@ snapshots: '@types/prop-types@15.7.15': {} - '@types/react-dom@19.2.2(@types/react@19.2.2)': + '@types/react-dom@19.2.2(@types/react@19.2.3)': dependencies: - '@types/react': 19.2.2 + '@types/react': 19.2.3 - '@types/react-transition-group@4.4.12(@types/react@19.2.2)': + '@types/react-transition-group@4.4.12(@types/react@19.2.3)': dependencies: - '@types/react': 19.2.2 + '@types/react': 19.2.3 - '@types/react@19.2.2': + '@types/react@19.2.3': dependencies: csstype: 3.1.3 @@ -8215,11 +8215,11 @@ snapshots: react-is@19.2.0: {} - react-markdown@10.1.0(@types/react@19.2.2)(react@19.2.0): + react-markdown@10.1.0(@types/react@19.2.3)(react@19.2.0): dependencies: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 - '@types/react': 19.2.2 + '@types/react': 19.2.3 devlop: 1.1.0 hast-util-to-jsx-runtime: 2.3.6 html-url-attributes: 3.0.1 From f3a3d0e8b0ed55ee56ab02b4e8aba85db19af904 Mon Sep 17 00:00:00 2001 From: Tunglies Date: Wed, 12 Nov 2025 07:36:24 +0800 Subject: [PATCH 44/49] refactor: update update_icon method to require a non-optional verge reference and adjust related calls (#5417) --- src-tauri/src/core/tray/mod.rs | 17 ++++------------- src-tauri/src/feat/clash.rs | 7 ++++++- src-tauri/src/feat/config.rs | 9 +++++++-- src-tauri/src/utils/init.rs | 10 +++++----- src-tauri/src/utils/network.rs | 4 ++-- src-tauri/src/utils/server.rs | 6 +++--- 6 files changed, 27 insertions(+), 26 deletions(-) diff --git a/src-tauri/src/core/tray/mod.rs b/src-tauri/src/core/tray/mod.rs index 5595d15a2..17ecd04f9 100644 --- a/src-tauri/src/core/tray/mod.rs +++ b/src-tauri/src/core/tray/mod.rs @@ -348,7 +348,7 @@ impl Tray { /// 更新托盘图标 #[cfg(target_os = "macos")] - pub async fn update_icon(&self, verge: Option<&IVerge>) -> Result<()> { + pub async fn update_icon(&self, verge: &IVerge) -> Result<()> { if handle::Handle::global().is_exiting() { logging!(debug, Type::Tray, "应用正在退出,跳过托盘图标更新"); return Ok(()); @@ -368,11 +368,6 @@ impl Tray { } }; - let verge = match verge { - Some(v) => v, - None => &Config::verge().await.data_arc(), - }; - let system_mode = verge.enable_system_proxy.as_ref().unwrap_or(&false); let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false); @@ -395,7 +390,7 @@ impl Tray { } #[cfg(not(target_os = "macos"))] - pub async fn update_icon(&self, verge: Option<&IVerge>) -> Result<()> { + pub async fn update_icon(&self, verge: &IVerge) -> Result<()> { if handle::Handle::global().is_exiting() { logging!(debug, Type::Tray, "应用正在退出,跳过托盘图标更新"); return Ok(()); @@ -415,11 +410,6 @@ impl Tray { } }; - let verge = match verge { - Some(v) => v, - None => &Config::verge().await.data_arc(), - }; - let system_mode = verge.enable_system_proxy.as_ref().unwrap_or(&false); let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false); @@ -510,8 +500,9 @@ impl Tray { logging!(debug, Type::Tray, "应用正在退出,跳过托盘局部更新"); return Ok(()); } + let verge = Config::verge().await.data_arc(); self.update_menu().await?; - self.update_icon(None).await?; + self.update_icon(&verge).await?; self.update_tooltip().await?; Ok(()) } diff --git a/src-tauri/src/feat/clash.rs b/src-tauri/src/feat/clash.rs index ce178c15e..43e76157a 100644 --- a/src-tauri/src/feat/clash.rs +++ b/src-tauri/src/feat/clash.rs @@ -82,7 +82,12 @@ pub async fn change_clash_mode(mode: String) { if clash_data.save_config().await.is_ok() { handle::Handle::refresh_clash(); logging_error!(Type::Tray, tray::Tray::global().update_menu().await); - logging_error!(Type::Tray, tray::Tray::global().update_icon(None).await); + logging_error!( + Type::Tray, + tray::Tray::global() + .update_icon(&Config::verge().await.data_arc()) + .await + ); } let is_auto_close_connection = Config::verge() diff --git a/src-tauri/src/feat/config.rs b/src-tauri/src/feat/config.rs index 33c11ea07..99c349bdb 100644 --- a/src-tauri/src/feat/config.rs +++ b/src-tauri/src/feat/config.rs @@ -22,7 +22,12 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> { } else { if patch.get("mode").is_some() { logging_error!(Type::Tray, tray::Tray::global().update_menu().await); - logging_error!(Type::Tray, tray::Tray::global().update_icon(None).await); + logging_error!( + Type::Tray, + tray::Tray::global() + .update_icon(&Config::verge().await.data_arc()) + .await + ); } Config::runtime() .await @@ -213,7 +218,7 @@ async fn process_terminated_flags(update_flags: i32, patch: &IVerge) -> Result<( } if (update_flags & (UpdateFlags::SystrayIcon as i32)) != 0 { tray::Tray::global() - .update_icon(Some(&Config::verge().await.latest_arc())) + .update_icon(&Config::verge().await.data_arc()) .await?; } if (update_flags & (UpdateFlags::SystrayTooltip as i32)) != 0 { diff --git a/src-tauri/src/utils/init.rs b/src-tauri/src/utils/init.rs index cbeea286a..66503ff63 100644 --- a/src-tauri/src/utils/init.rs +++ b/src-tauri/src/utils/init.rs @@ -31,7 +31,7 @@ pub async fn init_logger() -> Result<()> { // TODO 提供 runtime 级别实时修改 let (log_level, log_max_size, log_max_count) = { let verge_guard = Config::verge().await; - let verge = verge_guard.latest_arc(); + let verge = verge_guard.data_arc(); ( verge.get_log_level(), verge.app_log_max_size.unwrap_or(128), @@ -89,7 +89,7 @@ pub async fn init_logger() -> Result<()> { pub async fn sidecar_writer() -> Result { let (log_max_size, log_max_count) = { let verge_guard = Config::verge().await; - let verge = verge_guard.latest_arc(); + let verge = verge_guard.data_arc(); ( verge.app_log_max_size.unwrap_or(128), verge.app_log_max_count.unwrap_or(8), @@ -117,7 +117,7 @@ pub async fn sidecar_writer() -> Result { pub async fn service_writer_config() -> Result { let (log_max_size, log_max_count) = { let verge_guard = Config::verge().await; - let verge = verge_guard.latest_arc(); + let verge = verge_guard.data_arc(); ( verge.app_log_max_size.unwrap_or(128), verge.app_log_max_count.unwrap_or(8), @@ -142,7 +142,7 @@ pub async fn delete_log() -> Result<()> { let auto_log_clean = { let verge = Config::verge().await; - let verge = verge.latest_arc(); + let verge = verge.data_arc(); verge.auto_log_clean.unwrap_or(0) }; @@ -517,7 +517,7 @@ pub async fn startup_script() -> Result<()> { let app_handle = handle::Handle::app_handle(); let script_path = { let verge = Config::verge().await; - let verge = verge.latest_arc(); + let verge = verge.data_arc(); verge.startup_script.clone().unwrap_or_else(|| "".into()) }; diff --git a/src-tauri/src/utils/network.rs b/src-tauri/src/utils/network.rs index c4598a7b9..400bf21c3 100644 --- a/src-tauri/src/utils/network.rs +++ b/src-tauri/src/utils/network.rs @@ -159,10 +159,10 @@ impl NetworkManager { ProxyType::None => None, ProxyType::Localhost => { let port = { - let verge_port = Config::verge().await.latest_arc().verge_mixed_port; + let verge_port = Config::verge().await.data_arc().verge_mixed_port; match verge_port { Some(port) => port, - None => Config::clash().await.latest_arc().get_mixed_port(), + None => Config::clash().await.data_arc().get_mixed_port(), } }; let proxy_scheme = format!("http://127.0.0.1:{port}"); diff --git a/src-tauri/src/utils/server.rs b/src-tauri/src/utils/server.rs index f4d7ea6ad..4defbd759 100644 --- a/src-tauri/src/utils/server.rs +++ b/src-tauri/src/utils/server.rs @@ -91,15 +91,15 @@ pub fn embed_server() { let clash_config = Config::clash().await; let pac_content = verge_config - .latest_arc() + .data_arc() .pac_file_content .clone() .unwrap_or_else(|| DEFAULT_PAC.into()); let pac_port = verge_config - .latest_arc() + .data_arc() .verge_mixed_port - .unwrap_or_else(|| clash_config.latest_arc().get_mixed_port()); + .unwrap_or_else(|| clash_config.data_arc().get_mixed_port()); let pac = warp::path!("commands" / "pac").map(move || { let processed_content = pac_content.replace("%mixed-port%", &format!("{pac_port}")); From a5f2b163f102908a35cd58ec22d8b9839041f79d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 12 Nov 2025 10:55:48 +0800 Subject: [PATCH 45/49] chore(deps): update dependency @types/node to ^24.10.1 (#5420) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- pnpm-lock.yaml | 48 ++++++++++++++++++++++++------------------------ 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/package.json b/package.json index ebdd85ec1..ad857216f 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "@tauri-apps/cli": "2.9.4", "@types/js-yaml": "^4.0.9", "@types/lodash-es": "^4.17.12", - "@types/node": "^24.10.0", + "@types/node": "^24.10.1", "@types/react": "19.2.3", "@types/react-dom": "19.2.2", "@vitejs/plugin-legacy": "^7.2.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8a425bca7..ace9b8cb5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -154,8 +154,8 @@ importers: specifier: ^4.17.12 version: 4.17.12 '@types/node': - specifier: ^24.10.0 - version: 24.10.0 + specifier: ^24.10.1 + version: 24.10.1 '@types/react': specifier: 19.2.3 version: 19.2.3 @@ -164,10 +164,10 @@ importers: version: 19.2.2(@types/react@19.2.3) '@vitejs/plugin-legacy': specifier: ^7.2.1 - version: 7.2.1(terser@5.44.1)(vite@7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)) + version: 7.2.1(terser@5.44.1)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)) '@vitejs/plugin-react-swc': specifier: ^4.2.1 - version: 4.2.1(vite@7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)) + version: 4.2.1(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)) adm-zip: specifier: ^0.5.16 version: 0.5.16 @@ -248,16 +248,16 @@ importers: version: 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) vite: specifier: ^7.2.2 - version: 7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1) + version: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1) vite-plugin-monaco-editor-esm: specifier: ^2.0.2 version: 2.0.2(monaco-editor@0.54.0) vite-plugin-svgr: specifier: ^4.5.0 - version: 4.5.0(rollup@4.46.2)(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)) + version: 4.5.0(rollup@4.46.2)(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)) vitest: specifier: ^4.0.8 - version: 4.0.8(@types/debug@4.1.12)(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1) + version: 4.0.8(@types/debug@4.1.12)(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1) packages: @@ -1831,8 +1831,8 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - '@types/node@24.10.0': - resolution: {integrity: sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==} + '@types/node@24.10.1': + resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} @@ -5999,7 +5999,7 @@ snapshots: '@types/ms@2.1.0': {} - '@types/node@24.10.0': + '@types/node@24.10.1': dependencies: undici-types: 7.16.0 @@ -6177,7 +6177,7 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - '@vitejs/plugin-legacy@7.2.1(terser@5.44.1)(vite@7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))': + '@vitejs/plugin-legacy@7.2.1(terser@5.44.1)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))': dependencies: '@babel/core': 7.28.4 '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.28.4) @@ -6192,15 +6192,15 @@ snapshots: regenerator-runtime: 0.14.1 systemjs: 6.15.1 terser: 5.44.1 - vite: 7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1) + vite: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1) transitivePeerDependencies: - supports-color - '@vitejs/plugin-react-swc@4.2.1(vite@7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))': + '@vitejs/plugin-react-swc@4.2.1(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))': dependencies: '@rolldown/pluginutils': 1.0.0-beta.46 '@swc/core': 1.14.0 - vite: 7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1) + vite: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1) transitivePeerDependencies: - '@swc/helpers' @@ -6213,13 +6213,13 @@ snapshots: chai: 6.2.0 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.8(vite@7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))': + '@vitest/mocker@4.0.8(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))': dependencies: '@vitest/spy': 4.0.8 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1) + vite: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1) '@vitest/pretty-format@4.0.8': dependencies: @@ -8857,18 +8857,18 @@ snapshots: dependencies: monaco-editor: 0.54.0 - vite-plugin-svgr@4.5.0(rollup@4.46.2)(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)): + vite-plugin-svgr@4.5.0(rollup@4.46.2)(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)): dependencies: '@rollup/pluginutils': 5.2.0(rollup@4.46.2) '@svgr/core': 8.1.0(typescript@5.9.3) '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3)) - vite: 7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1) + vite: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1) transitivePeerDependencies: - rollup - supports-color - typescript - vite@7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1): + vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1): dependencies: esbuild: 0.25.4 fdir: 6.5.0(picomatch@4.0.3) @@ -8877,17 +8877,17 @@ snapshots: rollup: 4.46.2 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 24.10.0 + '@types/node': 24.10.1 fsevents: 2.3.3 jiti: 2.6.1 sass: 1.94.0 terser: 5.44.1 yaml: 2.8.1 - vitest@4.0.8(@types/debug@4.1.12)(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1): + vitest@4.0.8(@types/debug@4.1.12)(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1): dependencies: '@vitest/expect': 4.0.8 - '@vitest/mocker': 4.0.8(vite@7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)) + '@vitest/mocker': 4.0.8(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)) '@vitest/pretty-format': 4.0.8 '@vitest/runner': 4.0.8 '@vitest/snapshot': 4.0.8 @@ -8904,11 +8904,11 @@ snapshots: tinyexec: 0.3.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1) + vite: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 - '@types/node': 24.10.0 + '@types/node': 24.10.1 transitivePeerDependencies: - jiti - less From 2ea22feb9563575e2dfa13a63a8dbcfc95f88da1 Mon Sep 17 00:00:00 2001 From: Slinetrac Date: Wed, 12 Nov 2025 11:41:28 +0800 Subject: [PATCH 46/49] fix(tray): refresh icon with latest draft before apply() commits --- src-tauri/src/feat/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/src/feat/config.rs b/src-tauri/src/feat/config.rs index 99c349bdb..cfa9aef00 100644 --- a/src-tauri/src/feat/config.rs +++ b/src-tauri/src/feat/config.rs @@ -218,7 +218,7 @@ async fn process_terminated_flags(update_flags: i32, patch: &IVerge) -> Result<( } if (update_flags & (UpdateFlags::SystrayIcon as i32)) != 0 { tray::Tray::global() - .update_icon(&Config::verge().await.data_arc()) + .update_icon(&Config::verge().await.latest_arc()) .await?; } if (update_flags & (UpdateFlags::SystrayTooltip as i32)) != 0 { From 2a2de292245c6001cf5faf089031c2cda15b34be Mon Sep 17 00:00:00 2001 From: Sline Date: Wed, 12 Nov 2025 14:10:20 +0800 Subject: [PATCH 47/49] feat: tray menu enhance (#5412) * feat(tray): move outbound modes into a dedicated submenu * refactor: Enable 'Show Proxy Groups Inline' by default * refactor(tray): move `copyEnv` to `more` submenu --- src-tauri/locales/ar.yml | 4 +++ src-tauri/locales/de.yml | 4 +++ src-tauri/locales/en.yml | 4 +++ src-tauri/locales/es.yml | 4 +++ src-tauri/locales/fa.yml | 4 +++ src-tauri/locales/id.yml | 4 +++ src-tauri/locales/jp.yml | 4 +++ src-tauri/locales/ko.yml | 4 +++ src-tauri/locales/ru.yml | 4 +++ src-tauri/locales/tr.yml | 4 +++ src-tauri/locales/tt.yml | 4 +++ src-tauri/locales/zh.yml | 4 +++ src-tauri/locales/zhtw.yml | 4 +++ src-tauri/src/config/verge.rs | 2 +- src-tauri/src/core/tray/menu_def.rs | 1 + src-tauri/src/core/tray/mod.rs | 34 +++++++++++++------ src/components/setting/mods/layout-viewer.tsx | 2 +- 17 files changed, 78 insertions(+), 13 deletions(-) diff --git a/src-tauri/locales/ar.yml b/src-tauri/locales/ar.yml index 330cf3b50..a054627f7 100644 --- a/src-tauri/locales/ar.yml +++ b/src-tauri/locales/ar.yml @@ -28,6 +28,10 @@ tray: ruleMode: Rule Mode globalMode: Global Mode directMode: Direct Mode + outboundModes: Outbound Modes + rule: Rule + direct: Direct + global: Global profiles: Profiles proxies: Proxies systemProxy: System Proxy diff --git a/src-tauri/locales/de.yml b/src-tauri/locales/de.yml index 330cf3b50..2cda8afe8 100644 --- a/src-tauri/locales/de.yml +++ b/src-tauri/locales/de.yml @@ -28,6 +28,10 @@ tray: ruleMode: Rule Mode globalMode: Global Mode directMode: Direct Mode + outboundModes: Outbound Modes + rule: Regel + direct: Direkt + global: Global profiles: Profiles proxies: Proxies systemProxy: System Proxy diff --git a/src-tauri/locales/en.yml b/src-tauri/locales/en.yml index 330cf3b50..a054627f7 100644 --- a/src-tauri/locales/en.yml +++ b/src-tauri/locales/en.yml @@ -28,6 +28,10 @@ tray: ruleMode: Rule Mode globalMode: Global Mode directMode: Direct Mode + outboundModes: Outbound Modes + rule: Rule + direct: Direct + global: Global profiles: Profiles proxies: Proxies systemProxy: System Proxy diff --git a/src-tauri/locales/es.yml b/src-tauri/locales/es.yml index 330cf3b50..974899b15 100644 --- a/src-tauri/locales/es.yml +++ b/src-tauri/locales/es.yml @@ -28,6 +28,10 @@ tray: ruleMode: Rule Mode globalMode: Global Mode directMode: Direct Mode + outboundModes: Outbound Modes + rule: Regla + direct: Directo + global: Global profiles: Profiles proxies: Proxies systemProxy: System Proxy diff --git a/src-tauri/locales/fa.yml b/src-tauri/locales/fa.yml index 330cf3b50..a054627f7 100644 --- a/src-tauri/locales/fa.yml +++ b/src-tauri/locales/fa.yml @@ -28,6 +28,10 @@ tray: ruleMode: Rule Mode globalMode: Global Mode directMode: Direct Mode + outboundModes: Outbound Modes + rule: Rule + direct: Direct + global: Global profiles: Profiles proxies: Proxies systemProxy: System Proxy diff --git a/src-tauri/locales/id.yml b/src-tauri/locales/id.yml index 330cf3b50..587534e9c 100644 --- a/src-tauri/locales/id.yml +++ b/src-tauri/locales/id.yml @@ -28,6 +28,10 @@ tray: ruleMode: Rule Mode globalMode: Global Mode directMode: Direct Mode + outboundModes: Outbound Modes + rule: Aturan + direct: Langsung + global: Global profiles: Profiles proxies: Proxies systemProxy: System Proxy diff --git a/src-tauri/locales/jp.yml b/src-tauri/locales/jp.yml index 330cf3b50..4ed031a4a 100644 --- a/src-tauri/locales/jp.yml +++ b/src-tauri/locales/jp.yml @@ -28,6 +28,10 @@ tray: ruleMode: Rule Mode globalMode: Global Mode directMode: Direct Mode + outboundModes: Outbound Modes + rule: ルール + direct: ダイレクト + global: グローバル profiles: Profiles proxies: Proxies systemProxy: System Proxy diff --git a/src-tauri/locales/ko.yml b/src-tauri/locales/ko.yml index b671b8aec..51dccf885 100644 --- a/src-tauri/locales/ko.yml +++ b/src-tauri/locales/ko.yml @@ -28,6 +28,10 @@ tray: ruleMode: 규칙 모드 globalMode: 전역 모드 directMode: 직접 모드 + outboundModes: Outbound Modes + rule: 규칙 + direct: 직접 + global: 글로벌 profiles: 프로필 proxies: 프록시 systemProxy: 시스템 프록시 diff --git a/src-tauri/locales/ru.yml b/src-tauri/locales/ru.yml index 330cf3b50..cff8fe27c 100644 --- a/src-tauri/locales/ru.yml +++ b/src-tauri/locales/ru.yml @@ -28,6 +28,10 @@ tray: ruleMode: Rule Mode globalMode: Global Mode directMode: Direct Mode + outboundModes: Outbound Modes + rule: Правило + direct: Прямой + global: Глобальный profiles: Profiles proxies: Proxies systemProxy: System Proxy diff --git a/src-tauri/locales/tr.yml b/src-tauri/locales/tr.yml index 330cf3b50..f6ffb6895 100644 --- a/src-tauri/locales/tr.yml +++ b/src-tauri/locales/tr.yml @@ -28,6 +28,10 @@ tray: ruleMode: Rule Mode globalMode: Global Mode directMode: Direct Mode + outboundModes: Outbound Modes + rule: Kural + direct: Doğrudan + global: Küresel profiles: Profiles proxies: Proxies systemProxy: System Proxy diff --git a/src-tauri/locales/tt.yml b/src-tauri/locales/tt.yml index 330cf3b50..a054627f7 100644 --- a/src-tauri/locales/tt.yml +++ b/src-tauri/locales/tt.yml @@ -28,6 +28,10 @@ tray: ruleMode: Rule Mode globalMode: Global Mode directMode: Direct Mode + outboundModes: Outbound Modes + rule: Rule + direct: Direct + global: Global profiles: Profiles proxies: Proxies systemProxy: System Proxy diff --git a/src-tauri/locales/zh.yml b/src-tauri/locales/zh.yml index 10c2e6362..6b137faeb 100644 --- a/src-tauri/locales/zh.yml +++ b/src-tauri/locales/zh.yml @@ -28,6 +28,10 @@ tray: ruleMode: 规则模式 globalMode: 全局模式 directMode: 直连模式 + outboundModes: 出站模式 + rule: 规则 + direct: 直连 + global: 全局 profiles: 订阅 proxies: 代理 systemProxy: 系统代理 diff --git a/src-tauri/locales/zhtw.yml b/src-tauri/locales/zhtw.yml index 8e2b0845b..9e07ca714 100644 --- a/src-tauri/locales/zhtw.yml +++ b/src-tauri/locales/zhtw.yml @@ -28,6 +28,10 @@ tray: ruleMode: 規則模式 globalMode: 全域模式 directMode: 直連模式 + outboundModes: 出站模式 + rule: 規則 + direct: 直連 + global: 全域 profiles: 訂閱 proxies: 代理 systemProxy: 系統代理 diff --git a/src-tauri/src/config/verge.rs b/src-tauri/src/config/verge.rs index 9a25cf34c..70ad8964d 100644 --- a/src-tauri/src/config/verge.rs +++ b/src-tauri/src/config/verge.rs @@ -439,7 +439,7 @@ impl IVerge { webdav_password: None, enable_tray_speed: Some(false), // enable_tray_icon: Some(true), - tray_inline_proxy_groups: Some(false), + tray_inline_proxy_groups: Some(true), enable_global_hotkey: Some(true), enable_auto_light_weight_mode: Some(false), auto_light_weight_minutes: Some(10), diff --git a/src-tauri/src/core/tray/menu_def.rs b/src-tauri/src/core/tray/menu_def.rs index 78c76e6cb..24a488fdc 100644 --- a/src-tauri/src/core/tray/menu_def.rs +++ b/src-tauri/src/core/tray/menu_def.rs @@ -36,6 +36,7 @@ define_menu! { rule_mode => RULE_MODE, "tray_rule_mode", "tray.ruleMode", global_mode => GLOBAL_MODE, "tray_global_mode", "tray.globalMode", direct_mode => DIRECT_MODE, "tray_direct_mode", "tray.directMode", + outbound_modes => OUTBOUND_MODES, "tray_outbound_modes", "tray.outboundModes", profiles => PROFILES, "tray_profiles", "tray.profiles", proxies => PROXIES, "tray_proxies", "tray.proxies", system_proxy => SYSTEM_PROXY, "tray_system_proxy", "tray.systemProxy", diff --git a/src-tauri/src/core/tray/mod.rs b/src-tauri/src/core/tray/mod.rs index 17ecd04f9..ce29aecc0 100644 --- a/src-tauri/src/core/tray/mod.rs +++ b/src-tauri/src/core/tray/mod.rs @@ -870,7 +870,7 @@ async fn create_tray_menu( }); let verge_settings = Config::verge().await.latest_arc(); - let show_proxy_groups_inline = verge_settings.tray_inline_proxy_groups.unwrap_or(false); + let show_proxy_groups_inline = verge_settings.tray_inline_proxy_groups.unwrap_or(true); let version = env!("CARGO_PKG_VERSION"); @@ -895,6 +895,13 @@ async fn create_tray_menu( hotkeys.get("open_or_close_dashboard").map(|s| s.as_str()), )?; + let current_mode_text = match current_proxy_mode { + "global" => rust_i18n::t!("tray.global"), + "direct" => rust_i18n::t!("tray.direct"), + _ => rust_i18n::t!("tray.rule"), + }; + let outbound_modes_label = format!("{} ({})", texts.outbound_modes, current_mode_text); + let rule_mode = &CheckMenuItem::with_id( app_handle, MenuIds::RULE_MODE, @@ -922,6 +929,18 @@ async fn create_tray_menu( hotkeys.get("clash_mode_direct").map(|s| s.as_str()), )?; + let outbound_modes = &Submenu::with_id_and_items( + app_handle, + MenuIds::OUTBOUND_MODES, + outbound_modes_label.as_str(), + true, + &[ + rule_mode as &dyn IsMenuItem, + global_mode as &dyn IsMenuItem, + direct_mode as &dyn IsMenuItem, + ], + )?; + let profiles = &Submenu::with_id_and_items( app_handle, MenuIds::PROFILES, @@ -1072,6 +1091,7 @@ async fn create_tray_menu( &texts.more, true, &[ + copy_env as &dyn IsMenuItem, close_all_connections, restart_clash, restart_app, @@ -1090,15 +1110,8 @@ async fn create_tray_menu( let separator = &PredefinedMenuItem::separator(app_handle)?; // 动态构建菜单项 - let mut menu_items: Vec<&dyn IsMenuItem> = vec![ - open_window, - separator, - rule_mode, - global_mode, - direct_mode, - separator, - profiles, - ]; + let mut menu_items: Vec<&dyn IsMenuItem> = + vec![open_window, outbound_modes, separator, profiles]; // 如果有代理节点,添加代理节点菜单 if show_proxy_groups_inline { @@ -1115,7 +1128,6 @@ async fn create_tray_menu( tun_mode as &dyn IsMenuItem, separator, lightweight_mode as &dyn IsMenuItem, - copy_env as &dyn IsMenuItem, open_dir as &dyn IsMenuItem, more as &dyn IsMenuItem, separator, diff --git a/src/components/setting/mods/layout-viewer.tsx b/src/components/setting/mods/layout-viewer.tsx index 3f367a3fa..ebc9588df 100644 --- a/src/components/setting/mods/layout-viewer.tsx +++ b/src/components/setting/mods/layout-viewer.tsx @@ -372,7 +372,7 @@ export const LayoutViewer = forwardRef((_, ref) => { )} /> Date: Wed, 12 Nov 2025 14:13:43 +0800 Subject: [PATCH 48/49] chore(deps): update dependency @types/react-dom to v19.2.3 (#5424) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index ad857216f..0c8dcb6ea 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,7 @@ "@types/lodash-es": "^4.17.12", "@types/node": "^24.10.1", "@types/react": "19.2.3", - "@types/react-dom": "19.2.2", + "@types/react-dom": "19.2.3", "@vitejs/plugin-legacy": "^7.2.1", "@vitejs/plugin-react-swc": "^4.2.1", "adm-zip": "^0.5.16", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ace9b8cb5..f1dd7abeb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -160,8 +160,8 @@ importers: specifier: 19.2.3 version: 19.2.3 '@types/react-dom': - specifier: 19.2.2 - version: 19.2.2(@types/react@19.2.3) + specifier: 19.2.3 + version: 19.2.3(@types/react@19.2.3) '@vitejs/plugin-legacy': specifier: ^7.2.1 version: 7.2.1(terser@5.44.1)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)) @@ -1840,8 +1840,8 @@ packages: '@types/prop-types@15.7.15': resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} - '@types/react-dom@19.2.2': - resolution: {integrity: sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==} + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: '@types/react': ^19.2.0 @@ -6007,7 +6007,7 @@ snapshots: '@types/prop-types@15.7.15': {} - '@types/react-dom@19.2.2(@types/react@19.2.3)': + '@types/react-dom@19.2.3(@types/react@19.2.3)': dependencies: '@types/react': 19.2.3 From 3db69bd81660be4c378235695afda211f5d5ed10 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 12 Nov 2025 18:47:40 +0800 Subject: [PATCH 49/49] chore(deps): update npm dependencies (#5431) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 4 ++-- pnpm-lock.yaml | 30 +++++++++++++++--------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 0c8dcb6ea..13523de0b 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "react-dom": "19.2.0", "react-error-boundary": "6.0.0", "react-hook-form": "^7.66.0", - "react-i18next": "16.2.4", + "react-i18next": "16.3.0", "react-markdown": "10.1.0", "react-router": "^7.9.5", "react-virtuoso": "^4.14.1", @@ -90,7 +90,7 @@ "@types/react": "19.2.3", "@types/react-dom": "19.2.3", "@vitejs/plugin-legacy": "^7.2.1", - "@vitejs/plugin-react-swc": "^4.2.1", + "@vitejs/plugin-react-swc": "^4.2.2", "adm-zip": "^0.5.16", "cli-color": "^2.0.4", "commander": "^14.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f1dd7abeb..c2bb098a6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -114,8 +114,8 @@ importers: specifier: ^7.66.0 version: 7.66.0(react@19.2.0) react-i18next: - specifier: 16.2.4 - version: 16.2.4(i18next@25.6.2(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3) + specifier: 16.3.0 + version: 16.3.0(i18next@25.6.2(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3) react-markdown: specifier: 10.1.0 version: 10.1.0(@types/react@19.2.3)(react@19.2.0) @@ -166,8 +166,8 @@ importers: specifier: ^7.2.1 version: 7.2.1(terser@5.44.1)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)) '@vitejs/plugin-react-swc': - specifier: ^4.2.1 - version: 4.2.1(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)) + specifier: ^4.2.2 + version: 4.2.2(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)) adm-zip: specifier: ^0.5.16 version: 0.5.16 @@ -1430,8 +1430,8 @@ packages: '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} - '@rolldown/pluginutils@1.0.0-beta.46': - resolution: {integrity: sha512-xMNwJo/pHkEP/mhNVnW+zUiJDle6/hxrwO0mfSJuEVRbBfgrJFuUSRoZx/nYUw5pCjrysl9OkNXCkAdih8GCnA==} + '@rolldown/pluginutils@1.0.0-beta.47': + resolution: {integrity: sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==} '@rollup/pluginutils@5.2.0': resolution: {integrity: sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==} @@ -2023,8 +2023,8 @@ packages: terser: ^5.16.0 vite: ^7.0.0 - '@vitejs/plugin-react-swc@4.2.1': - resolution: {integrity: sha512-SIZ/XxeS2naLw4L2vVvpTyujM2OY+Rf+y6nWETqfoBrZpI3SFdyNJof3nQ8HbLhXJ1Eh9e9c0JGYC8GYPhLkCw==} + '@vitejs/plugin-react-swc@4.2.2': + resolution: {integrity: sha512-x+rE6tsxq/gxrEJN3Nv3dIV60lFflPj94c90b+NNo6n1QV1QQUTLoL0MpaOVasUZ0zqVBn7ead1B5ecx1JAGfA==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: vite: ^4 || ^5 || ^6 || ^7 @@ -3634,10 +3634,10 @@ packages: peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 - react-i18next@16.2.4: - resolution: {integrity: sha512-pvbcPQ+YuQQoRkKBA4VCU9aO8dOgP/vdKEizIYXcAk3+AmI8yQKSJaCzxQQu4Kgg2zWZm3ax9KqHv8ItUlRY0A==} + react-i18next@16.3.0: + resolution: {integrity: sha512-XGYIVU6gCOL4UQsfp87WbbvBc2WvgdkEDI8r4TwACzFg1bXY8pd1d9Cw6u9WJ2soTKHKaF1xQEyWA3/dUvtAGw==} peerDependencies: - i18next: '>= 25.5.2' + i18next: '>= 25.6.2' react: '>= 16.8.0' react-dom: '*' react-native: '*' @@ -5679,7 +5679,7 @@ snapshots: '@popperjs/core@2.11.8': {} - '@rolldown/pluginutils@1.0.0-beta.46': {} + '@rolldown/pluginutils@1.0.0-beta.47': {} '@rollup/pluginutils@5.2.0(rollup@4.46.2)': dependencies: @@ -6196,9 +6196,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitejs/plugin-react-swc@4.2.1(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))': + '@vitejs/plugin-react-swc@4.2.2(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))': dependencies: - '@rolldown/pluginutils': 1.0.0-beta.46 + '@rolldown/pluginutils': 1.0.0-beta.47 '@swc/core': 1.14.0 vite: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1) transitivePeerDependencies: @@ -8200,7 +8200,7 @@ snapshots: dependencies: react: 19.2.0 - react-i18next@16.2.4(i18next@25.6.2(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3): + react-i18next@16.3.0(i18next@25.6.2(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3): dependencies: '@babel/runtime': 7.28.4 html-parse-stringify: 3.0.1