|
|
@@ -8,6 +8,7 @@ import {
|
|
|
RocketOutlined,
|
|
|
SendOutlined,
|
|
|
SettingOutlined,
|
|
|
+ StopOutlined,
|
|
|
} from '@ant-design/icons';
|
|
|
import type { AllSetting } from '@/models/setting';
|
|
|
import { onNumber } from '@/utils/onNumber';
|
|
|
@@ -31,10 +32,17 @@ const DEFAULT_MUX = {
|
|
|
xudpConcurrency: 16,
|
|
|
xudpProxyUDP443: 'reject',
|
|
|
};
|
|
|
-const DEFAULT_RULES: { type: string; outboundTag: string; domain?: string[]; ip?: string[] }[] = [
|
|
|
+
|
|
|
+type SubJsonRule = { type: string; outboundTag: string; domain?: string[]; ip?: string[] };
|
|
|
+
|
|
|
+const DEFAULT_DIRECT_RULES: SubJsonRule[] = [
|
|
|
{ type: 'field', outboundTag: 'direct', domain: ['geosite:category-ir'] },
|
|
|
{ type: 'field', outboundTag: 'direct', ip: ['geoip:private', 'geoip:ir'] },
|
|
|
];
|
|
|
+const DEFAULT_BLOCK_RULES: SubJsonRule[] = [
|
|
|
+ { type: 'field', outboundTag: 'block', domain: ['geosite:category-ads-all'] },
|
|
|
+];
|
|
|
+const BLOCK_IP_RULE: SubJsonRule = { type: 'field', outboundTag: 'block', ip: [] };
|
|
|
|
|
|
const directIPsOptions = [
|
|
|
{ label: 'Private IP', value: 'geoip:private' },
|
|
|
@@ -57,6 +65,10 @@ const directDomainsOptions = [
|
|
|
{ label: 'Meta', value: 'geosite:meta' },
|
|
|
{ label: 'Google', value: 'geosite:google' },
|
|
|
];
|
|
|
+const blockDomainsOptions = [
|
|
|
+ { label: 'Ads All', value: 'geosite:category-ads-all' },
|
|
|
+ { label: 'Adult +18', value: 'geosite:category-porn' },
|
|
|
+];
|
|
|
|
|
|
function readJson<T>(raw: string, fallback: T): T {
|
|
|
try {
|
|
|
@@ -67,6 +79,11 @@ function readJson<T>(raw: string, fallback: T): T {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+function readRules(raw: string): SubJsonRule[] {
|
|
|
+ const parsed = readJson<unknown>(raw, null);
|
|
|
+ return Array.isArray(parsed) ? (parsed as SubJsonRule[]) : [];
|
|
|
+}
|
|
|
+
|
|
|
export default function SubscriptionFormatsTab({
|
|
|
allSetting,
|
|
|
updateSetting,
|
|
|
@@ -75,7 +92,6 @@ export default function SubscriptionFormatsTab({
|
|
|
const { isMobile } = useMediaQuery();
|
|
|
|
|
|
const muxEnabled = allSetting.subJsonMux !== '';
|
|
|
- const directEnabled = allSetting.subJsonRules !== '';
|
|
|
|
|
|
const muxObj = useMemo(
|
|
|
() =>
|
|
|
@@ -92,57 +108,48 @@ export default function SubscriptionFormatsTab({
|
|
|
updateSetting({ subJsonMux: JSON.stringify(next) });
|
|
|
}
|
|
|
|
|
|
- const ruleArray = useMemo(() => {
|
|
|
- if (!directEnabled) return null;
|
|
|
- return readJson<typeof DEFAULT_RULES | null>(allSetting.subJsonRules, null);
|
|
|
- }, [allSetting.subJsonRules, directEnabled]);
|
|
|
-
|
|
|
- const directIPs = useMemo(() => {
|
|
|
- if (!ruleArray) return [];
|
|
|
- const ipRule = ruleArray.find((r) => r.ip);
|
|
|
- return ipRule?.ip ?? [];
|
|
|
- }, [ruleArray]);
|
|
|
+ const ruleArray = useMemo(() => readRules(allSetting.subJsonRules), [allSetting.subJsonRules]);
|
|
|
+ const directEnabled = ruleArray.some((r) => r.outboundTag === 'direct');
|
|
|
+ const blockEnabled = ruleArray.some((r) => r.outboundTag === 'block');
|
|
|
|
|
|
- const directDomains = useMemo(() => {
|
|
|
- if (!ruleArray) return [];
|
|
|
- const dRule = ruleArray.find((r) => r.domain);
|
|
|
- return dRule?.domain ?? [];
|
|
|
- }, [ruleArray]);
|
|
|
+ const ruleValues = (tag: string, key: 'ip' | 'domain') =>
|
|
|
+ ruleArray.find((r) => r.outboundTag === tag && r[key])?.[key] ?? [];
|
|
|
|
|
|
- function setDirectEnabled(v: boolean) {
|
|
|
- updateSetting({ subJsonRules: v ? JSON.stringify(DEFAULT_RULES) : '' });
|
|
|
+ function writeRules(rules: SubJsonRule[]) {
|
|
|
+ updateSetting({ subJsonRules: rules.length > 0 ? JSON.stringify(rules) : '' });
|
|
|
}
|
|
|
|
|
|
- function setDirectIPs(value: string[]) {
|
|
|
- if (!ruleArray) return;
|
|
|
- let rules = [...ruleArray];
|
|
|
- if (value.length === 0) {
|
|
|
- rules = rules.filter((r) => !r.ip);
|
|
|
- } else {
|
|
|
- let idx = rules.findIndex((r) => r.ip);
|
|
|
- if (idx === -1) {
|
|
|
- rules.push({ ...DEFAULT_RULES[1] });
|
|
|
- idx = rules.length - 1;
|
|
|
- }
|
|
|
- rules[idx] = { ...rules[idx], ip: [...value] };
|
|
|
+ function setTagEnabled(tag: string, defaults: SubJsonRule[], enabled: boolean) {
|
|
|
+ const rest = ruleArray.filter((r) => r.outboundTag !== tag);
|
|
|
+ if (!enabled) {
|
|
|
+ // Turning off the last managed tag also drops foreign-tag leftovers so
|
|
|
+ // the panel still has a path back to an empty subJsonRules.
|
|
|
+ const hasManaged = rest.some((r) => r.outboundTag === 'direct' || r.outboundTag === 'block');
|
|
|
+ writeRules(hasManaged ? rest : []);
|
|
|
+ return;
|
|
|
}
|
|
|
- updateSetting({ subJsonRules: JSON.stringify(rules) });
|
|
|
+ // Prepend block defaults so ads match before direct; never re-sort the rest.
|
|
|
+ writeRules(tag === 'block' ? [...defaults, ...rest] : [...rest, ...defaults]);
|
|
|
}
|
|
|
|
|
|
- function setDirectDomains(value: string[]) {
|
|
|
- if (!ruleArray) return;
|
|
|
+ function setRuleValues(
|
|
|
+ tag: string,
|
|
|
+ key: 'ip' | 'domain',
|
|
|
+ template: SubJsonRule,
|
|
|
+ value: string[],
|
|
|
+ ) {
|
|
|
let rules = [...ruleArray];
|
|
|
if (value.length === 0) {
|
|
|
- rules = rules.filter((r) => !r.domain);
|
|
|
+ rules = rules.filter((r) => !(r.outboundTag === tag && r[key]));
|
|
|
} else {
|
|
|
- let idx = rules.findIndex((r) => r.domain);
|
|
|
- if (idx === -1) {
|
|
|
- rules.push({ ...DEFAULT_RULES[0] });
|
|
|
- idx = rules.length - 1;
|
|
|
+ let index = rules.findIndex((r) => r.outboundTag === tag && r[key]);
|
|
|
+ if (index === -1) {
|
|
|
+ rules.push({ ...template });
|
|
|
+ index = rules.length - 1;
|
|
|
}
|
|
|
- rules[idx] = { ...rules[idx], domain: [...value] };
|
|
|
+ rules[index] = { ...rules[index], [key]: [...value] };
|
|
|
}
|
|
|
- updateSetting({ subJsonRules: JSON.stringify(rules) });
|
|
|
+ writeRules(rules);
|
|
|
}
|
|
|
|
|
|
return (
|
|
|
@@ -385,16 +392,19 @@ export default function SubscriptionFormatsTab({
|
|
|
title={t('pages.settings.direct')}
|
|
|
description={t('pages.settings.directDesc')}
|
|
|
>
|
|
|
- <Switch checked={directEnabled} onChange={setDirectEnabled} />
|
|
|
+ <Switch
|
|
|
+ checked={directEnabled}
|
|
|
+ onChange={(v) => setTagEnabled('direct', DEFAULT_DIRECT_RULES, v)}
|
|
|
+ />
|
|
|
</SettingListItem>
|
|
|
{directEnabled && (
|
|
|
<div className="format-settings">
|
|
|
<SettingListItem paddings="small" title={<>{t('pages.settings.direct')} IPs</>}>
|
|
|
<Select
|
|
|
mode="tags"
|
|
|
- value={directIPs}
|
|
|
+ value={ruleValues('direct', 'ip')}
|
|
|
style={{ width: '100%' }}
|
|
|
- onChange={setDirectIPs}
|
|
|
+ onChange={(v) => setRuleValues('direct', 'ip', DEFAULT_DIRECT_RULES[1], v)}
|
|
|
options={directIPsOptions}
|
|
|
/>
|
|
|
</SettingListItem>
|
|
|
@@ -408,9 +418,11 @@ export default function SubscriptionFormatsTab({
|
|
|
>
|
|
|
<Select
|
|
|
mode="tags"
|
|
|
- value={directDomains}
|
|
|
+ value={ruleValues('direct', 'domain')}
|
|
|
style={{ width: '100%' }}
|
|
|
- onChange={setDirectDomains}
|
|
|
+ onChange={(v) =>
|
|
|
+ setRuleValues('direct', 'domain', DEFAULT_DIRECT_RULES[0], v)
|
|
|
+ }
|
|
|
options={directDomainsOptions}
|
|
|
/>
|
|
|
</SettingListItem>
|
|
|
@@ -419,6 +431,46 @@ export default function SubscriptionFormatsTab({
|
|
|
</>
|
|
|
),
|
|
|
},
|
|
|
+ {
|
|
|
+ key: '5',
|
|
|
+ label: catTabLabel(<StopOutlined />, t('pages.settings.block'), isMobile),
|
|
|
+ children: (
|
|
|
+ <>
|
|
|
+ <SettingListItem
|
|
|
+ paddings="small"
|
|
|
+ title={t('pages.settings.block')}
|
|
|
+ description={t('pages.settings.blockDesc')}
|
|
|
+ >
|
|
|
+ <Switch
|
|
|
+ checked={blockEnabled}
|
|
|
+ onChange={(v) => setTagEnabled('block', DEFAULT_BLOCK_RULES, v)}
|
|
|
+ />
|
|
|
+ </SettingListItem>
|
|
|
+ {blockEnabled && (
|
|
|
+ <div className="format-settings">
|
|
|
+ <SettingListItem paddings="small" title={t('pages.xray.blockdomains')}>
|
|
|
+ <Select
|
|
|
+ mode="tags"
|
|
|
+ value={ruleValues('block', 'domain')}
|
|
|
+ style={{ width: '100%' }}
|
|
|
+ onChange={(v) => setRuleValues('block', 'domain', DEFAULT_BLOCK_RULES[0], v)}
|
|
|
+ options={blockDomainsOptions}
|
|
|
+ />
|
|
|
+ </SettingListItem>
|
|
|
+ <SettingListItem paddings="small" title={t('pages.xray.blockips')}>
|
|
|
+ <Select
|
|
|
+ mode="tags"
|
|
|
+ value={ruleValues('block', 'ip')}
|
|
|
+ style={{ width: '100%' }}
|
|
|
+ onChange={(v) => setRuleValues('block', 'ip', BLOCK_IP_RULE, v)}
|
|
|
+ options={directIPsOptions}
|
|
|
+ />
|
|
|
+ </SettingListItem>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </>
|
|
|
+ ),
|
|
|
+ },
|
|
|
]}
|
|
|
/>
|
|
|
);
|