feat(happ): add routing editor, optional ad blocking, and LAN bypass preset (#6545)

* feat(happ): make ad blocking optional in routing presets

Add an independent AdBlock toggle for Iran, China, and global presets, applied only when generating routing rules. Update the China preset to Bypass-CN and cover preset behavior and localized controls.

* feat: add visual routing editor with JSON support and localization updates

- Implemented a new modal for editing routing profiles with basic and advanced tabs.
- Added functionality to load, parse, and generate routing profiles in JSON format.
- Enhanced user experience with validation and error handling for JSON input.
- Updated translations for Russian, Turkish, Ukrainian, Vietnamese, Chinese (Simplified and Traditional) to include new routing editor terms.
- Created helper functions for managing routing profiles and generating deep links.
- Added unit tests for routing editor functionalities and JSON handling.

* feat: update routing editor to preserve null lists in profiles and enhance validation messages
This commit is contained in:
NgaiYeanCoi
2026-09-27 04:57:01 +08:00
committed by GitHub
parent 94fa317e76
commit 5e15120cec
22 changed files with 1363 additions and 209 deletions
+64 -34
View File
@@ -7,16 +7,11 @@ export function toBase64Utf8(str: string): string {
);
}
// Splits multiline or comma-separated string into clean unique token arrays.
export function parseList(input: string): string[] {
return input
.split(/[\n,]+/)
.map((s) => s.trim())
.filter(Boolean);
}
// Build standard Happ routing deeplink or special state for curated presets.
export function buildHappPresetDeeplink(preset: string): string {
export function buildHappPresetDeeplink(preset: string, includeAdblock = false): string {
// Ad blocking is opt-in for each generated profile, independent of the base routing rules.
const blockSites = includeAdblock ? ['geosite:category-ads-all'] : [];
switch (preset) {
case 'off':
return 'happ://routing/off';
@@ -27,9 +22,20 @@ export function buildHappPresetDeeplink(preset: string): string {
JSON.stringify({
Name: 'Iran Bypass',
GlobalProxy: 'true',
DirectSites: ['domain:ir', 'regexp:.*\\.ir$'],
DirectIp: ['geoip:ir', '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'],
BlockSites: ['geosite:category-ads-all'],
RouteOrder: 'block-proxy-direct',
DirectSites: ['geosite:private', 'domain:ir', 'geosite:category-ir'],
DirectIp: [
'geoip:ir',
'geoip:private',
'127.0.0.0/8',
'10.0.0.0/8',
'172.16.0.0/12',
'192.168.0.0/16',
'169.254.0.0/16',
'224.0.0.0/4',
'255.255.255.255',
],
BlockSites: blockSites,
BlockIp: [],
ProxySites: [],
ProxyIp: [],
@@ -42,32 +48,38 @@ export function buildHappPresetDeeplink(preset: string): string {
'happ://routing/onadd/' +
toBase64Utf8(
JSON.stringify({
Name: 'China Direct',
Name: 'Bypass-CN',
GlobalProxy: 'true',
DirectSites: ['geosite:cn', 'geosite:geolocation-cn'],
DirectIp: ['geoip:cn', '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'],
BlockSites: ['geosite:category-ads-all'],
BlockIp: [],
RouteOrder: 'block-proxy-direct',
RemoteDNSType: 'DoH',
RemoteDNSDomain: 'https://cloudflare-dns.com/dns-query',
RemoteDNSIP: '1.1.1.1',
DomesticDNSType: 'DoH',
DomesticDNSDomain: 'https://dns.alidns.com/dns-query',
DomesticDNSIP: '223.5.5.5',
DnsHosts: {
'cloudflare-dns.com': '1.1.1.1',
'dns.alidns.com': '223.5.5.5',
},
DirectSites: ['geosite:private', 'geosite:cn', 'geosite:geolocation-cn'],
DirectIp: [
'geoip:cn',
'geoip:private',
'127.0.0.0/8',
'10.0.0.0/8',
'172.16.0.0/12',
'192.168.0.0/16',
'169.254.0.0/16',
'224.0.0.0/4',
'255.255.255.255',
],
ProxySites: [],
ProxyIp: [],
DomainStrategy: 'IPIfNonMatch',
}),
)
);
case 'adblock':
return (
'happ://routing/onadd/' +
toBase64Utf8(
JSON.stringify({
Name: 'AdBlock',
GlobalProxy: 'true',
DirectSites: [],
DirectIp: ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'],
BlockSites: ['geosite:category-ads-all'],
BlockSites: blockSites,
BlockIp: [],
ProxySites: [],
ProxyIp: [],
DomainStrategy: 'IPIfNonMatch',
FakeDNS: 'false',
UseChunkFiles: 'true',
}),
)
);
@@ -80,7 +92,25 @@ export function buildHappPresetDeeplink(preset: string): string {
GlobalProxy: 'true',
DirectSites: [],
DirectIp: [],
BlockSites: [],
BlockSites: blockSites,
BlockIp: [],
ProxySites: [],
ProxyIp: [],
DomainStrategy: 'AsIs',
}),
)
);
// LAN bypass has its own identity so applying it does not overwrite the Global profile.
case 'lan-bypass':
return (
'happ://routing/onadd/' +
toBase64Utf8(
JSON.stringify({
Name: 'Global Bypass Local Network',
GlobalProxy: 'true',
DirectSites: ['geosite:private'],
DirectIp: ['geoip:private'],
BlockSites: blockSites,
BlockIp: [],
ProxySites: [],
ProxyIp: [],