feat(settings): add searchable settings navigation

This commit is contained in:
Tunglies
2026-11-06 17:14:15 +08:00
parent 38faf20f90
commit 7fd537af08
47 changed files with 1832 additions and 337 deletions
@@ -133,7 +133,11 @@ export function AutoBackupSettings() {
return (
<Fragment>
<ListItem divider disableGutters>
<ListItem
data-setting-target="settings.modals.backup.auto.scheduleLabel"
divider
disableGutters
>
<Stack
direction="row"
spacing={1}
@@ -152,7 +156,11 @@ export function AutoBackupSettings() {
</Stack>
</ListItem>
<ListItem divider disableGutters>
<ListItem
data-setting-target="settings.modals.backup.auto.intervalLabel"
divider
disableGutters
>
<Stack
direction="row"
spacing={2}
@@ -194,7 +202,11 @@ export function AutoBackupSettings() {
</Stack>
</ListItem>
<ListItem divider disableGutters>
<ListItem
data-setting-target="settings.modals.backup.auto.changeLabel"
divider
disableGutters
>
<Stack
direction="row"
spacing={1}
@@ -173,6 +173,7 @@ export const BackupConfigViewer = memo(
<Grid container spacing={2}>
<Grid size={{ xs: 12 }}>
<TextField
data-setting-target="settings.modals.backup.fields.webdavUrl"
fullWidth
label={t('settings.modals.backup.fields.webdavUrl')}
variant="outlined"
@@ -187,6 +188,7 @@ export const BackupConfigViewer = memo(
</Grid>
<Grid size={{ xs: 6 }}>
<TextField
data-setting-target="settings.modals.backup.fields.username"
label={t('settings.modals.backup.fields.username')}
variant="outlined"
size="small"
@@ -199,6 +201,7 @@ export const BackupConfigViewer = memo(
</Grid>
<Grid size={{ xs: 6 }}>
<TextField
data-setting-target="webdav-password"
label={t('shared.labels.password')}
type="password"
variant="outlined"
@@ -230,6 +233,7 @@ export const BackupConfigViewer = memo(
color={'primary'}
sx={{ height: '100%' }}
type="button"
data-setting-target="webdav-save"
onClick={handleSubmit(save)}
>
{t('shared.actions.save')}
@@ -239,6 +243,7 @@ export const BackupConfigViewer = memo(
<Button
variant="contained"
color="success"
data-setting-target="webdav-backup"
onClick={handleBackup}
type="button"
size="large"
@@ -247,6 +252,7 @@ export const BackupConfigViewer = memo(
</Button>
<Button
variant="outlined"
data-setting-target="webdav-refresh"
onClick={onRefresh}
type="button"
size="large"
@@ -327,7 +327,13 @@ export const BackupHistoryViewer = ({
onCancel={onClose}
onClose={onClose}
>
<Box sx={{ position: 'relative', minHeight: 320 }}>
<Box
data-settings-dialog="backup-history"
data-search-ready={!loading}
data-setting-target="backup-history"
tabIndex={-1}
sx={{ position: 'relative', minHeight: 320 }}
>
<BaseLoadingOverlay isLoading={isBusy} />
<Stack spacing={2}>
<Stack
@@ -412,6 +418,7 @@ export const BackupHistoryViewer = ({
<IconButton
size="small"
disabled={isBusy}
data-setting-target="backup-history-export"
onClick={() => handleExport(row.filename)}
>
<DownloadRounded fontSize="small" />
@@ -420,6 +427,7 @@ export const BackupHistoryViewer = ({
<IconButton
size="small"
disabled={isBusy}
data-setting-target="backup-history-delete"
onClick={() => handleDelete(row.filename)}
>
<DeleteOutlined fontSize="small" />
@@ -427,6 +435,7 @@ export const BackupHistoryViewer = ({
<IconButton
size="small"
disabled={isBusy}
data-setting-target="backup-history-restore"
onClick={() => handleRestore(row.filename)}
>
<RestoreRounded fontSize="small" />
+36 -4
View File
@@ -9,7 +9,7 @@ import {
import { open as openDialog } from '@tauri-apps/plugin-dialog'
import { useLockFn } from 'ahooks'
import type { ReactNode, Ref } from 'react'
import { useCallback, useImperativeHandle, useState } from 'react'
import { useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { BaseDialog, DialogRef } from '@/components/base'
@@ -23,6 +23,8 @@ import { mutate } from '@/services/mutate'
import { errorDetail, showNotice } from '@/services/notice-service'
import { buildWebdavSignature, setWebdavStatus } from '@/services/webdav-status'
import { useSettingDialog } from '../settings-search-context'
import { AutoBackupSettings } from './auto-backup-settings'
import { BackupHistoryViewer } from './backup-history-viewer'
import { BackupWebdavDialog } from './backup-webdav-dialog'
@@ -39,9 +41,18 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
const [historySource, setHistorySource] = useState<BackupSource>('local')
const [historyPage, setHistoryPage] = useState(0)
const [webdavDialogOpen, setWebdavDialogOpen] = useState(false)
const searchParentRef = useRef<'webdav' | 'history' | null>(null)
const closeSearchDetail = (detail: 'webdav' | 'history') => {
if (detail === 'webdav') setWebdavDialogOpen(false)
else setHistoryOpen(false)
if (searchParentRef.current === detail) {
searchParentRef.current = null
setOpen(false)
}
}
const webdavSignature = buildWebdavSignature(verge)
useImperativeHandle(ref, () => ({
useSettingDialog(ref, 'backup-viewer', open, () => ({
open: () => setOpen(true),
close: () => setOpen(false),
}))
@@ -52,6 +63,23 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
setHistoryOpen(true)
}
useSettingDialog(undefined, 'backup-webdav', webdavDialogOpen, () => ({
open: () => {
if (!open) searchParentRef.current = 'webdav'
setOpen(true)
setWebdavDialogOpen(true)
},
close: () => setWebdavDialogOpen(false),
}))
useSettingDialog(undefined, 'backup-history', historyOpen, () => ({
open: () => {
if (!open) searchParentRef.current = 'history'
setOpen(true)
openHistory('local')
},
close: () => setHistoryOpen(false),
}))
const handleBackup = useLockFn(async (target: BackupSource) => {
try {
setBusyAction(target)
@@ -129,6 +157,7 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
onCancel={() => setOpen(false)}
onClose={() => setOpen(false)}
>
<span data-settings-dialog="backup-viewer" />
<Stack spacing={2}>
<Stack
spacing={1}
@@ -166,6 +195,7 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
description: t('settings.modals.backup.manual.local'),
actions: [
<Button
data-setting-target="settings.modals.backup.actions.backup"
key="backup"
variant="contained"
size="small"
@@ -176,6 +206,7 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
{t('settings.modals.backup.actions.backup')}
</Button>,
<Button
data-setting-target="settings.modals.backup.actions.viewHistory"
key="history"
variant="outlined"
size="small"
@@ -185,6 +216,7 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
{t('settings.modals.backup.actions.viewHistory')}
</Button>,
<Button
data-setting-target="settings.modals.backup.actions.importBackup"
key="import"
variant="text"
size="small"
@@ -263,11 +295,11 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
page={historyPage}
onSourceChange={setHistorySource}
onPageChange={setHistoryPage}
onClose={() => setHistoryOpen(false)}
onClose={() => closeSearchDetail('history')}
/>
<BackupWebdavDialog
open={webdavDialogOpen}
onClose={() => setWebdavDialogOpen(false)}
onClose={() => closeSearchDetail('webdav')}
onBackupSuccess={() => openHistory('webdav')}
setBusy={setWebdavBusy}
/>
@@ -76,7 +76,13 @@ export const BackupWebdavDialog = ({
onCancel={onClose}
onClose={onClose}
>
<Box sx={{ position: 'relative' }}>
<Box
data-settings-dialog="backup-webdav"
data-search-ready={!loading}
data-setting-target="webdav-state"
tabIndex={-1}
sx={{ position: 'relative' }}
>
<BaseLoadingOverlay isLoading={loading} />
<BackupConfigViewer
setLoading={handleLoading}
@@ -13,7 +13,7 @@ import {
} from '@mui/material'
import { useLockFn } from 'ahooks'
import type { Ref } from 'react'
import { useImperativeHandle, useState } from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { closeAllConnections } from 'tauri-plugin-mihomo-api'
@@ -24,6 +24,8 @@ import { changeClashCore, restartCore, upgradeClashCore } from '@/services/cmds'
import { mutate } from '@/services/mutate'
import { showNotice } from '@/services/notice-service'
import { useSettingDialog } from '../settings-search-context'
const VALID_CORE = [
{
name: 'Mihomo',
@@ -49,7 +51,7 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
const [restarting, setRestarting] = useState(false)
const [changingCore, setChangingCore] = useState<string | null>(null)
useImperativeHandle(ref, () => ({
useSettingDialog(ref, 'clash-core-viewer', open, () => ({
open: () => setOpen(true),
close: () => setOpen(false),
}))
@@ -139,6 +141,7 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
loading={upgrading}
disabled={restarting || changingCore !== null}
sx={{ marginRight: '8px' }}
data-setting-target="core-upgrade"
onClick={onUpgrade}
>
{t('shared.actions.upgrade')}
@@ -150,6 +153,7 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
loadingPosition="start"
loading={restarting}
disabled={upgrading}
data-setting-target="core-restart"
onClick={onRestart}
>
{t('shared.actions.restart')}
@@ -170,7 +174,12 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
>
<List component="nav">
<List
component="nav"
data-settings-dialog="clash-core-viewer"
data-setting-target="core-variants"
tabIndex={-1}
>
{VALID_CORE.map((each) => (
<ListItemButton
key={each.core}
@@ -9,7 +9,7 @@ import {
TextField,
} from '@mui/material'
import { useLockFn, useRequest } from 'ahooks'
import { forwardRef, useImperativeHandle, useState } from 'react'
import { forwardRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { BaseDialog, Switch } from '@/components/base'
@@ -22,6 +22,8 @@ import { showNotice } from '@/services/notice-service'
import type { TranslationKey } from '@/types/generated/i18n-keys'
import getSystem from '@/utils/get-system'
import { useSettingDialog } from '../settings-search-context'
const OS = getSystem()
interface ClashPortViewerRef {
@@ -87,6 +89,7 @@ const readPortSettings = (
})
interface PortRowProps {
searchTarget: string
label: string
secondary?: string
port: number
@@ -97,6 +100,7 @@ interface PortRowProps {
}
const PortRow = ({
searchTarget,
label,
secondary,
port,
@@ -107,7 +111,10 @@ const PortRow = ({
const { t } = useTranslation()
return (
<ListItem sx={{ padding: '4px 0', minHeight: 36 }}>
<ListItem
data-setting-target={searchTarget}
sx={{ padding: '4px 0', minHeight: 36 }}
>
<ListItemText
primary={label}
secondary={secondary}
@@ -129,6 +136,7 @@ const PortRow = ({
/>
<IconButton
size="small"
data-setting-target="port-random"
onClick={() => onPortChange(generateRandomPort())}
title={t('settings.modals.clashPort.actions.random')}
disabled={!enabled}
@@ -192,7 +200,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
},
)
useImperativeHandle(ref, () => ({
useSettingDialog(ref, 'clash-port-viewer', open, () => ({
open: () => {
setPorts(readPortSettings(verge, configuredMixedPort))
setOpen(true)
@@ -245,9 +253,11 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
onCancel={() => setOpen(false)}
onOk={onSave}
>
<span data-settings-dialog="clash-port-viewer" />
<List sx={{ width: '100%' }}>
<PortRow
label={t('settings.modals.clashPort.fields.mixed')}
searchTarget="settings.modals.clashPort.fields.mixed"
secondary={
displayedMixedPort !== configuredMixedPort
? t('settings.modals.clashPort.messages.runningPort', {
@@ -265,6 +275,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
<PortRow
key={key}
label={t(label)}
searchTarget={label}
port={ports[key].port}
enabled={ports[key].enabled}
onPortChange={(port) => updateListener(key, { port })}
@@ -12,7 +12,7 @@ import {
Tooltip,
} from '@mui/material'
import { useLockFn } from 'ahooks'
import { useImperativeHandle, useState, type Ref } from 'react'
import { useState, type Ref } from 'react'
import { useTranslation } from 'react-i18next'
import { BaseDialog, DialogRef, Switch } from '@/components/base'
@@ -20,6 +20,8 @@ import { useClashInfo } from '@/hooks/use-clash'
import { useVerge } from '@/hooks/use-verge'
import { showNotice } from '@/services/notice-service'
import { useSettingDialog } from '../settings-search-context'
export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
@@ -35,7 +37,7 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
)
// 对话框打开时初始化配置
useImperativeHandle(ref, () => ({
useSettingDialog(ref, 'controller-viewer', open, () => ({
open: async () => {
setOpen(true)
setController(clashInfo?.server || '')
@@ -124,8 +126,10 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
onCancel={() => setOpen(false)}
onOk={onSave}
>
<span data-settings-dialog="controller-viewer" />
<List>
<ListItem
data-setting-target="settings.sections.externalController.fields.enable"
sx={{
padding: '5px 2px',
display: 'flex',
@@ -144,6 +148,7 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
</ListItem>
<ListItem
data-setting-target="settings.sections.externalController.fields.address"
sx={{
padding: '5px 2px',
display: 'flex',
@@ -184,6 +189,7 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
</ListItem>
<ListItem
data-setting-target="settings.sections.externalController.fields.secret"
sx={{
padding: '5px 2px',
display: 'flex',
+79 -43
View File
@@ -16,14 +16,7 @@ import {
import { useLockFn } from 'ahooks'
import * as yaml from 'js-yaml'
import type { Ref } from 'react'
import {
useCallback,
useEffect,
useImperativeHandle,
useReducer,
useRef,
useState,
} from 'react'
import { useCallback, useEffect, useReducer, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
@@ -47,6 +40,8 @@ import { useThemeMode } from '@/services/states'
import type { MonacoEditorInstance } from '@/types/monaco'
import { MONACO_FONT_FAMILY } from '@/utils/font-family'
import { useSettingDialog } from '../settings-search-context'
const Item = styled(ListItem)(() => ({
padding: '5px 2px',
'& textarea': {
@@ -307,6 +302,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
const themeMode = useThemeMode()
const [open, setOpen] = useState(false)
const [initializing, setInitializing] = useState(false)
const [visualization, setVisualization] = useState(true)
const editorRef = useRef<MonacoEditorInstance | null>(null)
const [values, setValues] = useState<DnsFormValues>(DEFAULT_FORM_VALUES)
@@ -419,6 +415,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
}, [])
const initDnsConfig = useCallback(async () => {
setInitializing(true)
try {
const dnsConfig = await getDnsConfigContent()
@@ -433,20 +430,18 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
} catch (err) {
console.error('Failed to initialize DNS config', err)
resetToDefaults()
} finally {
setInitializing(false)
}
}, [resetToDefaults, updateValuesFromConfig])
useImperativeHandle(
ref,
() => ({
open: () => {
setOpen(true)
void initDnsConfig()
},
close: () => setOpen(false),
}),
[initDnsConfig],
)
useSettingDialog(ref, 'dns-viewer', open, () => ({
open: () => {
setOpen(true)
void initDnsConfig()
},
close: () => setOpen(false),
}))
const onSave = useLockFn(async () => {
try {
@@ -556,6 +551,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
{t('settings.modals.dns.dialog.title')}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Button
data-setting-target="dns-viewer-reset"
variant="outlined"
size="small"
color="warning"
@@ -567,6 +563,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
<Button
variant="contained"
size="small"
data-setting-target="dns-mode"
onClick={() => {
if (visualization) {
setYamlContent(buildDnsYaml(values))
@@ -596,6 +593,15 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
onCancel={() => setOpen(false)}
onOk={onSave}
>
<span
data-settings-dialog="dns-viewer"
data-search-ready={!initializing}
/>
{initializing && (
<Typography role="status" data-settings-loading tabIndex={-1}>
{t('shared.statuses.loading')}
</Typography>
)}
<Alert severity="info" sx={{ mb: 2 }}>
{t('settings.modals.dns.dialog.profileScope')}
</Alert>
@@ -616,7 +622,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
{t('settings.modals.dns.sections.general')}
</Typography>
<Item>
<Item data-setting-target="settings.modals.dns.fields.enable">
<ListItemText primary={t('settings.modals.dns.fields.enable')} />
<Switch
edge="end"
@@ -625,7 +631,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
/>
</Item>
<Item>
<Item data-setting-target="settings.modals.dns.fields.listen">
<ListItemText primary={t('settings.modals.dns.fields.listen')} />
<TextField
size="small"
@@ -638,7 +644,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
/>
</Item>
<Item>
<Item data-setting-target="settings.modals.dns.fields.enhancedMode">
<ListItemText
primary={t('settings.modals.dns.fields.enhancedMode')}
/>
@@ -653,7 +659,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
</FormControl>
</Item>
<Item>
<Item data-setting-target="settings.modals.dns.fields.fakeIpRange">
<ListItemText
primary={t('settings.modals.dns.fields.fakeIpRange')}
/>
@@ -668,7 +674,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
/>
</Item>
<Item>
<Item data-setting-target="settings.modals.dns.fields.fakeIpRange6">
<ListItemText
primary={t('settings.modals.dns.fields.fakeIpRange6')}
/>
@@ -683,7 +689,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
/>
</Item>
<Item>
<Item data-setting-target="settings.modals.dns.fields.fakeIpFilterMode">
<ListItemText
primary={t('settings.modals.dns.fields.fakeIpFilterMode')}
/>
@@ -698,7 +704,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
</FormControl>
</Item>
<Item>
<Item data-setting-target="settings.modals.dns.fields.ipv6.label">
<ListItemText
primary={t('settings.modals.dns.fields.ipv6.label')}
secondary={t('settings.modals.dns.fields.ipv6.description')}
@@ -710,7 +716,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
/>
</Item>
<Item>
<Item data-setting-target="settings.modals.dns.fields.preferH3.label">
<ListItemText
primary={t('settings.modals.dns.fields.preferH3.label')}
secondary={t('settings.modals.dns.fields.preferH3.description')}
@@ -722,7 +728,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
/>
</Item>
<Item>
<Item data-setting-target="settings.modals.dns.fields.respectRules.label">
<ListItemText
primary={t('settings.modals.dns.fields.respectRules.label')}
secondary={t(
@@ -736,7 +742,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
/>
</Item>
<Item>
<Item data-setting-target="settings.modals.dns.fields.useHosts.label">
<ListItemText
primary={t('settings.modals.dns.fields.useHosts.label')}
secondary={t('settings.modals.dns.fields.useHosts.description')}
@@ -748,7 +754,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
/>
</Item>
<Item>
<Item data-setting-target="settings.modals.dns.fields.useSystemHosts.label">
<ListItemText
primary={t('settings.modals.dns.fields.useSystemHosts.label')}
secondary={t(
@@ -762,7 +768,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
/>
</Item>
<Item>
<Item data-setting-target="settings.modals.dns.fields.directPolicy.label">
<ListItemText
primary={t('settings.modals.dns.fields.directPolicy.label')}
secondary={t(
@@ -776,7 +782,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
/>
</Item>
<Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
<Item
data-setting-target="settings.modals.dns.fields.defaultNameserver.label"
sx={{ flexDirection: 'column', alignItems: 'flex-start' }}
>
<ListItemText
primary={t('settings.modals.dns.fields.defaultNameserver.label')}
secondary={t(
@@ -796,7 +805,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
/>
</Item>
<Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
<Item
data-setting-target="settings.modals.dns.fields.nameserver.label"
sx={{ flexDirection: 'column', alignItems: 'flex-start' }}
>
<ListItemText
primary={t('settings.modals.dns.fields.nameserver.label')}
secondary={t('settings.modals.dns.fields.nameserver.description')}
@@ -814,7 +826,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
/>
</Item>
<Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
<Item
data-setting-target="settings.modals.dns.fields.fallback.label"
sx={{ flexDirection: 'column', alignItems: 'flex-start' }}
>
<ListItemText
primary={t('settings.modals.dns.fields.fallback.label')}
secondary={t('settings.modals.dns.fields.fallback.description')}
@@ -832,7 +847,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
/>
</Item>
<Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
<Item
data-setting-target="settings.modals.dns.fields.proxy.label"
sx={{ flexDirection: 'column', alignItems: 'flex-start' }}
>
<ListItemText
primary={t('settings.modals.dns.fields.proxy.label')}
secondary={t('settings.modals.dns.fields.proxy.description')}
@@ -850,7 +868,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
/>
</Item>
<Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
<Item
data-setting-target="settings.modals.dns.fields.directNameserver.label"
sx={{ flexDirection: 'column', alignItems: 'flex-start' }}
>
<ListItemText
primary={t('settings.modals.dns.fields.directNameserver.label')}
secondary={t(
@@ -870,7 +891,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
/>
</Item>
<Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
<Item
data-setting-target="settings.modals.dns.fields.fakeIpFilter.label"
sx={{ flexDirection: 'column', alignItems: 'flex-start' }}
>
<ListItemText
primary={t('settings.modals.dns.fields.fakeIpFilter.label')}
secondary={t(
@@ -890,7 +914,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
/>
</Item>
<Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
<Item
data-setting-target="settings.modals.dns.fields.nameserverPolicy.label"
sx={{ flexDirection: 'column', alignItems: 'flex-start' }}
>
<ListItemText
primary={t('settings.modals.dns.fields.nameserverPolicy.label')}
secondary={t(
@@ -917,7 +944,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
{t('settings.modals.dns.sections.fallbackFilter')}
</Typography>
<Item>
<Item data-setting-target="settings.modals.dns.fields.geoipFiltering.label">
<ListItemText
primary={t('settings.modals.dns.fields.geoipFiltering.label')}
secondary={t(
@@ -931,7 +958,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
/>
</Item>
<Item>
<Item data-setting-target="settings.modals.dns.fields.geoipCode">
<ListItemText primary={t('settings.modals.dns.fields.geoipCode')} />
<TextField
size="small"
@@ -944,7 +971,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
/>
</Item>
<Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
<Item
data-setting-target="settings.modals.dns.fields.fallbackIpCidr.label"
sx={{ flexDirection: 'column', alignItems: 'flex-start' }}
>
<ListItemText
primary={t('settings.modals.dns.fields.fallbackIpCidr.label')}
secondary={t(
@@ -964,7 +994,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
/>
</Item>
<Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
<Item
data-setting-target="settings.modals.dns.fields.fallbackDomain.label"
sx={{ flexDirection: 'column', alignItems: 'flex-start' }}
>
<ListItemText
primary={t('settings.modals.dns.fields.fallbackDomain.label')}
secondary={t(
@@ -991,7 +1024,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
{t('settings.modals.dns.sections.hosts')}
</Typography>
<Item sx={{ flexDirection: 'column', alignItems: 'flex-start' }}>
<Item
data-setting-target="settings.modals.dns.fields.hosts.label"
sx={{ flexDirection: 'column', alignItems: 'flex-start' }}
>
<ListItemText
primary={t('settings.modals.dns.fields.hosts.label')}
secondary={t('settings.modals.dns.fields.hosts.description')}
@@ -1,7 +1,7 @@
import { Delete as DeleteIcon } from '@mui/icons-material'
import { Box, Button, Divider, List, ListItem, TextField } from '@mui/material'
import { useLockFn, useRequest } from 'ahooks'
import { forwardRef, useImperativeHandle, useRef, useState } from 'react'
import { forwardRef, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { BaseDialog, Switch } from '@/components/base'
@@ -10,6 +10,8 @@ import { restartCore } from '@/services/cmds'
import { mutate } from '@/services/mutate'
import { showNotice } from '@/services/notice-service'
import { useSettingDialog } from '../settings-search-context'
// Development origins must never be persisted into production configuration.
const DEV_URLS = [
'tauri://localhost',
@@ -154,7 +156,7 @@ export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
},
)
useImperativeHandle(ref, () => ({
useSettingDialog(ref, 'external-controller-cors', open, () => ({
open: () => {
const cors = clash?.['external-controller-cors']
const origins = cors?.['allow-origins'] ?? []
@@ -186,8 +188,12 @@ export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
onCancel={() => setOpen(false)}
onOk={handleSave}
>
<span data-settings-dialog="external-controller-cors" />
<List sx={{ width: '90%', padding: 2 }}>
<ListItem sx={{ padding: '8px 0' }}>
<ListItem
data-setting-target="settings.sections.externalCors.fields.allowPrivateNetwork"
sx={{ padding: '8px 0' }}
>
<Box
sx={{
display: 'flex',
@@ -214,7 +220,10 @@ export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
<Divider sx={{ my: 2 }} />
<ListItem sx={{ padding: '8px 0' }}>
<ListItem
data-setting-target="settings.sections.externalCors.fields.allowedOrigins"
sx={{ padding: '8px 0' }}
>
<div style={{ width: '100%' }}>
<div style={{ marginBottom: 8, fontWeight: 'bold' }}>
{t('settings.sections.externalCors.fields.allowedOrigins')}
@@ -252,6 +261,7 @@ export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
</div>
))}
<Button
data-setting-target="settings.sections.externalCors.actions.add"
variant="contained"
size="small"
onClick={handleAddOrigin}
+10 -4
View File
@@ -1,12 +1,14 @@
import { styled, Typography } from '@mui/material'
import { useLockFn } from 'ahooks'
import { forwardRef, useImperativeHandle, useState } from 'react'
import { forwardRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { BaseDialog, DialogRef, Switch } from '@/components/base'
import { useVerge } from '@/hooks/use-verge'
import { showNotice } from '@/services/notice-service'
import { useSettingDialog } from '../settings-search-context'
import { HotkeyInput } from './hotkey-input'
const ItemWrapper = styled('div')`
@@ -51,7 +53,7 @@ export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
verge?.enable_global_hotkey ?? true,
)
useImperativeHandle(ref, () => ({
useSettingDialog(ref, 'hotkey-viewer', open, () => ({
open: () => {
setOpen(true)
@@ -111,7 +113,11 @@ export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
onCancel={() => setOpen(false)}
onOk={onSave}
>
<ItemWrapper style={{ marginBottom: 16 }}>
<span data-settings-dialog="hotkey-viewer" />
<ItemWrapper
data-setting-target="settings.modals.hotkey.toggles.enableGlobal"
style={{ marginBottom: 16 }}
>
<Typography>
{t('settings.modals.hotkey.toggles.enableGlobal')}
</Typography>
@@ -123,7 +129,7 @@ export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
</ItemWrapper>
{HOTKEY_FUNC.map((func) => (
<ItemWrapper key={func}>
<ItemWrapper key={func} data-setting-target={HOTKEY_FUNC_LABELS[func]}>
<Typography>{t(HOTKEY_FUNC_LABELS[func])}</Typography>
<HotkeyInput
value={hotkeyMap[func] ?? []}
+23 -19
View File
@@ -14,7 +14,7 @@ import { convertFileSrc } from '@tauri-apps/api/core'
import { join } from '@tauri-apps/api/path'
import { open as openDialog } from '@tauri-apps/plugin-dialog'
import { exists } from '@tauri-apps/plugin-fs'
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'
import { forwardRef, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { BaseDialog, DialogRef, Switch, TooltipIcon } from '@/components/base'
@@ -26,6 +26,8 @@ import { mutate } from '@/services/mutate'
import { showNotice } from '@/services/notice-service'
import getSystem from '@/utils/get-system'
import { useSettingDialog } from '../settings-search-context'
import { GuardState } from './guard-state'
const OS = getSystem()
@@ -97,7 +99,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
}
}
useImperativeHandle(ref, () => ({
useSettingDialog(ref, 'layout-viewer', open, () => ({
open: () => setOpen(true),
close: () => setOpen(false),
}))
@@ -120,8 +122,9 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
>
<span data-settings-dialog="layout-viewer" />
<List>
<Item>
<Item data-setting-target="settings.components.verge.layout.fields.preferSystemTitlebar">
<ListItemText
primary={t(
'settings.components.verge.layout.fields.preferSystemTitlebar',
@@ -140,7 +143,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
</GuardState>
</Item>
<Item>
<Item data-setting-target="settings.components.verge.layout.fields.trafficGraph">
<ListItemText
primary={t('settings.components.verge.layout.fields.trafficGraph')}
/>
@@ -156,7 +159,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
</GuardState>
</Item>
<Item>
<Item data-setting-target="settings.components.verge.layout.fields.memoryUsage">
<ListItemText
primary={t('settings.components.verge.layout.fields.memoryUsage')}
/>
@@ -172,7 +175,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
</GuardState>
</Item>
<Item>
<Item data-setting-target="settings.components.verge.layout.fields.proxyGroupIcon">
<ListItemText
primary={t(
'settings.components.verge.layout.fields.proxyGroupIcon',
@@ -190,7 +193,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
</GuardState>
</Item>
<Item>
<Item data-setting-target="settings.components.verge.layout.fields.pauseRenderTrafficStatsOnBlur">
<ListItemText
primary={t(
'settings.components.verge.layout.fields.pauseRenderTrafficStatsOnBlur',
@@ -212,7 +215,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
</GuardState>
</Item>
<Item>
<Item data-setting-target="settings.components.verge.layout.fields.toastPosition">
<ListItemText
primary={t('settings.components.verge.layout.fields.toastPosition')}
/>
@@ -248,7 +251,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
</GuardState>
</Item>
<Item>
<Item data-setting-target="settings.components.verge.layout.fields.hoverNavigator">
<ListItemText
primary={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
@@ -276,7 +279,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
</GuardState>
</Item>
<Item>
<Item data-setting-target="settings.components.verge.layout.fields.hoverNavigatorDelay">
<ListItemText
primary={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
@@ -335,7 +338,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
</GuardState>
</Item>
<Item>
<Item data-setting-target="settings.components.verge.layout.fields.navIcon">
<ListItemText
primary={t('settings.components.verge.layout.fields.navIcon')}
/>
@@ -360,7 +363,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
</GuardState>
</Item>
<Item>
<Item data-setting-target="settings.components.verge.layout.fields.collapseNavBar">
<ListItemText
primary={t(
'settings.components.verge.layout.fields.collapseNavBar',
@@ -379,7 +382,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
</Item>
{OS === 'macos' && (
<Item>
<Item data-setting-target="settings.components.verge.layout.fields.trayIcon">
<ListItemText
primary={t('settings.components.verge.layout.fields.trayIcon')}
/>
@@ -407,7 +410,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
</Item>
)}
{OS === 'macos' && (
<Item>
<Item data-setting-target="settings.components.verge.layout.fields.enableTraySpeed">
<ListItemText
primary={t(
'settings.components.verge.layout.fields.enableTraySpeed',
@@ -445,7 +448,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
</GuardState>
</Item>
)} */}
<Item>
<Item data-setting-target="settings.components.verge.layout.fields.proxyGroupsDisplayMode">
<ListItemText
primary={t(
'settings.components.verge.layout.fields.proxyGroupsDisplayMode',
@@ -481,7 +484,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
</Select>
</GuardState>
</Item>
<Item>
<Item data-setting-target="settings.components.verge.layout.fields.showOutboundModesInline">
<ListItemText
primary={t(
'settings.components.verge.layout.fields.showOutboundModesInline',
@@ -499,7 +502,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
</GuardState>
</Item>
<Item>
<Item data-setting-target="settings.components.verge.layout.fields.commonTrayIcon">
<ListItemText
primary={t(
'settings.components.verge.layout.fields.commonTrayIcon',
@@ -512,6 +515,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
onGuard={(e) => patchVerge({ common_tray_icon: e })}
>
<Button
data-setting-target="settings.components.verge.basic.actions.browse"
variant="outlined"
size="small"
startIcon={
@@ -554,7 +558,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
</GuardState>
</Item>
<Item>
<Item data-setting-target="settings.components.verge.layout.fields.systemProxyTrayIcon">
<ListItemText
primary={t(
'settings.components.verge.layout.fields.systemProxyTrayIcon',
@@ -609,7 +613,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
</GuardState>
</Item>
<Item>
<Item data-setting-target="settings.components.verge.layout.fields.tunTrayIcon">
<ListItemText
primary={t('settings.components.verge.layout.fields.tunTrayIcon')}
/>
@@ -8,7 +8,7 @@ import {
} from '@mui/material'
import { useLockFn } from 'ahooks'
import type { Ref } from 'react'
import { useImperativeHandle, useState } from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { BaseDialog, DialogRef, Switch, TooltipIcon } from '@/components/base'
@@ -17,6 +17,8 @@ import { entry_lightweight_mode } from '@/services/cmds'
import { mutate } from '@/services/mutate'
import { showNotice } from '@/services/notice-service'
import { useSettingDialog } from '../settings-search-context'
export function LiteModeViewer({ ref }: { ref?: Ref<DialogRef> }) {
const { t } = useTranslation()
const { verge, patchVerge } = useVerge()
@@ -27,7 +29,7 @@ export function LiteModeViewer({ ref }: { ref?: Ref<DialogRef> }) {
autoEnterLiteModeDelay: 10, // 默认10分钟
})
useImperativeHandle(ref, () => ({
useSettingDialog(ref, 'lite-mode-viewer', open, () => ({
open: () => {
setOpen(true)
setValues({
@@ -61,8 +63,12 @@ export function LiteModeViewer({ ref }: { ref?: Ref<DialogRef> }) {
onCancel={() => setOpen(false)}
onOk={onSave}
>
<span data-settings-dialog="lite-mode-viewer" />
<List>
<ListItem sx={{ padding: '5px 2px' }}>
<ListItem
data-setting-target="settings.modals.liteMode.actions.enterNow"
sx={{ padding: '5px 2px' }}
>
<ListItemText
primary={t('settings.modals.liteMode.actions.enterNow')}
/>
@@ -84,7 +90,10 @@ export function LiteModeViewer({ ref }: { ref?: Ref<DialogRef> }) {
</Typography>
</ListItem>
<ListItem sx={{ padding: '5px 2px' }}>
<ListItem
data-setting-target="settings.modals.liteMode.toggles.autoEnter"
sx={{ padding: '5px 2px' }}
>
<ListItemText
primary={t('settings.modals.liteMode.toggles.autoEnter')}
sx={{ maxWidth: 'fit-content' }}
@@ -105,7 +114,10 @@ export function LiteModeViewer({ ref }: { ref?: Ref<DialogRef> }) {
{values.autoEnterLiteMode && (
<>
<ListItem sx={{ padding: '5px 2px' }}>
<ListItem
data-setting-target="settings.modals.liteMode.fields.delay"
sx={{ padding: '5px 2px' }}
>
<ListItemText
primary={t('settings.modals.liteMode.fields.delay')}
/>
+53 -14
View File
@@ -8,13 +8,15 @@ import {
TextField,
} from '@mui/material'
import { useLockFn } from 'ahooks'
import { forwardRef, useImperativeHandle, useState } from 'react'
import { forwardRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { BaseDialog, DialogRef, Switch, TooltipIcon } from '@/components/base'
import { useVerge } from '@/hooks/use-verge'
import { showNotice } from '@/services/notice-service'
import { useSettingDialog } from '../settings-search-context'
const buildInitialValues = (verge?: IVergeConfig | null) => ({
appLogLevel: verge?.app_log_level ?? 'warn',
appLogMaxSize: verge?.app_log_max_size ?? 128,
@@ -38,7 +40,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
const [open, setOpen] = useState(false)
const [values, setValues] = useState(() => buildInitialValues(null))
useImperativeHandle(ref, () => ({
useSettingDialog(ref, 'misc-viewer', open, () => ({
open: () => {
setOpen(true)
setValues(buildInitialValues(verge))
@@ -80,8 +82,12 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
onCancel={() => setOpen(false)}
onOk={onSave}
>
<span data-settings-dialog="misc-viewer" />
<List>
<ListItem sx={{ padding: '5px 2px' }}>
<ListItem
data-setting-target="settings.modals.misc.fields.appLogLevel"
sx={{ padding: '5px 2px' }}
>
<ListItemText
primary={t('settings.modals.misc.fields.appLogLevel')}
/>
@@ -104,7 +110,10 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
</Select>
</ListItem>
<ListItem sx={{ padding: '5px 2px' }}>
<ListItem
data-setting-target="settings.modals.misc.fields.appLogMaxSize"
sx={{ padding: '5px 2px' }}
>
<ListItemText
primary={t('settings.modals.misc.fields.appLogMaxSize')}
sx={{ maxWidth: 'fit-content' }}
@@ -136,7 +145,10 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
/>
</ListItem>
<ListItem sx={{ padding: '5px 2px' }}>
<ListItem
data-setting-target="settings.modals.misc.fields.appLogMaxCount"
sx={{ padding: '5px 2px' }}
>
<ListItemText
primary={t('settings.modals.misc.fields.appLogMaxCount')}
sx={{ maxWidth: 'fit-content' }}
@@ -168,7 +180,10 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
/>
</ListItem>
<ListItem sx={{ padding: '5px 2px' }}>
<ListItem
data-setting-target="settings.modals.misc.fields.autoCloseConnections"
sx={{ padding: '5px 2px' }}
>
<ListItemText
primary={t('settings.modals.misc.fields.autoCloseConnections')}
sx={{ maxWidth: 'fit-content' }}
@@ -187,7 +202,10 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
/>
</ListItem>
<ListItem sx={{ padding: '5px 2px' }}>
<ListItem
data-setting-target="settings.modals.misc.fields.autoCheckUpdate"
sx={{ padding: '5px 2px' }}
>
<ListItemText
primary={t('settings.modals.misc.fields.autoCheckUpdate')}
/>
@@ -200,7 +218,10 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
/>
</ListItem>
<ListItem sx={{ padding: '5px 2px' }}>
<ListItem
data-setting-target="settings.modals.misc.fields.enableBuiltinEnhanced"
sx={{ padding: '5px 2px' }}
>
<ListItemText
primary={t('settings.modals.misc.fields.enableBuiltinEnhanced')}
sx={{ maxWidth: 'fit-content' }}
@@ -219,7 +240,10 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
/>
</ListItem>
<ListItem sx={{ padding: '5px 2px' }}>
<ListItem
data-setting-target="settings.modals.misc.fields.proxyLayoutColumns"
sx={{ padding: '5px 2px' }}
>
<ListItemText
primary={t('settings.modals.misc.fields.proxyLayoutColumns')}
/>
@@ -245,7 +269,10 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
</Select>
</ListItem>
<ListItem sx={{ padding: '5px 2px' }}>
<ListItem
data-setting-target="settings.modals.misc.fields.autoLogClean"
sx={{ padding: '5px 2px' }}
>
<ListItemText
primary={t('settings.modals.misc.fields.autoLogClean')}
/>
@@ -298,7 +325,10 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
</Select>
</ListItem>
<ListItem sx={{ padding: '5px 2px' }}>
<ListItem
data-setting-target="settings.modals.misc.fields.autoDelayDetection"
sx={{ padding: '5px 2px' }}
>
<ListItemText
primary={t('settings.modals.misc.fields.autoDelayDetection')}
sx={{ maxWidth: 'fit-content' }}
@@ -317,7 +347,10 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
/>
</ListItem>
<ListItem sx={{ padding: '5px 2px' }}>
<ListItem
data-setting-target="settings.modals.misc.fields.autoDelayDetectionInterval"
sx={{ padding: '5px 2px' }}
>
<ListItemText
primary={t(
'settings.modals.misc.fields.autoDelayDetectionInterval',
@@ -355,7 +388,10 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
/>
</ListItem>
<ListItem sx={{ padding: '5px 2px' }}>
<ListItem
data-setting-target="settings.modals.misc.fields.defaultLatencyTest"
sx={{ padding: '5px 2px' }}
>
<ListItemText
primary={t('settings.modals.misc.fields.defaultLatencyTest')}
sx={{ maxWidth: 'fit-content' }}
@@ -379,7 +415,10 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
/>
</ListItem>
<ListItem sx={{ padding: '5px 2px' }}>
<ListItem
data-setting-target="settings.modals.misc.fields.defaultLatencyTimeout"
sx={{ padding: '5px 2px' }}
>
<ListItemText
primary={t('settings.modals.misc.fields.defaultLatencyTimeout')}
/>
@@ -2,19 +2,21 @@ import { ContentCopyRounded } from '@mui/icons-material'
import { alpha, Box, Button, CircularProgress, IconButton } from '@mui/material'
import { writeText } from '@tauri-apps/plugin-clipboard-manager'
import type { Ref } from 'react'
import { useImperativeHandle, useState } from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { BaseDialog, BaseEmpty, DialogRef } from '@/components/base'
import { useNetworkInterfaces } from '@/hooks/use-network'
import { showNotice } from '@/services/notice-service'
import { useSettingDialog } from '../settings-search-context'
export function NetworkInterfaceViewer({ ref }: { ref?: Ref<DialogRef> }) {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const [isV4, setIsV4] = useState(true)
useImperativeHandle(ref, () => ({
useSettingDialog(ref, 'network-interface-viewer', open, () => ({
open: () => {
setOpen(true)
},
@@ -51,41 +53,49 @@ export function NetworkInterfaceViewer({ ref }: { ref?: Ref<DialogRef> }) {
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
>
{loading && isEmpty ? (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
<CircularProgress size={24} />
</Box>
) : isEmpty ? (
<Box sx={{ minHeight: 160 }}>
<BaseEmpty />
</Box>
) : (
networkInterfaces.map((item) => (
<Box key={item.name}>
<h4>{item.name}</h4>
<Box>
{item.addr.map((address) => {
const ip = getAddressIp(address)
return (
ip && (
<AddressDisplay
key={ip}
label={t(
'settings.modals.networkInterface.fields.ipAddress',
)}
content={ip}
/>
)
)
})}
<AddressDisplay
label={t('settings.modals.networkInterface.fields.macAddress')}
content={item.mac_addr ?? ''}
/>
</Box>
<div
data-settings-dialog="network-interface-viewer"
data-setting-target="network-interface-viewer"
tabIndex={-1}
>
{loading && isEmpty ? (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
<CircularProgress size={24} />
</Box>
))
)}
) : isEmpty ? (
<Box sx={{ minHeight: 160 }}>
<BaseEmpty />
</Box>
) : (
networkInterfaces.map((item) => (
<Box key={item.name}>
<h4>{item.name}</h4>
<Box>
{item.addr.map((address) => {
const ip = getAddressIp(address)
return (
ip && (
<AddressDisplay
key={ip}
label={t(
'settings.modals.networkInterface.fields.ipAddress',
)}
content={ip}
/>
)
)
})}
<AddressDisplay
label={t(
'settings.modals.networkInterface.fields.macAddress',
)}
content={item.mac_addr ?? ''}
/>
</Box>
</Box>
))
)}
</div>
</BaseDialog>
)
}
+4 -2
View File
@@ -13,6 +13,7 @@ import React, { ReactNode, useState } from 'react'
import isAsyncFunction from '@/utils/is-async-function'
interface ItemProps {
'data-setting-target'?: string
label: ReactNode
extra?: ReactNode
children?: ReactNode
@@ -21,6 +22,7 @@ interface ItemProps {
}
export const SettingItem: React.FC<ItemProps> = ({
'data-setting-target': searchTarget,
label,
extra,
children,
@@ -49,7 +51,7 @@ export const SettingItem: React.FC<ItemProps> = ({
}
return clickable ? (
<ListItem disablePadding>
<ListItem data-setting-target={searchTarget} disablePadding>
<ListItemButton onClick={handleClick} disabled={isLoading}>
<ListItemText primary={primary} secondary={secondary} />
{isLoading ? (
@@ -60,7 +62,7 @@ export const SettingItem: React.FC<ItemProps> = ({
</ListItemButton>
</ListItem>
) : (
<ListItem sx={{ pt: '5px', pb: '5px' }}>
<ListItem data-setting-target={searchTarget} sx={{ pt: '5px', pb: '5px' }}>
<ListItemText primary={primary} secondary={secondary} />
{children}
</ListItem>
+69 -37
View File
@@ -13,7 +13,7 @@ import {
Typography,
} from '@mui/material'
import { useLockFn } from 'ahooks'
import { forwardRef, useImperativeHandle, useMemo, useState } from 'react'
import { forwardRef, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
@@ -39,6 +39,8 @@ import { useQuery } from '@/services/query-client'
import { debugLog } from '@/utils/debug'
import getSystem from '@/utils/get-system'
import { useSettingDialog } from '../settings-search-context'
const DEFAULT_PAC = `function FindProxyForURL(url, host) {
return "PROXY %proxy_host%:%mixed-port%; SOCKS5 %proxy_host%:%mixed-port%; DIRECT;";
}`
@@ -183,7 +185,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
setPacEditorSavedValue(nextPac)
})
useImperativeHandle(ref, () => ({
useSettingDialog(ref, 'sysproxy-viewer', open, () => ({
open: () => {
setOpen(true)
setValue(buildInitialValues(verge))
@@ -355,41 +357,49 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
loading={saving}
disableOk={saving}
>
<span data-settings-dialog="sysproxy-viewer" />
<List>
<BaseFieldset
label={t('settings.modals.sysproxy.fieldsets.currentStatus')}
padding="15px 10px"
>
<FlexBox>
<Typography className="label">
{t('settings.modals.sysproxy.fields.enableStatus')}
</Typography>
<Typography className="value">
{isProxyReallyEnabled
? t('shared.statuses.enabled')
: t('shared.statuses.disabled')}
</Typography>
</FlexBox>
{!value.pac && (
<Box data-setting-target="sysproxy-status" tabIndex={-1}>
<BaseFieldset
label={t('settings.modals.sysproxy.fieldsets.currentStatus')}
padding="15px 10px"
>
<FlexBox>
<Typography className="label">
{t('settings.modals.sysproxy.fields.serverAddr')}
</Typography>
<Typography className="value">{getSystemProxyAddress}</Typography>
</FlexBox>
)}
{value.pac && (
<FlexBox>
<Typography className="label">
{t('settings.modals.sysproxy.fields.pacUrl')}
{t('settings.modals.sysproxy.fields.enableStatus')}
</Typography>
<Typography className="value">
{getCurrentPacUrl || '-'}
{isProxyReallyEnabled
? t('shared.statuses.enabled')
: t('shared.statuses.disabled')}
</Typography>
</FlexBox>
)}
</BaseFieldset>
<ListItem sx={{ padding: '5px 2px' }}>
{!value.pac && (
<FlexBox>
<Typography className="label">
{t('settings.modals.sysproxy.fields.serverAddr')}
</Typography>
<Typography className="value">
{getSystemProxyAddress}
</Typography>
</FlexBox>
)}
{value.pac && (
<FlexBox>
<Typography className="label">
{t('settings.modals.sysproxy.fields.pacUrl')}
</Typography>
<Typography className="value">
{getCurrentPacUrl || '-'}
</Typography>
</FlexBox>
)}
</BaseFieldset>
</Box>
<ListItem
data-setting-target="settings.modals.sysproxy.fields.proxyHost"
sx={{ padding: '5px 2px' }}
>
<ListItemText
primary={t('settings.modals.sysproxy.fields.proxyHost')}
/>
@@ -416,7 +426,10 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
}}
/>
</ListItem>
<ListItem sx={{ padding: '5px 2px' }}>
<ListItem
data-setting-target="settings.modals.sysproxy.fields.usePacMode"
sx={{ padding: '5px 2px' }}
>
<ListItemText
primary={t('settings.modals.sysproxy.fields.usePacMode')}
/>
@@ -428,7 +441,10 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
/>
</ListItem>
<ListItem sx={{ padding: '5px 2px' }}>
<ListItem
data-setting-target="settings.modals.sysproxy.fields.proxyGuard"
sx={{ padding: '5px 2px' }}
>
<ListItemText
primary={t('settings.modals.sysproxy.fields.proxyGuard')}
sx={{ maxWidth: 'fit-content' }}
@@ -446,7 +462,10 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
/>
</ListItem>
<ListItem sx={{ padding: '5px 2px' }}>
<ListItem
data-setting-target="settings.modals.sysproxy.fields.guardDuration"
sx={{ padding: '5px 2px' }}
>
<ListItemText
primary={t('settings.modals.sysproxy.fields.guardDuration')}
/>
@@ -469,7 +488,10 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
/>
</ListItem>
{!value.pac && (
<ListItem sx={{ padding: '5px 2px' }}>
<ListItem
data-setting-target="settings.modals.sysproxy.fields.alwaysUseDefaultBypass"
sx={{ padding: '5px 2px' }}
>
<ListItemText
primary={t(
'settings.modals.sysproxy.fields.alwaysUseDefaultBypass',
@@ -496,7 +518,10 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
)}
{!value.pac && (
<ListItem sx={{ padding: '5px 2px' }}>
<ListItem
data-setting-target="settings.modals.sysproxy.fields.enableBypassCheck"
sx={{ padding: '5px 2px' }}
>
<ListItemText
primary={t('settings.modals.sysproxy.fields.enableBypassCheck')}
/>
@@ -528,7 +553,10 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
setValue((v) => ({ ...v, bypass: nextValue }))
}}
renderHeader={(modeToggle) => (
<ListItem sx={{ padding: '5px 2px' }}>
<ListItem
data-setting-target="settings.modals.sysproxy.fields.proxyBypass"
sx={{ padding: '5px 2px' }}
>
<ListItemText
primary={t('settings.modals.sysproxy.fields.proxyBypass')}
/>
@@ -556,12 +584,16 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
)}
{value.pac && (
<ListItem sx={{ padding: '5px 2px', alignItems: 'start' }}>
<ListItem
data-setting-target="settings.modals.sysproxy.fields.pacScriptContent"
sx={{ padding: '5px 2px', alignItems: 'start' }}
>
<ListItemText
primary={t('settings.modals.sysproxy.fields.pacScriptContent')}
sx={{ padding: '3px 0' }}
/>
<Button
data-setting-target="settings.modals.sysproxy.actions.editPac"
startIcon={<EditRounded />}
variant="outlined"
onClick={openPacEditor}
+9 -11
View File
@@ -9,13 +9,7 @@ import {
useTheme,
} from '@mui/material'
import { useLockFn } from 'ahooks'
import {
useEffect,
useImperativeHandle,
useMemo,
useRef,
useState,
} from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { BaseDialog, DialogRef } from '@/components/base'
@@ -24,6 +18,8 @@ import { useVerge } from '@/hooks/use-verge'
import { defaultDarkTheme, defaultTheme } from '@/pages/_theme'
import { showNotice } from '@/services/notice-service'
import { useSettingDialog } from '../settings-search-context'
export function ThemeViewer(props: { ref?: React.Ref<DialogRef> }) {
const { ref } = props
const { t } = useTranslation()
@@ -41,7 +37,7 @@ export function ThemeViewer(props: { ref?: React.Ref<DialogRef> }) {
themeRef.current = theme
}, [theme])
useImperativeHandle(ref, () => ({
useSettingDialog(ref, 'theme-viewer', open, () => ({
open: () => {
setOpen(true)
setTheme({ ...theme_setting })
@@ -128,7 +124,7 @@ export function ThemeViewer(props: { ref?: React.Ref<DialogRef> }) {
const renderItem = (labelKey: string, key: ThemeKey) => {
const label = t(labelKey)
return (
<Item key={key}>
<Item key={key} data-setting-target={labelKey}>
<ListItemText primary={label} />
<Round sx={{ background: theme[key] || dt[key] }} />
<TextField
@@ -153,10 +149,11 @@ export function ThemeViewer(props: { ref?: React.Ref<DialogRef> }) {
onCancel={() => setOpen(false)}
onOk={onSave}
>
<span data-settings-dialog="theme-viewer" />
<List sx={{ pt: 0 }}>
{fieldDefinitions.map((field) => renderItem(field.labelKey, field.key))}
<Item>
<Item data-setting-target="settings.components.verge.theme.fields.fontFamily">
<ListItemText
primary={t('settings.components.verge.theme.fields.fontFamily')}
/>
@@ -167,11 +164,12 @@ export function ThemeViewer(props: { ref?: React.Ref<DialogRef> }) {
onKeyDown={(e) => e.key === 'Enter' && onSave()}
/>
</Item>
<Item>
<Item data-setting-target="settings.components.verge.theme.fields.cssInjection">
<ListItemText
primary={t('settings.components.verge.theme.fields.cssInjection')}
/>
<Button
data-setting-target="settings.components.verge.theme.actions.editCss"
startIcon={<EditRounded />}
variant="outlined"
onClick={openCssEditor}
+46 -12
View File
@@ -9,7 +9,7 @@ import {
} from '@mui/material'
import { useLockFn } from 'ahooks'
import type { Ref } from 'react'
import { useImperativeHandle, useState } from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
@@ -26,6 +26,8 @@ import { showNotice } from '@/services/notice-service'
import getSystem from '@/utils/get-system'
import { areValidIpCidrs } from '@/utils/network'
import { useSettingDialog } from '../settings-search-context'
import { DnsModeSwitch } from './dns-mode-switch'
import { StackModeSwitch } from './stack-mode-switch'
@@ -67,7 +69,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
? t('settings.modals.tun.messages.invalidRouteExcludeAddress')
: t('settings.modals.tun.messages.routeExcludeAddressHint')
useImperativeHandle(ref, () => ({
useSettingDialog(ref, 'tun-viewer', open, () => ({
open: () => {
setOpen(true)
const nextAutoRoute = clash?.tun['auto-route'] ?? true
@@ -152,6 +154,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
<Box sx={{ display: 'flex', justifyContent: 'space-between', gap: 1 }}>
<Typography variant="h6">{t('settings.modals.tun.title')}</Typography>
<Button
data-setting-target="tun-viewer-reset"
variant="outlined"
size="small"
onClick={async () => {
@@ -204,8 +207,12 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
onCancel={() => setOpen(false)}
onOk={onSave}
>
<span data-settings-dialog="tun-viewer" />
<List>
<ListItem sx={{ padding: '5px 2px' }}>
<ListItem
data-setting-target="settings.modals.tun.fields.stack"
sx={{ padding: '5px 2px' }}
>
<ListItemText primary={t('settings.modals.tun.fields.stack')} />
<StackModeSwitch
value={values.stack}
@@ -218,7 +225,10 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
/>
</ListItem>
<ListItem sx={{ padding: '5px 2px' }}>
<ListItem
data-setting-target="settings.modals.tun.fields.dnsMode"
sx={{ padding: '5px 2px' }}
>
<ListItemText
primary={t('settings.modals.tun.fields.dnsMode')}
sx={{ maxWidth: 'fit-content' }}
@@ -237,7 +247,10 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
</Box>
</ListItem>
<ListItem sx={{ padding: '5px 2px' }}>
<ListItem
data-setting-target="settings.modals.tun.fields.device"
sx={{ padding: '5px 2px' }}
>
<ListItemText primary={t('settings.modals.tun.fields.device')} />
<TextField
autoComplete="new-password"
@@ -254,7 +267,10 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
/>
</ListItem>
<ListItem sx={{ padding: '5px 2px' }}>
<ListItem
data-setting-target="settings.modals.tun.fields.autoRoute"
sx={{ padding: '5px 2px' }}
>
<ListItemText primary={t('settings.modals.tun.fields.autoRoute')} />
<Switch
edge="end"
@@ -270,7 +286,10 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
</ListItem>
{OS === 'linux' && (
<ListItem sx={{ padding: '5px 2px' }}>
<ListItem
data-setting-target="settings.modals.tun.fields.autoRedirect"
sx={{ padding: '5px 2px' }}
>
<ListItemText
primary={t('settings.modals.tun.fields.autoRedirect')}
sx={{ maxWidth: 'fit-content' }}
@@ -294,7 +313,10 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
</ListItem>
)}
<ListItem sx={{ padding: '5px 2px' }}>
<ListItem
data-setting-target="settings.modals.tun.fields.strictRoute"
sx={{ padding: '5px 2px' }}
>
<ListItemText primary={t('settings.modals.tun.fields.strictRoute')} />
<Switch
edge="end"
@@ -303,7 +325,10 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
/>
</ListItem>
<ListItem sx={{ padding: '5px 2px' }}>
<ListItem
data-setting-target="settings.modals.tun.fields.autoDetectInterface"
sx={{ padding: '5px 2px' }}
>
<ListItemText
primary={t('settings.modals.tun.fields.autoDetectInterface')}
/>
@@ -316,7 +341,10 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
/>
</ListItem>
<ListItem sx={{ padding: '5px 2px' }}>
<ListItem
data-setting-target="settings.modals.tun.fields.dnsHijack"
sx={{ padding: '5px 2px' }}
>
<ListItemText primary={t('settings.modals.tun.fields.dnsHijack')} />
<TextField
autoComplete="new-password"
@@ -333,7 +361,10 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
/>
</ListItem>
<ListItem sx={{ padding: '5px 2px' }}>
<ListItem
data-setting-target="settings.modals.tun.fields.mtu"
sx={{ padding: '5px 2px' }}
>
<ListItemText primary={t('settings.modals.tun.fields.mtu')} />
<TextField
autoComplete="new-password"
@@ -365,7 +396,10 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
setValues((v) => ({ ...v, routeExcludeAddress: nextValue }))
}
renderHeader={(modeToggle) => (
<ListItem sx={{ padding: '5px 2px' }}>
<ListItem
data-setting-target="settings.modals.tun.fields.routeExcludeAddress"
sx={{ padding: '5px 2px' }}
>
<ListItemText
primary={t('settings.modals.tun.fields.routeExcludeAddress')}
/>
+40 -16
View File
@@ -11,13 +11,7 @@ import {
Select,
MenuItem,
} from '@mui/material'
import {
forwardRef,
useImperativeHandle,
useMemo,
useRef,
useState,
} from 'react'
import { forwardRef, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { BaseDialog } from '@/components/base'
@@ -40,6 +34,8 @@ import {
normalizeListenHost,
} from '@/utils/network'
import { useSettingDialog } from '../settings-search-context'
interface TunnelsViewerRef {
open: () => void
close: () => void
@@ -93,7 +89,7 @@ export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => {
const [values, setValues] = useState<TunnelFormValues>(buildInitialValues)
const [draftTunnels, setDraftTunnels] = useState<TunnelEntry[]>([])
useImperativeHandle(ref, () => ({
useSettingDialog(ref, 'tunnels-viewer', open, () => ({
open: () => {
setValues(buildInitialValues())
setDraftTunnels(() => clash?.tunnels ?? [])
@@ -301,10 +297,14 @@ export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => {
}}
onOk={handleSave}
>
<span data-settings-dialog="tunnels-viewer" />
<List>
{draftTunnels.length > 0 && (
<>
<ListItem sx={{ padding: '4px 0', opacity: 0.6 }}>
<ListItem
data-setting-target="settings.sections.clash.form.fields.tunnels.existing"
sx={{ padding: '4px 0', opacity: 0.6 }}
>
<ListItemText
primary={t(
'settings.sections.clash.form.fields.tunnels.existing',
@@ -314,6 +314,7 @@ export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => {
<List component="nav">
{tunnelEntries.map((item) => (
<ListItem
data-setting-target="settings.sections.clash.form.fields.tunnels.default"
key={`${item.key}`}
sx={{ padding: '4px 0' }}
secondaryAction={
@@ -342,6 +343,7 @@ export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => {
)}
<ListItemButton
sx={{ padding: '4px 0', opacity: 0.8 }}
data-setting-target="tunnels-expand"
onClick={() => setExpanded((v) => !v)}
>
<ListItemText
@@ -352,7 +354,10 @@ export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => {
{expanded ? <ExpandLess /> : <ExpandMore />}
</ListItemButton>
{expanded && (
<ListItem sx={{ padding: '8px 0' }}>
<ListItem
data-setting-target="settings.sections.clash.form.fields.tunnels.protocols"
sx={{ padding: '8px 0' }}
>
<div style={{ width: '100%' }}>
{/* 输入框区域 */}
{/* 协议 */}
@@ -380,7 +385,10 @@ export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => {
</ListItem>
{/* 本地监听地址 */}
<ListItem sx={{ padding: '6px 2px' }}>
<ListItem
data-setting-target="settings.sections.clash.form.fields.tunnels.localAddr"
sx={{ padding: '6px 2px' }}
>
<ListItemText
primary={t(
'settings.sections.clash.form.fields.tunnels.localAddr',
@@ -399,7 +407,10 @@ export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => {
</ListItem>
{/* 本地监听端口 */}
<ListItem sx={{ padding: '6px 2px' }}>
<ListItem
data-setting-target="settings.sections.clash.form.fields.tunnels.localPort"
sx={{ padding: '6px 2px' }}
>
<ListItemText
primary={t(
'settings.sections.clash.form.fields.tunnels.localPort',
@@ -419,7 +430,10 @@ export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => {
</ListItem>
{/* 目标服务器地址 */}
<ListItem sx={{ padding: '6px 2px' }}>
<ListItem
data-setting-target="settings.sections.clash.form.fields.tunnels.targetAddr"
sx={{ padding: '6px 2px' }}
>
<ListItemText
primary={t(
'settings.sections.clash.form.fields.tunnels.targetAddr',
@@ -438,7 +452,10 @@ export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => {
</ListItem>
{/* 目标服务器端口 */}
<ListItem sx={{ padding: '6px 2px' }}>
<ListItem
data-setting-target="settings.sections.clash.form.fields.tunnels.targetPort"
sx={{ padding: '6px 2px' }}
>
<ListItemText
primary={t(
'settings.sections.clash.form.fields.tunnels.targetPort',
@@ -458,7 +475,10 @@ export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => {
</ListItem>
{/* 代理组 */}
<ListItem sx={{ padding: '6px 2px' }}>
<ListItem
data-setting-target="settings.sections.clash.form.fields.tunnels.proxyGroup"
sx={{ padding: '6px 2px' }}
>
<ListItemText
primary={
<>
@@ -520,7 +540,10 @@ export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => {
</ListItem>
{/* 代理节点 */}
<ListItem sx={{ padding: '6px 2px' }}>
<ListItem
data-setting-target="settings.sections.clash.form.fields.tunnels.proxyNode"
sx={{ padding: '6px 2px' }}
>
<ListItemText
primary={
<>
@@ -581,6 +604,7 @@ export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => {
{/* 添加按钮 */}
<Button
data-setting-target="settings.sections.clash.form.fields.tunnels.actions.add"
variant="contained"
size="small"
sx={{
@@ -131,6 +131,7 @@ export const WebUIItem = (props: Props) => {
</Typography>
<IconButton
size="small"
data-setting-target="web-ui-open"
title={t('settings.modals.webUI.actions.openUrl')}
color="inherit"
onClick={() => onOpenUrl?.(value)}
@@ -139,6 +140,7 @@ export const WebUIItem = (props: Props) => {
</IconButton>
<IconButton
size="small"
data-setting-target="web-ui-edit"
title={t('shared.actions.edit')}
color="inherit"
onClick={() => {
@@ -150,6 +152,7 @@ export const WebUIItem = (props: Props) => {
</IconButton>
<IconButton
size="small"
data-setting-target="web-ui-delete"
title={t('shared.actions.delete')}
color="inherit"
onClick={onDelete}
+40 -31
View File
@@ -1,7 +1,7 @@
import { Box, Button, Typography } from '@mui/material'
import { useLockFn } from 'ahooks'
import type { Ref } from 'react'
import { useImperativeHandle, useMemo, useState } from 'react'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { BaseDialog, BaseEmpty, DialogRef } from '@/components/base'
@@ -10,6 +10,8 @@ import { useVerge } from '@/hooks/use-verge'
import { showNotice } from '@/services/notice-service'
import { openExternalUrl } from '@/utils/open-external-url'
import { useSettingDialog } from '../settings-search-context'
import { WebUIItem } from './web-ui-item'
const DEFAULT_WEB_UI_LIST = [
@@ -27,7 +29,7 @@ export function WebUIViewer({ ref }: { ref?: Ref<DialogRef> }) {
const [open, setOpen] = useState(false)
const [editing, setEditing] = useState(false)
useImperativeHandle(ref, () => ({
useSettingDialog(ref, 'web-ui-viewer', open, () => ({
open: () => setOpen(true),
close: () => setOpen(false),
}))
@@ -114,6 +116,7 @@ export function WebUIViewer({ ref }: { ref?: Ref<DialogRef> }) {
variant="contained"
size="small"
disabled={editing}
data-setting-target="web-ui-new"
onClick={() => setEditing(true)}
>
{t('shared.actions.new')}
@@ -132,36 +135,42 @@ export function WebUIViewer({ ref }: { ref?: Ref<DialogRef> }) {
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
>
{!editing && webUIList.length === 0 && (
<BaseEmpty
extra={
<Typography sx={{ mt: 2, fontSize: '12px' }}>
{t('settings.modals.webUI.messages.placeholderInstruction')}
</Typography>
}
/>
)}
<div
data-settings-dialog="web-ui-viewer"
data-setting-target="web-ui-viewer"
tabIndex={-1}
>
{!editing && webUIList.length === 0 && (
<BaseEmpty
extra={
<Typography sx={{ mt: 2, fontSize: '12px' }}>
{t('settings.modals.webUI.messages.placeholderInstruction')}
</Typography>
}
/>
)}
{webUIEntries.map(({ item, index, key }) => (
<WebUIItem
key={key}
value={item}
onChange={(v) => handleChange(index, v)}
onDelete={() => handleDelete(index)}
onOpenUrl={handleOpenUrl}
/>
))}
{editing && (
<WebUIItem
value=""
onlyEdit
onChange={(v) => {
setEditing(false)
handleAdd(v || '')
}}
onCancel={() => setEditing(false)}
/>
)}
{webUIEntries.map(({ item, index, key }) => (
<WebUIItem
key={key}
value={item}
onChange={(v) => handleChange(index, v)}
onDelete={() => handleDelete(index)}
onOpenUrl={handleOpenUrl}
/>
))}
{editing && (
<WebUIItem
value=""
onlyEdit
onChange={(v) => {
setEditing(false)
handleAdd(v || '')
}}
onCancel={() => setEditing(false)}
/>
)}
</div>
</BaseDialog>
)
}
+18 -2
View File
@@ -156,6 +156,7 @@ const SettingClash = ({ onError }: Props) => {
<HeaderConfiguration ref={corsRef} />
<TunnelsViewer ref={tunnelRef} />
<SettingItem
data-setting-target="settings.sections.clash.form.fields.allowLan"
label={t('settings.sections.clash.form.fields.allowLan')}
extra={
<TooltipIcon
@@ -181,6 +182,7 @@ const SettingClash = ({ onError }: Props) => {
</SettingItem>
<SettingItem
data-setting-target="settings.sections.clash.form.fields.dnsOverwrite"
label={t('settings.sections.clash.form.fields.dnsOverwrite')}
extra={
<>
@@ -203,7 +205,10 @@ const SettingClash = ({ onError }: Props) => {
/>
</SettingItem>
<SettingItem label={t('settings.sections.clash.form.fields.ipv6')}>
<SettingItem
data-setting-target="settings.sections.clash.form.fields.ipv6"
label={t('settings.sections.clash.form.fields.ipv6')}
>
<GuardState
value={ipv6 ?? false}
valueProps="checked"
@@ -217,6 +222,7 @@ const SettingClash = ({ onError }: Props) => {
</SettingItem>
<SettingItem
data-setting-target="settings.sections.clash.form.fields.unifiedDelay"
label={t('settings.sections.clash.form.fields.unifiedDelay')}
extra={
<TooltipIcon
@@ -238,6 +244,7 @@ const SettingClash = ({ onError }: Props) => {
</SettingItem>
<SettingItem
data-setting-target="settings.sections.clash.form.fields.logLevel"
label={t('settings.sections.clash.form.fields.logLevel')}
extra={
<TooltipIcon
@@ -279,7 +286,10 @@ const SettingClash = ({ onError }: Props) => {
</GuardState>
</SettingItem>
<SettingItem label={t('settings.sections.clash.form.fields.portConfig')}>
<SettingItem
data-setting-target="settings.sections.clash.form.fields.portConfig"
label={t('settings.sections.clash.form.fields.portConfig')}
>
<TextField
autoComplete="new-password"
disabled={false}
@@ -294,6 +304,7 @@ const SettingClash = ({ onError }: Props) => {
</SettingItem>
<SettingItem
data-setting-target="settings.sections.clash.form.fields.external"
label={t('settings.sections.clash.form.fields.external')}
extra={
<TooltipIcon
@@ -311,11 +322,13 @@ const SettingClash = ({ onError }: Props) => {
/>
<SettingItem
data-setting-target="settings.sections.clash.form.fields.webUI"
onClick={() => webRef.current?.open()}
label={t('settings.sections.clash.form.fields.webUI')}
/>
<SettingItem
data-setting-target="settings.sections.clash.form.fields.clashCore"
label={t('settings.sections.clash.form.fields.clashCore')}
extra={
<TooltipIcon
@@ -329,6 +342,7 @@ const SettingClash = ({ onError }: Props) => {
{isWIN && (
<SettingItem
data-setting-target="settings.sections.clash.form.fields.openUwpTool"
onClick={invoke_uwp_tool}
label={t('settings.sections.clash.form.fields.openUwpTool')}
extra={
@@ -341,11 +355,13 @@ const SettingClash = ({ onError }: Props) => {
)}
<SettingItem
data-setting-target="settings.sections.clash.form.fields.updateGeoData"
onClick={onUpdateGeo}
label={t('settings.sections.clash.form.fields.updateGeoData')}
/>
<SettingItem
data-setting-target="settings.sections.clash.form.fields.tunnels.title"
label={t('settings.sections.clash.form.fields.tunnels.title')}
onClick={() => tunnelRef.current?.open()}
/>
+18 -9
View File
@@ -1,3 +1,4 @@
import { Box } from '@mui/material'
import React from 'react'
import { useTranslation } from 'react-i18next'
@@ -29,17 +30,24 @@ const SettingSystem = ({ onError }: Props) => {
return (
<SettingList title={t('settings.sections.system.title')}>
<ProxyControlSwitches
label={t('settings.sections.system.toggles.tunMode')}
onError={onError}
/>
<Box data-setting-target="settings.sections.system.toggles.tunMode">
<ProxyControlSwitches
label={t('settings.sections.system.toggles.tunMode')}
onError={onError}
/>
</Box>
<ProxyControlSwitches
label={t('settings.sections.system.toggles.systemProxy')}
onError={onError}
/>
<Box data-setting-target="settings.sections.system.toggles.systemProxy">
<ProxyControlSwitches
label={t('settings.sections.system.toggles.systemProxy')}
onError={onError}
/>
</Box>
<SettingItem label={t('settings.sections.system.fields.autoLaunch')}>
<SettingItem
data-setting-target="settings.sections.system.fields.autoLaunch"
label={t('settings.sections.system.fields.autoLaunch')}
>
<GuardState
value={enable_auto_launch ?? false}
valueProps="checked"
@@ -64,6 +72,7 @@ const SettingSystem = ({ onError }: Props) => {
</SettingItem>
<SettingItem
data-setting-target="settings.sections.system.fields.silentStart"
label={t('settings.sections.system.fields.silentStart')}
extra={
<TooltipIcon
@@ -90,6 +90,7 @@ const SettingVergeAdvanced = ({ onError: _ }: Props) => {
<LiteModeViewer ref={liteModeRef} />
<SettingItem
data-setting-target="settings.components.verge.advanced.fields.backupSetting"
onClick={() => backupRef.current?.open()}
label={t('settings.components.verge.advanced.fields.backupSetting')}
extra={
@@ -101,11 +102,13 @@ const SettingVergeAdvanced = ({ onError: _ }: Props) => {
/>
<SettingItem
data-setting-target="settings.components.verge.advanced.fields.runtimeConfig"
onClick={() => configRef.current?.open()}
label={t('settings.components.verge.advanced.fields.runtimeConfig')}
/>
<SettingItem
data-setting-target="settings.components.verge.advanced.fields.openConfDir"
onClick={openAppDir}
label={t('settings.components.verge.advanced.fields.openConfDir')}
extra={
@@ -117,21 +120,25 @@ const SettingVergeAdvanced = ({ onError: _ }: Props) => {
/>
<SettingItem
data-setting-target="settings.components.verge.advanced.fields.openCoreDir"
onClick={openCoreDir}
label={t('settings.components.verge.advanced.fields.openCoreDir')}
/>
<SettingItem
data-setting-target="settings.components.verge.advanced.fields.openLogsDir"
onClick={openLogsDir}
label={t('settings.components.verge.advanced.fields.openLogsDir')}
/>
<SettingItem
data-setting-target="settings.components.verge.advanced.fields.checkUpdates"
onClick={onCheckUpdate}
label={t('settings.components.verge.advanced.fields.checkUpdates')}
/>
<SettingItem
data-setting-target="settings.components.verge.advanced.fields.openDevTools"
onClick={() => {
void mutate(() => openDevTools(), {
id: 'open-devtools',
@@ -142,6 +149,7 @@ const SettingVergeAdvanced = ({ onError: _ }: Props) => {
/>
<SettingItem
data-setting-target="settings.components.verge.advanced.fields.liteModeSettings"
label={t('settings.components.verge.advanced.fields.liteModeSettings')}
extra={
<TooltipIcon
@@ -153,6 +161,7 @@ const SettingVergeAdvanced = ({ onError: _ }: Props) => {
/>
<SettingItem
data-setting-target="settings.components.verge.advanced.fields.exit"
onClick={() => {
void mutate(() => exitApp(), {
id: 'exit-app',
@@ -163,6 +172,7 @@ const SettingVergeAdvanced = ({ onError: _ }: Props) => {
/>
<SettingItem
data-setting-target="settings.components.verge.advanced.fields.exportDiagnostics"
label={t('settings.components.verge.advanced.fields.exportDiagnostics')}
extra={
<TooltipIcon
@@ -173,6 +183,7 @@ const SettingVergeAdvanced = ({ onError: _ }: Props) => {
></SettingItem>
<SettingItem
data-setting-target="settings.components.verge.advanced.fields.vergeVersion"
label={t('settings.components.verge.advanced.fields.vergeVersion')}
extra={
<TooltipIcon
+14 -1
View File
@@ -100,7 +100,10 @@ const SettingVergeBasic = ({ onError }: Props) => {
<UpdateViewer ref={updateRef} />
<BackupViewer ref={backupRef} />
<SettingItem label={t('settings.components.verge.basic.fields.language')}>
<SettingItem
data-setting-target="settings.components.verge.basic.fields.language"
label={t('settings.components.verge.basic.fields.language')}
>
<GuardState
value={language ?? 'en'}
onCatch={onError}
@@ -119,6 +122,7 @@ const SettingVergeBasic = ({ onError }: Props) => {
</SettingItem>
<SettingItem
data-setting-target="settings.components.verge.basic.fields.themeMode"
label={t('settings.components.verge.basic.fields.themeMode')}
>
<GuardState
@@ -133,6 +137,7 @@ const SettingVergeBasic = ({ onError }: Props) => {
{OS !== 'linux' && (
<SettingItem
data-setting-target="settings.components.verge.basic.fields.trayClickEvent"
label={t('settings.components.verge.basic.fields.trayClickEvent')}
>
<GuardState
@@ -166,6 +171,7 @@ const SettingVergeBasic = ({ onError }: Props) => {
)}
<SettingItem
data-setting-target="settings.components.verge.basic.fields.copyEnvType"
label={t('settings.components.verge.basic.fields.copyEnvType')}
extra={
<TooltipIcon icon={ContentCopyRounded} onClick={onCopyClashEnv} />
@@ -189,6 +195,7 @@ const SettingVergeBasic = ({ onError }: Props) => {
</SettingItem>
<SettingItem
data-setting-target="settings.components.verge.basic.fields.startPage"
label={t('settings.components.verge.basic.fields.startPage')}
>
<GuardState
@@ -211,6 +218,7 @@ const SettingVergeBasic = ({ onError }: Props) => {
</SettingItem>
<SettingItem
data-setting-target="settings.components.verge.basic.fields.startupScript"
label={t('settings.components.verge.basic.fields.startupScript')}
>
<GuardState
@@ -228,6 +236,7 @@ const SettingVergeBasic = ({ onError }: Props) => {
endAdornment={
<>
<Button
data-setting-target="settings.components.verge.basic.actions.browse"
onClick={async () => {
const selected = await open({
directory: false,
@@ -264,21 +273,25 @@ const SettingVergeBasic = ({ onError }: Props) => {
</SettingItem>
<SettingItem
data-setting-target="settings.components.verge.basic.fields.themeSetting"
onClick={() => themeRef.current?.open()}
label={t('settings.components.verge.basic.fields.themeSetting')}
/>
<SettingItem
data-setting-target="settings.components.verge.basic.fields.layoutSetting"
onClick={() => layoutRef.current?.open()}
label={t('settings.components.verge.basic.fields.layoutSetting')}
/>
<SettingItem
data-setting-target="settings.components.verge.basic.fields.misc"
onClick={() => miscRef.current?.open()}
label={t('settings.components.verge.basic.fields.misc')}
/>
<SettingItem
data-setting-target="settings.components.verge.basic.fields.hotkeySetting"
onClick={() => hotkeyRef.current?.open()}
label={t('settings.components.verge.basic.fields.hotkeySetting')}
/>
+645
View File
@@ -0,0 +1,645 @@
import type { TranslationKey } from '@/types/generated/i18n-keys'
export interface SettingEntry {
id: string
name: TranslationKey
aliases: string
section: TranslationKey
path: TranslationKey
prerequisite?: string
target?: string
fallback?: string
dialog?: string
platforms?: readonly string[]
}
type GroupDefaults = Pick<SettingEntry, 'section' | 'path'> &
Partial<
Pick<SettingEntry, 'dialog' | 'fallback' | 'prerequisite' | 'aliases'>
>
type EntryOptions = Partial<Omit<SettingEntry, 'name'>>
type GroupEntries = Partial<Record<TranslationKey, string | EntryOptions>>
function group(defaults: GroupDefaults, entries: GroupEntries): SettingEntry[] {
return (
Object.entries(entries) as [TranslationKey, string | EntryOptions][]
).map(([name, value]) => {
const options = typeof value === 'string' ? { aliases: value } : value
return {
...defaults,
...options,
id: options.id ?? name,
name,
aliases: [options.aliases, defaults.aliases].filter(Boolean).join(' '),
}
})
}
const system = {
section: 'settings.sections.system.title',
path: 'settings.sections.system.title',
} satisfies GroupDefaults
const tun = {
section: 'settings.sections.system.title',
path: 'settings.modals.tun.title',
dialog: 'tun-viewer',
} satisfies GroupDefaults
const sysproxy = {
section: 'settings.sections.system.title',
path: 'settings.modals.sysproxy.title',
dialog: 'sysproxy-viewer',
fallback: 'settings.modals.sysproxy.fields.usePacMode',
prerequisite: 'sysproxy-status',
aliases: '代理 PAC bypass 绕过',
} satisfies GroupDefaults
const clash = {
section: 'settings.sections.clash.title',
path: 'settings.sections.clash.title',
} satisfies GroupDefaults
const networkInterface = {
section: 'settings.sections.clash.title',
path: 'settings.sections.clash.title',
dialog: 'network-interface-viewer',
aliases: 'network interface viewer',
} satisfies GroupDefaults
const dns = {
section: 'settings.sections.clash.title',
path: 'settings.modals.dns.dialog.title',
dialog: 'dns-viewer',
fallback: 'dns-mode',
aliases: 'DNS 域名解析 DNS 域名解析',
} satisfies GroupDefaults
const dnsReset = {
section: 'settings.sections.clash.title',
path: 'settings.modals.dns.dialog.title',
dialog: 'dns-viewer',
aliases: 'dns viewer reset DNS 域名解析',
} satisfies GroupDefaults
const clashPort = {
section: 'settings.sections.clash.title',
path: 'settings.modals.clashPort.title',
dialog: 'clash-port-viewer',
} satisfies GroupDefaults
const controller = {
section: 'settings.sections.clash.title',
path: 'settings.sections.externalController.title',
dialog: 'controller-viewer',
} satisfies GroupDefaults
const externalControllerCors = {
section: 'settings.sections.clash.title',
path: 'settings.sections.externalCors.title',
dialog: 'external-controller-cors',
} satisfies GroupDefaults
const webUi = {
section: 'settings.sections.clash.title',
path: 'settings.sections.clash.title',
dialog: 'web-ui-viewer',
aliases: 'web ui viewer',
} satisfies GroupDefaults
const webUiActions = {
section: 'settings.sections.clash.title',
path: 'settings.modals.webUI.title',
dialog: 'web-ui-viewer',
fallback: 'web-ui-viewer',
} satisfies GroupDefaults
const clashCore = {
section: 'settings.sections.clash.title',
path: 'settings.sections.clash.form.fields.clashCore',
dialog: 'clash-core-viewer',
fallback: 'core-variants',
} satisfies GroupDefaults
const coreVariants = {
section: 'settings.sections.clash.title',
path: 'settings.sections.clash.title',
dialog: 'clash-core-viewer',
aliases: 'core variants',
} satisfies GroupDefaults
const tunnels = {
section: 'settings.sections.clash.title',
path: 'settings.sections.clash.form.fields.tunnels.title',
dialog: 'tunnels-viewer',
fallback: 'tunnels-expand',
} satisfies GroupDefaults
const basic = {
section: 'settings.components.verge.basic.title',
path: 'settings.components.verge.basic.title',
} satisfies GroupDefaults
const theme = {
section: 'settings.components.verge.basic.title',
path: 'settings.components.verge.theme.title',
dialog: 'theme-viewer',
} satisfies GroupDefaults
const layout = {
section: 'settings.components.verge.basic.title',
path: 'settings.components.verge.layout.title',
dialog: 'layout-viewer',
aliases: 'browse layout 布局 托盘',
} satisfies GroupDefaults
const layoutFields = {
section: 'settings.components.verge.basic.title',
path: 'settings.components.verge.basic.fields.layoutSetting',
dialog: 'layout-viewer',
aliases: 'layout 布局 托盘',
} satisfies GroupDefaults
const misc = {
section: 'settings.components.verge.basic.title',
path: 'settings.modals.misc.title',
dialog: 'misc-viewer',
aliases: 'misc 杂项',
} satisfies GroupDefaults
const hotkey = {
section: 'settings.components.verge.basic.title',
path: 'settings.modals.hotkey.title',
dialog: 'hotkey-viewer',
} satisfies GroupDefaults
const advanced = {
section: 'settings.components.verge.advanced.title',
path: 'settings.components.verge.advanced.title',
} satisfies GroupDefaults
const backup = {
section: 'settings.components.verge.advanced.title',
path: 'settings.modals.backup.title',
dialog: 'backup-viewer',
aliases: 'backup 备份',
} satisfies GroupDefaults
const backupHistory = {
section: 'settings.components.verge.advanced.title',
path: 'settings.modals.backup.title',
dialog: 'backup-history',
fallback: 'backup-history',
} satisfies GroupDefaults
const backupWebdav = {
section: 'settings.components.verge.advanced.title',
path: 'settings.modals.backup.webdav.title',
dialog: 'backup-webdav',
fallback: 'webdav-state',
} satisfies GroupDefaults
const liteMode = {
section: 'settings.components.verge.advanced.title',
path: 'settings.modals.liteMode.title',
dialog: 'lite-mode-viewer',
} satisfies GroupDefaults
const settingsCatalog: readonly SettingEntry[] = [
...group(system, {
'settings.sections.system.toggles.tunMode': 'TUN 虚拟网卡 vpn',
'settings.sections.proxyControl.actions.uninstallService': {
id: 'service-uninstall',
fallback: 'settings.sections.system.toggles.tunMode',
aliases: 'service uninstall',
},
}),
...group(tun, {
'settings.modals.tun.fields.stack': 'stack TUN 虚拟网卡',
'settings.modals.tun.fields.dnsMode': 'dns mode TUN 虚拟网卡',
'settings.modals.tun.fields.device': 'device TUN 虚拟网卡',
'settings.modals.tun.fields.autoRoute': 'auto route TUN 虚拟网卡',
'settings.modals.tun.fields.autoRedirect': {
aliases: 'auto redirect TUN 虚拟网卡',
prerequisite: 'settings.modals.tun.fields.autoRoute',
platforms: ['linux'],
},
'settings.modals.tun.fields.strictRoute': 'strict route TUN 虚拟网卡',
'settings.modals.tun.fields.autoDetectInterface':
'auto detect interface TUN 虚拟网卡',
'settings.modals.tun.fields.dnsHijack': 'dns hijack TUN 虚拟网卡',
'settings.modals.tun.fields.mtu': 'mtu TUN 虚拟网卡',
'settings.modals.tun.fields.routeExcludeAddress': {
aliases: 'route exclude address TUN 虚拟网卡',
prerequisite: 'settings.modals.tun.fields.autoRoute',
},
'shared.actions.resetToDefault': {
id: 'tun-viewer-reset',
aliases: 'tun viewer reset',
},
}),
...group(system, {
'settings.sections.system.toggles.systemProxy': 'system proxy 系统代理',
}),
...group(sysproxy, {
'settings.modals.sysproxy.fields.proxyHost': 'proxy host',
'settings.modals.sysproxy.fields.usePacMode': 'use pac mode',
'settings.modals.sysproxy.fields.proxyGuard': 'proxy guard',
'settings.modals.sysproxy.fields.guardDuration': 'guard duration',
'settings.modals.sysproxy.fields.alwaysUseDefaultBypass':
'always use default bypass',
'settings.modals.sysproxy.fields.enableBypassCheck': 'enable bypass check',
'settings.modals.sysproxy.fields.proxyBypass': 'proxy bypass',
'settings.modals.sysproxy.fields.pacScriptContent': 'pac script content',
'settings.modals.sysproxy.actions.editPac': 'edit pac',
}),
...group(system, {
'settings.sections.system.fields.autoLaunch': 'auto launch ',
'settings.sections.system.fields.silentStart': 'silent start ',
}),
...group(clash, {
'settings.sections.clash.form.fields.allowLan': 'allow lan ',
}),
...group(networkInterface, {
'settings.modals.networkInterface.title': {
id: 'network-interface-viewer',
},
}),
...group(clash, {
'settings.sections.clash.form.fields.dnsOverwrite': 'dns overwrite ',
}),
...group(dns, {
'settings.modals.dns.fields.enable': 'enable',
'settings.modals.dns.fields.listen': 'listen',
'settings.modals.dns.fields.enhancedMode': 'enhancedMode',
'settings.modals.dns.fields.fakeIpRange': 'fakeIpRange',
'settings.modals.dns.fields.fakeIpRange6': 'fakeIpRange6',
'settings.modals.dns.fields.fakeIpFilterMode': 'fakeIpFilterMode',
'settings.modals.dns.fields.ipv6.label': 'label',
'settings.modals.dns.fields.preferH3.label': 'label',
'settings.modals.dns.fields.respectRules.label': 'label',
'settings.modals.dns.fields.useHosts.label': 'label',
'settings.modals.dns.fields.useSystemHosts.label': 'label',
'settings.modals.dns.fields.directPolicy.label': 'label',
'settings.modals.dns.fields.defaultNameserver.label': 'label',
'settings.modals.dns.fields.nameserver.label': 'label',
'settings.modals.dns.fields.fallback.label': 'label',
'settings.modals.dns.fields.proxy.label': 'label',
'settings.modals.dns.fields.directNameserver.label': 'label',
'settings.modals.dns.fields.fakeIpFilter.label': 'label',
'settings.modals.dns.fields.nameserverPolicy.label': 'label',
'settings.modals.dns.fields.geoipFiltering.label': 'label',
'settings.modals.dns.fields.geoipCode': 'geoipCode',
'settings.modals.dns.fields.fallbackIpCidr.label': 'label',
'settings.modals.dns.fields.fallbackDomain.label': 'label',
'settings.modals.dns.fields.hosts.label': 'label',
}),
...group(dnsReset, {
'shared.actions.resetToDefault': {
id: 'dns-viewer-reset',
},
}),
...group(clash, {
'settings.sections.clash.form.fields.ipv6': 'ipv6 ',
'settings.sections.clash.form.fields.unifiedDelay': 'unified delay ',
'settings.sections.clash.form.fields.logLevel': 'log level ',
'settings.sections.clash.form.fields.portConfig': 'port config ',
}),
...group(clashPort, {
'settings.modals.clashPort.fields.socks': 'clash port viewer',
'settings.modals.clashPort.fields.http': 'clash port viewer',
'settings.modals.clashPort.fields.redir': {
aliases: 'clash port viewer',
platforms: ['macos', 'linux'],
},
'settings.modals.clashPort.fields.tproxy': {
aliases: 'clash port viewer',
platforms: ['linux'],
},
'settings.modals.clashPort.fields.mixed': 'clash port viewer',
'settings.modals.clashPort.actions.random': {
id: 'port-random',
aliases: 'port random',
},
}),
...group(clash, {
'settings.sections.clash.form.fields.external': 'external ',
}),
...group(controller, {
'settings.sections.externalController.fields.enable': 'enable ',
'settings.sections.externalController.fields.address': {
aliases: 'address ',
prerequisite: 'settings.sections.externalController.fields.enable',
},
'settings.sections.externalController.fields.secret': {
aliases: 'secret ',
prerequisite: 'settings.sections.externalController.fields.enable',
},
}),
...group(externalControllerCors, {
'settings.sections.externalCors.fields.allowPrivateNetwork':
'allow private network ',
'settings.sections.externalCors.fields.allowedOrigins': 'allowed origins ',
'settings.sections.externalCors.actions.add': 'add ',
}),
...group(clash, {
'settings.sections.clash.form.fields.webUI': 'web ui ',
}),
...group(webUi, {
'settings.modals.webUI.title': {
id: 'web-ui-viewer',
},
}),
...group(webUiActions, {
'shared.actions.new': {
id: 'web-ui-new',
aliases: 'web ui new',
},
'shared.actions.edit': {
id: 'web-ui-edit',
aliases: 'web ui edit',
},
'shared.actions.delete': {
id: 'web-ui-delete',
aliases: 'web ui delete',
},
'settings.modals.webUI.actions.openUrl': {
id: 'web-ui-open',
aliases: 'web ui open',
},
}),
...group(clash, {
'settings.sections.clash.form.fields.clashCore': 'clash core ',
}),
...group(clashCore, {
'shared.actions.upgrade': {
id: 'core-upgrade',
aliases: 'core upgrade',
},
'shared.actions.restart': {
id: 'core-restart',
aliases: 'core restart',
},
}),
...group(coreVariants, {
'settings.sections.clash.form.fields.clashCore': {
id: 'core-variants',
},
}),
...group(clash, {
'settings.sections.clash.form.fields.openUwpTool': {
aliases: 'open uwp tool ',
platforms: ['windows'],
},
'settings.sections.clash.form.fields.updateGeoData': 'update geo data ',
'settings.sections.clash.form.fields.tunnels.title': 'title ',
}),
...group(tunnels, {
'settings.sections.clash.form.fields.tunnels.existing': 'existing ',
'settings.sections.clash.form.fields.tunnels.default': 'default ',
'settings.sections.clash.form.fields.tunnels.protocols': 'protocols ',
'settings.sections.clash.form.fields.tunnels.localAddr': 'local addr ',
'settings.sections.clash.form.fields.tunnels.localPort': 'local port ',
'settings.sections.clash.form.fields.tunnels.targetAddr': 'target addr ',
'settings.sections.clash.form.fields.tunnels.targetPort': 'target port ',
'settings.sections.clash.form.fields.tunnels.proxyGroup': 'proxy group ',
'settings.sections.clash.form.fields.tunnels.proxyNode': 'proxy node ',
'settings.sections.clash.form.fields.tunnels.actions.add': 'add ',
}),
...group(basic, {
'settings.components.verge.basic.fields.language': 'language ',
'settings.components.verge.basic.fields.themeMode': 'theme mode ',
'settings.components.verge.basic.fields.trayClickEvent': {
aliases: 'tray click event ',
platforms: ['windows', 'macos'],
},
'settings.components.verge.basic.fields.copyEnvType': 'copy env type ',
'settings.components.verge.basic.fields.startPage': 'start page ',
'settings.components.verge.basic.fields.startupScript': 'startup script ',
'settings.components.verge.basic.fields.themeSetting': 'theme setting ',
}),
...group(theme, {
'settings.components.verge.theme.actions.editCss':
'edit css theme 主题 外观',
'settings.components.verge.theme.fields.fontFamily':
'fontFamily theme 主题 外观',
'settings.components.verge.theme.fields.cssInjection':
'cssInjection theme 主题 外观',
'settings.components.verge.theme.fields.primaryColor': 'theme viewer',
'settings.components.verge.theme.fields.secondaryColor': 'theme viewer',
'settings.components.verge.theme.fields.primaryText': 'theme viewer',
'settings.components.verge.theme.fields.secondaryText': 'theme viewer',
'settings.components.verge.theme.fields.infoColor': 'theme viewer',
'settings.components.verge.theme.fields.warningColor': 'theme viewer',
'settings.components.verge.theme.fields.errorColor': 'theme viewer',
'settings.components.verge.theme.fields.successColor': 'theme viewer',
}),
...group(basic, {
'settings.components.verge.basic.fields.layoutSetting': 'layout setting ',
}),
...group(layout, {
'settings.components.verge.basic.actions.browse': '',
}),
...group(layoutFields, {
'settings.components.verge.layout.fields.preferSystemTitlebar':
'preferSystemTitlebar',
'settings.components.verge.layout.fields.trafficGraph': 'trafficGraph',
'settings.components.verge.layout.fields.memoryUsage': 'memoryUsage',
'settings.components.verge.layout.fields.proxyGroupIcon': 'proxyGroupIcon',
'settings.components.verge.layout.fields.pauseRenderTrafficStatsOnBlur':
'pauseRenderTrafficStatsOnBlur',
'settings.components.verge.layout.fields.toastPosition': 'toastPosition',
'settings.components.verge.layout.fields.hoverNavigator': 'hoverNavigator',
'settings.components.verge.layout.fields.hoverNavigatorDelay':
'hoverNavigatorDelay',
'settings.components.verge.layout.fields.navIcon': 'navIcon',
'settings.components.verge.layout.fields.collapseNavBar': 'collapseNavBar',
'settings.components.verge.layout.fields.trayIcon': {
aliases: 'trayIcon',
platforms: ['macos'],
},
'settings.components.verge.layout.fields.enableTraySpeed': {
aliases: 'enableTraySpeed',
platforms: ['macos'],
},
'settings.components.verge.layout.fields.proxyGroupsDisplayMode':
'proxyGroupsDisplayMode',
'settings.components.verge.layout.fields.showOutboundModesInline':
'showOutboundModesInline',
'settings.components.verge.layout.fields.commonTrayIcon': 'commonTrayIcon',
'settings.components.verge.layout.fields.systemProxyTrayIcon':
'systemProxyTrayIcon',
'settings.components.verge.layout.fields.tunTrayIcon': 'tunTrayIcon',
}),
...group(basic, {
'settings.components.verge.basic.fields.misc': 'misc ',
}),
...group(misc, {
'settings.modals.misc.fields.appLogLevel': 'app log level',
'settings.modals.misc.fields.appLogMaxSize': 'app log max size',
'settings.modals.misc.fields.appLogMaxCount': 'app log max count',
'settings.modals.misc.fields.autoCloseConnections':
'auto close connections',
'settings.modals.misc.fields.autoCheckUpdate': 'auto check update',
'settings.modals.misc.fields.enableBuiltinEnhanced':
'enable builtin enhanced',
'settings.modals.misc.fields.proxyLayoutColumns': 'proxy layout columns',
'settings.modals.misc.fields.autoLogClean': 'auto log clean',
'settings.modals.misc.fields.autoDelayDetection': 'auto delay detection',
'settings.modals.misc.fields.autoDelayDetectionInterval': {
aliases: 'auto delay detection interval',
prerequisite: 'settings.modals.misc.fields.autoDelayDetection',
},
'settings.modals.misc.fields.defaultLatencyTest': 'default latency test',
'settings.modals.misc.fields.defaultLatencyTimeout':
'default latency timeout',
}),
...group(basic, {
'settings.components.verge.basic.fields.hotkeySetting': 'hotkey setting ',
}),
...group(hotkey, {
'settings.modals.hotkey.functions.openOrCloseDashboard': 'hotkey viewer',
'settings.modals.hotkey.functions.rule': 'hotkey viewer',
'settings.modals.hotkey.functions.global': 'hotkey viewer',
'settings.modals.hotkey.functions.direct': 'hotkey viewer',
'settings.modals.hotkey.functions.toggleSystemProxy': 'hotkey viewer',
'settings.modals.hotkey.functions.toggleTunMode': 'hotkey viewer',
'settings.modals.hotkey.functions.entryLightweightMode': 'hotkey viewer',
'settings.modals.hotkey.functions.reactivateProfiles': 'hotkey viewer',
'settings.modals.hotkey.toggles.enableGlobal':
'settings.modals.hotkey.toggles.enableGlobal',
}),
...group(advanced, {
'settings.components.verge.advanced.fields.backupSetting':
'backup setting ',
}),
...group(backup, {
'settings.modals.backup.actions.backup': 'backup',
}),
...group(backupHistory, {
'settings.modals.backup.actions.viewHistory': {
aliases: 'view history backup 备份',
target: 'backup-history',
},
}),
...group(backup, {
'settings.modals.backup.actions.importBackup': 'import backup',
}),
...group(backupWebdav, {
'settings.modals.backup.fields.webdavUrl': 'webdavUrl WebDAV 云备份',
'settings.modals.backup.fields.username': 'username WebDAV 云备份',
}),
...group(backup, {
'settings.modals.backup.auto.scheduleLabel': 'scheduleLabel',
'settings.modals.backup.auto.intervalLabel': {
aliases: 'intervalLabel',
prerequisite: 'settings.modals.backup.auto.scheduleLabel',
},
'settings.modals.backup.auto.changeLabel': 'changeLabel',
}),
...group(backupHistory, {
'settings.modals.backup.history.title': {
aliases: '备份 历史 恢复 删除 restore delete history',
target: 'backup-history',
},
}),
...group(backupWebdav, {
'shared.labels.password': {
id: 'webdav-password',
aliases: 'webdav password',
},
'shared.actions.save': {
id: 'webdav-save',
aliases: 'webdav save',
},
'settings.modals.backup.actions.backup': {
id: 'webdav-backup',
aliases: 'webdav backup',
},
'shared.actions.refresh': {
id: 'webdav-refresh',
aliases: 'webdav refresh',
},
}),
...group(backupHistory, {
'settings.modals.backup.actions.deleteBackup': {
id: 'backup-history-delete',
aliases: 'backup history delete',
path: 'settings.modals.backup.history.title',
},
'settings.modals.backup.actions.restoreBackup': {
id: 'backup-history-restore',
aliases: 'backup history restore',
path: 'settings.modals.backup.history.title',
},
'settings.modals.backup.actions.exportBackup': {
id: 'backup-history-export',
aliases: 'backup history export',
path: 'settings.modals.backup.history.title',
},
}),
...group(advanced, {
'settings.components.verge.advanced.fields.runtimeConfig':
'runtime config ',
'settings.components.verge.advanced.fields.openConfDir': 'open conf dir ',
'settings.components.verge.advanced.fields.openCoreDir': 'open core dir ',
'settings.components.verge.advanced.fields.openLogsDir': 'open logs dir ',
'settings.components.verge.advanced.fields.checkUpdates': 'check updates ',
'settings.components.verge.advanced.fields.openDevTools': 'open dev tools ',
'settings.components.verge.advanced.fields.liteModeSettings':
'lite mode settings ',
}),
...group(liteMode, {
'settings.modals.liteMode.actions.enterNow': 'enter now ',
'settings.modals.liteMode.toggles.autoEnter': 'auto enter ',
'settings.modals.liteMode.fields.delay': {
fallback: 'settings.modals.liteMode.toggles.autoEnter',
aliases: 'delay ',
},
}),
...group(advanced, {
'settings.components.verge.advanced.fields.exit': 'exit ',
'settings.components.verge.advanced.fields.exportDiagnostics':
'export diagnostics ',
'settings.components.verge.advanced.fields.vergeVersion': 'verge version ',
}),
]
export function searchSettings(
query: string,
platform: string,
translate: (key: TranslationKey) => string,
english: (key: TranslationKey) => string,
) {
const words = query.trim().toLocaleLowerCase().split(/\s+/).filter(Boolean)
if (!words.length) return []
return settingsCatalog
.filter((e) => !e.platforms || e.platforms.includes(platform))
.map((entry, order) => {
const names = (
translate(entry.name) +
' ' +
english(entry.name)
).toLocaleLowerCase()
const other = (
translate(entry.section) +
' ' +
english(entry.section) +
' ' +
entry.aliases +
' ' +
translate(entry.path) +
' ' +
english(entry.path)
).toLocaleLowerCase()
const score = words.every(
(word) => names.includes(word) || other.includes(word),
)
? words.reduce((sum, word) => sum + (names.includes(word) ? 1 : 0), 0)
: -1
return { entry, order, score }
})
.filter((e) => e.score >= 0)
.sort((a, b) => b.score - a.score || a.order - b.order)
.map((e) => e.entry)
}
@@ -0,0 +1,42 @@
import {
createContext,
use,
useEffect,
useImperativeHandle,
type Ref,
} from 'react'
import type { DialogRef } from '@/components/base'
export interface SearchDialog {
id: string
open: () => void
isOpen: boolean
}
export const SettingsSearchContext = createContext<Set<SearchDialog> | null>(
null,
)
export function useSettingDialog(
ref: Ref<DialogRef> | undefined,
id: string,
open: boolean,
create: () => DialogRef,
) {
useImperativeHandle(ref, create)
const registry = use(SettingsSearchContext)
useEffect(() => {
if (!registry) return
const handle = {
id,
isOpen: open,
open: () => {
if (!open) create().open()
},
}
registry.add(handle)
return () => {
registry.delete(handle)
}
})
}
+317
View File
@@ -0,0 +1,317 @@
import { CloseRounded } from '@mui/icons-material'
import {
GlobalStyles,
Box,
Button,
IconButton,
List,
ListItemButton,
ListItemText,
TextField,
Typography,
} from '@mui/material'
import { useEffect, useRef, useState, type ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
import getSystem from '@/utils/get-system'
import { searchSettings, type SettingEntry } from './settings-catalog'
import {
SettingsSearchContext,
type SearchDialog,
} from './settings-search-context'
export function SettingsSearch({
children,
}: {
children: (
header: ReactNode,
results: ReactNode,
hidden: boolean,
) => ReactNode
}) {
const { t } = useTranslation()
const [registry] = useState(() => new Set<SearchDialog>())
const [query, setQuery] = useState('')
const [selected, setSelected] = useState(0)
const [showPage, setShowPage] = useState(false)
const rootRef = useRef<HTMLDivElement>(null)
const inputRef = useRef<HTMLInputElement>(null)
const originalScrollRef = useRef(0)
const resultScrollRef = useRef(0)
const cancelLocateRef = useRef<() => void>(() => {})
const results = searchSettings(
query,
getSystem(),
(key) => t(key),
(key) => t(key, { lng: 'en' }),
)
const searching = !!query.trim()
const scroller = () =>
rootRef.current?.querySelector('.base-container > section')
const restoreResults = () => {
cancelLocateRef.current()
setShowPage(false)
requestAnimationFrame(() => {
scroller()?.scrollTo({ top: resultScrollRef.current })
inputRef.current?.focus()
})
}
const changeQuery = (value: string) => {
cancelLocateRef.current()
if (!searching && value.trim())
originalScrollRef.current = scroller()?.scrollTop ?? 0
setQuery(value)
setSelected(0)
setShowPage(false)
resultScrollRef.current = 0
requestAnimationFrame(() =>
scroller()?.scrollTo({
top: value.trim() ? 0 : originalScrollRef.current,
}),
)
}
const locate = (entry: SettingEntry) => {
cancelLocateRef.current()
if (!showPage) resultScrollRef.current = scroller()?.scrollTop ?? 0
if (entry.dialog) {
const handles = [...registry].filter(
(handle) => handle.id === entry.dialog,
)
;(handles.find((handle) => handle.isOpen) ?? handles[0])?.open()
} else setShowPage(true)
let highlighted: HTMLElement | null = null
let stopped = false
let sawScope = false
let temporaryTabIndex = false
const cleanup = () => {
stopped = true
observer.disconnect()
clearTimeout(timeout)
highlighted?.removeAttribute('data-search-highlight')
if (temporaryTabIndex) highlighted?.removeAttribute('tabindex')
}
const find = (timedOut = false) => {
if (stopped) return
const marker = entry.dialog
? [
...document.querySelectorAll(
`[data-settings-dialog="${entry.dialog}"]`,
),
].find(
(marker) =>
marker.closest('[role="dialog"]')?.getClientRects().length,
)
: rootRef.current
const scope = entry.dialog ? marker?.closest('[role="dialog"]') : marker
if (!scope) {
if (sawScope) {
cleanup()
inputRef.current?.focus()
}
return
}
if (highlighted) return
sawScope = true
const pending = marker?.getAttribute('data-search-ready') === 'false'
if (pending && !timedOut) return
let target = pending
? (scope.querySelector<HTMLElement>('[data-settings-loading]') ??
(marker instanceof HTMLElement ? marker : null))
: scope.querySelector<HTMLElement>(
`[data-setting-target="${entry.target ?? entry.id}"]`,
)
const fields = target
? [
...target.querySelectorAll<HTMLElement>(
'input,textarea,select,[role="combobox"]',
),
]
: []
const disabled =
target?.matches(':disabled,[aria-disabled="true"]') ||
(fields.length > 0 &&
fields.every((field) =>
field.matches(':disabled,[aria-disabled="true"]'),
))
let fallback = pending
if (!target || !target.getClientRects().length || disabled) {
const fallbackId = disabled
? (entry.prerequisite ?? entry.fallback)
: entry.fallback
if (fallbackId) {
target = scope.querySelector<HTMLElement>(
`[data-setting-target="${fallbackId}"]`,
)
fallback = !disabled || !entry.prerequisite
}
}
if (!target || !target.getClientRects().length) return
highlighted = target
target.setAttribute('data-search-highlight', 'true')
target.scrollIntoView({ block: 'center' })
const targetFocusable = target.matches(
'button,input,textarea,select,a[href],[tabindex]',
)
const control =
!fallback && !targetFocusable
? [
...target.querySelectorAll<HTMLElement>(
'input:not(:disabled),textarea:not(:disabled),button:not(:disabled),[role="combobox"],a[href]',
),
].find(
(element) =>
element.getClientRects().length &&
element.getAttribute('aria-disabled') !== 'true',
)
: null
temporaryTabIndex = !control && !targetFocusable
if (temporaryTabIndex) target.tabIndex = -1
;(control ?? target).focus({ preventScroll: true })
}
const observer = new MutationObserver(() => find())
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['style', 'class', 'data-search-ready', 'disabled'],
})
const timeout = setTimeout(() => {
if (!highlighted) {
find(true)
if (!highlighted) cleanup()
}
}, 5000)
cancelLocateRef.current = cleanup
requestAnimationFrame(() => find())
}
useEffect(() => () => cancelLocateRef.current(), [])
const moveSelection = (
from: number,
direction: number,
focusResult = false,
) => {
const next = (from + direction + results.length) % results.length
setSelected(next)
const target = rootRef.current?.querySelector<HTMLElement>(
`[data-search-result="${next}"]`,
)
if (focusResult) target?.focus()
target?.scrollIntoView({ block: 'nearest' })
}
const header = (
<Box
sx={{ display: 'flex', gap: 1, alignItems: 'center', flexWrap: 'wrap' }}
>
<TextField
inputRef={inputRef}
size="small"
label={t('settings.search.placeholder')}
value={query}
onChange={(e) => changeQuery(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Escape') {
e.preventDefault()
if (showPage) restoreResults()
else changeQuery('')
}
if (e.key === 'Enter' && results[selected]) {
e.preventDefault()
locate(results[selected])
}
if (
(e.key === 'ArrowDown' || e.key === 'ArrowUp') &&
results.length
) {
e.preventDefault()
moveSelection(selected, e.key === 'ArrowDown' ? 1 : -1)
}
}}
/>
{searching && (
<IconButton
aria-label={t('settings.search.clear')}
onClick={() => changeQuery('')}
>
<CloseRounded />
</IconButton>
)}
{showPage && (
<Button onClick={restoreResults}>{t('settings.search.back')}</Button>
)}
</Box>
)
const resultView =
searching && !showPage ? (
<List aria-label={t('settings.search.results')}>
{results.length ? (
results.map((entry, index) => (
<ListItemButton
key={entry.id}
data-search-result={index}
selected={selected === index}
onFocus={() => setSelected(index)}
onClick={() => {
setSelected(index)
locate(entry)
}}
onKeyDown={(e) => {
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
e.preventDefault()
moveSelection(index, e.key === 'ArrowDown' ? 1 : -1, true)
}
if (e.key === 'Escape') {
e.preventDefault()
inputRef.current?.focus()
}
}}
>
<ListItemText
primary={t(entry.name)}
secondary={`${t(entry.section)} / ${t(entry.path)}`}
/>
</ListItemButton>
))
) : (
<Typography sx={{ p: 2 }}>{t('settings.search.empty')}</Typography>
)}
</List>
) : null
return (
<SettingsSearchContext value={registry}>
<GlobalStyles
styles={{
'[data-search-highlight]': {
outline: '2px solid #6b70e8',
outlineOffset: -2,
},
}}
/>
<Box
ref={rootRef}
onKeyDown={(e) => {
if (
e.key === 'Escape' &&
showPage &&
!(e.target as HTMLElement).closest('[role="dialog"]')
) {
e.stopPropagation()
restoreResults()
}
}}
sx={{
height: '100%',
'& .base-page > header': {
flex: '0 0 auto',
minHeight: 58,
flexWrap: 'wrap',
gap: 1,
py: 1,
},
}}
>
{children(header, resultView, searching && !showPage)}
</Box>
</SettingsSearchContext>
)
}
@@ -220,6 +220,7 @@ const ProxyControlSwitches = ({
)}
icon={DeleteForeverRounded}
color="secondary"
data-setting-target="service-uninstall"
onClick={onUninstallService}
sx={{ ml: 1 }}
/>
+7
View File
@@ -714,5 +714,12 @@
"installing": "جاري تثبيت الخدمة...",
"uninstalling": "Uninstalling Service..."
}
},
"search": {
"placeholder": "البحث في الإعدادات",
"clear": "مسح البحث",
"back": "العودة إلى النتائج",
"results": "نتائج البحث",
"empty": "لم يتم العثور على إعدادات"
}
}
+7
View File
@@ -714,5 +714,12 @@
"installing": "Service wird installiert...",
"uninstalling": "Service wird deinstalliert..."
}
},
"search": {
"placeholder": "Einstellungen suchen",
"clear": "Suche löschen",
"back": "Zurück zu Ergebnissen",
"results": "Suchergebnisse",
"empty": "Keine Einstellungen gefunden"
}
}
+7
View File
@@ -714,5 +714,12 @@
"installing": "Installing Service...",
"uninstalling": "Uninstalling Service..."
}
},
"search": {
"placeholder": "Search settings",
"clear": "Clear search",
"back": "Back to results",
"results": "Search results",
"empty": "No settings found"
}
}
+7
View File
@@ -714,5 +714,12 @@
"installing": "Instalando servicio...",
"uninstalling": "Desinstalando servicio..."
}
},
"search": {
"placeholder": "Buscar ajustes",
"clear": "Borrar búsqueda",
"back": "Volver a resultados",
"results": "Resultados de búsqueda",
"empty": "No se encontraron ajustes"
}
}
+7
View File
@@ -714,5 +714,12 @@
"installing": "در حال نصب سرویس...",
"uninstalling": "در حال حذف سرویس..."
}
},
"search": {
"placeholder": "جستجوی تنظیمات",
"clear": "پاک کردن جستجو",
"back": "بازگشت به نتایج",
"results": "نتایج جستجو",
"empty": "تنظیماتی یافت نشد"
}
}
+7
View File
@@ -714,5 +714,12 @@
"installing": "Memasang Layanan...",
"uninstalling": "Uninstalling Service..."
}
},
"search": {
"placeholder": "Cari pengaturan",
"clear": "Hapus pencarian",
"back": "Kembali ke hasil",
"results": "Hasil pencarian",
"empty": "Pengaturan tidak ditemukan"
}
}
+7
View File
@@ -714,5 +714,12 @@
"installing": "サービスをインストール中...",
"uninstalling": "サービスをアンインストール中..."
}
},
"search": {
"placeholder": "設定を検索",
"clear": "検索をクリア",
"back": "結果に戻る",
"results": "検索結果",
"empty": "設定が見つかりません"
}
}
+7
View File
@@ -714,5 +714,12 @@
"installing": "서비스 설치 중...",
"uninstalling": "서비스 제거 중..."
}
},
"search": {
"placeholder": "설정 검색",
"clear": "검색 지우기",
"back": "결과로 돌아가기",
"results": "검색 결과",
"empty": "설정을 찾을 수 없습니다"
}
}
+7
View File
@@ -714,5 +714,12 @@
"installing": "Instalando o Serviço...",
"uninstalling": "Desinstalando o Serviço..."
}
},
"search": {
"placeholder": "Pesquisar configurações",
"clear": "Limpar pesquisa",
"back": "Voltar aos resultados",
"results": "Resultados da pesquisa",
"empty": "Nenhuma configuração encontrada"
}
}
+7
View File
@@ -714,5 +714,12 @@
"installing": "Установка службы...",
"uninstalling": "Удаление службы..."
}
},
"search": {
"placeholder": "Поиск настроек",
"clear": "Очистить поиск",
"back": "Вернуться к результатам",
"results": "Результаты поиска",
"empty": "Настройки не найдены"
}
}
+7
View File
@@ -714,5 +714,12 @@
"installing": "Hizmet Kuruluyor...",
"uninstalling": "Hizmet Kaldırılıyor..."
}
},
"search": {
"placeholder": "Ayarları ara",
"clear": "Aramayı temizle",
"back": "Sonuçlara dön",
"results": "Arama sonuçları",
"empty": "Ayar bulunamadı"
}
}
+7
View File
@@ -714,5 +714,12 @@
"installing": "Хезмәт урнаштырыла...",
"uninstalling": "Uninstalling Service..."
}
},
"search": {
"placeholder": "Көйләүләрне эзләү",
"clear": "Эзләүне чистарту",
"back": "Нәтиҗәләргә кайту",
"results": "Эзләү нәтиҗәләре",
"empty": "Көйләүләр табылмады"
}
}
+7
View File
@@ -714,5 +714,12 @@
"installing": "安装服务中...",
"uninstalling": "卸载服务中..."
}
},
"search": {
"placeholder": "搜索设置",
"clear": "清空搜索",
"back": "返回结果",
"results": "搜索结果",
"empty": "未找到设置"
}
}
+7
View File
@@ -714,5 +714,12 @@
"installing": "安裝服務中...",
"uninstalling": "服務解除安裝中..."
}
},
"search": {
"placeholder": "搜尋設定",
"clear": "清除搜尋",
"back": "返回結果",
"results": "搜尋結果",
"empty": "找不到設定"
}
}
+95 -74
View File
@@ -8,6 +8,7 @@ import SettingClash from '@/components/setting/setting-clash'
import SettingSystem from '@/components/setting/setting-system'
import SettingVergeAdvanced from '@/components/setting/setting-verge-advanced'
import SettingVergeBasic from '@/components/setting/setting-verge-basic'
import { SettingsSearch } from '@/components/setting/settings-search'
import { showNotice } from '@/services/notice-service'
import { useThemeMode } from '@/services/states'
import { openExternalUrl } from '@/utils/open-external-url'
@@ -39,82 +40,102 @@ const SettingPage = () => {
const isDark = mode === 'light' ? false : true
return (
<BasePage
title={t('settings.page.title')}
header={
<ButtonGroup
variant="contained"
aria-label={t('settings.page.actionsGroupLabel')}
>
<IconButton
size="medium"
color="inherit"
title={t('settings.page.actions.manual')}
onClick={toGithubDoc}
>
<HelpOutlineRounded fontSize="inherit" />
</IconButton>
<IconButton
size="medium"
color="inherit"
title={t('settings.page.actions.telegram')}
onClick={toTelegramChannel}
>
<Telegram fontSize="inherit" />
</IconButton>
<SettingsSearch>
{(searchHeader, results, hidden) => (
<BasePage
title={t('settings.page.title')}
header={
<Box
sx={{
display: 'flex',
gap: 1,
flexWrap: 'wrap',
alignItems: 'center',
}}
>
{searchHeader}
<ButtonGroup
variant="contained"
aria-label={t('settings.page.actionsGroupLabel')}
>
<IconButton
size="medium"
color="inherit"
title={t('settings.page.actions.manual')}
onClick={toGithubDoc}
>
<HelpOutlineRounded fontSize="inherit" />
</IconButton>
<IconButton
size="medium"
color="inherit"
title={t('settings.page.actions.telegram')}
onClick={toTelegramChannel}
>
<Telegram fontSize="inherit" />
</IconButton>
<IconButton
size="medium"
color="inherit"
title={t('settings.page.actions.github')}
onClick={toGithubRepo}
<IconButton
size="medium"
color="inherit"
title={t('settings.page.actions.github')}
onClick={toGithubRepo}
>
<GitHub fontSize="inherit" />
</IconButton>
</ButtonGroup>
</Box>
}
>
{results}
<Grid
container
spacing={1.5}
columns={{ xs: 6, sm: 6, md: 12 }}
sx={{ display: hidden ? 'none' : undefined }}
>
<GitHub fontSize="inherit" />
</IconButton>
</ButtonGroup>
}
>
<Grid container spacing={1.5} columns={{ xs: 6, sm: 6, md: 12 }}>
<Grid size={6}>
<Box
sx={{
borderRadius: 2,
marginBottom: 1.5,
backgroundColor: isDark ? '#282a36' : '#ffffff',
}}
>
<SettingSystem onError={onError} />
</Box>
<Box
sx={{
borderRadius: 2,
backgroundColor: isDark ? '#282a36' : '#ffffff',
}}
>
<SettingClash onError={onError} />
</Box>
</Grid>
<Grid size={6}>
<Box
sx={{
borderRadius: 2,
marginBottom: 1.5,
backgroundColor: isDark ? '#282a36' : '#ffffff',
}}
>
<SettingVergeBasic onError={onError} />
</Box>
<Box
sx={{
borderRadius: 2,
backgroundColor: isDark ? '#282a36' : '#ffffff',
}}
>
<SettingVergeAdvanced onError={onError} />
</Box>
</Grid>
</Grid>
</BasePage>
<Grid size={6}>
<Box
sx={{
borderRadius: 2,
marginBottom: 1.5,
backgroundColor: isDark ? '#282a36' : '#ffffff',
}}
>
<SettingSystem onError={onError} />
</Box>
<Box
sx={{
borderRadius: 2,
backgroundColor: isDark ? '#282a36' : '#ffffff',
}}
>
<SettingClash onError={onError} />
</Box>
</Grid>
<Grid size={6}>
<Box
sx={{
borderRadius: 2,
marginBottom: 1.5,
backgroundColor: isDark ? '#282a36' : '#ffffff',
}}
>
<SettingVergeBasic onError={onError} />
</Box>
<Box
sx={{
borderRadius: 2,
backgroundColor: isDark ? '#282a36' : '#ffffff',
}}
>
<SettingVergeAdvanced onError={onError} />
</Box>
</Grid>
</Grid>
</BasePage>
)}
</SettingsSearch>
)
}
+5
View File
@@ -827,6 +827,11 @@ export const translationKeys = [
'settings.statuses.clash.restarting',
'settings.statuses.clashService.installing',
'settings.statuses.clashService.uninstalling',
'settings.search.placeholder',
'settings.search.clear',
'settings.search.back',
'settings.search.results',
'settings.search.empty',
'shared.actions.cancel',
'shared.actions.close',
'shared.actions.confirm',
+7
View File
@@ -1251,6 +1251,13 @@ export interface TranslationResources {
actionsGroupLabel: string
title: string
}
search: {
back: string
clear: string
empty: string
placeholder: string
results: string
}
sections: {
appearance: {
dark: string