Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/MHSanaei/3x-ui.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormhsanaei <ho3ein.sanaei@gmail.com>2025-09-14 02:22:42 +0300
committermhsanaei <ho3ein.sanaei@gmail.com>2025-09-14 19:56:31 +0300
commit10025ffa66c011fd756af780772260d833460795 (patch)
tree0cf2d3924f3a8c26d47c4d9fe20d2438b7c4b6fc /web/assets/js
parent5ee62b25ca9a3bf0ce683adbba5b1b64ddea074e (diff)
Subscription
Diffstat (limited to 'web/assets/js')
-rw-r--r--web/assets/js/subscription.js125
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 + '" }}'; } },
+ });
+})();