mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-23 04:02:10 +08:00
refactor: knip cleanup (#7468)
* chore(frontend): expand Knip reports * chore: remove unused Mihomo changelog sync script * refactor: remove unused value exports * refactor: remove unused type exports * refactor: remove circular route dependencies * chore(frontend): move Knip scope to config * chore(i18n): remove stale portable update message
This commit is contained in:
@@ -75,6 +75,6 @@ jobs:
|
||||
if: steps.check_frontend.outputs.frontend == 'true'
|
||||
run: pnpm typecheck
|
||||
|
||||
- name: Report unused frontend files
|
||||
- name: Report unused frontend code and dependencies
|
||||
if: steps.check_frontend.outputs.frontend == 'true'
|
||||
run: pnpm knip:report
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ args = ["typecheck"]
|
||||
command = "pnpm.cmd"
|
||||
|
||||
[tasks.knip-report]
|
||||
description = "Report unused frontend files"
|
||||
description = "Report unused frontend code and dependencies"
|
||||
command = "pnpm"
|
||||
args = ["knip:report"]
|
||||
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/knip@6/schema.json",
|
||||
"entry": ["src/polyfills/*.js!"],
|
||||
"project": ["src/**/*.{js,jsx,ts,tsx}!"]
|
||||
"project": ["src/**/*.{js,jsx,ts,tsx}!", "scripts/**/*.{js,mjs}!"],
|
||||
"include": [
|
||||
"files",
|
||||
"dependencies",
|
||||
"devDependencies",
|
||||
"unlisted",
|
||||
"unresolved",
|
||||
"binaries",
|
||||
"exports",
|
||||
"types",
|
||||
"duplicates",
|
||||
"cycles"
|
||||
],
|
||||
"ignoreBinaries": ["rustc"],
|
||||
"ignoreDependencies": ["lint-staged"],
|
||||
"ignoreIssues": {
|
||||
"src/types/generated/**": ["exports", "types"]
|
||||
},
|
||||
"cycles": {
|
||||
"dynamicImports": true
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@
|
||||
"i18n:format": "node scripts/cleanup-unused-i18n.mjs --align --apply",
|
||||
"i18n:types": "node scripts/generate-i18n-keys.mjs",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"knip:report": "knip --files --no-exit-code"
|
||||
"knip:report": "knip --no-exit-code"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
import fs from 'node:fs'
|
||||
import fsp from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
|
||||
const VERSION_URL =
|
||||
'https://github.com/MetaCubeX/mihomo/releases/latest/download/version.txt'
|
||||
const DEFAULT_CHANGELOG = 'Changelog.md'
|
||||
|
||||
const args = process.argv.slice(2)
|
||||
const CHECK = args.includes('--check')
|
||||
const DRY_RUN = args.includes('--dry-run')
|
||||
|
||||
const logError = (message, ...optionalParams) =>
|
||||
console.error(message, ...optionalParams)
|
||||
const logInfo = (message, ...optionalParams) =>
|
||||
console.log(message, ...optionalParams)
|
||||
|
||||
function readArgValue(name) {
|
||||
const index = args.indexOf(name)
|
||||
if (index === -1) return null
|
||||
return args[index + 1] ?? null
|
||||
}
|
||||
|
||||
function normalizeVersion(version) {
|
||||
const normalized = version.trim()
|
||||
return normalized.startsWith('v') ? normalized : `v${normalized}`
|
||||
}
|
||||
|
||||
async function fetchLatestVersion() {
|
||||
const explicitVersion = readArgValue('--version')
|
||||
if (explicitVersion) return normalizeVersion(explicitVersion)
|
||||
|
||||
const response = await fetch(VERSION_URL, {
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch ${VERSION_URL}: ${response.status}`)
|
||||
}
|
||||
|
||||
return normalizeVersion(await response.text())
|
||||
}
|
||||
|
||||
function replaceExistingLine(content, version) {
|
||||
const mihomoLine =
|
||||
/^(\s*-\s*(?:\*\*)?\s*Mihomo\(Meta\)\s*内核升级至\s*)v?\d+(?:\.\d+)+(?:[-+][^\s*]+)?((?:\*\*)?\s*)$/m
|
||||
|
||||
if (!mihomoLine.test(content)) return null
|
||||
|
||||
return content.replace(mihomoLine, `$1${version}$2`)
|
||||
}
|
||||
|
||||
function insertMihomoLine(content, version) {
|
||||
const title = /^## .+$/m
|
||||
const match = content.match(title)
|
||||
|
||||
if (!match || match.index === undefined) {
|
||||
throw new Error('could not find changelog title')
|
||||
}
|
||||
|
||||
const titleEnd = match.index + match[0].length
|
||||
const before = content.slice(0, titleEnd)
|
||||
const after = content.slice(titleEnd).replace(/^\n*/, '\n\n')
|
||||
|
||||
return `${before}\n\n- **Mihomo(Meta) 内核升级至 ${version}**${after}`
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const cwd = process.cwd()
|
||||
const changelogArg = readArgValue('--file')
|
||||
const changelogPath = path.join(cwd, changelogArg || DEFAULT_CHANGELOG)
|
||||
|
||||
if (!fs.existsSync(changelogPath)) {
|
||||
throw new Error(`could not find ${path.relative(cwd, changelogPath)}`)
|
||||
}
|
||||
|
||||
const version = await fetchLatestVersion()
|
||||
const content = await fsp.readFile(changelogPath, 'utf-8')
|
||||
const nextContent =
|
||||
replaceExistingLine(content, version) ?? insertMihomoLine(content, version)
|
||||
|
||||
if (nextContent === content) {
|
||||
logInfo(`Mihomo version in changelog is already ${version}`)
|
||||
return
|
||||
}
|
||||
|
||||
if (CHECK) {
|
||||
logError(`Mihomo version in changelog is not ${version}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (DRY_RUN) {
|
||||
logInfo(`Would update Mihomo version in changelog to ${version}`)
|
||||
return
|
||||
}
|
||||
|
||||
await fsp.writeFile(changelogPath, nextContent)
|
||||
logInfo(`Updated Mihomo version in changelog to ${version}`)
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
logError('sync-mihomo-version failed:', error.message)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -13,7 +13,7 @@ import type { ReactNode } from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export type BaseSplitChipEditorMode = 'visual' | 'advanced'
|
||||
type BaseSplitChipEditorMode = 'visual' | 'advanced'
|
||||
|
||||
interface BaseSplitChipEditorProps {
|
||||
value?: string
|
||||
|
||||
@@ -6,10 +6,7 @@ export { BaseLoading } from './base-loading'
|
||||
export { BaseLoadingOverlay } from './base-loading-overlay'
|
||||
export { BasePage } from './base-page'
|
||||
export { BaseSearchBox, type SearchState } from './base-search-box'
|
||||
export {
|
||||
BaseSplitChipEditor,
|
||||
type BaseSplitChipEditorMode,
|
||||
} from './base-split-chip-editor'
|
||||
export { BaseSplitChipEditor } from './base-split-chip-editor'
|
||||
export { BaseStyledSelect } from './base-styled-select'
|
||||
export { BaseStyledTextField } from './base-styled-text-field'
|
||||
export { MonacoEditor } from './monaco-editor'
|
||||
|
||||
@@ -28,7 +28,7 @@ import getSystem from '@/utils/get-system'
|
||||
|
||||
const appWindow = getCurrentWebviewWindow()
|
||||
|
||||
export type EditorLanguage = 'yaml' | 'javascript' | 'css'
|
||||
type EditorLanguage = 'yaml' | 'javascript' | 'css'
|
||||
|
||||
export interface EditorViewerProps {
|
||||
open: boolean
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { useEffect, useMemo, useReducer, useRef } from 'react'
|
||||
|
||||
import { useVerge } from '@/hooks/use-verge'
|
||||
import delayManager from '@/services/delay'
|
||||
import { compileStringMatcher } from '@/utils/search-matcher'
|
||||
|
||||
@@ -13,89 +10,6 @@ export type ProxySearchState = {
|
||||
useRegularExpression?: boolean
|
||||
}
|
||||
|
||||
export default function useFilterSort(
|
||||
proxies: IProxyItem[],
|
||||
groupName: string,
|
||||
filterText: string,
|
||||
sortType: ProxySortType,
|
||||
searchState?: ProxySearchState,
|
||||
) {
|
||||
const { verge } = useVerge()
|
||||
const [_, bumpRefresh] = useReducer((count: number) => count + 1, 0)
|
||||
const lastInputRef = useRef<{ text: string; sort: ProxySortType } | null>(
|
||||
null,
|
||||
)
|
||||
const debounceTimerRef = useRef<number | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
let last = 0
|
||||
|
||||
delayManager.setGroupListener(groupName, () => {
|
||||
// 简单节流
|
||||
const now = Date.now()
|
||||
if (now - last > 666) {
|
||||
last = now
|
||||
bumpRefresh()
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
delayManager.removeGroupListener(groupName)
|
||||
}
|
||||
}, [groupName])
|
||||
|
||||
const compute = useMemo(() => {
|
||||
const fp = filterProxies(proxies, groupName, filterText, searchState)
|
||||
const sp = sortProxies(
|
||||
fp,
|
||||
groupName,
|
||||
sortType,
|
||||
verge?.default_latency_timeout,
|
||||
)
|
||||
return sp
|
||||
}, [
|
||||
proxies,
|
||||
groupName,
|
||||
filterText,
|
||||
sortType,
|
||||
searchState,
|
||||
verge?.default_latency_timeout,
|
||||
])
|
||||
|
||||
const [result, setResult] = useReducer(
|
||||
(_prev: IProxyItem[], next: IProxyItem[]) => next,
|
||||
compute,
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (debounceTimerRef.current !== null) {
|
||||
window.clearTimeout(debounceTimerRef.current)
|
||||
debounceTimerRef.current = null
|
||||
}
|
||||
|
||||
const prev = lastInputRef.current
|
||||
const stableInputs =
|
||||
prev && prev.text === filterText && prev.sort === sortType
|
||||
|
||||
lastInputRef.current = { text: filterText, sort: sortType }
|
||||
|
||||
const delay = stableInputs ? 0 : 150
|
||||
debounceTimerRef.current = window.setTimeout(() => {
|
||||
setResult(compute)
|
||||
debounceTimerRef.current = null
|
||||
}, delay)
|
||||
|
||||
return () => {
|
||||
if (debounceTimerRef.current !== null) {
|
||||
window.clearTimeout(debounceTimerRef.current)
|
||||
debounceTimerRef.current = null
|
||||
}
|
||||
}
|
||||
}, [compute, filterText, sortType])
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function filterSort(
|
||||
proxies: IProxyItem[],
|
||||
groupName: string,
|
||||
|
||||
@@ -17,7 +17,6 @@ import type { Options as ReactMarkdownOptions } from 'react-markdown'
|
||||
|
||||
import { BaseDialog, DialogRef } from '@/components/base'
|
||||
import { useUpdate } from '@/hooks/use-update'
|
||||
import { portableFlag } from '@/pages/_layout'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
import { useSetUpdateState, useUpdateState } from '@/services/states'
|
||||
|
||||
@@ -160,10 +159,6 @@ export function UpdateViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
}, [updateInfo])
|
||||
|
||||
const onUpdate = useLockFn(async () => {
|
||||
if (portableFlag) {
|
||||
showNotice.error('settings.modals.update.messages.portableError')
|
||||
return
|
||||
}
|
||||
if (!updateInfo?.body) return
|
||||
if (breakChangeFlag) {
|
||||
showNotice.error('settings.modals.update.messages.breakChangeError')
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { DialogRef, TooltipIcon } from '@/components/base'
|
||||
import { useVerge } from '@/hooks/use-verge'
|
||||
import { navItems } from '@/pages/_routers'
|
||||
import { navigationItems } from '@/pages/_navigation-meta'
|
||||
import { copyClashEnv } from '@/services/cmds'
|
||||
import { supportedLanguages } from '@/services/i18n'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
@@ -187,7 +187,7 @@ const SettingVergeBasic = ({ onError }: Props) => {
|
||||
onGuard={(e) => patchVerge({ start_page: e })}
|
||||
>
|
||||
<Select size="small" sx={{ width: 140, '> div': { py: '7.5px' } }}>
|
||||
{navItems.map((page: { label: string; path: string }) => {
|
||||
{Object.values(navigationItems).map((page) => {
|
||||
return (
|
||||
<MenuItem key={page.path} value={page.path}>
|
||||
{t(page.label)}
|
||||
|
||||
@@ -10,25 +10,25 @@ type ConnectionListener = () => void
|
||||
|
||||
const metadataValue = (value?: string) => value || ''
|
||||
|
||||
export const initConnData: ConnectionMonitorData = {
|
||||
const initConnData: ConnectionMonitorData = {
|
||||
uploadTotal: 0,
|
||||
downloadTotal: 0,
|
||||
activeConnections: [],
|
||||
closedConnections: [],
|
||||
}
|
||||
|
||||
export interface ConnectionMonitorData {
|
||||
interface ConnectionMonitorData {
|
||||
uploadTotal: number
|
||||
downloadTotal: number
|
||||
activeConnections: IConnectionsItem[]
|
||||
closedConnections: IConnectionsItem[]
|
||||
}
|
||||
|
||||
export interface ConnectionSummaryData {
|
||||
interface ConnectionSummaryData {
|
||||
activeConnectionCount: number
|
||||
}
|
||||
|
||||
export const initConnSummaryData: ConnectionSummaryData = {
|
||||
const initConnSummaryData: ConnectionSummaryData = {
|
||||
activeConnectionCount: 0,
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
useQuery,
|
||||
} from '@/services/query-client'
|
||||
|
||||
export const RECONNECT_DELAY_MS = 1000
|
||||
const RECONNECT_DELAY_MS = 1000
|
||||
|
||||
interface SharedSubscriptionOwner {
|
||||
handleMessage: (data: string) => void
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useQuery } from '@/services/query-client'
|
||||
import { useVerge } from './use-verge'
|
||||
import { useVisibility } from './use-visibility'
|
||||
|
||||
export interface SystemState {
|
||||
interface SystemState {
|
||||
runningMode: 'Sidecar' | 'Service'
|
||||
isAdminMode: boolean
|
||||
isServiceOk: boolean
|
||||
|
||||
@@ -3,14 +3,6 @@ import { checkUpdateSafe } from '@/services/update'
|
||||
|
||||
import { useVerge } from './use-verge'
|
||||
|
||||
export interface UpdateInfo {
|
||||
version: string
|
||||
body: string
|
||||
date: string
|
||||
available: boolean
|
||||
downloadAndInstall: (onEvent?: any) => Promise<void>
|
||||
}
|
||||
|
||||
const LAST_CHECK_KEY = 'last_check_update'
|
||||
|
||||
export const readLastCheckTime = (): number | null => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { use } from 'react'
|
||||
|
||||
import { WindowContext, type WindowContextType } from '@/providers/window'
|
||||
|
||||
export const useWindow = () => {
|
||||
const useWindow = () => {
|
||||
const context = use(WindowContext)
|
||||
if (context === undefined) {
|
||||
throw new Error('useWindow must be used within WindowProvider')
|
||||
|
||||
@@ -429,7 +429,6 @@
|
||||
"update": "تحديث"
|
||||
},
|
||||
"messages": {
|
||||
"portableError": "الإصدار المحمول لا يدعم التحديث داخل التطبيق. يرجى التنزيل والاستبدال يدويًا",
|
||||
"breakChangeError": "هذا الإصدار هو تحديث رئيسي ولا يدعم التحديث داخل التطبيق. يرجى إلغاء التثبيت وتنزيل الإصدار الجديد وتثبيته يدويًا"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -429,7 +429,6 @@
|
||||
"update": "Aktualisieren"
|
||||
},
|
||||
"messages": {
|
||||
"portableError": "Die portable Version unterstützt keine In-App-Aktualisierung. Bitte laden Sie die Dateien manuell herunter und ersetzen Sie sie.",
|
||||
"breakChangeError": "Dies ist eine wichtige Aktualisierung. Die In-App-Aktualisierung wird nicht unterstützt. Bitte deinstallieren Sie die Software und laden Sie die neue Version manuell herunter und installieren Sie sie."
|
||||
}
|
||||
},
|
||||
|
||||
@@ -429,7 +429,6 @@
|
||||
"update": "Update"
|
||||
},
|
||||
"messages": {
|
||||
"portableError": "The portable version does not support in-app updates. Please manually download and replace it",
|
||||
"breakChangeError": "This version is a major update and does not support in-app updates. Please uninstall it and manually download and install the new version"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -429,7 +429,6 @@
|
||||
"update": "Actualizar"
|
||||
},
|
||||
"messages": {
|
||||
"portableError": "La versión portátil no admite la actualización desde dentro de la aplicación. Descargue e instale manualmente la actualización.",
|
||||
"breakChangeError": "Esta es una actualización importante y no se admite la actualización desde dentro de la aplicación. Desinstale e instale manualmente la nueva versión."
|
||||
}
|
||||
},
|
||||
|
||||
@@ -429,7 +429,6 @@
|
||||
"update": "بهروزرسانی"
|
||||
},
|
||||
"messages": {
|
||||
"portableError": "نسخه پرتابل از بهروزرسانی درون برنامهای پشتیبانی نمیکند. لطفاً به صورت دستی دانلود و جایگزین کنید",
|
||||
"breakChangeError": "این نسخه یک بهروزرسانی اساسی است و پشتیبانی از بهروزرسانی درون برنامه را پشتیبانی نمیکند. لطفاً پس از حذف، دستی دانلود و نصب کنید."
|
||||
}
|
||||
},
|
||||
|
||||
@@ -429,7 +429,6 @@
|
||||
"update": "Perbarui"
|
||||
},
|
||||
"messages": {
|
||||
"portableError": "Versi portabel tidak mendukung pembaruan dalam aplikasi. Harap unduh dan ganti secara manual",
|
||||
"breakChangeError": "Versi ini adalah pembaruan besar dan tidak mendukung pembaruan dalam aplikasi. Harap hapus instalasi dan unduh serta instal versi baru secara manual"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -429,7 +429,6 @@
|
||||
"update": "更新"
|
||||
},
|
||||
"messages": {
|
||||
"portableError": "ポータブル版ではアプリケーション内での更新はサポートされていません。手動でダウンロードして置き換えてください。",
|
||||
"breakChangeError": "このバージョンは重大な更新であり、アプリケーション内での更新はサポートされていません。アンインストールしてから手動でダウンロードしてインストールしてください。"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -429,7 +429,6 @@
|
||||
"update": "업데이트"
|
||||
},
|
||||
"messages": {
|
||||
"portableError": "포터블 버전은 앱 내 업데이트를 지원하지 않습니다. 수동으로 다운로드하여 교체하세요",
|
||||
"breakChangeError": "이 버전은 메이저 업데이트로 앱 내 업데이트를 지원하지 않습니다. 프로그램을 제거한 뒤 새 버전을 수동으로 설치하세요"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -429,7 +429,6 @@
|
||||
"update": "Обновить"
|
||||
},
|
||||
"messages": {
|
||||
"portableError": "Портативная версия не поддерживает обновление внутри приложения. Пожалуйста, скачайте и замените файлы вручную.",
|
||||
"breakChangeError": "Это крупное обновление, которое не поддерживает обновление внутри приложения. Пожалуйста, удалите текущую версию и загрузите установочный файл вручную."
|
||||
}
|
||||
},
|
||||
|
||||
@@ -429,7 +429,6 @@
|
||||
"update": "Güncelle"
|
||||
},
|
||||
"messages": {
|
||||
"portableError": "Taşınabilir sürüm uygulama içi güncellemeleri desteklemez. Lütfen manuel olarak indirip değiştirin",
|
||||
"breakChangeError": "Bu sürüm büyük bir güncellemedir ve uygulama içi güncellemeleri desteklemez. Lütfen kaldırın ve yeni sürümü manuel olarak indirip kurun"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -429,7 +429,6 @@
|
||||
"update": "Яңарту"
|
||||
},
|
||||
"messages": {
|
||||
"portableError": "Портатив версиядә кушымта эчендә яңарту хупланмый, кулдан төшереп алыгыз",
|
||||
"breakChangeError": "Бу зур яңарту, ул кушымта эчендә яңартылмый. Борып алып ташлап, яңадан урнаштыру сорала."
|
||||
}
|
||||
},
|
||||
|
||||
@@ -429,7 +429,6 @@
|
||||
"update": "更新"
|
||||
},
|
||||
"messages": {
|
||||
"portableError": "便携版不支持应用内更新,请手动下载替换",
|
||||
"breakChangeError": "此版本为重大更新,不支持应用内更新,请卸载后手动下载安装"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -429,7 +429,6 @@
|
||||
"update": "更新"
|
||||
},
|
||||
"messages": {
|
||||
"portableError": "可攜式版不支援應用程式內更新,請手動下載替換",
|
||||
"breakChangeError": "此版本為重大更新,不支援應用程式內更新,請解除安裝後手動下載安裝"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -62,13 +62,15 @@ import {
|
||||
useNavMenuOrder,
|
||||
} from './_layout/hooks'
|
||||
import { handleNoticeMessage } from './_layout/utils'
|
||||
import { navItems, preloadLogsPage, preloadNavigationRoutes } from './_routers'
|
||||
import {
|
||||
navItems,
|
||||
preloadLogsPage,
|
||||
preloadNavigationRoutes,
|
||||
} from './_navigation'
|
||||
|
||||
import 'dayjs/locale/ru'
|
||||
import 'dayjs/locale/zh-cn'
|
||||
|
||||
export const portableFlag = false
|
||||
|
||||
const LogsPage = lazy(() => preloadLogsPage())
|
||||
|
||||
type NavItem = (typeof navItems)[number]
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
export const navigationItems = {
|
||||
home: { label: 'layout.components.navigation.tabs.home', path: '/' },
|
||||
proxies: {
|
||||
label: 'layout.components.navigation.tabs.proxies',
|
||||
path: '/proxies',
|
||||
},
|
||||
profiles: {
|
||||
label: 'layout.components.navigation.tabs.profiles',
|
||||
path: '/profile',
|
||||
},
|
||||
connections: {
|
||||
label: 'layout.components.navigation.tabs.connections',
|
||||
path: '/connections',
|
||||
},
|
||||
rules: { label: 'layout.components.navigation.tabs.rules', path: '/rules' },
|
||||
logs: { label: 'layout.components.navigation.tabs.logs', path: '/logs' },
|
||||
unlock: {
|
||||
label: 'layout.components.navigation.tabs.unlock',
|
||||
path: '/unlock',
|
||||
},
|
||||
settings: {
|
||||
label: 'layout.components.navigation.tabs.settings',
|
||||
path: '/settings',
|
||||
},
|
||||
} as const
|
||||
@@ -0,0 +1,185 @@
|
||||
import DnsRoundedIcon from '@mui/icons-material/DnsRounded'
|
||||
import ForkRightRoundedIcon from '@mui/icons-material/ForkRightRounded'
|
||||
import HomeRoundedIcon from '@mui/icons-material/HomeRounded'
|
||||
import LanguageRoundedIcon from '@mui/icons-material/LanguageRounded'
|
||||
import LockOpenRoundedIcon from '@mui/icons-material/LockOpenRounded'
|
||||
import SettingsRoundedIcon from '@mui/icons-material/SettingsRounded'
|
||||
import SubjectRoundedIcon from '@mui/icons-material/SubjectRounded'
|
||||
import WifiRoundedIcon from '@mui/icons-material/WifiRounded'
|
||||
import { Box } from '@mui/material'
|
||||
import { lazy, Suspense, type ComponentType, type ReactNode } from 'react'
|
||||
|
||||
import ConnectionsSvg from '@/assets/image/itemicon/connections.svg?react'
|
||||
import HomeSvg from '@/assets/image/itemicon/home.svg?react'
|
||||
import LogsSvg from '@/assets/image/itemicon/logs.svg?react'
|
||||
import ProfilesSvg from '@/assets/image/itemicon/profiles.svg?react'
|
||||
import ProxiesSvg from '@/assets/image/itemicon/proxies.svg?react'
|
||||
import RulesSvg from '@/assets/image/itemicon/rules.svg?react'
|
||||
import SettingsSvg from '@/assets/image/itemicon/settings.svg?react'
|
||||
import UnlockSvg from '@/assets/image/itemicon/unlock.svg?react'
|
||||
import { BaseLoading } from '@/components/base'
|
||||
import { ensureLanguageSections } from '@/services/i18n'
|
||||
|
||||
import { navigationItems } from './_navigation-meta'
|
||||
import HomePage from './home'
|
||||
|
||||
type NavigationItem = {
|
||||
label: (typeof navigationItems)[keyof typeof navigationItems]['label']
|
||||
path: string
|
||||
icon: ReactNode[]
|
||||
Component: ComponentType
|
||||
preload?: () => Promise<{ default: ComponentType }>
|
||||
}
|
||||
|
||||
const waitForWarmupIdle = (signal: AbortSignal) =>
|
||||
new Promise<void>((resolve) => {
|
||||
let idleId: number | undefined
|
||||
let timeoutId: number | undefined
|
||||
|
||||
const cleanup = () => {
|
||||
signal.removeEventListener('abort', finish)
|
||||
if (idleId !== undefined) {
|
||||
window.cancelIdleCallback(idleId)
|
||||
}
|
||||
if (timeoutId !== undefined) {
|
||||
window.clearTimeout(timeoutId)
|
||||
}
|
||||
}
|
||||
|
||||
const finish = () => {
|
||||
cleanup()
|
||||
resolve()
|
||||
}
|
||||
|
||||
if (signal.aborted) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
signal.addEventListener('abort', finish, { once: true })
|
||||
|
||||
if (window.requestIdleCallback) {
|
||||
idleId = window.requestIdleCallback(finish, { timeout: 500 })
|
||||
} else {
|
||||
timeoutId = window.setTimeout(finish, 120)
|
||||
}
|
||||
})
|
||||
|
||||
const createRoutePreload = (
|
||||
load: () => Promise<{ default: ComponentType }>,
|
||||
sections?: string | readonly string[],
|
||||
) => {
|
||||
let componentPromise: Promise<{ default: ComponentType }> | undefined
|
||||
|
||||
const loadComponent = () => {
|
||||
componentPromise ??= load().catch((error) => {
|
||||
componentPromise = undefined
|
||||
throw error
|
||||
})
|
||||
|
||||
return componentPromise
|
||||
}
|
||||
|
||||
if (!sections) {
|
||||
return loadComponent
|
||||
}
|
||||
|
||||
return async () => {
|
||||
const [component] = await Promise.all([
|
||||
loadComponent(),
|
||||
ensureLanguageSections(sections),
|
||||
])
|
||||
return component
|
||||
}
|
||||
}
|
||||
|
||||
const createLazyRoute = (
|
||||
load: () => Promise<{ default: ComponentType }>,
|
||||
sections?: string | readonly string[],
|
||||
) => {
|
||||
const preload = createRoutePreload(load, sections)
|
||||
const Component = lazy(preload)
|
||||
const LazyRoute = () => (
|
||||
<Suspense
|
||||
fallback={
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
height: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<BaseLoading />
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Component />
|
||||
</Suspense>
|
||||
)
|
||||
|
||||
return { Component: LazyRoute, preload }
|
||||
}
|
||||
|
||||
export const preloadLogsPage = createRoutePreload(
|
||||
() => import('./logs'),
|
||||
'logs',
|
||||
)
|
||||
|
||||
export const navItems: NavigationItem[] = [
|
||||
{
|
||||
...navigationItems.home,
|
||||
icon: [<HomeRoundedIcon key="mui" />, <HomeSvg key="svg" />],
|
||||
Component: HomePage,
|
||||
},
|
||||
{
|
||||
...navigationItems.proxies,
|
||||
icon: [<WifiRoundedIcon key="mui" />, <ProxiesSvg key="svg" />],
|
||||
...createLazyRoute(() => import('./proxies')),
|
||||
},
|
||||
{
|
||||
...navigationItems.profiles,
|
||||
icon: [<DnsRoundedIcon key="mui" />, <ProfilesSvg key="svg" />],
|
||||
...createLazyRoute(() => import('./profiles'), 'rules'),
|
||||
},
|
||||
{
|
||||
...navigationItems.connections,
|
||||
icon: [<LanguageRoundedIcon key="mui" />, <ConnectionsSvg key="svg" />],
|
||||
...createLazyRoute(() => import('./connections'), 'connections'),
|
||||
},
|
||||
{
|
||||
...navigationItems.rules,
|
||||
icon: [<ForkRightRoundedIcon key="mui" />, <RulesSvg key="svg" />],
|
||||
...createLazyRoute(() => import('./rules'), 'rules'),
|
||||
},
|
||||
{
|
||||
...navigationItems.logs,
|
||||
icon: [<SubjectRoundedIcon key="mui" />, <LogsSvg key="svg" />],
|
||||
Component: () => null /* LogsPage rendered in Layout only on /logs route */,
|
||||
preload: preloadLogsPage,
|
||||
},
|
||||
{
|
||||
...navigationItems.unlock,
|
||||
icon: [<LockOpenRoundedIcon key="mui" />, <UnlockSvg key="svg" />],
|
||||
...createLazyRoute(() => import('./unlock')),
|
||||
},
|
||||
{
|
||||
...navigationItems.settings,
|
||||
icon: [<SettingsRoundedIcon key="mui" />, <SettingsSvg key="svg" />],
|
||||
...createLazyRoute(() => import('./settings')),
|
||||
},
|
||||
]
|
||||
|
||||
export const preloadNavigationRoutes = async (signal: AbortSignal) => {
|
||||
await waitForWarmupIdle(signal)
|
||||
if (signal.aborted) {
|
||||
return
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
navItems.map((item) => {
|
||||
const preload = 'preload' in item ? item.preload : undefined
|
||||
return preload?.().catch(() => {})
|
||||
}),
|
||||
)
|
||||
}
|
||||
+1
-183
@@ -1,189 +1,7 @@
|
||||
import DnsRoundedIcon from '@mui/icons-material/DnsRounded'
|
||||
import ForkRightRoundedIcon from '@mui/icons-material/ForkRightRounded'
|
||||
import HomeRoundedIcon from '@mui/icons-material/HomeRounded'
|
||||
import LanguageRoundedIcon from '@mui/icons-material/LanguageRounded'
|
||||
import LockOpenRoundedIcon from '@mui/icons-material/LockOpenRounded'
|
||||
import SettingsRoundedIcon from '@mui/icons-material/SettingsRounded'
|
||||
import SubjectRoundedIcon from '@mui/icons-material/SubjectRounded'
|
||||
import WifiRoundedIcon from '@mui/icons-material/WifiRounded'
|
||||
import { Box } from '@mui/material'
|
||||
import { lazy, Suspense, type ComponentType } from 'react'
|
||||
import { createBrowserRouter, RouteObject } from 'react-router'
|
||||
|
||||
import ConnectionsSvg from '@/assets/image/itemicon/connections.svg?react'
|
||||
import HomeSvg from '@/assets/image/itemicon/home.svg?react'
|
||||
import LogsSvg from '@/assets/image/itemicon/logs.svg?react'
|
||||
import ProfilesSvg from '@/assets/image/itemicon/profiles.svg?react'
|
||||
import ProxiesSvg from '@/assets/image/itemicon/proxies.svg?react'
|
||||
import RulesSvg from '@/assets/image/itemicon/rules.svg?react'
|
||||
import SettingsSvg from '@/assets/image/itemicon/settings.svg?react'
|
||||
import UnlockSvg from '@/assets/image/itemicon/unlock.svg?react'
|
||||
import { BaseLoading } from '@/components/base'
|
||||
import { ensureLanguageSections } from '@/services/i18n'
|
||||
|
||||
import Layout from './_layout'
|
||||
import HomePage from './home'
|
||||
|
||||
const waitForWarmupIdle = (signal: AbortSignal) =>
|
||||
new Promise<void>((resolve) => {
|
||||
let idleId: number | undefined
|
||||
let timeoutId: number | undefined
|
||||
|
||||
const cleanup = () => {
|
||||
signal.removeEventListener('abort', finish)
|
||||
if (idleId !== undefined) {
|
||||
window.cancelIdleCallback(idleId)
|
||||
}
|
||||
if (timeoutId !== undefined) {
|
||||
window.clearTimeout(timeoutId)
|
||||
}
|
||||
}
|
||||
|
||||
const finish = () => {
|
||||
cleanup()
|
||||
resolve()
|
||||
}
|
||||
|
||||
if (signal.aborted) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
signal.addEventListener('abort', finish, { once: true })
|
||||
|
||||
if (window.requestIdleCallback) {
|
||||
idleId = window.requestIdleCallback(finish, { timeout: 500 })
|
||||
} else {
|
||||
timeoutId = window.setTimeout(finish, 120)
|
||||
}
|
||||
})
|
||||
|
||||
const createRoutePreload = (
|
||||
load: () => Promise<{ default: ComponentType }>,
|
||||
sections?: string | readonly string[],
|
||||
) => {
|
||||
let componentPromise: Promise<{ default: ComponentType }> | undefined
|
||||
|
||||
const loadComponent = () => {
|
||||
componentPromise ??= load().catch((error) => {
|
||||
componentPromise = undefined
|
||||
throw error
|
||||
})
|
||||
|
||||
return componentPromise
|
||||
}
|
||||
|
||||
if (!sections) {
|
||||
return loadComponent
|
||||
}
|
||||
|
||||
return async () => {
|
||||
const [component] = await Promise.all([
|
||||
loadComponent(),
|
||||
ensureLanguageSections(sections),
|
||||
])
|
||||
return component
|
||||
}
|
||||
}
|
||||
|
||||
const createLazyRoute = (
|
||||
load: () => Promise<{ default: ComponentType }>,
|
||||
sections?: string | readonly string[],
|
||||
) => {
|
||||
const preload = createRoutePreload(load, sections)
|
||||
const Component = lazy(preload)
|
||||
const LazyRoute = () => (
|
||||
<Suspense
|
||||
fallback={
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
height: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<BaseLoading />
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Component />
|
||||
</Suspense>
|
||||
)
|
||||
|
||||
return { Component: LazyRoute, preload }
|
||||
}
|
||||
|
||||
export const preloadLogsPage = createRoutePreload(
|
||||
() => import('./logs'),
|
||||
'logs',
|
||||
)
|
||||
|
||||
export const navItems = [
|
||||
{
|
||||
label: 'layout.components.navigation.tabs.home',
|
||||
path: '/',
|
||||
icon: [<HomeRoundedIcon key="mui" />, <HomeSvg key="svg" />],
|
||||
Component: HomePage,
|
||||
},
|
||||
{
|
||||
label: 'layout.components.navigation.tabs.proxies',
|
||||
path: '/proxies',
|
||||
icon: [<WifiRoundedIcon key="mui" />, <ProxiesSvg key="svg" />],
|
||||
...createLazyRoute(() => import('./proxies')),
|
||||
},
|
||||
{
|
||||
label: 'layout.components.navigation.tabs.profiles',
|
||||
path: '/profile',
|
||||
icon: [<DnsRoundedIcon key="mui" />, <ProfilesSvg key="svg" />],
|
||||
...createLazyRoute(() => import('./profiles'), 'rules'),
|
||||
},
|
||||
{
|
||||
label: 'layout.components.navigation.tabs.connections',
|
||||
path: '/connections',
|
||||
icon: [<LanguageRoundedIcon key="mui" />, <ConnectionsSvg key="svg" />],
|
||||
...createLazyRoute(() => import('./connections'), 'connections'),
|
||||
},
|
||||
{
|
||||
label: 'layout.components.navigation.tabs.rules',
|
||||
path: '/rules',
|
||||
icon: [<ForkRightRoundedIcon key="mui" />, <RulesSvg key="svg" />],
|
||||
...createLazyRoute(() => import('./rules'), 'rules'),
|
||||
},
|
||||
{
|
||||
label: 'layout.components.navigation.tabs.logs',
|
||||
path: '/logs',
|
||||
icon: [<SubjectRoundedIcon key="mui" />, <LogsSvg key="svg" />],
|
||||
Component: () => null /* LogsPage rendered in Layout only on /logs route */,
|
||||
preload: preloadLogsPage,
|
||||
},
|
||||
{
|
||||
label: 'layout.components.navigation.tabs.unlock',
|
||||
path: '/unlock',
|
||||
icon: [<LockOpenRoundedIcon key="mui" />, <UnlockSvg key="svg" />],
|
||||
...createLazyRoute(() => import('./unlock')),
|
||||
},
|
||||
{
|
||||
label: 'layout.components.navigation.tabs.settings',
|
||||
path: '/settings',
|
||||
icon: [<SettingsRoundedIcon key="mui" />, <SettingsSvg key="svg" />],
|
||||
...createLazyRoute(() => import('./settings')),
|
||||
},
|
||||
]
|
||||
|
||||
export const preloadNavigationRoutes = async (signal: AbortSignal) => {
|
||||
await waitForWarmupIdle(signal)
|
||||
if (signal.aborted) {
|
||||
return
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
navItems.map((item) => {
|
||||
const preload = 'preload' in item ? item.preload : undefined
|
||||
return preload?.().catch(() => {})
|
||||
}),
|
||||
)
|
||||
}
|
||||
import { navItems } from './_navigation'
|
||||
|
||||
export const router = createBrowserRouter([
|
||||
{
|
||||
|
||||
@@ -6,39 +6,6 @@ import {
|
||||
RuleProvider,
|
||||
} from 'tauri-plugin-mihomo-api'
|
||||
|
||||
export interface AppDataContextType {
|
||||
proxies: any
|
||||
clashConfig: BaseConfig
|
||||
rules: Rule[]
|
||||
sysproxy: any
|
||||
runningMode?: string
|
||||
uptime: number
|
||||
proxyProviders: Record<string, ProxyProvider>
|
||||
ruleProviders: Record<string, RuleProvider>
|
||||
systemProxyAddress: string
|
||||
isCoreDataPending: boolean
|
||||
|
||||
refreshProxy: () => Promise<any>
|
||||
refreshClashConfig: () => Promise<any>
|
||||
refreshRules: () => Promise<any>
|
||||
refreshSysproxy: () => Promise<any>
|
||||
refreshProxyProviders: () => Promise<any>
|
||||
refreshRuleProviders: () => Promise<any>
|
||||
refreshAll: () => Promise<any>
|
||||
}
|
||||
|
||||
export interface ConnectionWithSpeed extends IConnectionsItem {
|
||||
curUpload: number
|
||||
curDownload: number
|
||||
}
|
||||
|
||||
export interface ConnectionSpeedData {
|
||||
id: string
|
||||
upload: number
|
||||
download: number
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
export interface ProxiesContextType {
|
||||
proxies: any
|
||||
proxyProviders: Record<string, ProxyProvider | undefined>
|
||||
@@ -134,27 +101,3 @@ export const useAppRefreshers = (): RefreshersContextType =>
|
||||
|
||||
export const useCoreDataStatus = (): CoreDataStatusContextType =>
|
||||
useCtx(CoreDataStatusContext, 'useCoreDataStatus')
|
||||
|
||||
export const useAppData = (): AppDataContextType => {
|
||||
const { proxies, proxyProviders } = useProxiesData()
|
||||
const { rules, ruleProviders } = useRulesData()
|
||||
const { clashConfig } = useClashConfigData()
|
||||
const { sysproxy, runningMode, systemProxyAddress } = useSystemData()
|
||||
const { uptime } = useUptimeData()
|
||||
const { isCoreDataPending } = useCoreDataStatus()
|
||||
const refreshers = useAppRefreshers()
|
||||
|
||||
return {
|
||||
proxies,
|
||||
clashConfig: clashConfig as BaseConfig,
|
||||
rules,
|
||||
sysproxy,
|
||||
runningMode,
|
||||
uptime,
|
||||
proxyProviders: proxyProviders as Record<string, ProxyProvider>,
|
||||
ruleProviders: ruleProviders as Record<string, RuleProvider>,
|
||||
systemProxyAddress,
|
||||
isCoreDataPending,
|
||||
...refreshers,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,10 +100,6 @@ export async function getRuntimeYaml() {
|
||||
return invoke<string | null>('get_runtime_yaml')
|
||||
}
|
||||
|
||||
export async function getRuntimeExists() {
|
||||
return invoke<string[]>('get_runtime_exists')
|
||||
}
|
||||
|
||||
export async function getRuntimeLogs() {
|
||||
return invoke<Record<string, [string, string][]>>('get_runtime_logs')
|
||||
}
|
||||
@@ -264,10 +260,6 @@ export async function getClashLogs() {
|
||||
}, [])
|
||||
}
|
||||
|
||||
export async function clearLogs() {
|
||||
return invoke<void>('clear_logs')
|
||||
}
|
||||
|
||||
export async function getVergeConfig() {
|
||||
return invoke<IVergeConfig>('get_verge_config')
|
||||
}
|
||||
@@ -302,23 +294,10 @@ export async function getAutotemProxy() {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAutoLaunchStatus() {
|
||||
try {
|
||||
return await invoke<boolean>('get_auto_launch_status')
|
||||
} catch (error) {
|
||||
console.error('获取自启动状态失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function changeClashCore(clashCore: string) {
|
||||
return invoke<string | null>('change_clash_core', { clashCore })
|
||||
}
|
||||
|
||||
export async function startCore() {
|
||||
return invoke<void>('start_core')
|
||||
}
|
||||
|
||||
export async function stopCore() {
|
||||
return invoke<void>('stop_core')
|
||||
}
|
||||
@@ -355,38 +334,6 @@ export const openWebUrl = async (url: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
export async function cmdGetProxyDelay(
|
||||
name: string,
|
||||
timeout: number,
|
||||
url?: string,
|
||||
) {
|
||||
// 确保URL不为空
|
||||
const testUrl = url || 'http://cp.cloudflare.com/generate_204'
|
||||
|
||||
try {
|
||||
// 不再在前端编码代理名称,由后端统一处理编码
|
||||
const result = await invoke<{ delay: number }>(
|
||||
'clash_api_get_proxy_delay',
|
||||
{
|
||||
name,
|
||||
url: testUrl, // 传递经过验证的URL
|
||||
timeout,
|
||||
},
|
||||
)
|
||||
|
||||
// 验证返回结果中是否有delay字段,并且值是一个有效的数字
|
||||
if (result && typeof result.delay === 'number') {
|
||||
return result
|
||||
} else {
|
||||
// 返回一个有效的结果对象,但标记为超时
|
||||
return { delay: 1e6 }
|
||||
}
|
||||
} catch {
|
||||
// 返回一个有效的结果对象,但标记为错误
|
||||
return { delay: 1e6 }
|
||||
}
|
||||
}
|
||||
|
||||
export async function cmdTestDelay(url: string) {
|
||||
return invoke<number>('test_delay', { url })
|
||||
}
|
||||
@@ -397,10 +344,6 @@ export async function invoke_uwp_tool() {
|
||||
)
|
||||
}
|
||||
|
||||
export async function getPortableFlag() {
|
||||
return invoke<boolean>('get_portable_flag')
|
||||
}
|
||||
|
||||
export async function openDevTools() {
|
||||
return invoke('open_devtools')
|
||||
}
|
||||
@@ -518,14 +461,6 @@ export async function listLocalBackup() {
|
||||
return invoke<ILocalBackupFile[]>('list_local_backup')
|
||||
}
|
||||
|
||||
export async function scriptValidateNotice(status: string, msg: string) {
|
||||
return invoke<void>('script_validate_notice', { status, msg })
|
||||
}
|
||||
|
||||
export async function validateScriptFile(filePath: string) {
|
||||
return invoke<ValidationOutcome>('validate_script_file', { filePath })
|
||||
}
|
||||
|
||||
// 获取当前运行模式
|
||||
export const getRunningMode = async () => {
|
||||
return invoke<string>('get_running_mode')
|
||||
@@ -546,16 +481,6 @@ export const uninstallService = async () => {
|
||||
return invoke<void>('uninstall_service')
|
||||
}
|
||||
|
||||
// 重装系统服务
|
||||
export const reinstallService = async () => {
|
||||
return invoke<void>('reinstall_service')
|
||||
}
|
||||
|
||||
// 修复系统服务
|
||||
export const repairService = async () => {
|
||||
return invoke<void>('repair_service')
|
||||
}
|
||||
|
||||
// 系统服务是否可用
|
||||
export const isServiceAvailable = async () => {
|
||||
try {
|
||||
@@ -569,10 +494,6 @@ export const entry_lightweight_mode = async () => {
|
||||
return invoke<void>('entry_lightweight_mode')
|
||||
}
|
||||
|
||||
export const exit_lightweight_mode = async () => {
|
||||
return invoke<void>('exit_lightweight_mode')
|
||||
}
|
||||
|
||||
export const isAdmin = async () => {
|
||||
try {
|
||||
return await invoke<boolean>('app_is_admin')
|
||||
|
||||
@@ -105,14 +105,6 @@ const localeLoaders = Object.entries(localeModules).reduce<
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
export const languages: Record<string, any> = supportedLanguages.reduce(
|
||||
(acc, lang) => {
|
||||
acc[lang] = {}
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, any>,
|
||||
)
|
||||
|
||||
const loadLanguageSections = async (
|
||||
language: string,
|
||||
sections: readonly string[],
|
||||
@@ -144,9 +136,6 @@ const loadLanguageSections = async (
|
||||
}
|
||||
}
|
||||
|
||||
export const loadLanguage = async (language: string) =>
|
||||
loadLanguageSections(language, STARTUP_LANGUAGE_SECTIONS)
|
||||
|
||||
const getLoadedLanguageSections = (language: string) =>
|
||||
Object.keys(i18n.getResourceBundle(language, 'translation') ?? {})
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ReactNode, isValidElement } from 'react'
|
||||
|
||||
type NoticeType = 'success' | 'error' | 'info'
|
||||
|
||||
export interface NoticeTranslationDescriptor {
|
||||
interface NoticeTranslationDescriptor {
|
||||
key: string
|
||||
params?: Record<string, unknown>
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ export const setPreloadConfig = (config: IVergeConfig | null) => {
|
||||
|
||||
export const getPreloadConfig = () => vergeConfigCache
|
||||
|
||||
export const preloadConfig = async () => {
|
||||
const preloadConfig = async () => {
|
||||
try {
|
||||
const config = await getVergeConfig()
|
||||
setPreloadConfig(config)
|
||||
@@ -57,7 +57,7 @@ export const preloadConfig = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
export const preloadLanguage = async (
|
||||
const preloadLanguage = async (
|
||||
vergeConfig?: IVergeConfig | null,
|
||||
loadConfig: () => Promise<IVergeConfig | null> = preloadConfig,
|
||||
) => {
|
||||
|
||||
@@ -31,7 +31,7 @@ type QueryResult<T> = SWRResponse<T> & {
|
||||
|
||||
const serializeQueryKey = (queryKey: QueryKey) => unstable_serialize(queryKey)
|
||||
|
||||
export const queryCache = new Map<string, unknown>()
|
||||
const queryCache = new Map<string, unknown>()
|
||||
|
||||
const setCachedData = <T>(queryKey: QueryKey, data: T | undefined) => {
|
||||
const cacheKey = serializeQueryKey(queryKey)
|
||||
|
||||
+7
-16
@@ -6,7 +6,7 @@ import {
|
||||
|
||||
import { version as appVersion } from '@root/package.json'
|
||||
|
||||
export type VersionParts = {
|
||||
type VersionParts = {
|
||||
main: number[]
|
||||
pre: (number | string)[]
|
||||
}
|
||||
@@ -16,33 +16,27 @@ const SEMVER_FULL_REGEX =
|
||||
const SEMVER_SEARCH_REGEX =
|
||||
/v?\d+(?:\.\d+){1,2}(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?/i
|
||||
|
||||
export const normalizeVersion = (
|
||||
input: string | null | undefined,
|
||||
): string | null => {
|
||||
const normalizeVersion = (input: string | null | undefined): string | null => {
|
||||
if (typeof input !== 'string') return null
|
||||
const trimmed = input.trim()
|
||||
if (!trimmed) return null
|
||||
return trimmed.replace(/^v/i, '')
|
||||
}
|
||||
|
||||
export const ensureSemver = (
|
||||
input: string | null | undefined,
|
||||
): string | null => {
|
||||
const ensureSemver = (input: string | null | undefined): string | null => {
|
||||
const normalized = normalizeVersion(input)
|
||||
if (!normalized) return null
|
||||
return SEMVER_FULL_REGEX.test(normalized) ? normalized : null
|
||||
}
|
||||
|
||||
export const extractSemver = (
|
||||
input: string | null | undefined,
|
||||
): string | null => {
|
||||
const extractSemver = (input: string | null | undefined): string | null => {
|
||||
if (typeof input !== 'string') return null
|
||||
const match = input.match(SEMVER_SEARCH_REGEX)
|
||||
if (!match) return null
|
||||
return normalizeVersion(match[0])
|
||||
}
|
||||
|
||||
export const splitVersion = (version: string | null): VersionParts | null => {
|
||||
const splitVersion = (version: string | null): VersionParts | null => {
|
||||
if (!version) return null
|
||||
const [mainPart, preRelease] = version.split('-')
|
||||
const main = mainPart
|
||||
@@ -93,17 +87,14 @@ const compareVersionParts = (a: VersionParts, b: VersionParts): number => {
|
||||
return 0
|
||||
}
|
||||
|
||||
export const compareVersions = (
|
||||
a: string | null,
|
||||
b: string | null,
|
||||
): number | null => {
|
||||
const compareVersions = (a: string | null, b: string | null): number | null => {
|
||||
const partsA = splitVersion(a)
|
||||
const partsB = splitVersion(b)
|
||||
if (!partsA || !partsB) return null
|
||||
return compareVersionParts(partsA, partsB)
|
||||
}
|
||||
|
||||
export const resolveRemoteVersion = (update: Update): string | null => {
|
||||
const resolveRemoteVersion = (update: Update): string | null => {
|
||||
const primary = ensureSemver(update.version)
|
||||
if (primary) return primary
|
||||
|
||||
|
||||
@@ -572,7 +572,6 @@ export const translationKeys = [
|
||||
'settings.modals.update.title',
|
||||
'settings.modals.update.actions.goToRelease',
|
||||
'settings.modals.update.actions.update',
|
||||
'settings.modals.update.messages.portableError',
|
||||
'settings.modals.update.messages.breakChangeError',
|
||||
'settings.modals.sysproxy.title',
|
||||
'settings.modals.sysproxy.fieldsets.currentStatus',
|
||||
|
||||
@@ -1042,7 +1042,6 @@ export interface TranslationResources {
|
||||
}
|
||||
messages: {
|
||||
breakChangeError: string
|
||||
portableError: string
|
||||
}
|
||||
title: string
|
||||
}
|
||||
|
||||
+2
-11
@@ -4,9 +4,7 @@
|
||||
* - env flag `VITE_ENABLE_DEBUG_LOGS` is truthy (1/true/yes)
|
||||
* - page sets `window.__VERGE_ENABLE_DEBUG_LOGS__ = true`
|
||||
* - localStorage item `VERGE_DEBUG_LOGS` is truthy (1/true/yes)
|
||||
* Use `setDebugLoggingEnabled` to force-enable/disable at runtime.
|
||||
*/
|
||||
let runtimeOverride: boolean | undefined
|
||||
let cachedDebugEnabled: boolean | undefined
|
||||
|
||||
const parseStringFlag = (value: unknown) => {
|
||||
@@ -46,15 +44,8 @@ const computeDebugEnabled = (): boolean => {
|
||||
return false
|
||||
}
|
||||
|
||||
export const setDebugLoggingEnabled = (enabled: boolean) => {
|
||||
runtimeOverride = enabled
|
||||
cachedDebugEnabled = enabled
|
||||
}
|
||||
|
||||
export const isDebugLoggingEnabled = () =>
|
||||
runtimeOverride ??
|
||||
cachedDebugEnabled ??
|
||||
(cachedDebugEnabled = computeDebugEnabled())
|
||||
const isDebugLoggingEnabled = () =>
|
||||
cachedDebugEnabled ?? (cachedDebugEnabled = computeDebugEnabled())
|
||||
|
||||
/**
|
||||
* Logs to the console only when debug logging is enabled.
|
||||
|
||||
@@ -9,7 +9,7 @@ export type CompileStringMatcherResult = {
|
||||
isValid: boolean
|
||||
}
|
||||
|
||||
export const escapeRegex = (value: string) => {
|
||||
const escapeRegex = (value: string) => {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user