perfectionist-plugins 2.4.8 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,152 +1,87 @@
1
- export const version = "1.2.6";
1
+ export const version = "2.0.0";
2
2
 
3
3
  const PLUGIN_ID = "unlock-profile-skins";
4
- const PICKER_BUTTON_SELECTOR = ".style-profile-skin-picker-button";
5
- const CURRENT_SUMMONER_URI = "/lol-summoner/v1/current-summoner";
6
- const INVENTORY_PATH = /^\/lol-champions\/v1\/inventories\/(\d+)\/champions$/;
7
- const AUGMENT_INVENTORY_PATH = /^\/lol-inventory\/v2\/inventory\/SKIN_AUGMENT$/i;
8
- const SUMMONER_TTL = 10 * 60 * 1000;
9
- const SUMMONER_WAIT_MS = 1200;
10
- const STATE_KEY = "perfectionist-plugins:state";
4
+
5
+ // The "Set profile background" window is the rcp-fe-lol-skins-picker client plugin, and nothing else in the
6
+ // client uses it. Its data binding is private to it, so unlocking the data there changes this window only:
7
+ // Collection, champ select, the store and loot keep seeing the real inventory.
8
+ const PICKER_PLUGIN = "rcp-fe-lol-skins-picker";
9
+ const CHAMPIONS_PATH = /^\/lol-champions\/v1\/inventories\/\d+\/champions$/;
10
+ const MINIMAL_PATH = /^\/lol-champions\/v1\/inventories\/\d+\/champions-minimal$/;
11
+ const AUGMENTS_PATH = /^\/lol-inventory\/v2\/inventory\/SKIN_AUGMENT$/i;
12
+ const GROUPING_SELECTOR = ".flyout-component lol-uikit-framed-dropdown.control-panel-grouping-options";
13
+ const TRANSLATIONS_URI = "/fe/lol-skins-picker/trans.json";
14
+ const TIER_KEY = "control_panel_grouping_by_tier";
15
+
11
16
  const FAKE_PURCHASE_DATE = 1577836800000;
12
17
  const FAKE_RENTAL_END = 4102444800000;
13
- const TIER_LABEL = "tier";
14
- const GROUPING_OPTIONS = "lol-uikit-dropdown-option";
15
- const HARVEST_WAIT_MS = 2000;
16
- const INVENTORY_TEXT_CACHE_LIMIT = 2;
18
+ const SHADOW_CHAMPION_ID = 60000;
17
19
 
18
- let filterActive = true;
19
20
  let api = null;
20
- let summonerId = null;
21
- let summonerPending = false;
22
- let rawFetch = null;
23
- let harvestPromise = null;
21
+ let active = false;
22
+ let handles = [];
23
+ let unwatchGrouping = null;
24
+ let pickerApi = null;
25
+ let pickerSelectSkin = null;
26
+ let tierLabel = null;
24
27
  const augmentIds = new Set();
25
- const pendingTargetLoads = new Set();
26
28
  const variantIdRemap = new Map();
27
- const inventoryTextCache = new Map();
28
-
29
- function readDefaultEnabled() {
30
- try {
31
- const saved = JSON.parse(localStorage.getItem(STATE_KEY) || "{}");
32
- return saved.enabled?.[PLUGIN_ID] !== false;
33
- } catch {
34
- return true;
35
- }
36
- }
37
29
 
38
30
  function logError(scope, error) {
39
- console.error(`[Perfectionist] Unlock Profile Skins: ${scope}`, error);
31
+ console.error(`[Perfectionism] Unlock Profile Skins: ${scope}`, error);
40
32
  }
41
33
 
