feat(settings): move the remark model control to the subscription tab
Some checks are pending
CI / go-test (push) Waiting to run
CI / govulncheck (push) Waiting to run
CI / frontend (push) Waiting to run
CodeQL Advanced / Analyze (go) (push) Waiting to run
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
Release 3X-UI / Build for Windows (push) Waiting to run

Relocate Remark Model & Separation Character from the General/Panel tab to the Subscription tab's Information section, beside Show Info and Email in Remark, since it only governs how share-link remarks are composed. The sample preview uses concrete example values and renders the separator literally.

Also drop the port from the subscription page link rows so each row shows just the inbound remark; the port still appears in the client QR modal and the client info modal.
This commit is contained in:
MHSanaei 2026-06-03 02:45:16 +02:00
parent d0998c1d6d
commit e63cde8fcb
No known key found for this signature in database
GPG key ID: 7E4060F2FBE5AB7A
5 changed files with 73 additions and 55 deletions

View file

@ -34,7 +34,7 @@ export class AllSetting {
subSupportUrl = ''; subSupportUrl = '';
subProfileUrl = ''; subProfileUrl = '';
subAnnounce = ''; subAnnounce = '';
subEnableRouting = true; subEnableRouting = false;
subRoutingRules = ''; subRoutingRules = '';
subListen = ''; subListen = '';
subPort = 2096; subPort = 2096;

View file

@ -5,7 +5,6 @@ import {
Input, Input,
InputNumber, InputNumber,
Select, Select,
Space,
Switch, Switch,
} from 'antd'; } from 'antd';
import type { AllSetting } from '@/models/setting'; import type { AllSetting } from '@/models/setting';
@ -23,8 +22,6 @@ interface GeneralTabProps {
updateSetting: (patch: Partial<AllSetting>) => void; updateSetting: (patch: Partial<AllSetting>) => void;
} }
const REMARK_MODELS: Record<string, string> = { i: 'Inbound', e: 'Email', o: 'Other' };
const REMARK_SEPARATORS = [' ', '-', '_', '@', ':', '~', '|', ',', '.', '/'];
const DATEPICKER_LIST: { name: string; value: 'gregorian' | 'jalalian' }[] = [ const DATEPICKER_LIST: { name: string; value: 'gregorian' | 'jalalian' }[] = [
{ name: 'Gregorian (Standard)', value: 'gregorian' }, { name: 'Gregorian (Standard)', value: 'gregorian' },
{ name: 'Jalalian (شمسی)', value: 'jalalian' }, { name: 'Jalalian (شمسی)', value: 'jalalian' },
@ -57,30 +54,6 @@ export default function GeneralTab({ allSetting, updateSetting }: GeneralTabProp
return () => { cancelled = true; }; return () => { cancelled = true; };
}, []); }, []);
const remarkModel = useMemo(() => {
const rm = allSetting.remarkModel || '';
return rm.length > 1 ? rm.substring(1).split('') : [];
}, [allSetting.remarkModel]);
const remarkSeparator = useMemo(() => {
const rm = allSetting.remarkModel || '-';
return rm.length > 1 ? rm.charAt(0) : '-';
}, [allSetting.remarkModel]);
const remarkSample = useMemo(() => {
const parts = remarkModel.map((k) => REMARK_MODELS[k]);
return parts.length === 0 ? '' : parts.join(remarkSeparator);
}, [remarkModel, remarkSeparator]);
function setRemarkModel(parts: string[]) {
updateSetting({ remarkModel: remarkSeparator + parts.join('') });
}
function setRemarkSeparator(sep: string) {
const tail = (allSetting.remarkModel || '-').substring(1);
updateSetting({ remarkModel: sep + tail });
}
const ldapInboundTagList = useMemo(() => { const ldapInboundTagList = useMemo(() => {
const csv = allSetting.ldapInboundTags || ''; const csv = allSetting.ldapInboundTags || '';
return csv.length ? csv.split(',').map((s) => s.trim()).filter(Boolean) : []; return csv.length ? csv.split(',').map((s) => s.trim()).filter(Boolean) : [];
@ -115,28 +88,6 @@ export default function GeneralTab({ allSetting, updateSetting }: GeneralTabProp
label: t('pages.settings.panelSettings'), label: t('pages.settings.panelSettings'),
children: ( children: (
<> <>
<SettingListItem
paddings="small"
title={t('pages.settings.remarkModel')}
description={<>{t('pages.settings.sampleRemark')}: <i>#{remarkSample}</i></>}
>
<Space.Compact style={{ width: '100%' }}>
<Select
mode="multiple"
value={remarkModel}
onChange={setRemarkModel}
style={{ paddingRight: '.5rem', minWidth: '80%', width: 'auto' }}
options={Object.entries(REMARK_MODELS).map(([k, l]) => ({ value: k, label: l }))}
/>
<Select
value={remarkSeparator}
onChange={setRemarkSeparator}
style={{ width: '20%' }}
options={REMARK_SEPARATORS.map((s) => ({ value: s, label: s }))}
/>
</Space.Compact>
</SettingListItem>
<SettingListItem paddings="small" title={t('pages.settings.panelListeningIP')} description={t('pages.settings.panelListeningIPDesc')}> <SettingListItem paddings="small" title={t('pages.settings.panelListeningIP')} description={t('pages.settings.panelListeningIPDesc')}>
<Input value={allSetting.webListen} onChange={(e) => updateSetting({ webListen: e.target.value })} /> <Input value={allSetting.webListen} onChange={(e) => updateSetting({ webListen: e.target.value })} />
</SettingListItem> </SettingListItem>

View file

@ -1,9 +1,14 @@
import { Collapse, Divider, Input, InputNumber, Switch } from 'antd'; import { useMemo } from 'react';
import { Collapse, Divider, Input, InputNumber, Select, Space, Switch } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type { AllSetting } from '@/models/setting'; import type { AllSetting } from '@/models/setting';
import { SettingListItem } from '@/components/ui'; import { SettingListItem } from '@/components/ui';
import { sanitizePath, normalizePath } from './uriPath'; import { sanitizePath, normalizePath } from './uriPath';
const REMARK_MODELS: Record<string, string> = { i: 'Inbound', e: 'Email', o: 'Other' };
const REMARK_SAMPLES: Record<string, string> = { i: 'Germany', e: 'john', o: 'Relay' };
const REMARK_SEPARATORS = [' ', '-', '_', '@', ':', '~', '|', ',', '.', '/'];
interface SubscriptionGeneralTabProps { interface SubscriptionGeneralTabProps {
allSetting: AllSetting; allSetting: AllSetting;
updateSetting: (patch: Partial<AllSetting>) => void; updateSetting: (patch: Partial<AllSetting>) => void;
@ -12,6 +17,30 @@ interface SubscriptionGeneralTabProps {
export default function SubscriptionGeneralTab({ allSetting, updateSetting }: SubscriptionGeneralTabProps) { export default function SubscriptionGeneralTab({ allSetting, updateSetting }: SubscriptionGeneralTabProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const remarkModel = useMemo(() => {
const rm = allSetting.remarkModel || '';
return rm.length > 1 ? rm.substring(1).split('') : [];
}, [allSetting.remarkModel]);
const remarkSeparator = useMemo(() => {
const rm = allSetting.remarkModel || '-';
return rm.length > 1 ? rm.charAt(0) : '-';
}, [allSetting.remarkModel]);
const remarkSample = useMemo(() => {
const parts = remarkModel.map((k) => REMARK_SAMPLES[k]);
return parts.length === 0 ? '' : parts.join(remarkSeparator);
}, [remarkModel, remarkSeparator]);
function setRemarkModel(parts: string[]) {
updateSetting({ remarkModel: remarkSeparator + parts.join('') });
}
function setRemarkSeparator(sep: string) {
const tail = (allSetting.remarkModel || '-').substring(1);
updateSetting({ remarkModel: sep + tail });
}
return ( return (
<Collapse defaultActiveKey="1" items={[ <Collapse defaultActiveKey="1" items={[
{ {
@ -68,6 +97,44 @@ export default function SubscriptionGeneralTab({ allSetting, updateSetting }: Su
<Switch checked={allSetting.subEmailInRemark} onChange={(v) => updateSetting({ subEmailInRemark: v })} /> <Switch checked={allSetting.subEmailInRemark} onChange={(v) => updateSetting({ subEmailInRemark: v })} />
</SettingListItem> </SettingListItem>
<SettingListItem
paddings="small"
title={t('pages.settings.remarkModel')}
description={
<>
{t('pages.settings.sampleRemark')}:{' '}
<span
style={{
fontFamily: 'monospace',
padding: '1px 6px',
borderRadius: 4,
border: '1px solid var(--ant-color-border)',
background: 'var(--ant-color-fill-tertiary)',
whiteSpace: 'pre',
}}
>
{remarkSample ? `#${remarkSample}` : '—'}
</span>
</>
}
>
<Space.Compact style={{ width: '100%' }}>
<Select
mode="multiple"
value={remarkModel}
onChange={setRemarkModel}
style={{ paddingRight: '.5rem', minWidth: '80%', width: 'auto' }}
options={Object.entries(REMARK_MODELS).map(([k, l]) => ({ value: k, label: l }))}
/>
<Select
value={remarkSeparator}
onChange={setRemarkSeparator}
style={{ width: '20%' }}
options={REMARK_SEPARATORS.map((s) => ({ value: s, label: s === ' ' ? '␣' : s }))}
/>
</Space.Compact>
</SettingListItem>
<Divider>{t('pages.settings.subTitle')}</Divider> <Divider>{t('pages.settings.subTitle')}</Divider>
<SettingListItem paddings="small" title={t('pages.settings.subTitle')} description={t('pages.settings.subTitleDesc')}> <SettingListItem paddings="small" title={t('pages.settings.subTitle')} description={t('pages.settings.subTitleDesc')}>

View file

@ -32,7 +32,7 @@ import {
import { ClipboardManager, IntlUtil, LanguageManager } from '@/utils'; import { ClipboardManager, IntlUtil, LanguageManager } from '@/utils';
import { isPostQuantumLink } from '@/lib/xray/inbound-link'; import { isPostQuantumLink } from '@/lib/xray/inbound-link';
import { LinkTags, linkMetaText, parseLinkParts } from '@/lib/xray/link-label'; import { LinkTags, parseLinkParts } from '@/lib/xray/link-label';
import { setMessageInstance } from '@/utils/messageBus'; import { setMessageInstance } from '@/utils/messageBus';
import { pauseAnimationsUntilLeave, useTheme } from '@/hooks/useTheme'; import { pauseAnimationsUntilLeave, useTheme } from '@/hooks/useTheme';
import SubUsageSummary from './SubUsageSummary'; import SubUsageSummary from './SubUsageSummary';
@ -396,7 +396,7 @@ export default function SubPage() {
{links.map((link, idx) => { {links.map((link, idx) => {
const parts = parseLinkParts(link, linkEmails[idx] || ''); const parts = parseLinkParts(link, linkEmails[idx] || '');
const fallback = `Link ${idx + 1}`; const fallback = `Link ${idx + 1}`;
const rowTitle = (parts && linkMetaText(parts)) || fallback; const rowTitle = parts?.remark || fallback;
const qrLabel = [parts?.remark, linkEmails[idx]].filter(Boolean).join('-') || rowTitle; const qrLabel = [parts?.remark, linkEmails[idx]].filter(Boolean).join('-') || rowTitle;
const canQr = !isPostQuantumLink(link); const canQr = !isPostQuantumLink(link);
return ( return (

View file

@ -61,7 +61,7 @@ var defaultValueMap = map[string]string{
"subSupportUrl": "", "subSupportUrl": "",
"subProfileUrl": "", "subProfileUrl": "",
"subAnnounce": "", "subAnnounce": "",
"subEnableRouting": "true", "subEnableRouting": "false",
"subRoutingRules": "", "subRoutingRules": "",
"subListen": "", "subListen": "",
"subPort": "2096", "subPort": "2096",
@ -76,7 +76,7 @@ var defaultValueMap = map[string]string{
"subURI": "", "subURI": "",
"subJsonPath": "/json/", "subJsonPath": "/json/",
"subJsonURI": "", "subJsonURI": "",
"subClashEnable": "true", "subClashEnable": "false",
"subClashPath": "/clash/", "subClashPath": "/clash/",
"subClashURI": "", "subClashURI": "",
"subJsonFragment": "", "subJsonFragment": "",