feat(settings): move the remark model control to the subscription tab

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
5 changed files with 73 additions and 55 deletions

View File

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

View File

@@ -5,7 +5,6 @@ import {
Input,
InputNumber,
Select,
Space,
Switch,
} from 'antd';
import type { AllSetting } from '@/models/setting';
@@ -23,8 +22,6 @@ interface GeneralTabProps {
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' }[] = [
{ name: 'Gregorian (Standard)', value: 'gregorian' },
{ name: 'Jalalian (شمسی)', value: 'jalalian' },
@@ -57,30 +54,6 @@ export default function GeneralTab({ allSetting, updateSetting }: GeneralTabProp
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 csv = allSetting.ldapInboundTags || '';
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'),
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')}>
<Input value={allSetting.webListen} onChange={(e) => updateSetting({ webListen: e.target.value })} />
</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 type { AllSetting } from '@/models/setting';
import { SettingListItem } from '@/components/ui';
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 {
allSetting: AllSetting;
updateSetting: (patch: Partial<AllSetting>) => void;
@@ -12,6 +17,30 @@ interface SubscriptionGeneralTabProps {
export default function SubscriptionGeneralTab({ allSetting, updateSetting }: SubscriptionGeneralTabProps) {
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 (
<Collapse defaultActiveKey="1" items={[
{
@@ -68,6 +97,44 @@ export default function SubscriptionGeneralTab({ allSetting, updateSetting }: Su
<Switch checked={allSetting.subEmailInRemark} onChange={(v) => updateSetting({ subEmailInRemark: v })} />
</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>
<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 { 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 { pauseAnimationsUntilLeave, useTheme } from '@/hooks/useTheme';
import SubUsageSummary from './SubUsageSummary';
@@ -396,7 +396,7 @@ export default function SubPage() {
{links.map((link, idx) => {
const parts = parseLinkParts(link, linkEmails[idx] || '');
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 canQr = !isPostQuantumLink(link);
return (

View File

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