mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-18 16:17:16 +00:00
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:
@@ -0,0 +1,93 @@
|
||||
const DAY_MS = 86_400_000;
|
||||
|
||||
export type SubStatus = 'active' | 'unlimited' | 'expired' | 'depleted' | 'disabled';
|
||||
|
||||
export interface SubUsage {
|
||||
enabled: boolean;
|
||||
usedByte: number;
|
||||
totalByte: number;
|
||||
expireMs: number;
|
||||
}
|
||||
|
||||
export function resolveSubStatus(sub: SubUsage, now: number): SubStatus {
|
||||
if (!sub.enabled) return 'disabled';
|
||||
if (sub.expireMs > 0 && now >= sub.expireMs) return 'expired';
|
||||
if (sub.totalByte > 0 && sub.usedByte >= sub.totalByte) return 'depleted';
|
||||
if (sub.totalByte <= 0 && sub.expireMs === 0) return 'unlimited';
|
||||
return 'active';
|
||||
}
|
||||
|
||||
export function daysUntil(expireMs: number, now: number): number | null {
|
||||
if (expireMs <= 0) return null;
|
||||
return Math.max(0, Math.ceil((expireMs - now) / DAY_MS));
|
||||
}
|
||||
|
||||
export function usagePercent(usedByte: number, totalByte: number): number {
|
||||
if (totalByte <= 0) return 0;
|
||||
const pct = (usedByte / totalByte) * 100;
|
||||
return Number.isFinite(pct) ? Math.min(100, Math.max(0, pct)) : 0;
|
||||
}
|
||||
|
||||
export type AppPlatform = 'android' | 'ios';
|
||||
|
||||
export function detectPlatform(userAgent: string): AppPlatform {
|
||||
// iPadOS sends a Macintosh UA, and App Store clients also run on Apple-silicon Macs.
|
||||
if (/iphone|ipad|ipod|macintosh/i.test(userAgent)) return 'ios';
|
||||
return 'android';
|
||||
}
|
||||
|
||||
export interface SubApp {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface SubAppSource {
|
||||
subUrl: string;
|
||||
sId: string;
|
||||
subTitle: string;
|
||||
}
|
||||
|
||||
export function buildSubApps({
|
||||
subUrl,
|
||||
sId,
|
||||
subTitle,
|
||||
}: SubAppSource): Record<AppPlatform, SubApp[]> {
|
||||
const encSub = encodeURIComponent(subUrl);
|
||||
const profileName = encodeURIComponent(subTitle || sId);
|
||||
|
||||
const v2box = {
|
||||
name: 'V2Box',
|
||||
url: `v2box://install-sub?url=${encSub}&name=${encodeURIComponent(sId)}`,
|
||||
};
|
||||
const singBox = {
|
||||
name: 'Sing-box',
|
||||
url: `sing-box://import-remote-profile?url=${encSub}#${profileName}`,
|
||||
};
|
||||
const v2raytun = { name: 'V2RayTun', url: `v2raytun://import/${subUrl}` };
|
||||
const happ = { name: 'Happ', url: `happ://add/${subUrl}` };
|
||||
const incy = { name: 'Incy', url: `incy://add/${subUrl}` };
|
||||
const rocketSource = `${subUrl}${subUrl.includes('?') ? '&' : '?'}flag=shadowrocket`;
|
||||
const rocketRemark = encodeURIComponent(subTitle || sId || 'Subscription');
|
||||
|
||||
return {
|
||||
android: [
|
||||
v2box,
|
||||
{ name: 'V2RayNG', url: `v2rayng://install-config?url=${encSub}` },
|
||||
singBox,
|
||||
v2raytun,
|
||||
happ,
|
||||
incy,
|
||||
],
|
||||
ios: [
|
||||
{
|
||||
name: 'Shadowrocket',
|
||||
url: `shadowrocket://add/sub://${btoa(rocketSource)}?remark=${rocketRemark}`,
|
||||
},
|
||||
v2box,
|
||||
{ name: 'Streisand', url: `streisand://import/${encSub}` },
|
||||
v2raytun,
|
||||
happ,
|
||||
incy,
|
||||
],
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user