Compare commits

..
Author SHA1 Message Date
renovate[bot] 1336b8a9e0 chore(deps): update tauri 2026-11-10 13:42:16 +00:00
18 changed files with 363 additions and 1131 deletions
+3 -3
View File
@@ -5,10 +5,10 @@
"version": "v9.0.0",
"sha": "3a2844b7e9c422d3c10d287c895573f7108da1b3"
},
"github/gh-aw-actions/setup@v1.2.2": {
"github/gh-aw-actions/setup@v1.0.1": {
"repo": "github/gh-aw-actions/setup",
"version": "v1.2.2",
"sha": "53d0749e19424a918acd82b4d140264ce48f2f14"
"version": "v1.0.1",
"sha": "221ba4f4319de6b8e809d1a15528b3ae6a818aa5"
}
},
"containers": {
+1 -1
View File
@@ -31,7 +31,7 @@ jobs:
with:
persist-credentials: false
- name: Install gh-aw extension
uses: github/gh-aw-actions/setup-cli@53d0749e19424a918acd82b4d140264ce48f2f14 # v1.2.2
uses: github/gh-aw-actions/setup-cli@406c7719e31d7fe354224d4bb8fe3d4aeb47da1c # v1.2.1
with:
version: v1.0.1
- name: Install Rust Stable
File diff suppressed because one or more lines are too long
-1
View File
@@ -33,7 +33,6 @@ max-daily-ai-credits: 5000
permissions:
issues: read
pull-requests: read
copilot-requests: none
pre-agent-steps:
- name: Make Copilot quota exhaustion non-blocking
+107 -288
View File
File diff suppressed because one or more lines are too long
-1
View File
@@ -30,7 +30,6 @@ permissions:
contents: read
issues: read
pull-requests: read
copilot-requests: none
steps:
- name: Load review rules from the base revision
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
File diff suppressed because one or more lines are too long
@@ -28,7 +28,6 @@ permissions:
contents: read
issues: read
pull-requests: read
copilot-requests: none
max-ai-credits: 60
max-turns: 12
max-daily-ai-credits: 500
Generated
+11 -10
View File
@@ -159,9 +159,9 @@ dependencies = [
[[package]]
name = "arc-swap"
version = "1.9.5"
version = "1.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84a22b5d51209f6de83d37a0b33d320acb8da88823e42a94c075140708f032a5"
checksum = "db4c94f57a570fce5d381be3289b3f79aef6332d72948fe82ab5772e423ccf92"
dependencies = [
"rustversion",
]
@@ -7520,14 +7520,15 @@ dependencies = [
[[package]]
name = "tauri-plugin-http"
version = "2.8.3"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c57e3451553f95a1db3bfed6a358a77ce5080b6399a242c6eadac113f97abd2"
checksum = "c278945f30912c8c32728265c66d16ca1b139509c4874d00579f5c8c5276605a"
dependencies = [
"bytes",
"cookie_store",
"data-url",
"http",
"percent-encoding",
"regex",
"reqwest 0.12.28",
"schemars 0.8.22",
@@ -7569,9 +7570,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-notification"
version = "2.8.0"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bc6c194a95a535abdfbe3bc1af9a43935b8b9b29c216b8e71af8b59132703cf"
checksum = "14a226bfb8189ec4ab2c1936e66330ad06c44d9f6c54a674338208491dee177f"
dependencies = [
"log",
"notify-rust",
@@ -7589,9 +7590,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-opener"
version = "2.7.2"
version = "2.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53810be1b4fed22bb6ac3f4cab5a2b80d9391d1af436bda145607532bc0bb219"
checksum = "54592c6718b42ab198bf11bda6a428a3553ca2f2f8e506df56f6a063ec328331"
dependencies = [
"dunce",
"glob",
@@ -10126,9 +10127,9 @@ dependencies = [
[[package]]
name = "zip"
version = "9.5.0"
version = "9.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0700e6c427aff64acdbf02e85fbfd946c2ee12319d1af013092f3a2178914ff8"
checksum = "df3845f734baabdd02de353a20580e30ceccd03897def44a4eb8e9654992e294"
dependencies = [
"aes",
"bzip2",
-1
View File
@@ -48,7 +48,6 @@
- 优化连接、规则和日志筛选:切换页面后保留过滤条件
- 优化全局模式代理列表:仅显示 `GLOBAL` 节点,选中其中的策略组时才显示该组
- 优化代理页面和首页节点卡片,减少延迟更新时的卡顿
**🍎 macOS**
+5 -5
View File
@@ -72,12 +72,12 @@ sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs.git", branch
] }
network-interface = { version = "2.0.5", features = ["serde"] }
tauri-plugin-shell = "2.6.0"
tauri-plugin-opener = "2.7.2"
tauri-plugin-opener = "2.7.3"
tauri-plugin-dialog = "2.8.4"
tauri-plugin-fs = "2.7.1"
tauri-plugin-deep-link = "2.6.3"
tauri-plugin-window-state = "2.5.0"
zip = "9.5.0"
zip = "9.4.2"
flate2 = "1.1.11"
reqwest_dav = "0.3.3"
aes-gcm = { version = "0.11.1" }
@@ -87,9 +87,9 @@ getrandom = "0.4.3"
futures = "0.3.34"
gethostname = "1.1.0"
scopeguard = "1.2.0"
tauri-plugin-notification = "2.8.0"
tauri-plugin-notification = "2.8.1"
backon = { version = "1.6.0", features = ["tokio-sleep"] }
tauri-plugin-http = "2.8.3"
tauri-plugin-http = "2.9.0"
tracing = { workspace = true }
tauri-plugin-devtools = { version = "2.2.3", optional = true }
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo", rev = "7a03d16f4d39b1bb885b7babe66b9e153e34b615" }
@@ -97,7 +97,7 @@ async-trait = "0.1.92"
clash_verge_service_ipc = { git = "https://github.com/clash-verge-rev/clash-verge-service-ipc.git", tag = "v2.7.7", version = "2.7.7", features = [
"client",
] }
arc-swap = "1.9.5"
arc-swap = "1.9.4"
tokio-rustls = "0.26.6"
rustls = { version = "0.23.46", features = ["ring"] }
webpki-roots = "1.0.11"
+8 -16
View File
@@ -605,14 +605,12 @@ export const CurrentProxyCard = () => {
[getProfileStorageKey],
)
const onSelectionError = useCallback(
(error: unknown) => {
const { select } = useProxySelection({
onError: (error) => {
console.error('代理切换失败', error)
refreshProxy()
},
[refreshProxy],
)
const { select } = useProxySelection({ onError: onSelectionError })
})
const mode = clashConfig?.mode?.toLowerCase() || 'rule'
const isGlobalMode = mode === 'global'
@@ -675,6 +673,7 @@ export const CurrentProxyCard = () => {
if (proxyView && lastGroupName !== selectedGroupName) {
setLastGroupName(selectedGroupName)
}
const delays = useGroupDelays(selectedGroupName || null)
const selectedGroup = useMemo<ProxyGroupView | null>(() => {
if (!proxyView || isDirectMode) return null
@@ -768,17 +767,10 @@ export const CurrentProxyCard = () => {
const currentProxy = currentMember ? memberDetails(currentMember) : undefined
const selectedProxyName = currentMember?.ref.name ?? ''
const delays = useGroupDelays(
selectedGroupName,
currentMember,
openSelect === 'proxy',
)
const currentDelay =
typeof delays === 'number'
? delays
: currentMember
? delays.of(currentMember)
: -1
currentMember && selectedGroupName
? delayManager.getDelayFix(currentMember, selectedGroupName)
: -1
const signalInfo =
currentProxy && selectedGroupName
@@ -900,7 +892,7 @@ export const CurrentProxyCard = () => {
const proxyOptions = useMemo(
() =>
isDirectMode || openSelect !== 'proxy' || typeof delays === 'number'
isDirectMode || openSelect !== 'proxy'
? []
: sortProxyOptions(
unsortedProxyOptions,
+3 -5
View File
@@ -469,14 +469,12 @@ function NormalProxyGroups(props: { mode: string }) {
}
}, [handleScroll])
const onSelectionError = useCallback(
(error: unknown) => {
const { select } = useProxySelection({
onError: (error) => {
console.error('代理切换失败', error)
onProxies()
},
[onProxies],
)
const { select } = useProxySelection({ onError: onSelectionError })
})
const handleChangeProxy = useCallback(
(group: ProxyGroupView, member: ResolvedProxyMember) => {
-3
View File
@@ -29,9 +29,6 @@ export function filterSort(
const regex1 = /delay([=<>])(\d+|timeout|error)/i
const regex2 = /type=(.*)/i
export const isDelayFilter = (filterText: string) =>
regex1.test(filterText.trim())
function filterProxies(
proxies: ResolvedMemberOccurrence[],
groupName: string,
@@ -1,119 +0,0 @@
import { expect, it, vi } from 'vitest'
import type { DelaySnapshot } from '@/services/delay'
import { resolveMember, type ProxyViewV1 } from '@/types/proxy-view'
import { filterSort } from './use-filter-sort'
import { useRenderList as readRenderList } from './use-render-list'
const hooks = vi.hoisted(() => {
const slots: { value: unknown; deps?: unknown[] }[] = []
let cursor = 0
return {
reset: () => {
cursor = 0
},
memo: (factory: () => unknown, deps: unknown[]) => {
const index = cursor++
const old = slots[index]
if (!old || deps.some((dep, i) => !Object.is(dep, old.deps?.[i]))) {
slots[index] = { value: factory(), deps }
}
return slots[index].value
},
ref: (current: unknown) => {
const index = cursor++
slots[index] ??= { value: { current } }
return slots[index].value
},
}
})
const state = vi.hoisted(() => ({
view: null as ProxyViewV1 | null,
delays: new Map<string, DelaySnapshot>(),
}))
vi.mock('react', () => ({
useMemo: hooks.memo,
useRef: hooks.ref,
useEffect: () => {},
}))
vi.mock('@/hooks/use-profiles', () => ({}))
vi.mock('@/hooks/use-clash', () => ({
useRuntimeConfig: () => ({ data: null }),
}))
vi.mock('@/hooks/use-verge', () => ({
useVerge: () => ({ verge: { proxy_layout_column: 1 } }),
}))
vi.mock('@/providers/app-data-context', () => ({
useProxiesData: () => ({ proxyView: state.view }),
useAppRefreshers: () => ({ refreshProxy: () => {} }),
}))
vi.mock('@/hooks/use-group-delays', () => ({
useGroupsDelays: (names: string[]) =>
new Map(names.map((name) => [name, state.delays.get(name)])),
}))
vi.mock('./use-window-width', () => ({
useWindowWidth: () => ({ width: 800 }),
}))
vi.mock('./use-head-state', async (original) => {
const actual = await original<typeof import('./use-head-state')>()
const heads = {
plain: { ...actual.DEFAULT_STATE, open: true },
sorted: { ...actual.DEFAULT_STATE, open: true, sortType: 1 },
}
return { ...actual, useHeadStateNew: () => [heads, () => {}] }
})
vi.mock('./use-filter-sort', async (original) => {
const actual = await original<typeof import('./use-filter-sort')>()
return { ...actual, filterSort: vi.fn(actual.filterSort) }
})
vi.mock('@/types/proxy-view', async (original) => {
const actual = await original<typeof import('@/types/proxy-view')>()
return { ...actual, resolveMember: vi.fn(actual.resolveMember) }
})
it('skips delay-independent projections and reuses members for delay sorting', () => {
const group = (name: string) => ({
name,
type: 'Selector',
history: [],
udp: false,
xudp: false,
tfo: false,
mptcp: false,
smux: false,
members: Array.from({ length: 500 }, (_, i) => ({
kind: 'unresolved' as const,
name: `${i}`,
reason: 'missing' as const,
})),
})
state.view = {
schemaVersion: 1,
orderSource: 'runtime',
providerState: 'ready',
global: null,
direct: null,
groups: [group('plain'), group('sorted')],
records: {},
standalone: [],
providers: [],
}
const render = () => {
hooks.reset()
return readRenderList('rule')
}
state.delays.set('plain', { of: () => 42 })
state.delays.set('sorted', { of: () => 42 })
render()
vi.mocked(filterSort).mockClear()
vi.mocked(resolveMember).mockClear()
state.delays.set('plain', { of: () => 43 })
render()
expect(filterSort).toHaveBeenCalledTimes(0)
state.delays.set('sorted', { of: () => 43 })
render()
expect(filterSort).toHaveBeenCalledTimes(1)
expect(resolveMember).toHaveBeenCalledTimes(0)
})
+17 -58
View File
@@ -17,7 +17,7 @@ import {
} from '@/types/proxy-view'
import { debugLog } from '@/utils/debug'
import { filterSort, isDelayFilter } from './use-filter-sort'
import { filterSort } from './use-filter-sort'
import {
DEFAULT_STATE,
useHeadStateNew,
@@ -60,14 +60,14 @@ export const hasRenderableItems = (
): boolean => renderList.some((item) => item.type !== 0 || !item.group.hidden)
type GroupCache = {
group: ProxyGroupView
now: string | undefined
members: ProxyGroupView['members']
headState: HeadState
col: number
latencyTimeout: number | undefined
/// This group's own delays. Compared by identity so that a test settling in one group
/// does not throw away every other group's sorted order.
delays: DelaySnapshot | undefined
occurrences: ResolvedMemberOccurrence[] | undefined
items: IRenderItem[]
}
@@ -222,51 +222,19 @@ export const useRenderList = (
return current?.kind === 'group' ? [global, current.group] : [global]
}, [mode, proxyView])
const repeatedNameGroups = useMemo(() => {
const repeated = new Set<string>()
if (isChainMode) return repeated
for (const group of renderGroups) {
const names = new Set<string>()
if (
group.members.some(({ name }) => {
if (names.has(name)) return true
names.add(name)
return false
})
) {
repeated.add(group.name)
}
}
return repeated
}, [isChainMode, renderGroups])
// Rows own delay updates unless repeated names share the current item listener.
// Every group this list draws, so a test settling in any of them re-sorts that group.
const renderedGroupNames = useMemo(() => {
if (!proxyView || isChainMode) return []
return renderGroups
.filter((group) => {
const state = headStates[group.name] || DEFAULT_STATE
return (
(state.open || group === proxyView.global) &&
(state.sortType === 1 ||
isDelayFilter(state.filterText) ||
repeatedNameGroups.has(group.name))
)
})
.map(({ name }) => name)
}, [headStates, isChainMode, proxyView, renderGroups, repeatedNameGroups])
if (!proxyView) return []
if (isChainMode)
return selectedGroup ? [selectedGroup] : [CHAIN_DELAY_GROUP]
return renderGroups.map(({ name }) => name)
}, [isChainMode, proxyView, renderGroups, selectedGroup])
const groupDelays = useGroupsDelays(renderedGroupNames)
const groupCacheRef = useRef<Map<string, GroupCache> | null>(null)
const cachedViewRef = useRef(proxyView)
const groupCacheRef = useRef<Map<string, GroupCache>>(new Map())
const prevListRef = useRef<IRenderItem[]>([])
const renderList = useMemo<IRenderItem[]>(() => {
const cache = (groupCacheRef.current ??= new Map())
if (cachedViewRef.current !== proxyView) {
cache.clear()
cachedViewRef.current = proxyView
}
if (!proxyView) return []
if (isChainMode) {
@@ -301,13 +269,16 @@ export const useRenderList = (
}))
}
const cache = groupCacheRef.current
const retList = renderGroups.flatMap((group) => {
const isFlat = group === proxyView.global
const headState = headStates[group.name] || DEFAULT_STATE
const cached = cache.get(group.name)
if (
cached &&
cached.group === group &&
cached.now === group.now &&
cached.members === group.members &&
cached.headState === headState &&
cached.col === col &&
cached.latencyTimeout === latencyTimeout &&
@@ -329,18 +300,9 @@ export const useRenderList = (
},
]
// Duplicate names share one item listener until the delay-store fix lands.
const refreshRepeated =
repeatedNameGroups.has(group.name) &&
cached?.delays !== groupDelays.get(group.name)
let resolved =
cached?.group === group && !refreshRepeated
? cached.occurrences
: undefined
if (headState.open || isFlat) {
resolved ??= resolveOccurrences(proxyView, group)
const occurrences = filterSort(
resolved,
resolveOccurrences(proxyView, group),
group.name,
headState.filterText,
headState.sortType,
@@ -375,17 +337,15 @@ export const useRenderList = (
})),
)
}
} else {
resolved = undefined
}
cache.set(group.name, {
group,
now: group.now!,
members: group.members,
headState,
col,
latencyTimeout: latencyTimeout!,
delays: groupDelays.get(group.name),
occurrences: resolved,
items: ret,
})
return ret
@@ -411,7 +371,6 @@ export const useRenderList = (
mode,
proxyView,
renderGroups,
repeatedNameGroups,
selectedGroup,
])
-45
View File
@@ -1,45 +0,0 @@
import { useSyncExternalStore } from 'react'
import { expect, it, vi } from 'vitest'
import delayManager from '@/services/delay'
import type { ResolvedProxyMember } from '@/types/proxy-view'
import { useGroupDelays } from './use-group-delays'
vi.mock('react', () => ({
useCallback: (callback: unknown) => callback,
useSyncExternalStore: vi.fn((_subscribe, read) => read()),
}))
it('keeps the closed-menu snapshot unchanged when another member settles', () => {
const member: ResolvedProxyMember = {
kind: 'node',
ref: { kind: 'node', name: 'current', recordId: 'current' },
node: {
recordId: 'current',
name: 'current',
type: 'Shadowsocks',
source: { kind: 'core', proxyName: 'current' },
history: [],
udp: false,
xudp: false,
tfo: false,
mptcp: false,
smux: false,
},
}
vi.spyOn(delayManager, 'getDelayFix').mockReturnValue(42)
const snapshot = vi.spyOn(delayManager, 'groupDelays')
snapshot.mockReturnValue({ of: () => 42 })
useGroupDelays('group', member, false)
const [, read] = vi.mocked(useSyncExternalStore).mock.calls.at(-1)!
let previous = read()
let changes = 0
for (let i = 0; i < 120; i++) {
snapshot.mockReturnValue({ of: () => 42 })
const next = read()
if (!Object.is(next, previous)) changes++
previous = next
}
expect(changes).toBe(0)
})
+6 -14
View File
@@ -1,27 +1,19 @@
import { useCallback, useSyncExternalStore } from 'react'
import delayManager, { type DelaySnapshot } from '@/services/delay'
import type { ResolvedProxyMember } from '@/types/proxy-view'
/** A closed menu observes only its current member, not every settled group delay. */
export const useGroupDelays = (
group: string | null,
member: ResolvedProxyMember | undefined,
menuOpen: boolean,
): DelaySnapshot | number => {
const NO_DELAYS: DelaySnapshot = { of: () => -1 }
/** Exposes the external delay store to React, updating sort order only after a test settles. */
export const useGroupDelays = (group: string | null): DelaySnapshot => {
const subscribe = useCallback(
(onSettle: () => void) =>
group ? delayManager.addGroupListener(group, onSettle) : () => {},
[group],
)
const read = useCallback(
() =>
group && menuOpen
? delayManager.groupDelays(group)
: group && member
? delayManager.getDelayFix(member, group)
: -1,
[group, member, menuOpen],
() => (group ? delayManager.groupDelays(group) : NO_DELAYS),
[group],
)
return useSyncExternalStore(subscribe, read, read)