Compare commits

..

4 commits

Author SHA1 Message Date
javadtgh
b2ad033bfc
Merge 5e953bae45 into 99c79d4056 2025-09-18 00:55:27 +05:30
mhsanaei
99c79d4056
fix: online
Some checks are pending
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
Release 3X-UI / Build for Windows (push) Waiting to run
2025-09-17 20:02:58 +02:00
RahGozar
fcdeb1fc79
feat: add UUID to ClientTraffic (#3491)
* Update client_traffic.go

* Update inbound.go
2025-09-17 17:45:28 +02:00
Harry NG
0a58b5e745
Fix: Shadowrocket link using base64 encoding (#3489) 2025-09-17 17:43:09 +02:00
5 changed files with 35 additions and 15 deletions

View file

@ -50,7 +50,11 @@
} }
function drawQR(value) { function drawQR(value) {
try { new QRious({ element: document.getElementById('qrcode'), value, size: 220 }); } catch (e) { console.warn(e); } 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 // Try to extract a human label (email/ps) from different link types
@ -61,22 +65,18 @@
if (json.ps) return json.ps; if (json.ps) return json.ps;
if (json.add && json.id) return json.add; // fallback host if (json.add && json.id) return json.add; // fallback host
} else if (link.startsWith('vless://') || link.startsWith('trojan://')) { } else if (link.startsWith('vless://') || link.startsWith('trojan://')) {
// vless://<id>@host:port?...#name
const hashIdx = link.indexOf('#'); const hashIdx = link.indexOf('#');
if (hashIdx !== -1) return decodeURIComponent(link.substring(hashIdx + 1)); if (hashIdx !== -1) return decodeURIComponent(link.substring(hashIdx + 1));
// email sometimes in query params like sni or remark
const qIdx = link.indexOf('?'); const qIdx = link.indexOf('?');
if (qIdx !== -1) { if (qIdx !== -1) {
const qs = new URL('http://x/?' + link.substring(qIdx + 1, hashIdx !== -1 ? hashIdx : undefined)).searchParams; 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('remark')) return qs.get('remark');
if (qs.get('email')) return qs.get('email'); if (qs.get('email')) return qs.get('email');
} }
// else take user@host
const at = link.indexOf('@'); const at = link.indexOf('@');
const protSep = link.indexOf('://'); const protSep = link.indexOf('://');
if (at !== -1 && protSep !== -1) return link.substring(protSep + 3, at); if (at !== -1 && protSep !== -1) return link.substring(protSep + 3, at);
} else if (link.startsWith('ss://')) { } else if (link.startsWith('ss://')) {
// shadowsocks: label often after #
const hashIdx = link.indexOf('#'); const hashIdx = link.indexOf('#');
if (hashIdx !== -1) return decodeURIComponent(link.substring(hashIdx + 1)); if (hashIdx !== -1) return decodeURIComponent(link.substring(hashIdx + 1));
} }
@ -96,14 +96,13 @@
}, },
async mounted() { async mounted() {
this.lang = LanguageManager.getLanguage(); this.lang = LanguageManager.getLanguage();
// Discover subJsonUrl if provided via template bootstrap
const tpl = document.getElementById('subscription-data'); const tpl = document.getElementById('subscription-data');
const sj = tpl ? tpl.getAttribute('data-subjson-url') : ''; const sj = tpl ? tpl.getAttribute('data-subjson-url') : '';
if (sj) this.app.subJsonUrl = sj; if (sj) this.app.subJsonUrl = sj;
drawQR(this.app.subUrl); drawQR(this.app.subUrl);
// Draw second QR if available try {
try { new QRious({ element: document.getElementById('qrcode-subjson'), value: this.app.subJsonUrl || '', size: 220 }); } catch (e) { /* ignore */ } new QRious({ element: document.getElementById('qrcode-subjson'), value: this.app.subJsonUrl || '', size: 220 });
// Track viewport width for responsive behavior } catch (e) { /* ignore */ }
this._onResize = () => { this.viewportWidth = window.innerWidth; }; this._onResize = () => { this.viewportWidth = window.innerWidth; };
window.addEventListener('resize', this._onResize); window.addEventListener('resize', this._onResize);
}, },
@ -111,15 +110,33 @@
if (this._onResize) window.removeEventListener('resize', this._onResize); if (this._onResize) window.removeEventListener('resize', this._onResize);
}, },
computed: { computed: {
isMobile() { return this.viewportWidth < 576; }, isMobile() {
isUnlimited() { return !this.app.totalByte; }, return this.viewportWidth < 576;
},
isUnlimited() {
return !this.app.totalByte;
},
isActive() { isActive() {
const now = Date.now(); const now = Date.now();
const expiryOk = !this.app.expireMs || this.app.expireMs >= now; const expiryOk = !this.app.expireMs || this.app.expireMs >= now;
const trafficOk = !this.app.totalByte || (this.app.uploadByte + this.app.downloadByte) <= this.app.totalByte; const trafficOk = !this.app.totalByte || (this.app.uploadByte + this.app.downloadByte) <= this.app.totalByte;
return expiryOk && trafficOk; return expiryOk && trafficOk;
}, },
shadowrocketUrl() {
const rawUrl = this.app.subUrl + '?flag=shadowrocket';
const base64Url = btoa(rawUrl);
const remark = encodeURIComponent(this.app.sId || 'Subscription');
return `shadowrocket://add/sub/${base64Url}?remark=${remark}`;
}
},
methods: {
renderLink,
copy,
open,
linkName,
i18nLabel(key) {
return '{{ i18n "' + key + '" }}';
},
}, },
methods: { renderLink, copy, open, linkName, i18nLabel(key) { return '{{ i18n "' + key + '" }}'; } },
}); });
})(); })();

