1
0

subscription.js 4.5 KB

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