subPageModel.ts 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
  1. const DAY_MS = 86_400_000;
  2. export type SubStatus = 'active' | 'unlimited' | 'expired' | 'depleted' | 'disabled';
  3. export interface SubUsage {
  4. enabled: boolean;
  5. usedByte: number;
  6. totalByte: number;
  7. expireMs: number;
  8. }
  9. export function resolveSubStatus(sub: SubUsage, now: number): SubStatus {
  10. if (!sub.enabled) return 'disabled';
  11. if (sub.expireMs > 0 && now >= sub.expireMs) return 'expired';
  12. if (sub.totalByte > 0 && sub.usedByte >= sub.totalByte) return 'depleted';
  13. if (sub.totalByte <= 0 && sub.expireMs === 0) return 'unlimited';
  14. return 'active';
  15. }
  16. export function daysUntil(expireMs: number, now: number): number | null {
  17. if (expireMs <= 0) return null;
  18. return Math.max(0, Math.ceil((expireMs - now) / DAY_MS));
  19. }
  20. export function usagePercent(usedByte: number, totalByte: number): number {
  21. if (totalByte <= 0) return 0;
  22. const pct = (usedByte / totalByte) * 100;
  23. return Number.isFinite(pct) ? Math.min(100, Math.max(0, pct)) : 0;
  24. }
  25. export type AppPlatform = 'android' | 'ios';
  26. export function detectPlatform(userAgent: string): AppPlatform {
  27. // iPadOS sends a Macintosh UA, and App Store clients also run on Apple-silicon Macs.
  28. if (/iphone|ipad|ipod|macintosh/i.test(userAgent)) return 'ios';
  29. return 'android';
  30. }
  31. export interface SubApp {
  32. name: string;
  33. url: string;
  34. }
  35. export interface SubAppSource {
  36. subUrl: string;
  37. sId: string;
  38. subTitle: string;
  39. }
  40. export function buildSubApps({
  41. subUrl,
  42. sId,
  43. subTitle,
  44. }: SubAppSource): Record<AppPlatform, SubApp[]> {
  45. const encSub = encodeURIComponent(subUrl);
  46. const profileName = encodeURIComponent(subTitle || sId);
  47. const v2box = {
  48. name: 'V2Box',
  49. url: `v2box://install-sub?url=${encSub}&name=${encodeURIComponent(sId)}`,
  50. };
  51. const singBox = {
  52. name: 'Sing-box',
  53. url: `sing-box://import-remote-profile?url=${encSub}#${profileName}`,
  54. };
  55. const v2raytun = { name: 'V2RayTun', url: `v2raytun://import/${subUrl}` };
  56. const happ = { name: 'Happ', url: `happ://add/${subUrl}` };
  57. const incy = { name: 'Incy', url: `incy://add/${subUrl}` };
  58. const rocketSource = `${subUrl}${subUrl.includes('?') ? '&' : '?'}flag=shadowrocket`;
  59. const rocketRemark = encodeURIComponent(subTitle || sId || 'Subscription');
  60. return {
  61. android: [
  62. v2box,
  63. { name: 'V2RayNG', url: `v2rayng://install-config?url=${encSub}` },
  64. singBox,
  65. v2raytun,
  66. happ,
  67. incy,
  68. ],
  69. ios: [
  70. {
  71. name: 'Shadowrocket',
  72. url: `shadowrocket://add/sub://${btoa(rocketSource)}?remark=${rocketRemark}`,
  73. },
  74. v2box,
  75. { name: 'Streisand', url: `streisand://import/${encSub}` },
  76. v2raytun,
  77. happ,
  78. incy,
  79. ],
  80. };
  81. }