subscription.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  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. enabled: (el.getAttribute('data-enabled') || '').toLowerCase() === 'true',
  10. subUrl: el.getAttribute('data-sub-url') || '',
  11. subJsonUrl: el.getAttribute('data-subjson-url') || '',
  12. subClashUrl: el.getAttribute('data-subclash-url') || '',
  13. download: el.getAttribute('data-download') || '',
  14. upload: el.getAttribute('data-upload') || '',
  15. used: el.getAttribute('data-used') || '',
  16. total: el.getAttribute('data-total') || '',
  17. remained: el.getAttribute('data-remained') || '',
  18. expireMs: (parseInt(el.getAttribute('data-expire') || '0', 10) || 0) * 1000,
  19. lastOnlineMs: (parseInt(el.getAttribute('data-lastonline') || '0', 10) || 0),
  20. downloadByte: parseInt(el.getAttribute('data-downloadbyte') || '0', 10) || 0,
  21. uploadByte: parseInt(el.getAttribute('data-uploadbyte') || '0', 10) || 0,
  22. totalByte: parseInt(el.getAttribute('data-totalbyte') || '0', 10) || 0,
  23. datepicker: el.getAttribute('data-datepicker') || 'gregorian',
  24. };
  25. // Normalize lastOnline to milliseconds if it looks like seconds
  26. if (data.lastOnlineMs && data.lastOnlineMs < 10_000_000_000) {
  27. data.lastOnlineMs *= 1000;
  28. }
  29. function renderLink(item) {
  30. return (
  31. Vue.h('a-list-item', {}, [
  32. Vue.h('a-space', { props: { size: 'small' } }, [
  33. Vue.h('a-button', { props: { size: 'small' }, on: { click: () => copy(item) } }, [Vue.h('a-icon', { props: { type: 'copy' } })]),
  34. Vue.h('span', { class: 'break-all' }, item)
  35. ])
  36. ])
  37. );
  38. }
  39. function copy(text) {
  40. ClipboardManager.copyText(text).then(ok => {
  41. const messageType = ok ? 'success' : 'error';
  42. Vue.prototype.$message[messageType](ok ? 'Copied' : 'Copy failed');
  43. });
  44. }
  45. function open(url) {
  46. window.location.href = url;
  47. }
  48. function drawQR(value) {
  49. try {
  50. new QRious({ element: document.getElementById('qrcode'), value, size: 220 });
  51. } catch (e) {
  52. console.warn(e);
  53. }
  54. }
  55. // Try to extract a human label (email/ps) from different link types
  56. function linkName(link, idx) {
  57. try {
  58. if (link.startsWith('vmess://')) {
  59. const json = JSON.parse(atob(link.replace('vmess://', '')));
  60. if (json.ps) return json.ps;
  61. if (json.add && json.id) return json.add; // fallback host
  62. } else if (link.startsWith('vless://') || link.startsWith('trojan://')) {
  63. const hashIdx = link.indexOf('#');
  64. if (hashIdx !== -1) return decodeURIComponent(link.substring(hashIdx + 1));
  65. const qIdx = link.indexOf('?');
  66. if (qIdx !== -1) {
  67. const qs = new URL('http://x/?' + link.substring(qIdx + 1, hashIdx !== -1 ? hashIdx : undefined)).searchParams;
  68. if (qs.get('remark')) return qs.get('remark');
  69. if (qs.get('email')) return qs.get('email');
  70. }
  71. const at = link.indexOf('@');
  72. const protSep = link.indexOf('://');
  73. if (at !== -1 && protSep !== -1) return link.substring(protSep + 3, at);
  74. } else if (link.startsWith('ss://')) {
  75. const hashIdx = link.indexOf('#');
  76. if (hashIdx !== -1) return decodeURIComponent(link.substring(hashIdx + 1));
  77. }
  78. } catch (e) { /* ignore and fallback */ }
  79. return 'Link ' + (idx + 1);
  80. }
  81. const app = new Vue({
  82. delimiters: ['[[', ']]'],
  83. el: '#app',
  84. data: {
  85. themeSwitcher,
  86. app: data,
  87. links: rawLinks,
  88. lang: '',
  89. viewportWidth: (typeof window !== 'undefined' ? window.innerWidth : 1024),
  90. },
  91. async mounted() {
  92. this.lang = LanguageManager.getLanguage();
  93. const tpl = document.getElementById('subscription-data');
  94. const sj = tpl ? tpl.getAttribute('data-subjson-url') : '';
  95. const sc = tpl ? tpl.getAttribute('data-subclash-url') : '';
  96. if (sj) this.app.subJsonUrl = sj;
  97. if (sc) this.app.subClashUrl = sc;
  98. drawQR(this.app.subUrl);
  99. try {
  100. const elJson = document.getElementById('qrcode-subjson');
  101. if (elJson && this.app.subJsonUrl) {
  102. new QRious({ element: elJson, value: this.app.subJsonUrl, size: 220 });
  103. }
  104. const elClash = document.getElementById('qrcode-subclash');
  105. if (elClash && this.app.subClashUrl) {
  106. new QRious({ element: elClash, value: this.app.subClashUrl, size: 220 });
  107. }
  108. } catch (e) { /* ignore */ }
  109. this._onResize = () => { this.viewportWidth = window.innerWidth; };
  110. window.addEventListener('resize', this._onResize);
  111. },
  112. beforeDestroy() {
  113. if (this._onResize) window.removeEventListener('resize', this._onResize);
  114. },
  115. computed: {
  116. isMobile() {
  117. return this.viewportWidth < 576;
  118. },
  119. isUnlimited() {
  120. return !this.app.totalByte;
  121. },
  122. isActive() {
  123. const now = Date.now();
  124. const enabledOk = this.app.enabled;
  125. const expiryOk = !this.app.expireMs || this.app.expireMs >= now;
  126. const trafficOk = !this.app.totalByte || (this.app.uploadByte + this.app.downloadByte) <= this.app.totalByte;
  127. return enabledOk && expiryOk && trafficOk;
  128. },
  129. shadowrocketUrl() {
  130. const rawUrl = this.app.subUrl + '?flag=shadowrocket';
  131. const base64Url = btoa(rawUrl);
  132. const remark = encodeURIComponent(this.app.sId || 'Subscription');
  133. return `shadowrocket://add/sub/${base64Url}?remark=${remark}`;
  134. },
  135. v2boxUrl() {
  136. return `v2box://install-sub?url=${encodeURIComponent(this.app.subUrl)}&name=${encodeURIComponent(this.app.sId)}`;
  137. },
  138. streisandUrl() {
  139. return `streisand://import/${encodeURIComponent(this.app.subUrl)}`;
  140. },
  141. v2raytunUrl() {
  142. return this.app.subUrl;
  143. },
  144. npvtunUrl() {
  145. return this.app.subUrl;
  146. },
  147. happUrl() {
  148. return `happ://add/${this.app.subUrl}`;
  149. }
  150. },
  151. methods: {
  152. renderLink,
  153. copy,
  154. open,
  155. linkName,
  156. i18nLabel(key) {
  157. return '{{ i18n "' + key + '" }}';
  158. },
  159. },
  160. });
  161. })();