View file

@ -37,7 +37,7 @@
<template slot="content" > <template slot="content" >
{{ i18n "lastOnline" }}: [[ formatLastOnline(client.email) ]] {{ i18n "lastOnline" }}: [[ formatLastOnline(client.email) ]]
</template> </template>
<template v-if="client.enable && isClientOnline(client.email) && !isClientDepleted"> <template v-if="client.enable && isClientOnline(client.email)">
<a-tag color="green">{{ i18n "online" }}</a-tag> <a-tag color="green">{{ i18n "online" }}</a-tag>
</template> </template>
<template v-else> <template v-else>
@ -51,7 +51,7 @@
<template slot="title"> <template slot="title">
<template v-if="isClientDepleted">{{ i18n "depleted" }}</template> <template v-if="isClientDepleted">{{ i18n "depleted" }}</template>
<template v-if="!isClientDepleted && !client.enable">{{ i18n "disabled" }}</template> <template v-if="!isClientDepleted && !client.enable">{{ i18n "disabled" }}</template>
<template v-if="!isClientDepleted && client.enable && isClientOnline(client.email)">{{ i18n "online" }}</template> <template v-if="client.enable && isClientOnline(client.email)">{{ i18n "online" }}</template>
</template> </template>
<a-badge :class="isClientOnline(client.email)? 'online-animation' : ''" :color="client.enable ? statsExpColor(record, client.email) : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'"></a-badge> <a-badge :class="isClientOnline(client.email)? 'online-animation' : ''" :color="client.enable ? statsExpColor(record, client.email) : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'"></a-badge>
</a-tooltip> </a-tooltip>

View file

@ -233,7 +233,7 @@
<a-menu slot="overlay" <a-menu slot="overlay"
:class="themeSwitcher.currentTheme"> :class="themeSwitcher.currentTheme">
<a-menu-item key="ios-shadowrocket" <a-menu-item key="ios-shadowrocket"
@click="open('shadowrocket://add/subscription?url=' + encodeURIComponent(app.subUrl) + '&remark=' + encodeURIComponent(app.sId))">Shadowrocket</a-menu-item> @click="open(shadowrocketUrl)">Shadowrocket</a-menu-item>
<a-menu-item key="ios-v2box" <a-menu-item key="ios-v2box"
@click="open('v2box://install-sub?url=' + encodeURIComponent(app.subUrl) + '&name=' + encodeURIComponent(app.sId))">V2Box</a-menu-item> @click="open('v2box://install-sub?url=' + encodeURIComponent(app.subUrl) + '&name=' + encodeURIComponent(app.sId))">V2Box</a-menu-item>
<a-menu-item key="ios-streisand" <a-menu-item key="ios-streisand"

View file

@ -1972,6 +1972,7 @@ func (s *InboundService) GetClientTrafficByEmail(email string) (traffic *xray.Cl
if t != nil && client != nil { if t != nil && client != nil {
t.Enable = client.Enable t.Enable = client.Enable
t.SubId = client.SubID t.SubId = client.SubID
t.UUID = client.ID
return t, nil return t, nil
} }
return nil, nil return nil, nil
@ -2013,6 +2014,7 @@ func (s *InboundService) GetClientTrafficByID(id string) ([]xray.ClientTraffic,
if ct, client, e := s.GetClientByEmail(traffics[i].Email); e == nil && ct != nil && client != nil { if ct, client, e := s.GetClientByEmail(traffics[i].Email); e == nil && ct != nil && client != nil {
traffics[i].Enable = client.Enable traffics[i].Enable = client.Enable
traffics[i].SubId = client.SubID traffics[i].SubId = client.SubID
traffics[i].UUID = client.ID
} }
} }
return traffics, err return traffics, err

View file

@ -5,6 +5,7 @@ type ClientTraffic struct {
InboundId int `json:"inboundId" form:"inboundId"` InboundId int `json:"inboundId" form:"inboundId"`
Enable bool `json:"enable" form:"enable"` Enable bool `json:"enable" form:"enable"`
Email string `json:"email" form:"email" gorm:"unique"` Email string `json:"email" form:"email" gorm:"unique"`
UUID string `json:"uuid" form:"uuid" gorm:"unique;type:char(36)"`
SubId string `json:"subId" form:"subId" gorm:"-"` SubId string `json:"subId" form:"subId" gorm:"-"`
Up int64 `json:"up" form:"up"` Up int64 `json:"up" form:"up"`
Down int64 `json:"down" form:"down"` Down int64 `json:"down" form:"down"`