42
- const PICKER_ARM_MS = 5000;
43
- let pickerArmUntil = 0;
44
- let pickerArmTimer = null;
45
-
46
- function isPickerOpen() {
47
- for (const dialog of document.querySelectorAll("lol-uikit-dialog-frame")) {
48
- const rect = dialog.getBoundingClientRect();
49
- if (rect.width <= 0 || rect.height <= 0) continue;
50
- if (/set profile background/i.test(dialog.textContent || "")) return true;
34
+ function cloneData(value) {
35
+ try {
36
+ return structuredClone(value);
37
+ } catch {
38
+ return JSON.parse(JSON.stringify(value));
51
39
  }
52
- return false;
53
- }
54
-
55
- function shouldFilter() {
56
- return filterActive && (isPickerOpen() || Date.now() < pickerArmUntil);
57
40
  }
58
41
 
59
- function armPicker() {
60
- pickerArmUntil = Date.now() + PICKER_ARM_MS;
61
- if (pickerArmTimer !== null) clearTimeout(pickerArmTimer);
62
- const waitForPicker = () => {
63
- if (isPickerOpen()) {
64
- pickerArmTimer = null;
65
- startTierGrouping();
66
- return;
67
- }
68
- if (Date.now() >= pickerArmUntil) {
69
- pickerArmTimer = null;
70
- return;
71
- }
72
- pickerArmTimer = setTimeout(waitForPicker, 100);
42
+ function ownedRecord() {
43
+ return {
44
+ owned: true,
45
+ loyaltyReward: false,
46
+ xboxGPReward: false,
47
+ rental: { rented: false, purchaseDate: FAKE_PURCHASE_DATE, endDate: 0, winCountRemaining: 0 },
73
48
  };
74
- pickerArmTimer = setTimeout(waitForPicker, 50);
75
- }
76
-
77
- function cleanPath(url) {
78
- let path = String(url ?? "").split("?")[0];
79
- const origin = /^[a-z][a-z0-9+.-]*:\/\/[^/]+/i.exec(path);
80
- if (origin) path = path.slice(origin[0].length) || "/";
81
- return path;
82
- }
83
-
84
- function pathIdOf(url) {
85
- const match = INVENTORY_PATH.exec(cleanPath(url));
86
- return match ? Number(match[1]) : null;
87
- }
88
-
89
- function isTargetUrl(url) {
90
- const id = pathIdOf(url);
91
- return id !== null && summonerId !== null && id === summonerId;
92
- }
93
-
94
- function isAugmentUrl(url) {
95
- return AUGMENT_INVENTORY_PATH.test(cleanPath(url));
96
49
  }
97
50
 
98
51
  function unlockOwnership(ownership) {
99
- if (!ownership || typeof ownership !== "object") return false;
100
- let changed = false;
101
- const needsFakeDate = ownership.owned !== true || ownership.rental?.rented === true;
102
- if (ownership.owned !== true) {
103
- ownership.owned = true;
104
- changed = true;
52
+ if (!ownership || typeof ownership !== "object") return;
53
+ const wasOwned = ownership.owned === true && ownership.rental?.rented !== true;
54
+ ownership.owned = true;
55
+ if (wasOwned) return;
56
+ const rental = ownership.rental;
57
+ if (rental && typeof rental === "object") {
58
+ rental.rented = false;
59
+ rental.purchaseDate = FAKE_PURCHASE_DATE;
60
+ if (rental.endDate !== undefined) rental.endDate = FAKE_RENTAL_END;
105
61
  }
106
- if (needsFakeDate) {
107
- const rental = ownership.rental;
108
- if (rental && typeof rental === "object") {
109
- if (rental.purchaseDate !== FAKE_PURCHASE_DATE) {
110
- rental.purchaseDate = FAKE_PURCHASE_DATE;
111
- changed = true;
112
- }
113
- if (rental.endDate !== undefined && rental.endDate !== FAKE_RENTAL_END) {
114
- rental.endDate = FAKE_RENTAL_END;
115
- changed = true;
116
- }
117
- }
118
- if (ownership.purchaseDate !== undefined && ownership.purchaseDate !== FAKE_PURCHASE_DATE) {
119
- ownership.purchaseDate = FAKE_PURCHASE_DATE;
120
- changed = true;
121
- }
122
- }
123
- return changed;
62
+ if (ownership.purchaseDate !== undefined) ownership.purchaseDate = FAKE_PURCHASE_DATE;
124
63
  }
125
64
 
126
- function synthesizedOwnership() {
127
- return {
128
- owned: true,
129
- loyaltyReward: false,
130
- xboxGPReward: false,
131
- rental: { rented: false, purchaseDate: 0, endDate: 0, winCountRemaining: 0 },
132
- };
65
+ function unlockItem(item) {
66
+ if (!item || typeof item !== "object") return;
67
+ if (item.ownership && typeof item.ownership === "object") unlockOwnership(item.ownership);
68
+ else if (typeof item.id === "number" || typeof item.contentId === "string") item.ownership = ownedRecord();
69
+ if (item.owned === false) item.owned = true;
70
+ if (item.unlocked === false) item.unlocked = true;
133
71
  }
134
72
 
135
- function harvestFromPayload(node) {
136
- if (!node || typeof node !== "object") return;
137
- const list = node.skinAugments?.augments;
138
- if (Array.isArray(list)) {
139
- for (const aug of list) {
140
- if (aug && typeof aug.contentId === "string") augmentIds.add(aug.contentId);
141
- }
142
- }
143
- const values = Array.isArray(node) ? node : Object.values(node);
144
- for (const value of values) {
145
- if (value && typeof value === "object") harvestFromPayload(value);
73
+ function unlockAugments(holder) {
74
+ const list = holder?.skinAugments?.augments;
75
+ if (!Array.isArray(list)) return;
76
+ for (const aug of list) {
77
+ if (!aug || typeof aug !== "object") continue;
78
+ if (typeof aug.contentId === "string") augmentIds.add(aug.contentId);
79
+ if (aug.ownership && typeof aug.ownership === "object") unlockOwnership(aug.ownership);
80
+ else aug.ownership = ownedRecord();
146
81
  }
147
82
  }
148
83
 
149
- function variantLabelFromOverlays(overlays) {
84
+ function variantLabel(overlays) {
150
85
  for (const overlay of overlays || []) {
151
86
  const match = /splash_tile_([a-z0-9]+)\.png$/i.exec(String(overlay?.tileLCOverlayPath || ""));
152
87
  if (match) return match[1].charAt(0).toUpperCase() + match[1].slice(1);
@@ -154,600 +89,186 @@ function variantLabelFromOverlays(overlays) {
154
89
  return "Variant";
155
90
  }
156
91
 
92
+ // Signature/event skins keep their colour variants as augments with tile overlays on one tier. Split each
93
+ // overlay variant into its own tile so every variant can be picked; the fake ids are mapped back to the real
94
+ // tier id when the profile saves the choice (see wrapSelectSkin).
157
95
  function splitVariantTiers(skin) {
158
- const tiers = skin.questSkinInfo.tiers;
96
+ const tiers = skin?.questSkinInfo?.tiers;
97
+ if (!Array.isArray(tiers)) return;
159
98
  const parent = typeof skin.id === "number" ? skin.id : null;
160
99
  const taken = new Set(tiers.map((tier) => tier?.id));
161
100
  const out = [];
162
- let changed = false;
163
101
  let sibling = 0;
164
102
  for (const tier of tiers) {
165
103
  out.push(tier);
166
104
  const augments = tier?.skinAugments?.augments;
167
105
  if (!Array.isArray(augments) || augments.length < 2) continue;
168
- const withOverlays = augments.filter((aug) => Array.isArray(aug?.overlays) && aug.overlays.some((o) => o && o.tileLCOverlayPath));
169
- const plain = augments.filter((aug) => withOverlays.indexOf(aug) === -1);
106
+ const withOverlays = augments.filter((aug) => Array.isArray(aug?.overlays) && aug.overlays.some((o) => o?.tileLCOverlayPath));
107
+ const plain = augments.filter((aug) => !withOverlays.includes(aug));
170
108
  if (withOverlays.length === 0 || plain.length === 0) continue;
171
109
  tier.skinAugments = { ...tier.skinAugments, augments: plain };
172
110
  for (const aug of withOverlays) {
173
- let id = parent !== null ? parent + 1000000 * sibling : -(tier.id ?? 0) * 10 - sibling;
111
+ let id = parent !== null ? parent + 1000000 * (sibling + 1) : -((tier.id ?? 0) * 10) - sibling - 1;
174
112
  while (taken.has(id)) id += parent !== null ? 1000000 : -1;
175
113
  taken.add(id);
176
- sibling++;
114
+ sibling += 1;
177
115
  variantIdRemap.set(id, tier.id);
178
116
  out.push({
179
117
  ...tier,
180
118
  id,
181
- name: `${tier.name ?? ""} ${variantLabelFromOverlays(aug.overlays)}`.trim(),
119
+ name: `${tier.name ?? ""} ${variantLabel(aug.overlays)}`.trim(),
182
120
  skinAugments: { ...tier.skinAugments, augments: [aug] },
183
121
  });
184
- changed = true;
185
122
  }
186
123
  }
187
- if (changed) skin.questSkinInfo.tiers = out;
188
- return changed;
124
+ skin.questSkinInfo.tiers = out;
189
125
  }
190
126
 
191
- function remapVariantIds(body) {
192
- if (typeof body !== "string" || variantIdRemap.size === 0) return body;
193
- let out = body;
194
- for (const [from, to] of variantIdRemap) {
195
- out = out.replace(new RegExp(`(["'\\[,:\\s])${from}(?![0-9])`, "g"), `$1${to}`);
127
+ function unlockSkin(skin) {
128
+ unlockItem(skin);
129
+ unlockAugments(skin);
130
+ for (const chroma of Array.isArray(skin?.chromas) ? skin.chromas : []) {
131
+ unlockItem(chroma);
132
+ unlockAugments(chroma);
133
+ }
134
+ const tiers = skin?.questSkinInfo?.tiers;
135
+ if (Array.isArray(tiers)) {
136
+ for (const tier of tiers) {
137
+ unlockItem(tier);
138
+ unlockAugments(tier);
139
+ }
140
+ splitVariantTiers(skin);
196
141
  }
197
- return out;
198
142
  }
199
143
 
200
- function isShadowEntry(entry) {
201
- if (!entry || typeof entry !== "object") return false;
202
- if (Array.isArray(entry.skins)) return typeof entry.id === "number" && entry.id >= 60000;
203
- return typeof entry.championId === "number" && entry.championId >= 60000 && typeof entry.id === "number";
144
+ function unlockChampions(data) {
145
+ if (!Array.isArray(data)) return data;
146
+ const before = augmentIds.size;
147
+ const out = cloneData(data).filter((champion) => !(typeof champion?.id === "number" && champion.id >= SHADOW_CHAMPION_ID));
148
+ for (const champion of out) {
149
+ if (!champion || typeof champion !== "object") continue;
150
+ // The picker copies the champion's ownership onto its base skin and every tier, so an unowned
151
+ // champion would hide all of its skins. Unlock the champion too (only inside this window).
152
+ unlockItem(champion);
153
+ for (const skin of Array.isArray(champion.skins) ? champion.skins : []) unlockSkin(skin);
154
+ }
155
+ if (augmentIds.size !== before) queueMicrotask(() => augmentHandle()?.reapply());
156
+ return out;
204
157
  }
205
158
 
206
- function unlockDeep(node) {
207
- if (!node || typeof node !== "object") return false;
208
- let changed = false;
209
- if (Array.isArray(node)) {
210
- for (let i = node.length - 1; i >= 0; i--) {
211
- if (isShadowEntry(node[i])) {
212
- node.splice(i, 1);
213
- changed = true;
214
- }
215
- }
216
- }
217
- if (!Array.isArray(node) && node.questSkinInfo && Array.isArray(node.questSkinInfo.tiers)) {
218
- changed = splitVariantTiers(node) || changed;
219
- }
220
- const augList = node.skinAugments?.augments;
221
- if (Array.isArray(augList)) {
222
- for (const aug of augList) {
223
- if (!aug || typeof aug !== "object") continue;
224
- if (typeof aug.contentId === "string") augmentIds.add(aug.contentId);
225
- if (aug.ownership && typeof aug.ownership === "object") {
226
- changed = unlockOwnership(aug.ownership) || changed;
227
- } else {
228
- aug.ownership = synthesizedOwnership();
229
- changed = true;
230
- }
231
- }
232
- }
233
- const championNode = Array.isArray(node.skins);
234
- if (!championNode) {
235
- if (node.ownership && typeof node.ownership === "object") {
236
- changed = unlockOwnership(node.ownership) || changed;
237
- } else if (typeof node.contentId === "string" && Array.isArray(node.overlays)) {
238
- node.ownership = synthesizedOwnership();
239
- changed = true;
240
- }
241
- if (node.owned === false) {
242
- node.owned = true;
243
- changed = true;
244
- }
245
- }
246
- const values = Array.isArray(node)
247
- ? node
248
- : Object.keys(node)
249
- .filter((key) => key !== "ownership")
250
- .map((key) => node[key]);
251
- for (const value of values) {
252
- if (value && typeof value === "object") changed = unlockDeep(value) || changed;
253
- }
254
- return changed;
159
+ function unlockMinimal(data) {
160
+ if (!Array.isArray(data)) return data;
161
+ return cloneData(data)
162
+ .filter((champion) => !(typeof champion?.id === "number" && champion.id >= SHADOW_CHAMPION_ID))
163
+ .map((champion) => {
164
+ unlockItem(champion);
165
+ return champion;
166
+ });
255
167
  }
256
168
 
257
- function unlockAugmentInventory(items) {
258
- if (!Array.isArray(items)) return false;
259
- let changed = false;
169
+ function unlockAugmentInventory(data) {
170
+ if (!Array.isArray(data)) return data;
171
+ const out = cloneData(data);
260
172
  const present = new Set();
261
- for (const item of items) {
173
+ for (const item of out) {
262
174
  if (!item || typeof item !== "object") continue;
263
175
  if (typeof item.uuid === "string") present.add(item.uuid);
264
- if (item.owned !== true) {
265
- item.owned = true;
266
- changed = true;
267
- }
268
- if (item.ownershipType !== undefined && item.ownershipType !== "OWNED") {
269
- item.ownershipType = "OWNED";
270
- changed = true;
271
- }
176
+ item.owned = true;
177
+ if (item.ownershipType !== undefined) item.ownershipType = "OWNED";
272
178
  }
273
179
  for (const id of augmentIds) {
274
180
  if (present.has(id)) continue;
275
- items.push({
276
- uuid: id,
277
- inventoryType: "SKIN_AUGMENT",
278
- ownershipType: "OWNED",
279
- owned: true,
280
- quantity: 1,
281
- purchaseDate: FAKE_PURCHASE_DATE,
282
- });
283
- changed = true;
284
- }
285
- return changed;
286
- }
287
-
288
- function ensureAugmentIds() {
289
- if (augmentIds.size > 0) return Promise.resolve();
290
- if (harvestPromise) return harvestPromise;
291
- harvestPromise = (async () => {
292
- const waits = [...pendingTargetLoads];
293
- if (waits.length > 0) {
294
- await Promise.race([
295
- Promise.allSettled(waits),
296
- new Promise((resolve) => setTimeout(resolve, HARVEST_WAIT_MS)),
297
- ]);
298
- }
299
- if (augmentIds.size > 0) return;
300
- try {
301
- const get = rawFetch || ((...args) => window.fetch(...args));
302
- let id = summonerId;
303
- if (id === null) {
304
- const response = await get(CURRENT_SUMMONER_URI);
305
- const summoner = await response.json().catch(() => null);
306
- id = typeof summoner?.summonerId === "number" ? summoner.summonerId : null;
307
- if (id !== null) summonerId = id;
308
- }
309
- if (id === null) return;
310
- const response = await get(`/lol-champions/v1/inventories/${id}/champions`);
311
- const json = await response.json().catch(() => null);
312
- if (json) harvestFromPayload(json);
313
- } catch {}
314
- })().finally(() => {
315
- harvestPromise = null;
316
- });
317
- return harvestPromise;
318
- }
319
-
320
- let tierObserver = null;
321
- let tierAppliedForOpen = false;
322
- let tierDropdown = null;
323
- let tierArmListener = null;
324
-
325
- function findGroupingDropdown() {
326
- for (const dd of document.querySelectorAll("lol-uikit-framed-dropdown")) {
327
- const labels = [...dd.querySelectorAll(GROUPING_OPTIONS)].map((o) => o.textContent.trim().toLowerCase());
328
- if (labels.includes("my collection") && labels.includes(TIER_LABEL)) return dd;
329
- }
330
- return null;
331
- }
332
-
333
- function applyTierGrouping() {
334
- if (tierDropdown && !tierDropdown.isConnected) {
335
- tierDropdown = null;
336
- tierAppliedForOpen = false;
181
+ out.push({ uuid: id, inventoryType: "SKIN_AUGMENT", ownershipType: "OWNED", owned: true, quantity: 1, purchaseDate: FAKE_PURCHASE_DATE });
337
182
  }
338
- if (tierAppliedForOpen) return;
339
- if (!isPickerOpen()) return;
340
- const dd = tierDropdown ?? findGroupingDropdown();
341
- if (!dd) {
342
- tierAppliedForOpen = false;
343
- return;
344
- }
345
- tierDropdown = dd;
346
- const options = [...dd.querySelectorAll(GROUPING_OPTIONS)];
347
- const tier = options.find((o) => o.textContent.trim().toLowerCase() === TIER_LABEL);
348
- if (!tier) return;
349
- const current = options.find((o) => o.hasAttribute("selected"));
350
- if (current !== tier) tier.click();
351
- tierAppliedForOpen = true;
352
- }
353
-
354
- function startTierGrouping() {
355
- if (tierObserver) return;
356
- tierObserver = new MutationObserver(() => {
357
- if (!isPickerOpen()) {
358
- stopTierGrouping();
359
- return;
360
- }
361
- try {
362
- applyTierGrouping();
363
- } catch (error) {
364
- logError("tier grouping failed.", error);
365
- }
366
- });
367
- tierObserver.observe(document.documentElement ?? document, { childList: true, subtree: true });
368
- applyTierGrouping();
369
- }
370
-
371
- function stopTierGrouping() {
372
- tierObserver?.disconnect();
373
- tierObserver = null;
374
- tierAppliedForOpen = false;
375
- tierDropdown = null;
183
+ return out;
376
184
  }
377
185
 
378
- function maybeRefreshSummoner() {
379
- if (summonerId !== null || summonerPending || !isPickerOpen()) return;
380
- ensureSummonerId();
186
+ function augmentHandle() {
187
+ return handles[2] ?? null;
381
188
  }
382
189
 
383
- function whenSummonerKnown(timeout) {
384
- if (summonerId !== null) return Promise.resolve(true);
385
- ensureSummonerId();
386
- const deadline = Date.now() + timeout;
387
- return new Promise((resolve) => {
388
- const step = () => {
389
- if (summonerId !== null) return resolve(true);
390
- if (Date.now() >= deadline) return resolve(false);
391
- setTimeout(step, 40);
392
- };
393
- setTimeout(step, 0);
394
- });
190
+ function remapId(id) {
191
+ const numeric = Number(id);
192
+ return variantIdRemap.has(numeric) ? variantIdRemap.get(numeric) : id;
395
193
  }
396
194
 
397
- function ensureSummonerId() {
398
- if (summonerId !== null || summonerPending) return;
399
- summonerPending = true;
400
- let request;
195
+ async function wrapSelectSkin() {
401
196
  try {
402
- const get = api?.lcu?.get;
403
- request = get
404
- ? get(CURRENT_SUMMONER_URI, { ttl: SUMMONER_TTL })
405
- : (rawFetch || window.fetch)(CURRENT_SUMMONER_URI).then((response) => response.json());
406
- } catch {
407
- summonerPending = false;
408
- return;
409
- }
410
- Promise.resolve(request)
411
- .then((summoner) => {
412
- summonerId = typeof summoner?.summonerId === "number" ? summoner.summonerId : null;
413
- })
414
- .catch(() => {})
415
- .finally(() => {
416
- summonerPending = false;
417
- });
418
- }
419
- function rewriteInventoryText(raw) {
420
- if (!shouldFilter() || typeof raw !== "string") return raw;
421
- if (inventoryTextCache.has(raw)) {
422
- const cached = inventoryTextCache.get(raw);
423
- inventoryTextCache.delete(raw);
424
- inventoryTextCache.set(raw, cached);
425
- return cached;
426
- }
427
- let rewritten = raw;
428
- try {
429
- const json = JSON.parse(raw);
430
- if (unlockDeep(json)) rewritten = JSON.stringify(json);
431
- } catch {}
432
- inventoryTextCache.set(raw, rewritten);
433
- while (inventoryTextCache.size > INVENTORY_TEXT_CACHE_LIMIT) {
434
- inventoryTextCache.delete(inventoryTextCache.keys().next().value);
197
+ const picker = await api.whenRcpReady(PICKER_PLUGIN, 60000);
198
+ if (!active || !picker || typeof picker.selectSkin !== "function" || pickerApi) return;
199
+ const original = picker.selectSkin;
200
+ pickerApi = picker;
201
+ pickerSelectSkin = original;
202
+ picker.selectSkin = function (onSelected, onDefault, onReady) {
203
+ const select =
204
+ typeof onSelected === "function"
205
+ ? function (id, ...rest) {
206
+ return onSelected.call(this, active ? remapId(id) : id, ...rest);
207
+ }
208
+ : onSelected;
209
+ return original.call(this, select, onDefault, onReady);
210
+ };
211
+ } catch (error) {
212
+ logError("could not hook the skin picker.", error);
435
213
  }
436
- return rewritten;
437
214
  }
438
215
 
439
- function rewriteWebSocketFrame(text) {
440
- if (!shouldFilter() || typeof text !== "string") return null;
441
- const targetFrame = text.indexOf("lol-champions/v1/inventories") !== -1;
442
- const augmentFrame = text.indexOf("SKIN_AUGMENT") !== -1;
443
- if (!targetFrame && !augmentFrame) return null;
444
- let frame;
445
- try {
446
- frame = JSON.parse(text);
447
- } catch {
448
- return null;
449
- }
450
- if (!Array.isArray(frame) || frame.length < 3) return null;
451
- const payload = frame[2];
452
- if (!payload || typeof payload !== "object" || typeof payload.uri !== "string") return null;
453
- let changed;
454
- if (isTargetUrl(payload.uri)) {
455
- changed = unlockDeep(payload.data);
456
- } else if (isAugmentUrl(payload.uri)) {
457
- changed = unlockAugmentInventory(payload.data);
458
- } else {
459
- return null;
216
+ function unwrapSelectSkin() {
217
+ if (pickerApi && pickerSelectSkin) {
218
+ if (Object.prototype.hasOwnProperty.call(pickerApi, "selectSkin")) delete pickerApi.selectSkin;
219
+ if (pickerApi.selectSkin !== pickerSelectSkin && typeof pickerSelectSkin === "function") pickerApi.selectSkin = pickerSelectSkin;
460
220
  }
461
- if (!changed) return null;
462
- return JSON.stringify(frame);
463
- }
464
-
465
- function wrapMessageListener(listener) {
466
- if (typeof listener !== "function" || listener.__perfProfileWrap) return listener;
467
- const wrapped = function (event) {
468
- if (event && typeof event.data === "string") {
469
- const rewritten = rewriteWebSocketFrame(event.data);
470
- if (rewritten !== null) {
471
- const fresh = new MessageEvent("message", {
472
- data: rewritten,
473
- origin: event.origin,
474
- lastEventId: event.lastEventId,
475
- ports: event.ports ? [...event.ports] : [],
476
- source: event.source ?? null,
477
- });
478
- return listener.call(this, fresh);
479
- }
480
- }
481
- return listener.call(this, event);
482
- };
483
- wrapped.__perfProfileWrap = true;
484
- wrapped.__perfProfileOriginal = listener;
485
- return wrapped;
221
+ pickerApi = null;
222
+ pickerSelectSkin = null;
486
223
  }
487
224
 
488
- function installWebSocketFilter() {
489
- const proto = window.WebSocket && window.WebSocket.prototype;
490
- if (!proto || proto.addEventListener.__perfProfileFilter) return;
491
-
492
- const origAdd = proto.addEventListener;
493
- const origRemove = proto.removeEventListener;
494
- const origDispatch = proto.dispatchEvent;
495
-
496
- proto.addEventListener = function (type, listener, options) {
497
- return origAdd.call(this, type, type === "message" ? wrapMessageListener(listener) : listener, options);
498
- };
499
- proto.removeEventListener = function (type, listener, options) {
500
- return origRemove.call(this, type, (listener && listener.__perfProfileOriginal) || listener, options);
501
- };
502
- proto.dispatchEvent = function (event) {
503
- if (event && event.type === "message" && typeof event.data === "string") {
504
- const rewritten = rewriteWebSocketFrame(event.data);
505
- if (rewritten !== null) {
506
- return origDispatch.call(
507
- this,
508
- new MessageEvent("message", {
509
- data: rewritten,
510
- origin: event.origin,
511
- lastEventId: event.lastEventId,
512
- ports: event.ports ? [...event.ports] : [],
513
- source: event.source ?? null,
514
- }),
515
- );
516
- }
517
- }
518
- return origDispatch.call(this, event);
519
- };
520
-
521
- const descriptor = Object.getOwnPropertyDescriptor(proto, "onmessage");
522
- if (descriptor && descriptor.set && descriptor.get) {
523
- Object.defineProperty(proto, "onmessage", {
524
- configurable: true,
525
- enumerable: descriptor.enumerable,
526
- get: descriptor.get,
527
- set(value) {
528
- descriptor.set.call(this, typeof value === "function" ? wrapMessageListener(value) : value);
529
- },
530
- });
531
- }
532
-
533
- proto.addEventListener.__perfProfileFilter = true;
534
- }
535
-
536
- function installFetchFilter() {
537
- const original = window.fetch;
538
- if (typeof original !== "function" || original.__perfProfileFilter) return;
539
- rawFetch = (...args) => original.call(window, ...args);
540
-
541
- const wrapped = function (input, init) {
542
- const url = typeof input === "string" ? input : (input && input.url) || "";
543
- const inventory = pathIdOf(url) !== null;
544
- const augmentRequest = isAugmentUrl(url);
545
- let options = init;
546
- if (shouldFilter() && options && typeof options.body === "string") {
547
- const mapped = remapVariantIds(options.body);
548
- if (mapped !== options.body) options = { ...options, body: mapped };
549
- }
550
- const promise = original.call(this, input, options);
551
- if (!shouldFilter() || (!inventory && !augmentRequest)) return promise;
552
- if (inventory) maybeRefreshSummoner();
553
-
554
- return whenSummonerKnown(SUMMONER_WAIT_MS).then(() => {
555
- const target = isTargetUrl(url);
556
- if (!augmentRequest && !target) return promise;
557
- if (target && augmentIds.size === 0) {
558
- const latch = (async () => {
559
- try {
560
- const response = await promise;
561
- if (response && response.ok && augmentIds.size === 0) {
562
- const json = await response.clone().json().catch(() => null);
563
- if (json) harvestFromPayload(json);
564
- }
565
- } catch {}
566
- pendingTargetLoads.delete(latch);
567
- })();
568
- pendingTargetLoads.add(latch);
569
- }
570
- const ready = augmentRequest && augmentIds.size === 0 ? ensureAugmentIds() : Promise.resolve();
571
- return ready
572
- .then(() => promise)
573
- .then((response) => {
574
- if (!response || !response.ok) return response;
575
- if (augmentRequest) {
576
- return response
577
- .clone()
578
- .json()
579
- .catch(() => null)
580
- .then((json) => {
581
- if (!json || !unlockAugmentInventory(json)) return response;
582
- return new Response(JSON.stringify(json), {
583
- status: response.status,
584
- statusText: response.statusText,
585
- headers: response.headers,
586
- });
587
- });
588
- }
589
- return response
590
- .clone()
591
- .text()
592
- .catch(() => null)
593
- .then((text) => {
594
- if (text === null) return response;
595
- const rewritten = rewriteInventoryText(text);
596
- if (rewritten === text) return response;
597
- return new Response(rewritten, {
598
- status: response.status,
599
- statusText: response.statusText,
600
- headers: response.headers,
601
- });
602
- });
603
- });
604
- });
605
- };
606
- wrapped.__perfProfileFilter = true;
607
- window.fetch = wrapped;
608
- }
609
- function installXhrFilter() {
610
- const proto = window.XMLHttpRequest && window.XMLHttpRequest.prototype;
611
- if (!proto || proto.open.__perfProfileFilter) return;
612
-
613
- const origOpen = proto.open;
614
- const origSend = proto.send;
615
- const textDescriptor = Object.getOwnPropertyDescriptor(proto, "responseText");
616
- const responseDescriptor = Object.getOwnPropertyDescriptor(proto, "response");
617
-
618
- proto.open = function (method, url, ...rest) {
619
- this.__perfProfileUrl = typeof url === "string" ? url : "";
620
- return origOpen.call(this, method, url, ...rest);
621
- };
622
- proto.open.__perfProfileFilter = true;
623
-
624
- proto.send = function (...args) {
625
- const url = this.__perfProfileUrl || "";
626
- const target = isTargetUrl(url);
627
- const augment = !target && isAugmentUrl(url);
628
- if (shouldFilter() && pathIdOf(url) !== null) maybeRefreshSummoner();
629
- if (shouldFilter() && typeof args[0] === "string") {
630
- const mapped = remapVariantIds(args[0]);
631
- if (mapped !== args[0]) args = [mapped, ...args.slice(1)];
632
- }
633
- const xhr = this;
634
- if (target && shouldFilter() && augmentIds.size === 0 && textDescriptor && textDescriptor.get) {
635
- const latch = new Promise((resolve) => {
636
- const finish = () => {
637
- if (augmentIds.size === 0) {
638
- try {
639
- harvestFromPayload(JSON.parse(textDescriptor.get.call(xhr)));
640
- } catch {}
641
- }
642
- pendingTargetLoads.delete(latch);
643
- resolve();
644
- };
645
- xhr.addEventListener("load", finish, { once: true });
646
- xhr.addEventListener("error", finish, { once: true });
647
- xhr.addEventListener("abort", finish, { once: true });
648
- });
649
- pendingTargetLoads.add(latch);
650
- }
651
- if (textDescriptor && textDescriptor.get && (pathIdOf(url) !== null || augment)) {
652
- const cache = { raw: null, gate: null, filtered: null };
653
- const gateOpen = () => shouldFilter() && (augment || isTargetUrl(url));
654
- const rewriteBody = (json) => (augment ? unlockAugmentInventory(json) : unlockDeep(json));
655
- const filteredText = () => {
656
- const gate = gateOpen();
657
- if (cache.filtered !== null && cache.gate === gate) return cache.filtered;
658
- const raw = textDescriptor.get.call(xhr);
659
- cache.gate = gate;
660
- cache.filtered = raw;
661
- if (gate) {
662
- if (augment) {
663
- try {
664
- const json = JSON.parse(raw);
665
- if (rewriteBody(json)) cache.filtered = JSON.stringify(json);
666
- } catch {}
667
- } else {
668
- cache.filtered = rewriteInventoryText(raw);
669
- }
670
- }
671
- return cache.filtered;
672
- };
673
- Object.defineProperty(xhr, "responseText", { configurable: true, get: filteredText });
674
- if (responseDescriptor && responseDescriptor.get) {
675
- Object.defineProperty(xhr, "response", {
676
- configurable: true,
677
- get() {
678
- const type = xhr.responseType;
679
- if (type === "" || type === "text") return filteredText();
680
- const value = responseDescriptor.get.call(xhr);
681
- if (type === "json" && gateOpen() && value && typeof value === "object") rewriteBody(value);
682
- return value;
683
- },
684
- });
685
- }
686
- }
687
- if (augment && shouldFilter() && augmentIds.size === 0) {
688
- const sendArgs = args;
689
- ensureAugmentIds().finally(() => {
690
- try {
691
- origSend.apply(xhr, sendArgs);
692
- } catch {}
693
- });
694
- return;
695
- }
696
- return origSend.apply(this, args);
697
- };
698
- }
699
-
700
- function installSourceFilter() {
225
+ async function loadTierLabel() {
226
+ if (tierLabel !== null) return tierLabel;
701
227
  try {
702
- filterActive = readDefaultEnabled();
703
- installWebSocketFilter();
704
- installFetchFilter();
705
- installXhrFilter();
706
- ensureSummonerId();
707
- } catch (error) {
708
- logError("could not install the source filter.", error);
228
+ const response = await fetch(TRANSLATIONS_URI);
229
+ const strings = response.ok ? await response.json() : null;
230
+ tierLabel = String(strings?.[TIER_KEY] ?? "").trim().toLowerCase();
231
+ } catch {
232
+ tierLabel = "";
709
233
  }
234
+ return tierLabel;
710
235
  }
711
236
 
712
- function ensureSourceFilter() {
713
- try {
714
- installWebSocketFilter();
715
- installFetchFilter();
716
- installXhrFilter();
717
- } catch (error) {
718
- logError("could not refresh the source filter.", error);
719
- }
237
+ async function groupByTier(dropdown) {
238
+ const label = await loadTierLabel();
239
+ if (!active || !dropdown.isConnected) return;
240
+ const options = [...dropdown.querySelectorAll("lol-uikit-dropdown-option")];
241
+ if (options.length === 0) return;
242
+ const tier = (label && options.find((option) => option.textContent.trim().toLowerCase() === label)) ?? options[options.length - 1];
243
+ if (tier && !tier.hasAttribute("selected")) tier.click();
720
244
  }
721
245
 
722
246
  function onEnable(utils) {
723
247
  api = utils;
724
- filterActive = true;
725
- ensureSourceFilter();
726
- ensureSummonerId();
727
- tierArmListener = (event) => {
728
- const target = event.target instanceof Element ? event.target.closest(PICKER_BUTTON_SELECTOR) : null;
729
- if (!target) return;
730
- armPicker();
731
- };
732
- document.addEventListener("click", tierArmListener, true);
733
- if (isPickerOpen()) startTierGrouping();
248
+ active = true;
249
+ const transform = (match, fn) => utils.transformBinding({ identifier: PICKER_PLUGIN, match: (path) => match.test(path), transform: fn });
250
+ handles = [
251
+ transform(CHAMPIONS_PATH, unlockChampions),
252
+ transform(MINIMAL_PATH, unlockMinimal),
253
+ transform(AUGMENTS_PATH, unlockAugmentInventory),
254
+ ];
255
+ unwatchGrouping = utils.watch(GROUPING_SELECTOR, (dropdown) => {
256
+ groupByTier(dropdown).catch((error) => logError("tier grouping failed.", error));
257
+ });
258
+ wrapSelectSkin();
734
259
  }
735
260
 
736
261
  function onDisable() {
737
- filterActive = false;
738
- inventoryTextCache.clear();
739
- if (tierArmListener) document.removeEventListener("click", tierArmListener, true);
740
- tierArmListener = null;
741
- if (pickerArmTimer !== null) clearTimeout(pickerArmTimer);
742
- pickerArmTimer = null;
743
- pickerArmUntil = 0;
744
- stopTierGrouping();
262
+ active = false;
263
+ for (const handle of handles) handle.dispose();
264
+ handles = [];
265
+ unwatchGrouping?.();
266
+ unwatchGrouping = null;
267
+ unwrapSelectSkin();
745
268
  api = null;
746
269
  }
747
270
 
748
271
  export function init() {
749
- installSourceFilter();
750
-
751
272
  const registerWith = (utils) => {
752
273
  utils.registerPlugin({
753
274
  id: PLUGIN_ID,
@@ -762,6 +283,6 @@ export function init() {
762
283
  if (window.PerfectionistUtils) {
763
284
  registerWith(window.PerfectionistUtils);
764
285
  } else {
765
- addEventListener("perfectionist:utils-ready", (event) => registerWith(event.detail), { once: true });
286
+ addEventListener("perfectionist:utils-ready", (event) => registerWith(event.detail ?? window.PerfectionistUtils), { once: true });
766
287
  }
767
- }
288
+ }