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