subscription.js 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. (function () {
  2. // Vue app for Subscription page
  3. const el = document.getElementById('subscription-data');
  4. if (!el) return;
  5. const textarea = document.getElementById('subscription-links');
  6. const rawLinks = (textarea?.value || '').split('\n').filter(Boolean);
  7. const data = {
  8. sId: el.getAttribute('data-sid') || '',
  9. subUrl: el.getAttribute('data-sub-url') || '',
  10. subJsonUrl: el.getAttribute('data-subjson-url') || '',
  11. download: el.getAttribute('data-download') || '',
  12. upload: el.getAttribute('data-upload') || '',
  13. used: el.getAttribute('data-used') || '',
  14. total: el.getAttribute('data-total') || '',
  15. remained: el.getAttribute('data-remained') || '',
  16. expireMs: (parseInt(el.getAttribute('data-expire') || '0', 10) || 0) * 1000,
  17. lastOnlineMs: (parseInt(el.getAttribute('data-lastonline') || '0', 10) || 0),
  18. downloadByte: parseInt(el.getAttribute('data-downloadbyte') || '0', 10) || 0,
  19. uploadByte: parseInt(el.getAttribute('data-uploadbyte') || '0', 10) || 0,
  20. totalByte: parseInt(el.getAttribute('data-totalbyte') || '0', 10) || 0,
  21. datepicker: el.getAttribute('data-datepicker') || 'gregorian',
  22. };
  23. // Normalize lastOnline to milliseconds if it looks like seconds
  24. if (data.lastOnlineMs && data.lastOnlineMs < 10_000_000_000) {
  25. data.lastOnlineMs *= 1000;
  26. }
  27. function renderLink(item) {
  28. return (
  29. Vue.h('a-list-item', {}, [
  30. Vue.h('a-space', { props: { size: 'small' } }, [
  31. Vue.h('a-button', { props: { size: 'small' }, on: { click: () => copy(item) } }, [Vue.h('a-icon', { props: { type: 'copy' } })]),
  32. Vue.h('span', { class: 'break-all' }, item)
  33. ])
  34. ])
  35. );
  36. }
  37. function copy(text) {
  38. ClipboardManager.copyText(text).then(ok => {
  39. const messageType = ok ? 'success' : 'error';
  40. Vue.prototype.$message[messageType](ok ? 'Copied' : 'Copy failed');
  41. });
  42. }
  43. function open(url) {
  44. window.location.href = url;
  45. }
  46. function drawQR(value) {
  47. try { new QRious({ element: document.getElementById('qrcode'), value, size: 220 }); } catch (e) { console.warn(e); }
  48. }
  49. // Try to extract a human label (email/ps) from different link types
  50. function linkName(link, idx) {
  51. try {
  52. if (link.startsWith('vmess://')) {
  53. const json = JSON.parse(atob(link.replace('vmess://', '')));
  54. if (json.ps) return json.ps;
  55. if (json.add && json.id) return json.add; // fallback host
  56. } else if (link.startsWith('vless://') || link.startsWith('trojan://')) {
  57. // vless://<id>@host:port?...#name
  58. const hashIdx = link.indexOf('#');
  59. if (hashIdx !== -1) return decodeURIComponent(link.substring(hashIdx + 1));
  60. // email sometimes in query params like sni or remark
  61. const qIdx = link.indexOf('?');
  62. if (qIdx !== -1) {
  63. const qs = new URL('http://x/?' + link.substring(qIdx + 1, hashIdx !== -1 ? hashIdx : undefined)).searchParams;
  64. if (qs.get('remark')) return qs.get('remark');
  65. if (qs.get('email')) return qs.get('email');
  66. }
  67. // else take user@host
  68. const at = link.indexOf('@');
  69. const protSep = link.indexOf('://');
  70. if (at !== -1 && protSep !== -1) return link.substring(protSep + 3, at);
  71. } else if (link.startsWith('ss://')) {
  72. // shadowsocks: label often after #
  73. const hashIdx = link.indexOf('#');
  74. if (hashIdx !== -1) return decodeURIComponent(link.substring(hashIdx + 1));
  75. }
  76. } catch (e) { /* ignore and fallback */ }
  77. return 'Link ' + (idx + 1);
  78. }
  79. const app = new Vue({
  80. delimiters: ['[[', ']]'],
  81. el: '#app',
  82. data: {
  83. themeSwitcher,
  84. app: data,
  85. links: rawLinks,
  86. lang: '',
  87. viewportWidth: (typeof window !== 'undefined' ? window.innerWidth : 1024),
  88. },
  89. async mounted() {
  90. this.lang = LanguageManager.getLanguage();
  91. // Discover subJsonUrl if provided via template bootstrap
  92. const tpl = document.getElementById('subscription-data');
  93. const sj = tpl ? tpl.getAttribute('data-subjson-url') : '';
  94. if (sj) this.app.subJsonUrl = sj;
  95. drawQR(this.app.subUrl);
  96. // Draw second QR if available
  97. try { new QRious({ element: document.getElementById('qrcode-subjson'), value: this.app.subJsonUrl || '', size: 220 }); } catch (e) { /* ignore */ }
  98. // Track viewport width for responsive behavior
  99. this._onResize = () => { this.viewportWidth = window.innerWidth; };
  100. window.addEventListener('resize', this._onResize);
  101. },
  102. beforeDestroy() {
  103. if (this._onResize) window.removeEventListener('resize', this._onResize);
  104. },
  105. computed: {
  106. isMobile() { return this.viewportWidth < 576; },
  107. isUnlimited() { return !this.app.totalByte; },
  108. isActive() {
  109. const now = Date.now();
  110. const expiryOk = !this.app.expireMs || this.app.expireMs >= now;
  111. const trafficOk = !this.app.totalByte || (this.app.uploadByte + this.app.downloadByte) <= this.app.totalByte;
  112. return expiryOk && trafficOk;
  113. },
  114. },
  115. methods: { renderLink, copy, open, linkName, i18nLabel(key) { return '{{ i18n "' + key + '" }}'; } },
  116. });
  117. })();