diff --git a/shared/providers.test.ts b/shared/providers.test.ts index 300498d..5f88c77 100644 --- a/shared/providers.test.ts +++ b/shared/providers.test.ts @@ -5,6 +5,7 @@ import { addFileSubscription, addSubscription, listSubscriptions, + moveSubscription, providerNames, removeSubscription, renameSubscription, @@ -280,6 +281,48 @@ describe('删除前的引用检查', () => { }) }) +describe('调整订阅顺序', () => { + /** 三个用户订阅,外加一份接在中间的订阅副本。 */ + function three(): ConfigObject { + let data: ConfigObject = {} + for (const name of ['A', 'B', 'C']) data = addSubscription(data, name, `https://${name}.example`) + return data + } + + it('上移和下移交换相邻两个订阅', () => { + expect(listSubscriptions(moveSubscription(three(), 'B', -1)).map((s) => s.name)).toEqual([ + 'B', + 'A', + 'C', + ]) + expect(listSubscriptions(moveSubscription(three(), 'B', 1)).map((s) => s.name)).toEqual([ + 'A', + 'C', + 'B', + ]) + }) + + it('到头了原样返回,配置对象不变', () => { + const data = three() + expect(moveSubscription(data, 'A', -1)).toBe(data) + expect(moveSubscription(data, 'C', 1)).toBe(data) + expect(moveSubscription(data, '不存在', 1)).toBe(data) + }) + + it('夹在中间的订阅副本留在原地', () => { + const withCopy = applySetup( + { ...three(), listeners: [{ name: 'test1', type: 'mixed', port: 7778 }] }, + 'test1', + { proxy: { sources: ['B'], filter: null }, chain: { sources: ['B'], filter: null } }, + ) + const copy = chainProviderName('test1', 'B') + const before = Object.keys(withCopy['proxy-providers'] as ConfigObject).indexOf(copy) + const after = moveSubscription(withCopy, 'A', 1) + expect(listSubscriptions(after).map((s) => s.name)).toEqual(['B', 'A', 'C']) + expect(Object.keys(after['proxy-providers'] as ConfigObject).indexOf(copy)).toBe(before) + }) +}) + describe('缓存路径撞名', () => { it('名字只差符号时净化成同一个路径,能认出来', () => { const data = addSubscription({}, '订阅 A', 'https://a.example/sub') diff --git a/shared/providers.ts b/shared/providers.ts index 1e57236..e03dcec 100644 --- a/shared/providers.ts +++ b/shared/providers.ts @@ -238,6 +238,36 @@ export function renameSubscription( return next } +/** + * 把一个订阅在列表里上移或下移一位。 + * + * 顺序只是呈现顺序,内核不看 `proxy-providers` 里条目的先后。交换的是两个用户订阅在 + * 映射里的位置,中间夹着的副本留在原地——它们不在列表上,跟着挪只会把 diff 撑大。 + * 已经在头(尾)还要上(下)移时原样返回,调用方据此不发起保存。 + */ +export function moveSubscription( + data: ConfigObject, + name: string, + delta: -1 | 1, +): ConfigObject { + const entries = Object.entries(providersOf(data)) + const keys = entries.map(([key]) => key) + const mine = keys.filter((key) => !isGenerated(key)) + const at = mine.indexOf(name) + const target = mine[at + delta] + if (at < 0 || target === undefined) return data + + const from = keys.indexOf(name) + const onto = keys.indexOf(target) + const order = [...entries] + order[from] = entries[onto]! + order[onto] = entries[from]! + + const next = { ...data } + next['proxy-providers'] = Object.fromEntries(order) + return next +} + /** 删除一个订阅。删到最后一个时把整个键去掉,不在文件里留空映射。 */ export function removeSubscription(data: ConfigObject, name: string): ConfigObject { const providers = { ...providersOf(data) } diff --git a/web/components/ProviderList.vue b/web/components/ProviderList.vue index 298aae6..2056a18 100644 --- a/web/components/ProviderList.vue +++ b/web/components/ProviderList.vue @@ -1,9 +1,11 @@