From e26cf1d3ed11b9bf2b796c0ec36ce5aff9e856c2 Mon Sep 17 00:00:00 2001 From: Sanaei Date: Wed, 16 Sep 2026 01:17:04 +0200 Subject: [PATCH] feat(sub): redesign the subscription page around usage, tabs and app imports The info page was a long key/value table followed by every link and two app dropdowns, and it rendered left-to-right even for Persian and Arabic. It now leads with a usage ring, the remaining quota and a stats grid, and splits the rest into Subscription / Apps / Configs tabs. - Status tells expired, data-used-up and disabled apart instead of one "Inactive", replacing the hard-coded English expiry chip. - The Apps tab keeps every Android and iOS app with its existing deep link, preselects the visitor's platform and adds Windows: Hiddify and Clash Verge Rev import directly, v2rayN copies the link. - fa-IR and ar-EG render right-to-left; URLs, IDs and sizes stay LTR. - The footer shows the support link and the client refresh interval, so subPageContext now carries subUpdates (also in ?format=info). - Status, days-left and app deep-link logic lives in subPageModel.ts, with unit tests pinning the deep links the page already shipped. --- frontend/src/env.d.ts | 2 + frontend/src/pages/inbounds/qr/QrPanel.css | 2 +- frontend/src/pages/inbounds/qr/QrPanel.tsx | 2 +- frontend/src/pages/sub/SubAppsTab.tsx | 60 ++ frontend/src/pages/sub/SubConfigsTab.tsx | 80 ++ frontend/src/pages/sub/SubHeader.tsx | 112 +++ frontend/src/pages/sub/SubHero.tsx | 127 +++ frontend/src/pages/sub/SubLinksTab.tsx | 87 ++ frontend/src/pages/sub/SubPage.css | 741 +++++++++++++++-- frontend/src/pages/sub/SubPage.tsx | 749 ++++-------------- frontend/src/pages/sub/SubQrButton.tsx | 69 ++ frontend/src/pages/sub/SubUsageSummary.css | 87 -- frontend/src/pages/sub/SubUsageSummary.tsx | 96 --- frontend/src/pages/sub/app-icons/README.md | 25 + frontend/src/pages/sub/app-icons/happ.svg | 1 + frontend/src/pages/sub/app-icons/incy.webp | Bin 0 -> 1732 bytes .../src/pages/sub/app-icons/shadowrocket.webp | Bin 0 -> 1094 bytes frontend/src/pages/sub/app-icons/sing-box.svg | 1 + .../src/pages/sub/app-icons/streisand.webp | Bin 0 -> 2234 bytes frontend/src/pages/sub/app-icons/v2box.webp | Bin 0 -> 1206 bytes frontend/src/pages/sub/app-icons/v2rayng.svg | 1 + frontend/src/pages/sub/app-icons/v2raytun.svg | 1 + frontend/src/pages/sub/appIcons.ts | 20 + frontend/src/pages/sub/subPageModel.ts | 93 +++ frontend/src/test/qr-panel-readable.test.tsx | 8 +- frontend/src/test/sub-app-icons.test.ts | 28 + frontend/src/test/sub-page-model.test.ts | 135 ++++ internal/sub/controller.go | 4 + internal/sub/info_endpoint_test.go | 3 + internal/web/translation/ar-EG.json | 16 +- internal/web/translation/en-US.json | 18 +- internal/web/translation/es-ES.json | 16 +- internal/web/translation/fa-IR.json | 18 +- internal/web/translation/id-ID.json | 16 +- internal/web/translation/ja-JP.json | 16 +- internal/web/translation/pt-BR.json | 16 +- internal/web/translation/ru-RU.json | 16 +- internal/web/translation/tr-TR.json | 16 +- internal/web/translation/uk-UA.json | 16 +- internal/web/translation/vi-VN.json | 16 +- internal/web/translation/zh-CN.json | 16 +- internal/web/translation/zh-TW.json | 16 +- 42 files changed, 1869 insertions(+), 877 deletions(-) create mode 100644 frontend/src/pages/sub/SubAppsTab.tsx create mode 100644 frontend/src/pages/sub/SubConfigsTab.tsx create mode 100644 frontend/src/pages/sub/SubHeader.tsx create mode 100644 frontend/src/pages/sub/SubHero.tsx create mode 100644 frontend/src/pages/sub/SubLinksTab.tsx create mode 100644 frontend/src/pages/sub/SubQrButton.tsx delete mode 100644 frontend/src/pages/sub/SubUsageSummary.css delete mode 100644 frontend/src/pages/sub/SubUsageSummary.tsx create mode 100644 frontend/src/pages/sub/app-icons/README.md create mode 100644 frontend/src/pages/sub/app-icons/happ.svg create mode 100644 frontend/src/pages/sub/app-icons/incy.webp create mode 100644 frontend/src/pages/sub/app-icons/shadowrocket.webp create mode 100644 frontend/src/pages/sub/app-icons/sing-box.svg create mode 100644 frontend/src/pages/sub/app-icons/streisand.webp create mode 100644 frontend/src/pages/sub/app-icons/v2box.webp create mode 100644 frontend/src/pages/sub/app-icons/v2rayng.svg create mode 100644 frontend/src/pages/sub/app-icons/v2raytun.svg create mode 100644 frontend/src/pages/sub/appIcons.ts create mode 100644 frontend/src/pages/sub/subPageModel.ts create mode 100644 frontend/src/test/sub-app-icons.test.ts create mode 100644 frontend/src/test/sub-page-model.test.ts diff --git a/frontend/src/env.d.ts b/frontend/src/env.d.ts index 2dbc5aa13..6aff7f8be 100644 --- a/frontend/src/env.d.ts +++ b/frontend/src/env.d.ts @@ -15,6 +15,8 @@ interface SubPageData { subJsonUrl?: string; subClashUrl?: string; subTitle?: string; + subSupportUrl?: string; + subUpdates?: number; links?: string[]; emails?: string[]; datepicker?: 'gregorian' | 'jalalian'; diff --git a/frontend/src/pages/inbounds/qr/QrPanel.css b/frontend/src/pages/inbounds/qr/QrPanel.css index b2e2c1d8b..d7d3955c8 100644 --- a/frontend/src/pages/inbounds/qr/QrPanel.css +++ b/frontend/src/pages/inbounds/qr/QrPanel.css @@ -28,7 +28,7 @@ .qr-panel-canvas .qr-code { cursor: pointer; background: #fff; - border-radius: 4px; + border-radius: 8px; line-height: 0; } diff --git a/frontend/src/pages/inbounds/qr/QrPanel.tsx b/frontend/src/pages/inbounds/qr/QrPanel.tsx index a035a7175..482b77935 100644 --- a/frontend/src/pages/inbounds/qr/QrPanel.tsx +++ b/frontend/src/pages/inbounds/qr/QrPanel.tsx @@ -141,7 +141,7 @@ export default function QrPanel({ value={value} size={size} errorLevel="L" - marginSize={4} + marginSize={2} type="svg" bordered={false} color="#000000" diff --git a/frontend/src/pages/sub/SubAppsTab.tsx b/frontend/src/pages/sub/SubAppsTab.tsx new file mode 100644 index 000000000..3ada0bfee --- /dev/null +++ b/frontend/src/pages/sub/SubAppsTab.tsx @@ -0,0 +1,60 @@ +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Button, Segmented } from 'antd'; +import { AndroidOutlined, AppleOutlined } from '@ant-design/icons'; + +import { APP_ICONS } from './appIcons'; +import type { AppPlatform, SubApp } from './subPageModel'; + +interface SubAppsTabProps { + apps: Record; + initialPlatform: AppPlatform; + onOpen: (url: string) => void; +} + +const PLATFORM_OPTIONS = [ + { value: 'android' as const, label: 'Android', icon: }, + { value: 'ios' as const, label: 'iOS', icon: }, +]; + +function AppIcon({ name }: { name: string }) { + const icon = APP_ICONS[name]; + if (!icon) { + return ( + + ); + } + if (icon.tinted) { + const mask = `url("${icon.src}")`; + return ( +