Compare commits

...

4 Commits

Author SHA1 Message Date
renovate[bot]
b5853fa4aa
chore(deps): update npm dependencies 2026-03-30 09:28:15 +00:00
wonfen
56291d3d91
chore: replace update URL 2026-03-30 12:01:05 +08:00
Tunglies
7a06a5a069
fix: remove dead localStorage writes and hardcoded key
- Remove unused `dns_settings_enabled` localStorage writes in
  setting-clash.tsx — state is sourced from verge config, these
  writes were never read anywhere.
- Replace hardcoded `'last_check_update'` localStorage read in
  system-info-card.tsx with exported `readLastCheckTime()` from
  the useUpdate hook, keeping the key in a single source of truth.
2026-03-29 02:06:08 +08:00
Tunglies
99bbd7ee5a
fix(home): unify last check update timestamp across settings and home page (#6605)
The settings page "Check for updates" did not update the homepage
"last check update time" because each page managed the timestamp
independently. Centralizes the timestamp in the useUpdate hook
via SWR + localStorage so both pages share a single data source.

Closes https://github.com/clash-verge-rev/clash-verge-rev/issues/6605#issuecomment-4147144987
2026-03-29 01:56:34 +08:00
12 changed files with 91 additions and 122 deletions

View File

@ -60,7 +60,7 @@
"dayjs": "1.11.20",
"foxact": "^0.3.0",
"foxts": "^5.3.0",
"i18next": "^25.10.4",
"i18next": "^26.0.0",
"js-yaml": "^4.1.1",
"lodash-es": "^4.17.23",
"meta-json-schema": "^1.19.21",
@ -71,7 +71,7 @@
"react-dom": "19.2.4",
"react-error-boundary": "6.1.1",
"react-hook-form": "^7.72.0",
"react-i18next": "16.6.6",
"react-i18next": "17.0.1",
"react-markdown": "10.1.0",
"react-router": "^7.13.1",
"react-virtuoso": "^4.18.3",
@ -120,7 +120,7 @@
"typescript": "^6.0.0",
"typescript-eslint": "^8.57.1",
"vite": "^8.0.1",
"vite-plugin-svgr": "^4.5.0"
"vite-plugin-svgr": "^5.0.0"
},
"lint-staged": {
"*.{ts,tsx}": [

38
pnpm-lock.yaml generated
View File

@ -87,8 +87,8 @@ importers:
specifier: ^5.3.0
version: 5.3.0
i18next:
specifier: ^25.10.4
version: 25.10.4(typescript@6.0.2)
specifier: ^26.0.0
version: 26.0.2(typescript@6.0.2)
js-yaml:
specifier: ^4.1.1
version: 4.1.1
@ -120,8 +120,8 @@ importers:
specifier: ^7.72.0
version: 7.72.0(react@19.2.4)
react-i18next:
specifier: 16.6.6
version: 16.6.6(i18next@25.10.4(typescript@6.0.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@6.0.2)
specifier: 17.0.1
version: 17.0.1(i18next@26.0.2(typescript@6.0.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@6.0.2)
react-markdown:
specifier: 10.1.0
version: 10.1.0(@types/react@19.2.14)(react@19.2.4)
@ -262,8 +262,8 @@ importers:
specifier: ^8.0.1
version: 8.0.1(@types/node@24.12.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(yaml@2.8.3)
vite-plugin-svgr:
specifier: ^4.5.0
version: 4.5.0(typescript@6.0.2)(vite@8.0.1(@types/node@24.12.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(yaml@2.8.3))
specifier: ^5.0.0
version: 5.0.0(typescript@6.0.2)(vite@8.0.1(@types/node@24.12.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(yaml@2.8.3))
packages:
@ -2491,10 +2491,10 @@ packages:
engines: {node: '>=18'}
hasBin: true
i18next@25.10.4:
resolution: {integrity: sha512-XsE/6eawy090meuFU0BTY9BtmWr1m9NSwLr0NK7/A04LA58wdAvDsi9WNOJ40Qb1E9NIPbvnVLZEN2fWDd3/3Q==}
i18next@26.0.2:
resolution: {integrity: sha512-WsK0SdP+7tGzsxpT+Us1s2nvOyx657DatBodaNZe4KcPTPYzkVfRKUygN69mB+sCbbnifRuKz+Ya5JRzd8DNHw==}
peerDependencies:
typescript: ^5
typescript: ^5 || ^6
peerDependenciesMeta:
typescript:
optional: true
@ -3055,10 +3055,10 @@ packages:
peerDependencies:
react: ^16.8.0 || ^17 || ^18 || ^19
react-i18next@16.6.6:
resolution: {integrity: sha512-ZgL2HUoW34UKUkOV7uSQFE1CDnRPD+tCR3ywSuWH7u2iapnz86U8Bi3Vrs620qNDzCf1F47NxglCEkchCTDOHw==}
react-i18next@17.0.1:
resolution: {integrity: sha512-iG65FGnFHcYyHNuT01ukffYWCOBFTWSdVD8EZd/dCVWgtjFPObcSsvYYNwcsokO/rDcTb5d6D8Acv8MrOdm6Hw==}
peerDependencies:
i18next: '>= 25.10.9'
i18next: '>= 26.0.1'
react: '>= 16.8.0'
react-dom: '*'
react-native: '*'
@ -3434,10 +3434,10 @@ packages:
vfile@6.0.3:
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
vite-plugin-svgr@4.5.0:
resolution: {integrity: sha512-W+uoSpmVkSmNOGPSsDCWVW/DDAyv+9fap9AZXBvWiQqrboJ08j2vh0tFxTD/LjwqwAd3yYSVJgm54S/1GhbdnA==}
vite-plugin-svgr@5.0.0:
resolution: {integrity: sha512-CZFWDtbWSLnF6C+uv8u7E5Ao6UVQYBpJrS6212XsEod/Lm4ErhOoFc01/po4ie5hqvMCr5KYrlMrSGQQEtMtBg==}
peerDependencies:
vite: '>=2.6.0'
vite: '>=3.0.0'
vite@8.0.1:
resolution: {integrity: sha512-wt+Z2qIhfFt85uiyRt5LPU4oVEJBXj8hZNWKeqFG4gRG/0RaRGJ7njQCwzFVjO+v4+Ipmf5CY7VdmZRAYYBPHw==}
@ -6016,7 +6016,7 @@ snapshots:
husky@9.1.7: {}
i18next@25.10.4(typescript@6.0.2):
i18next@26.0.2(typescript@6.0.2):
dependencies:
'@babel/runtime': 7.29.2
optionalDependencies:
@ -6656,11 +6656,11 @@ snapshots:
dependencies:
react: 19.2.4
react-i18next@16.6.6(i18next@25.10.4(typescript@6.0.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@6.0.2):
react-i18next@17.0.1(i18next@26.0.2(typescript@6.0.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@6.0.2):
dependencies:
'@babel/runtime': 7.29.2
html-parse-stringify: 3.0.1
i18next: 25.10.4(typescript@6.0.2)
i18next: 26.0.2(typescript@6.0.2)
react: 19.2.4
use-sync-external-store: 1.6.0(react@19.2.4)
optionalDependencies:
@ -7078,7 +7078,7 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.3
vite-plugin-svgr@4.5.0(typescript@6.0.2)(vite@8.0.1(@types/node@24.12.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(yaml@2.8.3)):
vite-plugin-svgr@5.0.0(typescript@6.0.2)(vite@8.0.1(@types/node@24.12.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(yaml@2.8.3)):
dependencies:
'@rollup/pluginutils': 5.3.0
'@svgr/core': 8.1.0(typescript@6.0.2)

View File

@ -100,8 +100,7 @@ async function resolveUpdater() {
Object.entries(updateDataNew.platforms).forEach(([key, value]) => {
if (value.url) {
updateDataNew.platforms[key].url =
'https://download.clashverge.dev/' + value.url
updateDataNew.platforms[key].url = 'https://update.hwdns.net/' + value.url
} else {
console.log(`[Error]: updateDataNew.platforms.${key} is null`)
}

View File

@ -204,7 +204,7 @@ async function processRelease(github, options, tag, isAlpha) {
Object.entries(updateDataNew.platforms).forEach(([key, value]) => {
if (value.url) {
updateDataNew.platforms[key].url =
'https://download.clashverge.dev/' + value.url
'https://update.hwdns.net/' + value.url
} else {
console.log(`[Error]: updateDataNew.platforms.${key} is null`)
}

View File

@ -32,8 +32,8 @@
"updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK",
"endpoints": [
"https://download.clashverge.dev/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json",
"https://gh-proxy.com/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json",
"https://update.hwdns.net/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json",
"https://gh-proxy.org/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json",
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update.json"
],
"windows": {

View File

@ -25,7 +25,7 @@
"active": true,
"dialog": false,
"endpoints": [
"https://download.clashverge.dev/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2-proxy.json",
"https://update.hwdns.net/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2-proxy.json",
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2.json"
],
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK"

View File

@ -25,7 +25,7 @@
"active": true,
"dialog": false,
"endpoints": [
"https://download.clashverge.dev/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2-proxy.json",
"https://update.hwdns.net/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2-proxy.json",
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2.json"
],
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK"

View File

@ -25,7 +25,7 @@
"active": true,
"dialog": false,
"endpoints": [
"https://download.clashverge.dev/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2-proxy.json",
"https://update.hwdns.net/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2-proxy.json",
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2.json"
],
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK"

View File

@ -7,13 +7,17 @@ import {
} from '@mui/icons-material'
import { Typography, Stack, Divider, Chip, IconButton } from '@mui/material'
import { useLockFn } from 'ahooks'
import { useCallback, useEffect, useMemo, useReducer } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router'
import { useServiceInstaller } from '@/hooks/use-service-installer'
import { useSystemState } from '@/hooks/use-system-state'
import { useUpdate } from '@/hooks/use-update'
import {
useUpdate,
updateLastCheckTime,
readLastCheckTime,
} from '@/hooks/use-update'
import { useVerge } from '@/hooks/use-verge'
import { getSystemInfo } from '@/services/cmds'
import { showNotice } from '@/services/notice-service'
@ -21,29 +25,6 @@ import { version as appVersion } from '@root/package.json'
import { EnhancedCard } from './enhanced-card'
interface SystemState {
osInfo: string
lastCheckUpdate: string
}
type SystemStateAction =
| { type: 'set-os-info'; payload: string }
| { type: 'set-last-check-update'; payload: string }
const systemStateReducer = (
state: SystemState,
action: SystemStateAction,
): SystemState => {
switch (action.type) {
case 'set-os-info':
return { ...state, osInfo: action.payload }
case 'set-last-check-update':
return { ...state, lastCheckUpdate: action.payload }
default:
return state
}
}
export const SystemInfoCard = () => {
const { t } = useTranslation()
const { verge, patchVerge } = useVerge()
@ -51,28 +32,18 @@ export const SystemInfoCard = () => {
const { isAdminMode, isSidecarMode } = useSystemState()
const { installServiceAndRestartCore } = useServiceInstaller()
// 自动检查更新逻辑
const { checkUpdate: triggerCheckUpdate } = useUpdate(true, {
onSuccess: () => {
const now = Date.now()
localStorage.setItem('last_check_update', now.toString())
dispatchSystemState({
type: 'set-last-check-update',
payload: new Date(now).toLocaleString(),
})
},
})
// 自动检查更新逻辑lastCheckUpdate 由 useUpdate 统一管理)
const { checkUpdate: triggerCheckUpdate, lastCheckUpdate } = useUpdate(true)
// 系统信息状态
const [systemState, dispatchSystemState] = useReducer(systemStateReducer, {
osInfo: '',
lastCheckUpdate: '-',
})
const [osInfo, setOsInfo] = useState('')
const lastCheckUpdateText = useMemo(
() => (lastCheckUpdate ? new Date(lastCheckUpdate).toLocaleString() : '-'),
[lastCheckUpdate],
)
// 初始化系统信息
useEffect(() => {
let timeoutId: number | undefined
getSystemInfo()
.then((info) => {
const lines = info.split('\n')
@ -87,49 +58,23 @@ export const SystemInfoCard = () => {
sysVersion = sysVersion.substring(sysName.length).trim()
}
dispatchSystemState({
type: 'set-os-info',
payload: `${sysName} ${sysVersion}`,
})
setOsInfo(`${sysName} ${sysVersion}`)
}
})
.catch(console.error)
}, [])
// 获取最后检查更新时间
const lastCheck = localStorage.getItem('last_check_update')
if (lastCheck) {
try {
const timestamp = parseInt(lastCheck, 10)
if (!isNaN(timestamp)) {
dispatchSystemState({
type: 'set-last-check-update',
payload: new Date(timestamp).toLocaleString(),
})
}
} catch (e) {
console.error('Error parsing last check update time', e)
}
} else if (verge?.auto_check_update) {
// 如果启用了自动检查更新但没有记录,设置当前时间并延迟检查
const now = Date.now()
localStorage.setItem('last_check_update', now.toString())
dispatchSystemState({
type: 'set-last-check-update',
payload: new Date(now).toLocaleString(),
})
// 如果启用了自动检查更新但没有记录,设置当前时间并延迟检查
useEffect(() => {
if (!verge?.auto_check_update) return
if (readLastCheckTime() !== null) return
timeoutId = window.setTimeout(() => {
if (verge?.auto_check_update) {
triggerCheckUpdate().catch(console.error)
}
}, 5000)
}
return () => {
if (timeoutId !== undefined) {
window.clearTimeout(timeoutId)
}
}
}, [verge?.auto_check_update, dispatchSystemState, triggerCheckUpdate])
updateLastCheckTime()
const timeoutId = window.setTimeout(() => {
triggerCheckUpdate().catch(console.error)
}, 5000)
return () => window.clearTimeout(timeoutId)
}, [verge?.auto_check_update, triggerCheckUpdate])
// 导航到设置页面
const goToSettings = useCallback(() => {
@ -157,12 +102,6 @@ export const SystemInfoCard = () => {
const onCheckUpdate = useLockFn(async () => {
try {
const info = await triggerCheckUpdate()
const now = Date.now()
localStorage.setItem('last_check_update', now.toString())
dispatchSystemState({
type: 'set-last-check-update',
payload: new Date(now).toLocaleString(),
})
if (!info?.available) {
showNotice.success(
'settings.components.verge.advanced.notifications.latestVersion',
@ -280,7 +219,7 @@ export const SystemInfoCard = () => {
{t('home.components.systemInfo.fields.osInfo')}
</Typography>
<Typography variant="body2" fontWeight="medium">
{systemState.osInfo}
{osInfo}
</Typography>
</Stack>
<Divider />
@ -341,7 +280,7 @@ export const SystemInfoCard = () => {
'&:hover': { opacity: 0.7 },
}}
>
{systemState.lastCheckUpdate}
{lastCheckUpdateText}
</Typography>
</Stack>
<Divider />

View File

@ -78,7 +78,6 @@ const SettingClash = ({ onError }: Props) => {
const handleDnsToggle = useLockFn(async (enable: boolean) => {
try {
setDnsSettingsEnabled(enable)
localStorage.setItem('dns_settings_enabled', String(enable))
await patchVerge({ enable_dns_settings: enable })
await invoke('apply_dns_config', { apply: enable })
setTimeout(() => {
@ -86,7 +85,6 @@ const SettingClash = ({ onError }: Props) => {
}, 500)
} catch (err: any) {
setDnsSettingsEnabled(!enable)
localStorage.setItem('dns_settings_enabled', String(!enable))
showNotice.error(err)
await patchVerge({ enable_dns_settings: !enable }).catch(() => {})
throw err

View File

@ -4,6 +4,7 @@ import { useCallback, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { DialogRef, TooltipIcon } from '@/components/base'
import { updateLastCheckTime } from '@/hooks/use-update'
import {
exitApp,
exportDiagnosticInfo,
@ -45,6 +46,7 @@ const SettingVergeAdvanced = ({ onError: _ }: Props) => {
const onCheckUpdate = async () => {
try {
const info = await checkUpdate()
updateLastCheckTime()
if (!info?.available) {
showNotice.success(
'settings.components.verge.advanced.notifications.latestVersion',

View File

@ -1,4 +1,4 @@
import useSWR, { SWRConfiguration } from 'swr'
import useSWR, { mutate as globalMutate, SWRConfiguration } from 'swr'
import { checkUpdateSafe } from '@/services/update'
@ -12,6 +12,26 @@ export interface UpdateInfo {
downloadAndInstall: (onEvent?: any) => Promise<void>
}
// --- Last check timestamp (shared via SWR + localStorage) ---
const LAST_CHECK_KEY = 'last_check_update'
export const readLastCheckTime = (): number | null => {
const stored = localStorage.getItem(LAST_CHECK_KEY)
if (!stored) return null
const ts = parseInt(stored, 10)
return isNaN(ts) ? null : ts
}
export const updateLastCheckTime = (timestamp?: number): number => {
const now = timestamp ?? Date.now()
localStorage.setItem(LAST_CHECK_KEY, now.toString())
globalMutate(LAST_CHECK_KEY, now, false)
return now
}
// --- useUpdate hook ---
export const useUpdate = (
enabled: boolean = true,
options?: SWRConfiguration,
@ -36,11 +56,22 @@ export const useUpdate = (
refreshInterval: 24 * 60 * 60 * 1000, // 24 hours
dedupingInterval: 60 * 60 * 1000, // 1 hour
...options,
onSuccess: (...args) => {
updateLastCheckTime()
options?.onSuccess?.(...args)
},
})
// Shared last check timestamp
const { data: lastCheckUpdate } = useSWR(LAST_CHECK_KEY, readLastCheckTime, {
revalidateOnFocus: false,
revalidateOnReconnect: false,
})
return {
updateInfo,
checkUpdate,
loading: isValidating,
lastCheckUpdate: lastCheckUpdate ?? null,
}
}