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
+69
View File
@@ -0,0 +1,69 @@
import { useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, Input, Modal, QRCode } from 'antd';
import { CopyOutlined, DownloadOutlined, QrcodeOutlined } from '@ant-design/icons';
interface SubQrButtonProps {
value: string;
label: string;
onCopy: (value: string) => void;
}
export default function SubQrButton({ value, label, onCopy }: SubQrButtonProps) {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const qrRef = useRef<HTMLDivElement>(null);
const saveQr = () => {
const canvas = qrRef.current?.querySelector('canvas');
if (!canvas) return;
const link = document.createElement('a');
link.href = canvas.toDataURL('image/png');
link.download = `${label || 'qrcode'}.png`;
link.click();
};
return (
<>
<Button icon={<QrcodeOutlined />} aria-label="QR" title="QR" onClick={() => setOpen(true)} />
<Modal
open={open}
onCancel={() => setOpen(false)}
footer={null}
width={440}
centered
destroyOnHidden
rootClassName="sub-qr-modal"
title={t('subscription.qrTitle')}
>
<p className="sub-muted sub-qr-modal-hint">{t('subscription.qrHint')}</p>
<div ref={qrRef} className="sub-qr-modal-code">
<QRCode
value={value}
size={240}
type="canvas"
marginSize={2}
bordered={false}
color="#000000"
bgColor="#ffffff"
/>
</div>
<Input.TextArea
className="sub-qr-modal-link"
value={value}
readOnly
dir="ltr"
autoSize={{ minRows: 2, maxRows: 5 }}
/>
<div className="sub-qr-modal-actions">
<Button type="primary" size="large" icon={<CopyOutlined />} onClick={() => onCopy(value)}>
{t('copy')}
</Button>
<Button size="large" icon={<DownloadOutlined />} onClick={saveQr}>
{t('subscription.saveQr')}
</Button>
</div>
</Modal>
</>
);
}