(function(){ // Vue app for Subscription page const el = document.getElementById('subscription-data'); if (!el) return; const textarea = document.getElementById('subscription-links'); const rawLinks = (textarea?.value || '').split('\n').filter(Boolean); const data = { sId: el.getAttribute('data-sid') || '', subUrl: el.getAttribute('data-sub-url') || '', download: el.getAttribute('data-download') || '', upload: el.getAttribute('data-upload') || '', used: el.getAttribute('data-used') || '', total: el.getAttribute('data-total') || '', remained: el.getAttribute('data-remained') || '', expireMs: (parseInt(el.getAttribute('data-expire') || '0', 10) || 0) * 1000, lastOnlineMs: (parseInt(el.getAttribute('data-lastonline') || '0', 10) || 0), downloadByte: parseInt(el.getAttribute('data-downloadbyte') || '0', 10) || 0, uploadByte: parseInt(el.getAttribute('data-uploadbyte') || '0', 10) || 0, totalByte: parseInt(el.getAttribute('data-totalbyte') || '0', 10) || 0, datepicker: el.getAttribute('data-datepicker') || 'gregorian', }; // Normalize lastOnline to milliseconds if it looks like seconds if (data.lastOnlineMs && data.lastOnlineMs < 10_000_000_000) { data.lastOnlineMs *= 1000; } function renderLink(item) { return ( Vue.h('a-list-item', {}, [ Vue.h('a-space', {props:{size:'small'}}, [ Vue.h('a-button', {props:{size:'small'}, on:{click: () => copy(item)}}, [Vue.h('a-icon', {props:{type:'copy'}})]), Vue.h('span', {class:'break-all'}, item) ]) ]) ); } function copy(text){ ClipboardManager.copyText(text).then(ok => { const messageType = ok ? 'success' : 'error'; Vue.prototype.$message[messageType](ok ? 'Copied' : 'Copy failed'); }); } function open(url){ window.location.href = url; } function drawQR(value){ try{ new QRious({ element: document.getElementById('qrcode'), value, size: 220 }); }catch(e){ console.warn(e); } } // Try to extract a human label (email/ps) from different link types function linkName(link, idx){ try{ if (link.startsWith('vmess://')){ const json = JSON.parse(atob(link.replace('vmess://',''))); if (json.ps) return json.ps; if (json.add && json.id) return json.add; // fallback host } else if (link.startsWith('vless://') || link.startsWith('trojan://')){ // vless://@host:port?...#name const hashIdx = link.indexOf('#'); if (hashIdx !== -1) return decodeURIComponent(link.substring(hashIdx+1)); // email sometimes in query params like sni or remark const qIdx = link.indexOf('?'); if (qIdx !== -1){ const qs = new URL('http://x/?' + link.substring(qIdx+1, hashIdx !== -1 ? hashIdx : undefined)).searchParams; if (qs.get('remark')) return qs.get('remark'); if (qs.get('email')) return qs.get('email'); } // else take user@host const at = link.indexOf('@'); const protSep = link.indexOf('://'); if (at !== -1 && protSep !== -1) return link.substring(protSep+3, at); } else if (link.startsWith('ss://')){ // shadowsocks: label often after # const hashIdx = link.indexOf('#'); if (hashIdx !== -1) return decodeURIComponent(link.substring(hashIdx+1)); } }catch(e){ /* ignore and fallback */ } return 'Link ' + (idx+1); } const app = new Vue({ delimiters: ['[[', ']]'], el: '#app', data: { themeSwitcher, app: data, links: rawLinks, lang: '', viewportWidth: (typeof window !== 'undefined' ? window.innerWidth : 1024), }, async mounted(){ this.lang = LanguageManager.getLanguage(); drawQR(this.app.subUrl); // Track viewport width for responsive behavior this._onResize = () => { this.viewportWidth = window.innerWidth; }; window.addEventListener('resize', this._onResize); }, beforeDestroy(){ if (this._onResize) window.removeEventListener('resize', this._onResize); }, computed: { isMobile(){ return this.viewportWidth < 576; }, isUnlimited(){ return !this.app.totalByte; }, isActive(){ const now = Date.now(); const expiryOk = !this.app.expireMs || this.app.expireMs >= now; const trafficOk = !this.app.totalByte || (this.app.uploadByte + this.app.downloadByte) <= this.app.totalByte; return expiryOk && trafficOk; }, }, methods: { renderLink, copy, open, linkName, i18nLabel(key){ return '{{ i18n "' + key + '" }}'; } }, }); })();