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.
This commit is contained in:
Sanaei
2026-09-16 01:17:04 +02:00
parent 01ce2bcecb
commit e26cf1d3ed
42 changed files with 1869 additions and 877 deletions
+4
View File
@@ -13,6 +13,7 @@ import (
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
"time"
@@ -660,6 +661,8 @@ func (a *SUBController) subPageContext(page PageData) map[string]any {
if datepicker == "" {
datepicker = "gregorian"
}
subUpdates, _ := a.settingService.GetSubUpdates()
updateHours, _ := strconv.Atoi(subUpdates)
return map[string]any{
"sId": page.SId,
@@ -680,6 +683,7 @@ func (a *SUBController) subPageContext(page PageData) map[string]any {
"subClashUrl": page.SubClashUrl,
"subTitle": page.SubTitle,
"subSupportUrl": page.SubSupportUrl,
"subUpdates": updateHours,
"links": page.Result,
"emails": page.Emails,
"datepicker": datepicker,
+3
View File
@@ -76,6 +76,9 @@ func TestSubInfoEndpoint_ServesStatusJSONEvenForBrowsers(t *testing.T) {
t.Fatalf("info payload missing %q; body=%s", key, w.Body.String())
}
}
if info["subUpdates"] != float64(12) {
t.Fatalf("subUpdates = %v, want the default 12-hour interval", info["subUpdates"])
}
}
func TestSubInfoEndpoint_UnknownSubIs404(t *testing.T) {
+14 -2
View File
@@ -80,7 +80,6 @@
"somethingWentWrong": "حدث خطأ ما",
"subscription": {
"title": "معلومات الاشتراك",
"subId": "معرّف الاشتراك",
"status": "الحالة",
"downloaded": "التنزيل",
"uploaded": "الرفع",
@@ -93,7 +92,20 @@
"noExpiry": "بدون انتهاء",
"copyAllConfigs": "نسخ جميع الإعدادات",
"copyAllConfigsCopied": "تم نسخ جميع الإعدادات",
"email": "البريد"
"daysLeft": "الأيام المتبقية",
"expired": "منتهي الصلاحية",
"depleted": "نفدت البيانات",
"ofTotal": "من {total}",
"tabLinks": "الاشتراك",
"tabApps": "التطبيقات",
"tabConfigs": "الكونفيجات",
"scanTitle": "امسح بهاتفك",
"scanHint": "امسح هذا الرمز في تطبيق VPN لإضافة الاشتراك دون نسخ الرابط.",
"updateInterval": "تحديث تلقائي كل {hours} ساعة",
"support": "الدعم",
"qrTitle": "امسح. استورد. اتصل.",
"qrHint": "امسح الرمز بتطبيق العميل. لا تشارك هذا الرمز مع أحد.",
"saveQr": "حفظ QR"
},
"menu": {
"theme": "الثيم",
+15 -3
View File
@@ -80,8 +80,6 @@
"somethingWentWrong": "Something went wrong",
"subscription": {
"title": "Subscription info",
"subId": "Subscription ID",
"email": "Email",
"status": "Status",
"downloaded": "Downloaded",
"uploaded": "Uploaded",
@@ -93,7 +91,21 @@
"unlimited": "Unlimited",
"noExpiry": "No expiry",
"copyAllConfigs": "Copy All Configs",
"copyAllConfigsCopied": "All configs copied"
"copyAllConfigsCopied": "All configs copied",
"daysLeft": "Days left",
"expired": "Expired",
"depleted": "Data used up",
"ofTotal": "of {total}",
"tabLinks": "Subscription",
"tabApps": "Apps",
"tabConfigs": "Configs",
"scanTitle": "Scan with your phone",
"scanHint": "Scan this code in your VPN app to add the subscription without copying the link.",
"updateInterval": "Auto-updates every {hours} hours",
"support": "Support",
"qrTitle": "Scan. Import. Connect.",
"qrHint": "Scan with your client app. Keep this code private.",
"saveQr": "Save QR"
},
"menu": {
"theme": "Theme",
+14 -2
View File
@@ -80,7 +80,6 @@
"somethingWentWrong": "Algo salió mal",
"subscription": {
"title": "Información de suscripción",
"subId": "ID de suscripción",
"status": "Estado",
"downloaded": "Descargado",
"uploaded": "Subido",
@@ -93,7 +92,20 @@
"noExpiry": "Sin caducidad",
"copyAllConfigs": "Copiar Todas las Configuraciones",
"copyAllConfigsCopied": "Todas las configuraciones copiadas",
"email": "Email"
"daysLeft": "Días restantes",
"expired": "Caducada",
"depleted": "Datos agotados",
"ofTotal": "de {total}",
"tabLinks": "Suscripción",
"tabApps": "Aplicaciones",
"tabConfigs": "Configuraciones",
"scanTitle": "Escanea con tu teléfono",
"scanHint": "Escanea este código en tu aplicación VPN para añadir la suscripción sin copiar el enlace.",
"updateInterval": "Se actualiza automáticamente cada {hours} horas",
"support": "Soporte",
"qrTitle": "Escanea. Importa. Conéctate.",
"qrHint": "Escanéalo con tu aplicación cliente. Mantén este código en privado.",
"saveQr": "Guardar QR"
},
"menu": {
"theme": "Tema",
+15 -3
View File
@@ -80,8 +80,6 @@
"somethingWentWrong": "مشکلی پیش آمد",
"subscription": {
"title": "اطلاعات سابسکریپشن",
"subId": "شناسه اشتراک",
"email": "ایمیل",
"status": "وضعیت",
"downloaded": "دانلود",
"uploaded": "آپلود",
@@ -93,7 +91,21 @@
"unlimited": "نامحدود",
"noExpiry": "بدون انقضا",
"copyAllConfigs": "کپی همه کانفیگ‌ها",
"copyAllConfigsCopied": "همه کانفیگ‌ها کپی شدند"
"copyAllConfigsCopied": "همه کانفیگ‌ها کپی شدند",
"daysLeft": "روز باقی‌مانده",
"expired": "منقضی شده",
"depleted": "حجم تمام شده",
"ofTotal": "از {total}",
"tabLinks": "اشتراک",
"tabApps": "اپلیکیشن‌ها",
"tabConfigs": "کانفیگ‌ها",
"scanTitle": "اسکن با گوشی",
"scanHint": "این کد را در اپلیکیشن VPN اسکن کنید تا اشتراک بدون کپی‌کردن لینک اضافه شود.",
"updateInterval": "به‌روزرسانی خودکار هر {hours} ساعت",
"support": "پشتیبانی",
"qrTitle": "اسکن. افزودن. اتصال.",
"qrHint": "با اپلیکیشن کلاینت اسکن کنید. این کد را به کسی ندهید.",
"saveQr": "ذخیره QR"
},
"menu": {
"theme": "تم",
+14 -2
View File
@@ -80,7 +80,6 @@
"somethingWentWrong": "Terjadi kesalahan",
"subscription": {
"title": "Info langganan",
"subId": "ID langganan",
"status": "Status",
"downloaded": "Diunduh",
"uploaded": "Diunggah",
@@ -93,7 +92,20 @@
"noExpiry": "Tanpa kedaluwarsa",
"copyAllConfigs": "Salin Semua Konfigurasi",
"copyAllConfigsCopied": "Semua konfigurasi tersalin",
"email": "Email"
"daysLeft": "Sisa hari",
"expired": "Kedaluwarsa",
"depleted": "Kuota habis",
"ofTotal": "dari {total}",
"tabLinks": "Langganan",
"tabApps": "Aplikasi",
"tabConfigs": "Konfigurasi",
"scanTitle": "Pindai dengan ponsel",
"scanHint": "Pindai kode ini di aplikasi VPN untuk menambahkan langganan tanpa menyalin tautan.",
"updateInterval": "Diperbarui otomatis setiap {hours} jam",
"support": "Dukungan",
"qrTitle": "Pindai. Impor. Terhubung.",
"qrHint": "Pindai dengan aplikasi klien Anda. Jaga kerahasiaan kode ini.",
"saveQr": "Simpan QR"
},
"menu": {
"theme": "Tema",
+14 -2
View File
@@ -80,7 +80,6 @@
"somethingWentWrong": "エラーが発生しました",
"subscription": {
"title": "サブスクリプション情報",
"subId": "サブスクリプションID",
"status": "ステータス",
"downloaded": "ダウンロード",
"uploaded": "アップロード",
@@ -93,7 +92,20 @@
"noExpiry": "期限なし",
"copyAllConfigs": "すべての設定をコピー",
"copyAllConfigsCopied": "すべての設定をコピーしました",
"email": "メール"
"daysLeft": "残り日数",
"expired": "期限切れ",
"depleted": "データ上限到達",
"ofTotal": "/ {total}",
"tabLinks": "サブスクリプション",
"tabApps": "アプリ",
"tabConfigs": "設定",
"scanTitle": "スマートフォンでスキャン",
"scanHint": "VPN アプリでこのコードをスキャンすると、リンクをコピーせずにサブスクリプションを追加できます。",
"updateInterval": "{hours} 時間ごとに自動更新",
"support": "サポート",
"qrTitle": "スキャン。インポート。接続。",
"qrHint": "クライアントアプリでスキャンしてください。このコードは他人に見せないでください。",
"saveQr": "QR を保存"
},
"menu": {
"theme": "テーマ",
+14 -2
View File
@@ -80,7 +80,6 @@
"somethingWentWrong": "Algo deu errado",
"subscription": {
"title": "Informações da assinatura",
"subId": "ID da assinatura",
"status": "Status",
"downloaded": "Baixado",
"uploaded": "Enviado",
@@ -93,7 +92,20 @@
"noExpiry": "Sem validade",
"copyAllConfigs": "Copiar Todas as Configurações",
"copyAllConfigsCopied": "Todas as configurações copiadas",
"email": "Email"
"daysLeft": "Dias restantes",
"expired": "Expirada",
"depleted": "Dados esgotados",
"ofTotal": "de {total}",
"tabLinks": "Assinatura",
"tabApps": "Aplicativos",
"tabConfigs": "Configurações",
"scanTitle": "Escaneie com o celular",
"scanHint": "Escaneie este código no seu aplicativo de VPN para adicionar a assinatura sem copiar o link.",
"updateInterval": "Atualiza automaticamente a cada {hours} horas",
"support": "Suporte",
"qrTitle": "Escaneie. Importe. Conecte.",
"qrHint": "Escaneie com seu aplicativo cliente. Mantenha este código em sigilo.",
"saveQr": "Salvar QR"
},
"menu": {
"theme": "Tema",
+14 -2
View File
@@ -80,7 +80,6 @@
"somethingWentWrong": "Что-то пошло не так",
"subscription": {
"title": "Информация о подписке",
"subId": "ID подписки",
"status": "Статус",
"downloaded": "Загружено",
"uploaded": "Отправлено",
@@ -93,7 +92,20 @@
"noExpiry": "Бессрочно",
"copyAllConfigs": "Копировать все конфигурации",
"copyAllConfigsCopied": "Все конфигурации скопированы",
"email": "Email"
"daysLeft": "Осталось дней",
"expired": "Истёк",
"depleted": "Трафик исчерпан",
"ofTotal": "из {total}",
"tabLinks": "Подписка",
"tabApps": "Приложения",
"tabConfigs": "Конфиги",
"scanTitle": "Сканируйте телефоном",
"scanHint": "Отсканируйте этот код в VPN-приложении, чтобы добавить подписку без копирования ссылки.",
"updateInterval": "Автообновление каждые {hours} ч",
"support": "Поддержка",
"qrTitle": "Сканируйте. Импортируйте. Подключайтесь.",
"qrHint": "Отсканируйте в клиентском приложении. Никому не показывайте этот код.",
"saveQr": "Сохранить QR"
},
"menu": {
"theme": "Тема",
+14 -2
View File
@@ -80,7 +80,6 @@
"somethingWentWrong": "Bir hata oluştu",
"subscription": {
"title": "Abonelik Bilgisi",
"subId": "Abonelik Kimliği",
"status": "Durum",
"downloaded": "İndirilen",
"uploaded": "Yüklenen",
@@ -93,7 +92,20 @@
"noExpiry": "Süresiz",
"copyAllConfigs": "Tüm Yapılandırmaları Kopyala",
"copyAllConfigsCopied": "Tüm yapılandırmalar kopyalandı",
"email": "E-posta"
"daysLeft": "Kalan gün",
"expired": "Süresi doldu",
"depleted": "Kota doldu",
"ofTotal": "/ {total}",
"tabLinks": "Abonelik",
"tabApps": "Uygulamalar",
"tabConfigs": "Yapılandırmalar",
"scanTitle": "Telefonunuzla tarayın",
"scanHint": "Bağlantıyı kopyalamadan aboneliği eklemek için bu kodu VPN uygulamanızda tarayın.",
"updateInterval": "Her {hours} saatte bir otomatik güncellenir",
"support": "Destek",
"qrTitle": "Tara. İçe aktar. Bağlan.",
"qrHint": "İstemci uygulamanızla tarayın. Bu kodu kimseyle paylaşmayın.",
"saveQr": "QR'ı kaydet"
},
"menu": {
"theme": "Tema",
+14 -2
View File
@@ -80,7 +80,6 @@
"somethingWentWrong": "Щось пішло не так",
"subscription": {
"title": "Інформація про підписку",
"subId": "ID підписки",
"status": "Статус",
"downloaded": "Завантажено",
"uploaded": "Відвантажено",
@@ -93,7 +92,20 @@
"noExpiry": "Без строку",
"copyAllConfigs": "Копіювати всі конфігурації",
"copyAllConfigsCopied": "Всі конфігурації скопійовано",
"email": "Email"
"daysLeft": "Залишилось днів",
"expired": "Термін дії минув",
"depleted": "Трафік вичерпано",
"ofTotal": "з {total}",
"tabLinks": "Підписка",
"tabApps": "Застосунки",
"tabConfigs": "Конфіги",
"scanTitle": "Скануйте телефоном",
"scanHint": "Відскануйте цей код у VPN-застосунку, щоб додати підписку без копіювання посилання.",
"updateInterval": "Автооновлення кожні {hours} год",
"support": "Підтримка",
"qrTitle": "Скануйте. Імпортуйте. Підключайтеся.",
"qrHint": "Відскануйте в клієнтському застосунку. Нікому не показуйте цей код.",
"saveQr": "Зберегти QR"
},
"menu": {
"theme": "Тема",
+14 -2
View File
@@ -80,7 +80,6 @@
"somethingWentWrong": "Đã xảy ra lỗi",
"subscription": {
"title": "Thông tin đăng ký",
"subId": "ID đăng ký",
"status": "Trạng thái",
"downloaded": "Đã tải xuống",
"uploaded": "Đã tải lên",
@@ -93,7 +92,20 @@
"noExpiry": "Không hết hạn",
"copyAllConfigs": "Sao chép tất cả cấu hình",
"copyAllConfigsCopied": "Đã sao chép tất cả cấu hình",
"email": "Email"
"daysLeft": "Số ngày còn lại",
"expired": "Đã hết hạn",
"depleted": "Hết dung lượng",
"ofTotal": "trên {total}",
"tabLinks": "Gói đăng ký",
"tabApps": "Ứng dụng",
"tabConfigs": "Cấu hình",
"scanTitle": "Quét bằng điện thoại",
"scanHint": "Quét mã này trong ứng dụng VPN để thêm gói đăng ký mà không cần sao chép liên kết.",
"updateInterval": "Tự động cập nhật mỗi {hours} giờ",
"support": "Hỗ trợ",
"qrTitle": "Quét. Nhập. Kết nối.",
"qrHint": "Quét bằng ứng dụng khách. Không chia sẻ mã này cho người khác.",
"saveQr": "Lưu QR"
},
"menu": {
"theme": "Chủ đề",
+14 -2
View File
@@ -80,7 +80,6 @@
"somethingWentWrong": "出了点问题",
"subscription": {
"title": "订阅信息",
"subId": "订阅 ID",
"status": "状态",
"downloaded": "已下载",
"uploaded": "已上传",
@@ -93,7 +92,20 @@
"noExpiry": "无到期",
"copyAllConfigs": "复制全部配置",
"copyAllConfigsCopied": "已复制全部配置",
"email": "邮箱"
"daysLeft": "剩余天数",
"expired": "已过期",
"depleted": "流量已用完",
"ofTotal": "共 {total}",
"tabLinks": "订阅",
"tabApps": "应用",
"tabConfigs": "配置",
"scanTitle": "用手机扫码",
"scanHint": "在 VPN 应用中扫描此二维码,无需复制链接即可添加订阅。",
"updateInterval": "每 {hours} 小时自动更新",
"support": "支持",
"qrTitle": "扫码。导入。连接。",
"qrHint": "使用客户端应用扫描。请勿泄露此二维码。",
"saveQr": "保存二维码"
},
"menu": {
"theme": "主题",
+14 -2
View File
@@ -80,7 +80,6 @@
"somethingWentWrong": "發生錯誤",
"subscription": {
"title": "訂閱資訊",
"subId": "訂閱 ID",
"status": "狀態",
"downloaded": "已下載",
"uploaded": "已上傳",
@@ -93,7 +92,20 @@
"noExpiry": "無到期",
"copyAllConfigs": "複製全部配置",
"copyAllConfigsCopied": "已複製全部配置",
"email": "電子郵件"
"daysLeft": "剩餘天數",
"expired": "已過期",
"depleted": "流量已用完",
"ofTotal": "共 {total}",
"tabLinks": "訂閱",
"tabApps": "應用程式",
"tabConfigs": "設定檔",
"scanTitle": "用手機掃描",
"scanHint": "在 VPN 應用程式中掃描此 QR 碼,無需複製連結即可新增訂閱。",
"updateInterval": "每 {hours} 小時自動更新",
"support": "支援",
"qrTitle": "掃描。匯入。連線。",
"qrHint": "使用用戶端應用程式掃描。請勿外洩此 QR 碼。",
"saveQr": "儲存 QR 碼"
},
"menu": {
"theme": "主題",