feat(hosts): add drag-and-drop and global subscription ordering (#6895)

Hosts already had one saved order on the Hosts page, but subscriptions honoured
it only inside each inbound, so a host of a later inbound could never come before
one of an earlier inbound (#6894). A new opt-in setting makes raw/base64,
Clash/Mihomo and JSON subscriptions follow the Hosts page order across inbounds;
the default keeps today's inbound-first output unchanged. The Hosts table gains
drag-and-drop reordering next to the Up/Down buttons, on a pointer-reorder hook
the Routing Rules table now shares.
This commit is contained in:
Hamed
2026-10-24 08:57:47 +03:30
committed by GitHub
parent a69ae2b0ea
commit 136c1c4d50
51 changed files with 1643 additions and 149 deletions
+42 -3
View File
@@ -136,6 +136,7 @@ func (s *SubJsonService) GetJson(subId string, host string, alwaysReturnArray bo
seenEmails := make(map[string]struct{})
entries := make([]subConfigEntry, 0, len(inbounds))
var ordered []orderedSubEntry[func() subConfigEntry]
// Prepare Inbounds
for _, inbound := range inbounds {
clients := subReq.matchingClients(inbound, subId)
@@ -149,8 +150,10 @@ func (s *SubJsonService) GetJson(subId string, host string, alwaysReturnArray bo
continue
}
subReq.projectThroughFallbackMaster(inbound)
if hostEps := subReq.hostEndpoints(inbound, "json"); len(hostEps) > 0 {
injectExternalProxy(inbound, hostEps)
if !subReq.hostsFirst {
if hostEps := subReq.hostEndpoints(inbound, "json"); len(hostEps) > 0 {
injectExternalProxy(inbound, hostEps)
}
}
var inboundConfigs []json_util.RawMessage
@@ -159,7 +162,17 @@ func (s *SubJsonService) GetJson(subId string, host string, alwaysReturnArray bo
hasEnabledClient = true
}
seenEmails[client.Email] = struct{}{}
inboundConfigs = append(inboundConfigs, s.getConfig(subReq, inbound, client, host)...)
if subReq.hostsFirst {
for _, variant := range subReq.subscriptionHostVariants(inbound, "json") {
render := func() subConfigEntry {
configs := s.getConfig(subReq, variant.inbound(subReq, inbound), client, host)
return subConfigEntry{sortIndex: inbound.SubSortIndex, id: inbound.Id, configs: configs, hostOrder: variant.order}
}
ordered = append(ordered, orderedSubEntry[func() subConfigEntry]{variant.order, render})
}
} else {
inboundConfigs = append(inboundConfigs, s.getConfig(subReq, inbound, client, host)...)
}
}
if len(inboundConfigs) > 0 {
entries = append(entries, subConfigEntry{
@@ -169,6 +182,14 @@ func (s *SubJsonService) GetJson(subId string, host string, alwaysReturnArray bo
})
}
}
if subReq.hostsFirst {
// Render in final endpoint order so first-link usage remarks stay first.
for _, render := range orderedSubValues(ordered) {
if entry := render(); len(entry.configs) > 0 {
entries = append(entries, entry)
}
}
}
entries = s.appendBalancerEntries(entries)
// Inbounds arrive sorted by (sub_sort_index, id); balancers interleave by
@@ -183,6 +204,23 @@ func (s *SubJsonService) GetJson(subId string, host string, alwaysReturnArray bo
return entries[i].id < entries[j].id
})
var configArray []json_util.RawMessage
if subReq.hostsFirst {
// Preserve balancer slots from the legacy sort; reorder endpoint slots only.
var endpoints []subConfigEntry
for _, entry := range entries {
if entry.kind == 0 {
endpoints = append(endpoints, entry)
}
}
sort.SliceStable(endpoints, func(i, j int) bool { return hostOrderLess(endpoints[i].hostOrder, endpoints[j].hostOrder) })
next := 0
for i := range entries {
if entries[i].kind == 0 {
entries[i] = endpoints[next]
next++
}
}
}
for _, entry := range entries {
configArray = append(configArray, entry.configs...)
}
@@ -255,6 +293,7 @@ func (s *SubJsonService) GetJson(subId string, host string, alwaysReturnArray bo
// subConfigEntry is one ordered block of the JSON subscription: an inbound's
// configs (kind 0) or a balancer config (kind 1).
type subConfigEntry struct {
hostOrder *hostEntryOrder
sortIndex int
kind int
id int