import { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Button, Card, Col, ConfigProvider, Descriptions, Dropdown, Layout, Menu, message, Popover, QRCode, Row, Space, Tag, } from 'antd'; import { AndroidOutlined, AppleOutlined, CopyOutlined, DownOutlined, MoonFilled, MoonOutlined, SunOutlined, TranslationOutlined, } from '@ant-design/icons'; import { ClipboardManager, IntlUtil, LanguageManager } from '@/utils'; import { setMessageInstance } from '@/utils/messageBus'; import { pauseAnimationsUntilLeave, useTheme } from '@/hooks/useTheme'; import './SubPage.css'; const QR_SIZE = 240; const subData = window.__SUB_PAGE_DATA__ || {}; const sId = subData.sId || ''; const enabled = !!subData.enabled; const download = subData.download || '0'; const upload = subData.upload || '0'; const total = subData.total || '∞'; const used = subData.used || '0'; const remained = subData.remained || ''; const totalByte = Number(subData.totalByte || 0); const expireMs = Number(subData.expire || 0) * 1000; const lastOnlineMs = Number(subData.lastOnline || 0); const subUrl = subData.subUrl || ''; const subJsonUrl = subData.subJsonUrl || ''; const subClashUrl = subData.subClashUrl || ''; const subTitle = subData.subTitle || ''; const links: string[] = Array.isArray(subData.links) ? subData.links : []; const datepicker = subData.datepicker || 'gregorian'; const isUnlimited = totalByte <= 0 && expireMs === 0; const isActive = (() => { if (!enabled) return false; if (totalByte > 0) { const usedByteCalc = Number(subData.usedByte || 0) || (Number(subData.downloadByte || 0) + Number(subData.uploadByte || 0)); if (usedByteCalc >= totalByte) return false; } if (expireMs > 0 && Date.now() >= expireMs) return false; return true; })(); function linkName(link: string, idx: number): string { if (!link) return `Link ${idx + 1}`; const hashIdx = link.indexOf('#'); if (hashIdx >= 0 && hashIdx + 1 < link.length) { try { return decodeURIComponent(link.slice(hashIdx + 1)); } catch { return link.slice(hashIdx + 1); } } const proto = link.split('://')[0]; return `${proto.toUpperCase()} ${idx + 1}`; } export default function SubPage() { const { t } = useTranslation(); const { isDark, isUltra, toggleTheme, toggleUltra, antdThemeConfig } = useTheme(); const [messageApi, messageContextHolder] = message.useMessage(); useEffect(() => { setMessageInstance(messageApi); }, [messageApi]); const [isMobile, setIsMobile] = useState(() => window.innerWidth < 576); const [lang, setLang] = useState(() => LanguageManager.getLanguage()); useEffect(() => { const onResize = () => setIsMobile(window.innerWidth < 576); window.addEventListener('resize', onResize); return () => window.removeEventListener('resize', onResize); }, []); const onLangChange = useCallback((next: string) => { setLang(next); LanguageManager.setLanguage(next); }, []); const cycleTheme = useCallback(() => { pauseAnimationsUntilLeave('sub-theme-cycle'); if (!isDark) { toggleTheme(); if (isUltra) toggleUltra(); } else if (!isUltra) { toggleUltra(); } else { toggleUltra(); toggleTheme(); } }, [isDark, isUltra, toggleTheme, toggleUltra]); const copy = useCallback(async (value: string) => { if (!value) return; const ok = await ClipboardManager.copyText(value); if (ok) messageApi.success(t('copied')); }, [t, messageApi]); const open = useCallback((url: string) => { if (!url) return; window.open(url, '_blank'); }, []); const shadowrocketUrl = useMemo(() => { if (!subUrl) return ''; const separator = subUrl.includes('?') ? '&' : '?'; const rawUrl = subUrl + separator + 'flag=shadowrocket'; const base64Url = btoa(rawUrl).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); const remark = encodeURIComponent(subTitle || sId || 'Subscription'); return `shadowrocket://add/sub/${base64Url}?remark=${remark}`; }, []); const v2boxUrl = useMemo( () => `v2box://install-sub?url=${encodeURIComponent(subUrl)}&name=${encodeURIComponent(sId)}`, [], ); const streisandUrl = useMemo(() => `streisand://import/${encodeURIComponent(subUrl)}`, []); const happUrl = useMemo(() => `happ://add/${subUrl}`, []); const pageClass = useMemo(() => { const classes = ['subscription-page']; if (isDark) classes.push('is-dark'); if (isUltra) classes.push('is-ultra'); return classes.join(' '); }, [isDark, isUltra]); const descriptionsItems = useMemo(() => { const items = [ { key: 'subId', label: t('subscription.subId'), children: sId }, { key: 'status', label: t('subscription.status'), children: !enabled ? {t('subscription.inactive')} : isUnlimited ? {t('subscription.unlimited')} : {isActive ? t('subscription.active') : t('subscription.inactive')} , }, { key: 'down', label: t('subscription.downloaded'), children: download }, { key: 'up', label: t('subscription.uploaded'), children: upload }, { key: 'used', label: t('usage'), children: used }, { key: 'total', label: t('subscription.totalQuota'), children: total }, ]; if (totalByte > 0) { items.push({ key: 'remained', label: t('remained'), children: remained }); } items.push({ key: 'lastOnline', label: t('lastOnline'), children: lastOnlineMs > 0 ? IntlUtil.formatDate(lastOnlineMs, datepicker) : '-', }); items.push({ key: 'expiry', label: t('subscription.expiry'), children: expireMs === 0 ? t('subscription.noExpiry') : IntlUtil.formatDate(expireMs, datepicker), }); return items; }, [t]); const androidMenuItems = useMemo(() => [ { key: 'android-v2box', label: 'V2Box', onClick: () => open(`v2box://install-sub?url=${encodeURIComponent(subUrl)}&name=${encodeURIComponent(sId)}`), }, { key: 'android-v2rayng', label: 'V2RayNG', onClick: () => open(`v2rayng://install-config?url=${encodeURIComponent(subUrl)}`), }, { key: 'android-singbox', label: 'Sing-box', onClick: () => copy(subUrl) }, { key: 'android-v2raytun', label: 'V2RayTun', onClick: () => copy(subUrl) }, { key: 'android-npvtunnel', label: 'NPV Tunnel', onClick: () => copy(subUrl) }, { key: 'android-happ', label: 'Happ', onClick: () => open(`happ://add/${subUrl}`) }, ], [copy, open]); const iosMenuItems = useMemo(() => [ { key: 'ios-shadowrocket', label: 'Shadowrocket', onClick: () => open(shadowrocketUrl) }, { key: 'ios-v2box', label: 'V2Box', onClick: () => open(v2boxUrl) }, { key: 'ios-streisand', label: 'Streisand', onClick: () => open(streisandUrl) }, { key: 'ios-v2raytun', label: 'V2RayTun', onClick: () => copy(subUrl) }, { key: 'ios-npvtunnel', label: 'NPV Tunnel', onClick: () => copy(subUrl) }, { key: 'ios-happ', label: 'Happ', onClick: () => open(happUrl) }, ], [copy, open, shadowrocketUrl, v2boxUrl, streisandUrl, happUrl]); const langMenuItems = useMemo( () => (LanguageManager.supportedLanguages as { value: string; name: string; icon: string }[]).map((l) => ({ key: l.value, label: ( {l.name} ), })), [], ); const themeIcon = !isDark ? : !isUltra ? : ; const cardTitle = ( {t('subscription.title')} {sId} ); const cardExtra = ( ); }