From 145665802864b66abb00b1bde791ce21c3eb8da9 Mon Sep 17 00:00:00 2001 From: Pejman Yousefi Date: Thu, 10 Sep 2026 17:16:12 +0330 Subject: [PATCH] feat(sub): add Happ client integration, routing presets, and app management (#6434) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(sub): add Happ client integration, routing presets, and app management Implement comprehensive Happ proxy client integration according to official developer specifications. - Fix header emission on disabled routing and hidden settings to send explicit '0' headers rather than omitting, allowing Happ clients to reset cached settings. - Add support for 'happ://routing/off' deeplink in routing validation. - Preserve '?serverDescription=' query parameters in link fragments without escaping to support Happ server subtitles across VMess, VLESS, Trojan and SS. - Add Happ application management headers: ProviderID, New-Url, Fallback-Url, Sub-Info banners, Sub-Expire notifications, No-Limit mode, hardware ID enforcement, TUN modes/types, route exclusions, APNS exclusions, and per-app proxy settings. - Add curated routing presets (Iran Bypass, China Direct, AdBlock, Global) and interactive visual rule generator in frontend settings. - Synchronize all 13 translation locales with native Persian, Russian, and Chinese translations. * fix(sub): keep Happ header overrides behind the auto-detect opt-in The Routing-Enable/Hide-Settings off values were emitted on the User-Agent alone, so every panel that upgraded would push "Routing-Enable: 0" — documented by happ.su as disabling routing globally — to every Happ client without the operator enabling anything. They now ride subHappAutoDetect like every other Happ header. Two further mismatches against the vendor spec: - serverDescription was written as a key of the VMess base64 JSON object. happ.su documents it as a "#Title?serverDescription=" link parameter or a JSON "meta" entry, so the caption never reached Happ while every other VMess consumer received an unknown key. Dropped rather than moved: emitting the documented form is unsafe here because our own parser base64-decodes the whole VMess body (internal/util/link/outbound.go). - The TUN Mode dropdown stored the literal "default", forwarded as "Tun-Mode: default", where happ.su documents system|gvisor only. It now stores the unset value so no header is sent. TUN Type "default" is a documented value and is unchanged. Each fix carries a test that fails without it. --- docs/public/openapi.json | 186 +++++ frontend/public/openapi.json | 186 +++++ frontend/src/generated/examples.ts | 46 ++ frontend/src/generated/schemas.ts | 186 +++++ frontend/src/generated/types.ts | 46 ++ frontend/src/generated/zod.ts | 46 ++ frontend/src/models/setting.ts | 23 + .../pages/settings/HappSettingsContent.tsx | 637 ++++++++++++++++++ .../pages/settings/SubscriptionGeneralTab.tsx | 41 +- frontend/src/pages/settings/happPresets.ts | 118 ++++ frontend/src/schemas/setting.ts | 23 + frontend/src/test/happ-presets.test.ts | 85 +++ .../src/test/happ-settings-tun-mode.test.tsx | 72 ++ internal/sub/controller.go | 18 +- internal/sub/endpoint_test.go | 26 + internal/sub/happ.go | 141 ++++ internal/sub/happ_test.go | 227 +++++++ internal/sub/remote_routing.go | 3 + internal/sub/service.go | 3 +- internal/sub/sub.go | 26 + internal/web/entity/entity.go | 25 + internal/web/service/setting.go | 125 ++++ internal/web/translation/ar-EG.json | 94 ++- internal/web/translation/en-US.json | 94 ++- internal/web/translation/es-ES.json | 94 ++- internal/web/translation/fa-IR.json | 94 ++- internal/web/translation/id-ID.json | 94 ++- internal/web/translation/ja-JP.json | 94 ++- internal/web/translation/pt-BR.json | 94 ++- internal/web/translation/ru-RU.json | 94 ++- internal/web/translation/tr-TR.json | 94 ++- internal/web/translation/uk-UA.json | 94 ++- internal/web/translation/vi-VN.json | 94 ++- internal/web/translation/zh-CN.json | 94 ++- internal/web/translation/zh-TW.json | 94 ++- 35 files changed, 3460 insertions(+), 51 deletions(-) create mode 100644 frontend/src/pages/settings/HappSettingsContent.tsx create mode 100644 frontend/src/pages/settings/happPresets.ts create mode 100644 frontend/src/test/happ-presets.test.ts create mode 100644 frontend/src/test/happ-settings-tun-mode.test.tsx create mode 100644 internal/sub/happ.go create mode 100644 internal/sub/happ_test.go diff --git a/docs/public/openapi.json b/docs/public/openapi.json index d10f7a5e9..14e22aca5 100644 --- a/docs/public/openapi.json +++ b/docs/public/openapi.json @@ -220,6 +220,76 @@ "subExpiredTemplate": { "type": "string" }, + "subHappAlwaysHwid": { + "type": "boolean" + }, + "subHappAutoConnect": { + "type": "boolean" + }, + "subHappAutoConnectType": { + "type": "string" + }, + "subHappAutoDetect": { + "description": "Happ client customization settings (app-management / routing / UX).", + "type": "boolean" + }, + "subHappColorProfile": { + "type": "string" + }, + "subHappExcludeApns": { + "type": "boolean" + }, + "subHappExcludeRoutes": { + "type": "string" + }, + "subHappFallbackUrl": { + "type": "string" + }, + "subHappNewUrl": { + "type": "string" + }, + "subHappNoLimit": { + "type": "boolean" + }, + "subHappNotificationExpire": { + "type": "boolean" + }, + "subHappPerAppList": { + "type": "string" + }, + "subHappPerAppMode": { + "type": "string" + }, + "subHappPingType": { + "type": "string" + }, + "subHappProviderId": { + "type": "string" + }, + "subHappSubExpire": { + "type": "boolean" + }, + "subHappSubExpireButtonLink": { + "type": "string" + }, + "subHappSubInfoButtonLink": { + "type": "string" + }, + "subHappSubInfoButtonText": { + "type": "string" + }, + "subHappSubInfoColor": { + "type": "string" + }, + "subHappSubInfoText": { + "type": "string" + }, + "subHappTunMode": { + "type": "string" + }, + "subHappTunType": { + "type": "string" + }, "subHideSettings": { "type": "boolean" }, @@ -443,6 +513,29 @@ "subEnableRouting", "subEncrypt", "subExpiredTemplate", + "subHappAlwaysHwid", + "subHappAutoConnect", + "subHappAutoConnectType", + "subHappAutoDetect", + "subHappColorProfile", + "subHappExcludeApns", + "subHappExcludeRoutes", + "subHappFallbackUrl", + "subHappNewUrl", + "subHappNoLimit", + "subHappNotificationExpire", + "subHappPerAppList", + "subHappPerAppMode", + "subHappPingType", + "subHappProviderId", + "subHappSubExpire", + "subHappSubExpireButtonLink", + "subHappSubInfoButtonLink", + "subHappSubInfoButtonText", + "subHappSubInfoColor", + "subHappSubInfoText", + "subHappTunMode", + "subHappTunType", "subHideSettings", "subIncyEnableRouting", "subIncyRoutingRules", @@ -711,6 +804,76 @@ "subExpiredTemplate": { "type": "string" }, + "subHappAlwaysHwid": { + "type": "boolean" + }, + "subHappAutoConnect": { + "type": "boolean" + }, + "subHappAutoConnectType": { + "type": "string" + }, + "subHappAutoDetect": { + "description": "Happ client customization settings (app-management / routing / UX).", + "type": "boolean" + }, + "subHappColorProfile": { + "type": "string" + }, + "subHappExcludeApns": { + "type": "boolean" + }, + "subHappExcludeRoutes": { + "type": "string" + }, + "subHappFallbackUrl": { + "type": "string" + }, + "subHappNewUrl": { + "type": "string" + }, + "subHappNoLimit": { + "type": "boolean" + }, + "subHappNotificationExpire": { + "type": "boolean" + }, + "subHappPerAppList": { + "type": "string" + }, + "subHappPerAppMode": { + "type": "string" + }, + "subHappPingType": { + "type": "string" + }, + "subHappProviderId": { + "type": "string" + }, + "subHappSubExpire": { + "type": "boolean" + }, + "subHappSubExpireButtonLink": { + "type": "string" + }, + "subHappSubInfoButtonLink": { + "type": "string" + }, + "subHappSubInfoButtonText": { + "type": "string" + }, + "subHappSubInfoColor": { + "type": "string" + }, + "subHappSubInfoText": { + "type": "string" + }, + "subHappTunMode": { + "type": "string" + }, + "subHappTunType": { + "type": "string" + }, "subHideSettings": { "type": "boolean" }, @@ -941,6 +1104,29 @@ "subEnableRouting", "subEncrypt", "subExpiredTemplate", + "subHappAlwaysHwid", + "subHappAutoConnect", + "subHappAutoConnectType", + "subHappAutoDetect", + "subHappColorProfile", + "subHappExcludeApns", + "subHappExcludeRoutes", + "subHappFallbackUrl", + "subHappNewUrl", + "subHappNoLimit", + "subHappNotificationExpire", + "subHappPerAppList", + "subHappPerAppMode", + "subHappPingType", + "subHappProviderId", + "subHappSubExpire", + "subHappSubExpireButtonLink", + "subHappSubInfoButtonLink", + "subHappSubInfoButtonText", + "subHappSubInfoColor", + "subHappSubInfoText", + "subHappTunMode", + "subHappTunType", "subHideSettings", "subIncyEnableRouting", "subIncyRoutingRules", diff --git a/frontend/public/openapi.json b/frontend/public/openapi.json index d10f7a5e9..14e22aca5 100644 --- a/frontend/public/openapi.json +++ b/frontend/public/openapi.json @@ -220,6 +220,76 @@ "subExpiredTemplate": { "type": "string" }, + "subHappAlwaysHwid": { + "type": "boolean" + }, + "subHappAutoConnect": { + "type": "boolean" + }, + "subHappAutoConnectType": { + "type": "string" + }, + "subHappAutoDetect": { + "description": "Happ client customization settings (app-management / routing / UX).", + "type": "boolean" + }, + "subHappColorProfile": { + "type": "string" + }, + "subHappExcludeApns": { + "type": "boolean" + }, + "subHappExcludeRoutes": { + "type": "string" + }, + "subHappFallbackUrl": { + "type": "string" + }, + "subHappNewUrl": { + "type": "string" + }, + "subHappNoLimit": { + "type": "boolean" + }, + "subHappNotificationExpire": { + "type": "boolean" + }, + "subHappPerAppList": { + "type": "string" + }, + "subHappPerAppMode": { + "type": "string" + }, + "subHappPingType": { + "type": "string" + }, + "subHappProviderId": { + "type": "string" + }, + "subHappSubExpire": { + "type": "boolean" + }, + "subHappSubExpireButtonLink": { + "type": "string" + }, + "subHappSubInfoButtonLink": { + "type": "string" + }, + "subHappSubInfoButtonText": { + "type": "string" + }, + "subHappSubInfoColor": { + "type": "string" + }, + "subHappSubInfoText": { + "type": "string" + }, + "subHappTunMode": { + "type": "string" + }, + "subHappTunType": { + "type": "string" + }, "subHideSettings": { "type": "boolean" }, @@ -443,6 +513,29 @@ "subEnableRouting", "subEncrypt", "subExpiredTemplate", + "subHappAlwaysHwid", + "subHappAutoConnect", + "subHappAutoConnectType", + "subHappAutoDetect", + "subHappColorProfile", + "subHappExcludeApns", + "subHappExcludeRoutes", + "subHappFallbackUrl", + "subHappNewUrl", + "subHappNoLimit", + "subHappNotificationExpire", + "subHappPerAppList", + "subHappPerAppMode", + "subHappPingType", + "subHappProviderId", + "subHappSubExpire", + "subHappSubExpireButtonLink", + "subHappSubInfoButtonLink", + "subHappSubInfoButtonText", + "subHappSubInfoColor", + "subHappSubInfoText", + "subHappTunMode", + "subHappTunType", "subHideSettings", "subIncyEnableRouting", "subIncyRoutingRules", @@ -711,6 +804,76 @@ "subExpiredTemplate": { "type": "string" }, + "subHappAlwaysHwid": { + "type": "boolean" + }, + "subHappAutoConnect": { + "type": "boolean" + }, + "subHappAutoConnectType": { + "type": "string" + }, + "subHappAutoDetect": { + "description": "Happ client customization settings (app-management / routing / UX).", + "type": "boolean" + }, + "subHappColorProfile": { + "type": "string" + }, + "subHappExcludeApns": { + "type": "boolean" + }, + "subHappExcludeRoutes": { + "type": "string" + }, + "subHappFallbackUrl": { + "type": "string" + }, + "subHappNewUrl": { + "type": "string" + }, + "subHappNoLimit": { + "type": "boolean" + }, + "subHappNotificationExpire": { + "type": "boolean" + }, + "subHappPerAppList": { + "type": "string" + }, + "subHappPerAppMode": { + "type": "string" + }, + "subHappPingType": { + "type": "string" + }, + "subHappProviderId": { + "type": "string" + }, + "subHappSubExpire": { + "type": "boolean" + }, + "subHappSubExpireButtonLink": { + "type": "string" + }, + "subHappSubInfoButtonLink": { + "type": "string" + }, + "subHappSubInfoButtonText": { + "type": "string" + }, + "subHappSubInfoColor": { + "type": "string" + }, + "subHappSubInfoText": { + "type": "string" + }, + "subHappTunMode": { + "type": "string" + }, + "subHappTunType": { + "type": "string" + }, "subHideSettings": { "type": "boolean" }, @@ -941,6 +1104,29 @@ "subEnableRouting", "subEncrypt", "subExpiredTemplate", + "subHappAlwaysHwid", + "subHappAutoConnect", + "subHappAutoConnectType", + "subHappAutoDetect", + "subHappColorProfile", + "subHappExcludeApns", + "subHappExcludeRoutes", + "subHappFallbackUrl", + "subHappNewUrl", + "subHappNoLimit", + "subHappNotificationExpire", + "subHappPerAppList", + "subHappPerAppMode", + "subHappPingType", + "subHappProviderId", + "subHappSubExpire", + "subHappSubExpireButtonLink", + "subHappSubInfoButtonLink", + "subHappSubInfoButtonText", + "subHappSubInfoColor", + "subHappSubInfoText", + "subHappTunMode", + "subHappTunType", "subHideSettings", "subIncyEnableRouting", "subIncyRoutingRules", diff --git a/frontend/src/generated/examples.ts b/frontend/src/generated/examples.ts index 10cec0c6e..856b16c5a 100644 --- a/frontend/src/generated/examples.ts +++ b/frontend/src/generated/examples.ts @@ -59,6 +59,29 @@ export const EXAMPLES: Record = { "subEnableRouting": false, "subEncrypt": false, "subExpiredTemplate": "", + "subHappAlwaysHwid": false, + "subHappAutoConnect": false, + "subHappAutoConnectType": "", + "subHappAutoDetect": false, + "subHappColorProfile": "", + "subHappExcludeApns": false, + "subHappExcludeRoutes": "", + "subHappFallbackUrl": "", + "subHappNewUrl": "", + "subHappNoLimit": false, + "subHappNotificationExpire": false, + "subHappPerAppList": "", + "subHappPerAppMode": "", + "subHappPingType": "", + "subHappProviderId": "", + "subHappSubExpire": false, + "subHappSubExpireButtonLink": "", + "subHappSubInfoButtonLink": "", + "subHappSubInfoButtonText": "", + "subHappSubInfoColor": "", + "subHappSubInfoText": "", + "subHappTunMode": "", + "subHappTunType": "", "subHideSettings": false, "subIncyEnableRouting": false, "subIncyRoutingRules": "", @@ -176,6 +199,29 @@ export const EXAMPLES: Record = { "subEnableRouting": false, "subEncrypt": false, "subExpiredTemplate": "", + "subHappAlwaysHwid": false, + "subHappAutoConnect": false, + "subHappAutoConnectType": "", + "subHappAutoDetect": false, + "subHappColorProfile": "", + "subHappExcludeApns": false, + "subHappExcludeRoutes": "", + "subHappFallbackUrl": "", + "subHappNewUrl": "", + "subHappNoLimit": false, + "subHappNotificationExpire": false, + "subHappPerAppList": "", + "subHappPerAppMode": "", + "subHappPingType": "", + "subHappProviderId": "", + "subHappSubExpire": false, + "subHappSubExpireButtonLink": "", + "subHappSubInfoButtonLink": "", + "subHappSubInfoButtonText": "", + "subHappSubInfoColor": "", + "subHappSubInfoText": "", + "subHappTunMode": "", + "subHappTunType": "", "subHideSettings": false, "subIncyEnableRouting": false, "subIncyRoutingRules": "", diff --git a/frontend/src/generated/schemas.ts b/frontend/src/generated/schemas.ts index 233fe0d20..1a6d908f1 100644 --- a/frontend/src/generated/schemas.ts +++ b/frontend/src/generated/schemas.ts @@ -194,6 +194,76 @@ export const SCHEMAS: Record = { "subExpiredTemplate": { "type": "string" }, + "subHappAlwaysHwid": { + "type": "boolean" + }, + "subHappAutoConnect": { + "type": "boolean" + }, + "subHappAutoConnectType": { + "type": "string" + }, + "subHappAutoDetect": { + "description": "Happ client customization settings (app-management / routing / UX).", + "type": "boolean" + }, + "subHappColorProfile": { + "type": "string" + }, + "subHappExcludeApns": { + "type": "boolean" + }, + "subHappExcludeRoutes": { + "type": "string" + }, + "subHappFallbackUrl": { + "type": "string" + }, + "subHappNewUrl": { + "type": "string" + }, + "subHappNoLimit": { + "type": "boolean" + }, + "subHappNotificationExpire": { + "type": "boolean" + }, + "subHappPerAppList": { + "type": "string" + }, + "subHappPerAppMode": { + "type": "string" + }, + "subHappPingType": { + "type": "string" + }, + "subHappProviderId": { + "type": "string" + }, + "subHappSubExpire": { + "type": "boolean" + }, + "subHappSubExpireButtonLink": { + "type": "string" + }, + "subHappSubInfoButtonLink": { + "type": "string" + }, + "subHappSubInfoButtonText": { + "type": "string" + }, + "subHappSubInfoColor": { + "type": "string" + }, + "subHappSubInfoText": { + "type": "string" + }, + "subHappTunMode": { + "type": "string" + }, + "subHappTunType": { + "type": "string" + }, "subHideSettings": { "type": "boolean" }, @@ -417,6 +487,29 @@ export const SCHEMAS: Record = { "subEnableRouting", "subEncrypt", "subExpiredTemplate", + "subHappAlwaysHwid", + "subHappAutoConnect", + "subHappAutoConnectType", + "subHappAutoDetect", + "subHappColorProfile", + "subHappExcludeApns", + "subHappExcludeRoutes", + "subHappFallbackUrl", + "subHappNewUrl", + "subHappNoLimit", + "subHappNotificationExpire", + "subHappPerAppList", + "subHappPerAppMode", + "subHappPingType", + "subHappProviderId", + "subHappSubExpire", + "subHappSubExpireButtonLink", + "subHappSubInfoButtonLink", + "subHappSubInfoButtonText", + "subHappSubInfoColor", + "subHappSubInfoText", + "subHappTunMode", + "subHappTunType", "subHideSettings", "subIncyEnableRouting", "subIncyRoutingRules", @@ -685,6 +778,76 @@ export const SCHEMAS: Record = { "subExpiredTemplate": { "type": "string" }, + "subHappAlwaysHwid": { + "type": "boolean" + }, + "subHappAutoConnect": { + "type": "boolean" + }, + "subHappAutoConnectType": { + "type": "string" + }, + "subHappAutoDetect": { + "description": "Happ client customization settings (app-management / routing / UX).", + "type": "boolean" + }, + "subHappColorProfile": { + "type": "string" + }, + "subHappExcludeApns": { + "type": "boolean" + }, + "subHappExcludeRoutes": { + "type": "string" + }, + "subHappFallbackUrl": { + "type": "string" + }, + "subHappNewUrl": { + "type": "string" + }, + "subHappNoLimit": { + "type": "boolean" + }, + "subHappNotificationExpire": { + "type": "boolean" + }, + "subHappPerAppList": { + "type": "string" + }, + "subHappPerAppMode": { + "type": "string" + }, + "subHappPingType": { + "type": "string" + }, + "subHappProviderId": { + "type": "string" + }, + "subHappSubExpire": { + "type": "boolean" + }, + "subHappSubExpireButtonLink": { + "type": "string" + }, + "subHappSubInfoButtonLink": { + "type": "string" + }, + "subHappSubInfoButtonText": { + "type": "string" + }, + "subHappSubInfoColor": { + "type": "string" + }, + "subHappSubInfoText": { + "type": "string" + }, + "subHappTunMode": { + "type": "string" + }, + "subHappTunType": { + "type": "string" + }, "subHideSettings": { "type": "boolean" }, @@ -915,6 +1078,29 @@ export const SCHEMAS: Record = { "subEnableRouting", "subEncrypt", "subExpiredTemplate", + "subHappAlwaysHwid", + "subHappAutoConnect", + "subHappAutoConnectType", + "subHappAutoDetect", + "subHappColorProfile", + "subHappExcludeApns", + "subHappExcludeRoutes", + "subHappFallbackUrl", + "subHappNewUrl", + "subHappNoLimit", + "subHappNotificationExpire", + "subHappPerAppList", + "subHappPerAppMode", + "subHappPingType", + "subHappProviderId", + "subHappSubExpire", + "subHappSubExpireButtonLink", + "subHappSubInfoButtonLink", + "subHappSubInfoButtonText", + "subHappSubInfoColor", + "subHappSubInfoText", + "subHappTunMode", + "subHappTunType", "subHideSettings", "subIncyEnableRouting", "subIncyRoutingRules", diff --git a/frontend/src/generated/types.ts b/frontend/src/generated/types.ts index 85dc94dc8..37fac33e7 100644 --- a/frontend/src/generated/types.ts +++ b/frontend/src/generated/types.ts @@ -67,6 +67,29 @@ export interface AllSetting { subEnableRouting: boolean; subEncrypt: boolean; subExpiredTemplate: string; + subHappAlwaysHwid: boolean; + subHappAutoConnect: boolean; + subHappAutoConnectType: string; + subHappAutoDetect: boolean; + subHappColorProfile: string; + subHappExcludeApns: boolean; + subHappExcludeRoutes: string; + subHappFallbackUrl: string; + subHappNewUrl: string; + subHappNoLimit: boolean; + subHappNotificationExpire: boolean; + subHappPerAppList: string; + subHappPerAppMode: string; + subHappPingType: string; + subHappProviderId: string; + subHappSubExpire: boolean; + subHappSubExpireButtonLink: string; + subHappSubInfoButtonLink: string; + subHappSubInfoButtonText: string; + subHappSubInfoColor: string; + subHappSubInfoText: string; + subHappTunMode: string; + subHappTunType: string; subHideSettings: boolean; subIncyEnableRouting: boolean; subIncyRoutingRules: string; @@ -185,6 +208,29 @@ export interface AllSettingView { subEnableRouting: boolean; subEncrypt: boolean; subExpiredTemplate: string; + subHappAlwaysHwid: boolean; + subHappAutoConnect: boolean; + subHappAutoConnectType: string; + subHappAutoDetect: boolean; + subHappColorProfile: string; + subHappExcludeApns: boolean; + subHappExcludeRoutes: string; + subHappFallbackUrl: string; + subHappNewUrl: string; + subHappNoLimit: boolean; + subHappNotificationExpire: boolean; + subHappPerAppList: string; + subHappPerAppMode: string; + subHappPingType: string; + subHappProviderId: string; + subHappSubExpire: boolean; + subHappSubExpireButtonLink: string; + subHappSubInfoButtonLink: string; + subHappSubInfoButtonText: string; + subHappSubInfoColor: string; + subHappSubInfoText: string; + subHappTunMode: string; + subHappTunType: string; subHideSettings: boolean; subIncyEnableRouting: boolean; subIncyRoutingRules: string; diff --git a/frontend/src/generated/zod.ts b/frontend/src/generated/zod.ts index f1f002729..8b905548d 100644 --- a/frontend/src/generated/zod.ts +++ b/frontend/src/generated/zod.ts @@ -83,6 +83,29 @@ export const AllSettingSchema = z.object({ subEnableRouting: z.boolean(), subEncrypt: z.boolean(), subExpiredTemplate: z.string(), + subHappAlwaysHwid: z.boolean(), + subHappAutoConnect: z.boolean(), + subHappAutoConnectType: z.string(), + subHappAutoDetect: z.boolean(), + subHappColorProfile: z.string(), + subHappExcludeApns: z.boolean(), + subHappExcludeRoutes: z.string(), + subHappFallbackUrl: z.string(), + subHappNewUrl: z.string(), + subHappNoLimit: z.boolean(), + subHappNotificationExpire: z.boolean(), + subHappPerAppList: z.string(), + subHappPerAppMode: z.string(), + subHappPingType: z.string(), + subHappProviderId: z.string(), + subHappSubExpire: z.boolean(), + subHappSubExpireButtonLink: z.string(), + subHappSubInfoButtonLink: z.string(), + subHappSubInfoButtonText: z.string(), + subHappSubInfoColor: z.string(), + subHappSubInfoText: z.string(), + subHappTunMode: z.string(), + subHappTunType: z.string(), subHideSettings: z.boolean(), subIncyEnableRouting: z.boolean(), subIncyRoutingRules: z.string(), @@ -202,6 +225,29 @@ export const AllSettingViewSchema = z.object({ subEnableRouting: z.boolean(), subEncrypt: z.boolean(), subExpiredTemplate: z.string(), + subHappAlwaysHwid: z.boolean(), + subHappAutoConnect: z.boolean(), + subHappAutoConnectType: z.string(), + subHappAutoDetect: z.boolean(), + subHappColorProfile: z.string(), + subHappExcludeApns: z.boolean(), + subHappExcludeRoutes: z.string(), + subHappFallbackUrl: z.string(), + subHappNewUrl: z.string(), + subHappNoLimit: z.boolean(), + subHappNotificationExpire: z.boolean(), + subHappPerAppList: z.string(), + subHappPerAppMode: z.string(), + subHappPingType: z.string(), + subHappProviderId: z.string(), + subHappSubExpire: z.boolean(), + subHappSubExpireButtonLink: z.string(), + subHappSubInfoButtonLink: z.string(), + subHappSubInfoButtonText: z.string(), + subHappSubInfoColor: z.string(), + subHappSubInfoText: z.string(), + subHappTunMode: z.string(), + subHappTunType: z.string(), subHideSettings: z.boolean(), subIncyEnableRouting: z.boolean(), subIncyRoutingRules: z.string(), diff --git a/frontend/src/models/setting.ts b/frontend/src/models/setting.ts index 766cddbba..ffc930f26 100644 --- a/frontend/src/models/setting.ts +++ b/frontend/src/models/setting.ts @@ -73,6 +73,29 @@ export class AllSetting { subJsonObservatory = ''; subThemeDir = ''; subHideSettings = false; + subHappAutoDetect = false; + subHappProviderId = ''; + subHappNewUrl = ''; + subHappFallbackUrl = ''; + subHappSubInfoColor = 'blue'; + subHappSubInfoText = ''; + subHappSubInfoButtonText = ''; + subHappSubInfoButtonLink = ''; + subHappSubExpire = false; + subHappSubExpireButtonLink = ''; + subHappNotificationExpire = false; + subHappNoLimit = false; + subHappAlwaysHwid = false; + subHappTunMode = ''; + subHappTunType = ''; + subHappExcludeRoutes = ''; + subHappExcludeApns = false; + subHappColorProfile = ''; + subHappPingType = ''; + subHappAutoConnect = false; + subHappAutoConnectType = 'lowestdelay'; + subHappPerAppMode = 'off'; + subHappPerAppList = ''; timeLocation = 'Local'; diff --git a/frontend/src/pages/settings/HappSettingsContent.tsx b/frontend/src/pages/settings/HappSettingsContent.tsx new file mode 100644 index 000000000..69ba12c0a --- /dev/null +++ b/frontend/src/pages/settings/HappSettingsContent.tsx @@ -0,0 +1,637 @@ +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Button, Input, Modal, Select, Space, Switch, Tabs, message } from 'antd'; +import { + BranchesOutlined, + BuildOutlined, + CloudSyncOutlined, + DesktopOutlined, + MobileOutlined, + NotificationOutlined, + ThunderboltOutlined, +} from '@ant-design/icons'; +import type { AllSetting } from '@/models/setting'; +import { SettingListItem } from '@/components/ui'; +import { buildHappPresetDeeplink, parseList, toBase64Utf8 } from './happPresets'; + +interface HappSettingsContentProps { + allSetting: AllSetting; + updateSetting: (patch: Partial) => void; + isMobile: boolean; + remoteSourceBadge: (val: string) => React.ReactNode; +} + +export default function HappSettingsContent({ + allSetting, + updateSetting, + isMobile, + remoteSourceBadge, +}: HappSettingsContentProps) { + const { t } = useTranslation(); + const [selectedPreset, setSelectedPreset] = useState('iran-bypass'); + const [isModalOpen, setIsModalOpen] = useState(false); + + const [directDomains, setDirectDomains] = useState(''); + const [proxyDomains, setProxyDomains] = useState(''); + const [blockDomains, setBlockDomains] = useState(''); + const [directIPs, setDirectIPs] = useState(''); + const [proxyIPs, setProxyIPs] = useState(''); + const [blockIPs, setBlockIPs] = useState(''); + + const applyPreset = () => { + const payload = buildHappPresetDeeplink(selectedPreset); + if (payload) { + updateSetting({ subRoutingRules: payload }); + message.success(t('pages.settings.subHappPresetApplied')); + } + }; + + const handleBuildDeeplink = () => { + interface FieldRule { + type: string; + outboundTag: string; + domain?: string[]; + ip?: string[]; + network?: string; + } + const rules: FieldRule[] = []; + + const bDom = parseList(blockDomains); + const bIp = parseList(blockIPs); + if (bDom.length > 0 || bIp.length > 0) { + rules.push({ + type: 'field', + outboundTag: 'block', + ...(bDom.length > 0 ? { domain: bDom } : {}), + ...(bIp.length > 0 ? { ip: bIp } : {}), + }); + } + + const dDom = parseList(directDomains); + const dIp = parseList(directIPs); + if (dDom.length > 0 || dIp.length > 0) { + rules.push({ + type: 'field', + outboundTag: 'direct', + ...(dDom.length > 0 ? { domain: dDom } : {}), + ...(dIp.length > 0 ? { ip: dIp } : {}), + }); + } + + const pDom = parseList(proxyDomains); + const pIp = parseList(proxyIPs); + if (pDom.length > 0 || pIp.length > 0) { + rules.push({ + type: 'field', + outboundTag: 'proxy', + ...(pDom.length > 0 ? { domain: pDom } : {}), + ...(pIp.length > 0 ? { ip: pIp } : {}), + }); + } + + rules.push({ + type: 'field', + outboundTag: 'proxy', + network: 'tcp,udp', + }); + + const deeplink = 'happ://routing/onadd/' + toBase64Utf8(JSON.stringify({ rules })); + updateSetting({ subRoutingRules: deeplink }); + setIsModalOpen(false); + message.success(t('pages.settings.subHappDeeplinkGenerated')); + }; + + return ( + <> + + {!isMobile && t('pages.settings.subHappGroupRouting')} + + ), + children: ( + <> + + updateSetting({ subEnableRouting: v })} + /> + + + + + updateSetting({ subHappSubInfoText: e.target.value })} + /> + + + + updateSetting({ subHappSubInfoButtonText: e.target.value })} + /> + + + + updateSetting({ subHappSubInfoButtonLink: e.target.value })} + /> + + + + updateSetting({ subHappSubExpire: v })} + /> + + + + updateSetting({ subHappSubExpireButtonLink: e.target.value })} + /> + + + + updateSetting({ subHappNotificationExpire: v })} + /> + + + ), + }, + { + key: 'network', + label: ( + + {!isMobile && t('pages.settings.subHappGroupNetwork')} + + ), + children: ( + <> + + updateSetting({ subHappTunType: v })} + options={[ + { value: 'singbox', label: t('pages.settings.subHappTunTypeSingbox') }, + { value: 'tun2proxy', label: t('pages.settings.subHappTunTypeTun2proxy') }, + { value: 'default', label: t('pages.settings.subHappTunTypeDefault') }, + { value: 'xray', label: t('pages.settings.subHappTunTypeXray') }, + ]} + /> + + + + updateSetting({ subHappExcludeRoutes: e.target.value })} + /> + + + + updateSetting({ subHappExcludeApns: v })} + /> + + + + updateSetting({ subHappAutoConnectType: v })} + options={[ + { + value: 'lowestdelay', + label: t('pages.settings.subHappAutoConnectLowestDelay'), + }, + { value: 'lastused', label: t('pages.settings.subHappAutoConnectLastUsed') }, + { value: 'random', label: t('pages.settings.subHappAutoConnectRandom') }, + ]} + /> + + + ), + }, + { + key: 'themes', + label: ( + + {!isMobile && t('pages.settings.subHappGroupThemes')} + + ), + children: ( + <> + + updateSetting({ subHappColorProfile: e.target.value })} + /> + + + ), + }, + { + key: 'failover', + label: ( + + {!isMobile && t('pages.settings.subHappGroupFailover')} + + ), + children: ( + <> + + updateSetting({ subHappAutoDetect: v })} + /> + + + + updateSetting({ subHappProviderId: e.target.value })} + /> + + + + updateSetting({ subHappNewUrl: e.target.value })} + /> + + + + updateSetting({ subHappFallbackUrl: e.target.value })} + /> + + + + updateSetting({ subHappAlwaysHwid: v })} + /> + + + ), + }, + { + key: 'android', + label: ( + + {!isMobile && t('pages.settings.subHappGroupAndroid')} + + ), + children: ( + <> + +