diff options
| author | mhsanaei <ho3ein.sanaei@gmail.com> | 2025-09-14 02:22:42 +0300 |
|---|---|---|
| committer | mhsanaei <ho3ein.sanaei@gmail.com> | 2025-09-14 19:56:31 +0300 |
| commit | 10025ffa66c011fd756af780772260d833460795 (patch) | |
| tree | 0cf2d3924f3a8c26d47c4d9fe20d2438b7c4b6fc /web/assets/js | |
| parent | 5ee62b25ca9a3bf0ce683adbba5b1b64ddea074e (diff) | |
Subscription
Diffstat (limited to 'web/assets/js')
| -rw-r--r-- | web/assets/js/subscription.js | 125 |
1 files changed, 125 insertions, 0 deletions
diff --git a/web/assets/js/subscription.js b/web/assets/js/subscription.js new file mode 100644 index 00000000..625d55b3 --- /dev/null +++ b/web/assets/js/subscription.js @@ -0,0 +1,125 @@ +(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') || '', + subJsonUrl: el.getAttribute('data-subjson-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://<id>@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(); + // Discover subJsonUrl if provided via template bootstrap + const tpl = document.getElementById('subscription-data'); + const sj = tpl ? tpl.getAttribute('data-subjson-url') : ''; + if (sj) this.app.subJsonUrl = sj; + drawQR(this.app.subUrl); + // Draw second QR if available + try { new QRious({ element: document.getElementById('qrcode-subjson'), value: this.app.subJsonUrl || '', size: 220 }); } catch (e) { /* ignore */ } + // 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 + '" }}'; } }, + }); +})(); |
