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:
root 2026-04-25 22:46:35 +08:00
parent aa775a111f
commit 3ba7e43bc3
4 changed files with 80 additions and 4 deletions

View file

@ -1 +1 @@
v1.7.2.5
v1.7.2.6

View 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

View file

@ -508,6 +508,22 @@ func (a *InboundController) getUserSubscriptions(c *gin.Context) {
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
if v, ok := settings["subClashEnable"]; ok {
if b, ok2 := v.(bool); ok2 {
@ -526,6 +542,8 @@ func (a *InboundController) getUserSubscriptions(c *gin.Context) {
jsonObj(c, gin.H{
"subId": subId,
"subEnable": subEnable,
"subUrl": subUrl,
"subClashEnable": subClashEnable,
"subClashUrl": subClashUrl,
}, nil)

View file

@ -136,15 +136,15 @@
{{ i18n "pages.user.quickImport" }} <a-icon type="down" />
</a-button>
<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;" />
<span>{{ i18n "pages.user.android" }}</span>
</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;" />
<span>{{ i18n "pages.user.ios" }}</span>
</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;" />
<span>{{ i18n "pages.user.desktop" }}</span>
</a-menu-item>
@ -177,6 +177,8 @@
username: '',
traffic: null,
lang: '',
subEnable: false,
subUrl: '',
subClashEnable: false,
subClashUrl: '',
},
@ -201,6 +203,8 @@
try {
const msg = await HttpUtil.get('/panel/api/inbounds/userSubscriptions');
if (msg.success && msg.obj) {
this.subEnable = msg.obj.subEnable || false;
this.subUrl = msg.obj.subUrl || '';
this.subClashEnable = msg.obj.subClashEnable || false;
this.subClashUrl = msg.obj.subClashUrl || '';
}
@ -214,6 +218,32 @@
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) {
if (timestamp <= 0) return '{{ i18n "unlimited" }}';
const date = new Date(timestamp);