mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-07 01:22:15 +08:00
feat(settings): add searchable settings navigation
This commit is contained in:
@@ -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" />
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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] ?? []}
|
||||
|
||||
@@ -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')}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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}
|
||||
|
||||
@@ -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')}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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()}
|
||||
/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')}
|
||||
/>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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 }}
|
||||
/>
|
||||
|
||||
@@ -714,5 +714,12 @@
|
||||
"installing": "جاري تثبيت الخدمة...",
|
||||
"uninstalling": "Uninstalling Service..."
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "البحث في الإعدادات",
|
||||
"clear": "مسح البحث",
|
||||
"back": "العودة إلى النتائج",
|
||||
"results": "نتائج البحث",
|
||||
"empty": "لم يتم العثور على إعدادات"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -714,5 +714,12 @@
|
||||
"installing": "در حال نصب سرویس...",
|
||||
"uninstalling": "در حال حذف سرویس..."
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "جستجوی تنظیمات",
|
||||
"clear": "پاک کردن جستجو",
|
||||
"back": "بازگشت به نتایج",
|
||||
"results": "نتایج جستجو",
|
||||
"empty": "تنظیماتی یافت نشد"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -714,5 +714,12 @@
|
||||
"installing": "サービスをインストール中...",
|
||||
"uninstalling": "サービスをアンインストール中..."
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "設定を検索",
|
||||
"clear": "検索をクリア",
|
||||
"back": "結果に戻る",
|
||||
"results": "検索結果",
|
||||
"empty": "設定が見つかりません"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -714,5 +714,12 @@
|
||||
"installing": "서비스 설치 중...",
|
||||
"uninstalling": "서비스 제거 중..."
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "설정 검색",
|
||||
"clear": "검색 지우기",
|
||||
"back": "결과로 돌아가기",
|
||||
"results": "검색 결과",
|
||||
"empty": "설정을 찾을 수 없습니다"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -714,5 +714,12 @@
|
||||
"installing": "Установка службы...",
|
||||
"uninstalling": "Удаление службы..."
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Поиск настроек",
|
||||
"clear": "Очистить поиск",
|
||||
"back": "Вернуться к результатам",
|
||||
"results": "Результаты поиска",
|
||||
"empty": "Настройки не найдены"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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ı"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -714,5 +714,12 @@
|
||||
"installing": "Хезмәт урнаштырыла...",
|
||||
"uninstalling": "Uninstalling Service..."
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Көйләүләрне эзләү",
|
||||
"clear": "Эзләүне чистарту",
|
||||
"back": "Нәтиҗәләргә кайту",
|
||||
"results": "Эзләү нәтиҗәләре",
|
||||
"empty": "Көйләүләр табылмады"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -714,5 +714,12 @@
|
||||
"installing": "安装服务中...",
|
||||
"uninstalling": "卸载服务中..."
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "搜索设置",
|
||||
"clear": "清空搜索",
|
||||
"back": "返回结果",
|
||||
"results": "搜索结果",
|
||||
"empty": "未找到设置"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -714,5 +714,12 @@
|
||||
"installing": "安裝服務中...",
|
||||
"uninstalling": "服務解除安裝中..."
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "搜尋設定",
|
||||
"clear": "清除搜尋",
|
||||
"back": "返回結果",
|
||||
"results": "搜尋結果",
|
||||
"empty": "找不到設定"
|
||||
}
|
||||
}
|
||||
|
||||
+95
-74
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user