mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-07 13:44:24 +00:00
feat: add URL Schemes for Quick Import buttons (Android/iOS/Desktop)
- Android: clash://install-config (Clash Meta for Android) - iOS: shadowrocket://add/sub/ (Shadowrocket) - Desktop: clash-verge://install-config (Clash Verge) - Extended API to return subEnable/subUrl for standard subscription
This commit is contained in:
parent
aa775a111f
commit
3ba7e43bc3
4 changed files with 80 additions and 4 deletions
|
|
@ -1 +1 @@
|
||||||
v1.7.2.5
|
v1.7.2.6
|
||||||
|
|
|
||||||
28
docs/Tasktracking/2026-04-25-user-panel-url-schemes.md
Normal file
28
docs/Tasktracking/2026-04-25-user-panel-url-schemes.md
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
# 2026-04-25 — User Panel: Quick Import URL Schemes
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
Wired up the 3 Quick Import dropdown buttons (Android/iOS/Desktop) with deep link URL schemes to launch proxy client apps directly from the user panel.
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
### Backend (`web/controller/inbound.go`)
|
||||||
|
- Extended `getUserSubscriptions` API to also return `subEnable` and `subUrl` (standard subscription URL)
|
||||||
|
- Previously only returned `subClashEnable` and `subClashUrl`
|
||||||
|
|
||||||
|
### Frontend (`web/html/user.html`)
|
||||||
|
- Added `subEnable` and `subUrl` data fields
|
||||||
|
- Updated `loadSubscriptions()` to save the new fields
|
||||||
|
- Added 3 URL scheme methods:
|
||||||
|
- **Android** → `clash://install-config?url=<encoded_url>` (Clash Meta for Android)
|
||||||
|
- **iOS** → `shadowrocket://add/sub/<base64_url>?remark=<name>` (Shadowrocket)
|
||||||
|
- **Desktop** → `clash-verge://install-config?url=<encoded_url>&name=<name>` (Clash Verge)
|
||||||
|
- Added `@click` handlers on the 3 dropdown menu items
|
||||||
|
- Each method validates subscription availability before opening the URL scheme
|
||||||
|
|
||||||
|
### URL Scheme Priority
|
||||||
|
- Android/Desktop: prefers Clash URL (`subClashUrl`), falls back to standard URL (`subUrl`)
|
||||||
|
- iOS (Shadowrocket): prefers standard URL (`subUrl`), falls back to Clash URL
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
- `web/controller/inbound.go` — extended API response with subEnable/subUrl
|
||||||
|
- `web/html/user.html` — added URL scheme methods and click handlers
|
||||||
|
|
@ -508,6 +508,22 @@ func (a *InboundController) getUserSubscriptions(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
subEnable := false
|
||||||
|
if v, ok := settings["subEnable"]; ok {
|
||||||
|
if b, ok2 := v.(bool); ok2 {
|
||||||
|
subEnable = b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
subUrl := ""
|
||||||
|
if subEnable {
|
||||||
|
if uri, ok := settings["subURI"]; ok {
|
||||||
|
if s, ok2 := uri.(string); ok2 && s != "" {
|
||||||
|
subUrl = s + subId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
subClashEnable := false
|
subClashEnable := false
|
||||||
if v, ok := settings["subClashEnable"]; ok {
|
if v, ok := settings["subClashEnable"]; ok {
|
||||||
if b, ok2 := v.(bool); ok2 {
|
if b, ok2 := v.(bool); ok2 {
|
||||||
|
|
@ -526,6 +542,8 @@ func (a *InboundController) getUserSubscriptions(c *gin.Context) {
|
||||||
|
|
||||||
jsonObj(c, gin.H{
|
jsonObj(c, gin.H{
|
||||||
"subId": subId,
|
"subId": subId,
|
||||||
|
"subEnable": subEnable,
|
||||||
|
"subUrl": subUrl,
|
||||||
"subClashEnable": subClashEnable,
|
"subClashEnable": subClashEnable,
|
||||||
"subClashUrl": subClashUrl,
|
"subClashUrl": subClashUrl,
|
||||||
}, nil)
|
}, nil)
|
||||||
|
|
|
||||||
|
|
@ -136,15 +136,15 @@
|
||||||
{{ i18n "pages.user.quickImport" }} <a-icon type="down" />
|
{{ i18n "pages.user.quickImport" }} <a-icon type="down" />
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-menu slot="overlay" :class="themeSwitcher.currentTheme">
|
<a-menu slot="overlay" :class="themeSwitcher.currentTheme">
|
||||||
<a-menu-item key="android">
|
<a-menu-item key="android" @click="quickImportAndroid">
|
||||||
<a-icon type="android" style="margin-right: 8px; font-size: 16px;" />
|
<a-icon type="android" style="margin-right: 8px; font-size: 16px;" />
|
||||||
<span>{{ i18n "pages.user.android" }}</span>
|
<span>{{ i18n "pages.user.android" }}</span>
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
<a-menu-item key="ios">
|
<a-menu-item key="ios" @click="quickImportIOS">
|
||||||
<a-icon type="apple" style="margin-right: 8px; font-size: 16px;" />
|
<a-icon type="apple" style="margin-right: 8px; font-size: 16px;" />
|
||||||
<span>{{ i18n "pages.user.ios" }}</span>
|
<span>{{ i18n "pages.user.ios" }}</span>
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
<a-menu-item key="desktop">
|
<a-menu-item key="desktop" @click="quickImportDesktop">
|
||||||
<a-icon type="laptop" style="margin-right: 8px; font-size: 16px;" />
|
<a-icon type="laptop" style="margin-right: 8px; font-size: 16px;" />
|
||||||
<span>{{ i18n "pages.user.desktop" }}</span>
|
<span>{{ i18n "pages.user.desktop" }}</span>
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
|
|
@ -177,6 +177,8 @@
|
||||||
username: '',
|
username: '',
|
||||||
traffic: null,
|
traffic: null,
|
||||||
lang: '',
|
lang: '',
|
||||||
|
subEnable: false,
|
||||||
|
subUrl: '',
|
||||||
subClashEnable: false,
|
subClashEnable: false,
|
||||||
subClashUrl: '',
|
subClashUrl: '',
|
||||||
},
|
},
|
||||||
|
|
@ -201,6 +203,8 @@
|
||||||
try {
|
try {
|
||||||
const msg = await HttpUtil.get('/panel/api/inbounds/userSubscriptions');
|
const msg = await HttpUtil.get('/panel/api/inbounds/userSubscriptions');
|
||||||
if (msg.success && msg.obj) {
|
if (msg.success && msg.obj) {
|
||||||
|
this.subEnable = msg.obj.subEnable || false;
|
||||||
|
this.subUrl = msg.obj.subUrl || '';
|
||||||
this.subClashEnable = msg.obj.subClashEnable || false;
|
this.subClashEnable = msg.obj.subClashEnable || false;
|
||||||
this.subClashUrl = msg.obj.subClashUrl || '';
|
this.subClashUrl = msg.obj.subClashUrl || '';
|
||||||
}
|
}
|
||||||
|
|
@ -214,6 +218,32 @@
|
||||||
this.$message[messageType](ok ? '{{ i18n "pages.user.copied" }}' : 'Copy failed');
|
this.$message[messageType](ok ? '{{ i18n "pages.user.copied" }}' : 'Copy failed');
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
quickImportAndroid() {
|
||||||
|
const url = this.subClashUrl || this.subUrl;
|
||||||
|
if (!url) {
|
||||||
|
this.$message.warning('{{ i18n "pages.user.noSubscription" }}');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.location.href = 'clash://install-config?url=' + encodeURIComponent(url);
|
||||||
|
},
|
||||||
|
quickImportIOS() {
|
||||||
|
const url = this.subUrl || this.subClashUrl;
|
||||||
|
if (!url) {
|
||||||
|
this.$message.warning('{{ i18n "pages.user.noSubscription" }}');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const base64Url = btoa(url);
|
||||||
|
const remark = encodeURIComponent(this.username || 'Subscription');
|
||||||
|
window.location.href = 'shadowrocket://add/sub/' + base64Url + '?remark=' + remark;
|
||||||
|
},
|
||||||
|
quickImportDesktop() {
|
||||||
|
const url = this.subClashUrl || this.subUrl;
|
||||||
|
if (!url) {
|
||||||
|
this.$message.warning('{{ i18n "pages.user.noSubscription" }}');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.location.href = 'clash-verge://install-config?url=' + encodeURIComponent(url) + '&name=' + encodeURIComponent(this.username || 'Subscription');
|
||||||
|
},
|
||||||
formatExpiryTime(timestamp) {
|
formatExpiryTime(timestamp) {
|
||||||
if (timestamp <= 0) return '{{ i18n "unlimited" }}';
|
if (timestamp <= 0) return '{{ i18n "unlimited" }}';
|
||||||
const date = new Date(timestamp);
|
const date = new Date(timestamp);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue