perfectionist-plugins 1.0.4 → 1.2.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.
package/README.md CHANGED
@@ -40,6 +40,19 @@ Built for [Pengu Loader](https://pengu.lol)
40
40
  No more scrolling past skins you can't use.
41
41
  </td>
42
42
  </tr>
43
+ <tr>
44
+ <td>&nbsp;</td>
45
+ </tr>
46
+ <tr>
47
+ <th align="left">🔓 Unlock Profile Skins</th>
48
+ </tr>
49
+ <tr>
50
+ <td>
51
+ In the <b>Set profile background</b> window, every skin shows as unlocked, even the event skins (Signature).
52
+ <br><br>
53
+ Browse the full list and pick any skin you like as your profile background.
54
+ </td>
55
+ </tr>
43
56
  </table>
44
57
 
45
58
  ## Install
@@ -1,4 +1,4 @@
1
- export const version = "2.2.0";
1
+ export const version = "2.3.0";
2
2
 
3
3
  const PLUGIN_ID = "hide-unowned-skins";
4
4
  const STYLE_ID = "perfectionist-hide-unowned-skins-style";
@@ -12,6 +12,7 @@ const CURRENT_SUMMONER_URI = "/lol-summoner/v1/current-summoner";
12
12
  const SUMMONER_TTL = 10 * 60 * 1000;
13
13
  const INVENTORY_TTL = 60 * 1000;
14
14
  const REFRESH_DEBOUNCE_MS = 350;
15
+ const REPACK_SETTLE_MS = 120;
15
16
  const RETRY_MS = 5000;
16
17
  const STATE_KEY = "perfectionist-plugins:state";
17
18
 
@@ -271,18 +272,32 @@ let sessionMisses = 0;
271
272
  let championMisses = 0;
272
273
  let renderObserver = null;
273
274
  const repackOriginals = new Map();
275
+ const skinSuffixCache = new Map();
276
+ let repackTimer = null;
274
277
  let removeStyle = null;
275
278
 
276
279
  function logError(scope, error) {
277
280
  console.error(`[Perfectionist] Hide Unowned Skins: ${scope}`, error);
278
281
  }
279
282
 
283
+ function skinSuffixOf(text) {
284
+ const numbered = text.match(/Skins\/Skin(\d+)\//i) ?? text.match(/Skin(\d+)\//);
285
+ if (numbered) return Number(numbered[1]);
286
+ return /Skins\/Base\//i.test(text) ? 0 : null;
287
+ }
288
+
280
289
  function readSkinId(item) {
281
- const html = item.innerHTML;
282
- const numbered = html.match(/Skins\/Skin(\d+)\//i) ?? html.match(/Skin(\d+)\//);
283
- if (numbered) return currentChampionId * 1000 + Number(numbered[1]);
284
- if (/Skins\/Base\//i.test(html)) return currentChampionId * 1000;
285
- return null;
290
+ for (const img of item.querySelectorAll("img[src]")) {
291
+ const src = img.getAttribute("src");
292
+ let suffix = skinSuffixCache.get(src);
293
+ if (suffix === undefined) {
294
+ suffix = skinSuffixOf(src);
295
+ skinSuffixCache.set(src, suffix);
296
+ }
297
+ if (suffix !== null) return currentChampionId * 1000 + suffix;
298
+ }
299
+ const suffix = skinSuffixOf(item.innerHTML);
300
+ return suffix === null ? null : currentChampionId * 1000 + suffix;
286
301
  }
287
302
 
288
303
  function hideTargetFor(item) {
@@ -297,10 +312,17 @@ function hideTargetFor(item) {
297
312
 
298
313
  function repackVisibleItems(items) {
299
314
  const visible = items.filter((item) => !item.classList.contains(HIDDEN_CLASS));
300
- if (visible.length < 2) return;
315
+ if (visible.length < 2) {
316
+ restoreRepack();
317
+ return;
318
+ }
301
319
 
302
320
  const selectedIndex = items.findIndex((item) => item.classList.contains("skin-selection-item-selected"));
303
- const anchor = selectedIndex >= 0 ? items[selectedIndex] : visible[Math.floor(visible.length / 2)];
321
+ let anchor = selectedIndex >= 0 ? items[selectedIndex] : null;
322
+ if (!anchor && selectedSkinId !== null) {
323
+ anchor = visible.find((item) => readSkinId(item) === selectedSkinId) ?? null;
324
+ }
325
+ if (!anchor) anchor = visible[Math.floor(visible.length / 2)];
304
326
  const anchorDomIndex = items.indexOf(anchor);
305
327
 
306
328
  const widthOf = (item) => item.getBoundingClientRect().width || (item === anchor ? 117 : 73);
@@ -378,10 +400,28 @@ function applyAll() {
378
400
  else if (dotsHidden === 0 && dotList.hasAttribute("data-perf-dots")) dotList.removeAttribute("data-perf-dots");
379
401
  }
380
402
 
381
- if (ownedSkinIds !== null && hidden > 0) repackVisibleItems(items);
403
+ if (ownedSkinIds !== null && hidden > 0) scheduleRepack();
382
404
  else if (hidden === 0) restoreRepack();
383
405
  }
384
406
 
407
+ function scheduleRepack() {
408
+ clearTimeout(repackTimer);
409
+ repackTimer = setTimeout(runRepack, REPACK_SETTLE_MS);
410
+ }
411
+
412
+ function runRepack() {
413
+ repackTimer = null;
414
+ renderObserver?.disconnect();
415
+ try {
416
+ const items = [...document.querySelectorAll(SKIN_ITEM_SELECTOR)];
417
+ const hidden = items.some((item) => item.classList.contains(HIDDEN_CLASS));
418
+ if (ownedSkinIds !== null && hidden) repackVisibleItems(items);
419
+ else restoreRepack();
420
+ } finally {
421
+ renderObserver?.observe(document.documentElement ?? document, OBSERVER_OPTIONS);
422
+ }
423
+ }
424
+
385
425
  function applyAllGuarded() {
386
426
  renderObserver?.disconnect();
387
427
  try {
@@ -565,6 +605,8 @@ function onDisable() {
565
605
  unsubscribe = null;
566
606
  clearTimeout(refreshTimer);
567
607
  refreshTimer = null;
608
+ clearTimeout(repackTimer);
609
+ repackTimer = null;
568
610
  renderObserver?.disconnect();
569
611
  removeStyle?.();
570
612
  renderObserver = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perfectionist-plugins",
3
- "version": "1.0.4",
3
+ "version": "1.2.0",
4
4
  "description": "Small plugins that fix the annoying parts of League of Legends champ select. Built for Pengu Loader.",
5
5
  "type": "module",
6
6
  "main": "perfectionist-utils.js",
@@ -8,12 +8,14 @@
8
8
  ".": "./perfectionist-utils.js",
9
9
  "./perfectionist-utils.js": "./perfectionist-utils.js",
10
10
  "./ban-blocker.js": "./ban-blocker.js",
11
- "./hide-unowned-skins.js": "./hide-unowned-skins.js"
11
+ "./hide-unowned-skins.js": "./hide-unowned-skins.js",
12
+ "./unlock-profile-skins.js": "./unlock-profile-skins.js"
12
13
  },
13
14
  "files": [
14
15
  "perfectionist-utils.js",
15
16
  "ban-blocker.js",
16
- "hide-unowned-skins.js"
17
+ "hide-unowned-skins.js",
18
+ "unlock-profile-skins.js"
17
19
  ],
18
20
  "repository": {
19
21
  "type": "git",
@@ -6,7 +6,7 @@ const PANEL_ID = "perfectionist-plugins-panel";
6
6
  const STYLE_ID = "perfectionist-plugins-style";
7
7
  const TAB_TITLE = "Perfectionism";
8
8
 
9
- export const version = "1.0.4";
9
+ export const version = "1.1.0";
10
10
 
11
11
  const PKG = "perfectionist-plugins";
12
12
  const CDN = "https://cdn.jsdelivr.net/npm";
@@ -36,6 +36,13 @@ const BUNDLED_MODULES = [
36
36
  version: "2.2.0",
37
37
  description: "In champion select, only shows the skins you own for the picked champion.",
38
38
  },
39
+ {
40
+ id: "unlock-profile-skins",
41
+ file: "unlock-profile-skins.js",
42
+ name: "Unlock Profile Skins",
43
+ version: "1.0.0",
44
+ description: "Unlocks every skin in the Set profile background window.",
45
+ },
39
46
  ];
40
47
 
41
48
  const STYLE_CSS = `
@@ -0,0 +1,648 @@
1
+ export const version = "1.2.0";
2
+
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 STATE_KEY = "perfectionist-plugins:state";
10
+ const FAKE_PURCHASE_DATE = 1577836800000;
11
+ const FAKE_RENTAL_END = 4102444800000;
12
+ const TIER_LABEL = "tier";
13
+ const GROUPING_OPTIONS = "lol-uikit-dropdown-option";
14
+ const HARVEST_WAIT_MS = 2000;
15
+
16
+ let filterActive = true;
17
+ let api = null;
18
+ let summonerId = null;
19
+ let summonerPending = false;
20
+ let rawFetch = null;
21
+ let harvestPromise = null;
22
+ const augmentIds = new Set();
23
+ const pendingTargetLoads = new Set();
24
+ const variantIdRemap = new Map();
25
+
26
+ function readDefaultEnabled() {
27
+ try {
28
+ const saved = JSON.parse(localStorage.getItem(STATE_KEY) || "{}");
29
+ return saved.enabled?.[PLUGIN_ID] !== false;
30
+ } catch {
31
+ return true;
32
+ }
33
+ }
34
+
35
+ function logError(scope, error) {
36
+ console.error(`[Perfectionist] Unlock Profile Skins: ${scope}`, error);
37
+ }
38
+
39
+ function isPickerOpen() {
40
+ return document.querySelector(PICKER_BUTTON_SELECTOR) !== null;
41
+ }
42
+
43
+ function cleanPath(url) {
44
+ let path = String(url ?? "").split("?")[0];
45
+ const origin = /^[a-z][a-z0-9+.-]*:\/\/[^/]+/i.exec(path);
46
+ if (origin) path = path.slice(origin[0].length) || "/";
47
+ return path;
48
+ }
49
+
50
+ function pathIdOf(url) {
51
+ const match = INVENTORY_PATH.exec(cleanPath(url));
52
+ return match ? Number(match[1]) : null;
53
+ }
54
+
55
+ function isTargetUrl(url) {
56
+ const id = pathIdOf(url);
57
+ return id !== null && summonerId !== null && id === summonerId;
58
+ }
59
+
60
+ function isAugmentUrl(url) {
61
+ return AUGMENT_INVENTORY_PATH.test(cleanPath(url));
62
+ }
63
+
64
+ function unlockOwnership(ownership) {
65
+ if (!ownership || typeof ownership !== "object") return false;
66
+ let changed = false;
67
+ const needsFakeDate = ownership.owned !== true || ownership.rental?.rented === true;
68
+ if (ownership.owned !== true) {
69
+ ownership.owned = true;
70
+ changed = true;
71
+ }
72
+ if (needsFakeDate) {
73
+ const rental = ownership.rental;
74
+ if (rental && typeof rental === "object") {
75
+ if (rental.purchaseDate !== FAKE_PURCHASE_DATE) {
76
+ rental.purchaseDate = FAKE_PURCHASE_DATE;
77
+ changed = true;
78
+ }
79
+ if (rental.endDate !== undefined && rental.endDate !== FAKE_RENTAL_END) {
80
+ rental.endDate = FAKE_RENTAL_END;
81
+ changed = true;
82
+ }
83
+ }
84
+ if (ownership.purchaseDate !== undefined && ownership.purchaseDate !== FAKE_PURCHASE_DATE) {
85
+ ownership.purchaseDate = FAKE_PURCHASE_DATE;
86
+ changed = true;
87
+ }
88
+ }
89
+ return changed;
90
+ }
91
+
92
+ function synthesizedOwnership() {
93
+ return {
94
+ owned: true,
95
+ loyaltyReward: false,
96
+ xboxGPReward: false,
97
+ rental: { rented: false, purchaseDate: 0, endDate: 0, winCountRemaining: 0 },
98
+ };
99
+ }
100
+
101
+ function harvestFromPayload(node) {
102
+ if (!node || typeof node !== "object") return;
103
+ const list = node.skinAugments?.augments;
104
+ if (Array.isArray(list)) {
105
+ for (const aug of list) {
106
+ if (aug && typeof aug.contentId === "string") augmentIds.add(aug.contentId);
107
+ }
108
+ }
109
+ const values = Array.isArray(node) ? node : Object.values(node);
110
+ for (const value of values) {
111
+ if (value && typeof value === "object") harvestFromPayload(value);
112
+ }
113
+ }
114
+
115
+ function variantLabelFromOverlays(overlays) {
116
+ for (const overlay of overlays || []) {
117
+ const match = /splash_tile_([a-z0-9]+)\.png$/i.exec(String(overlay?.tileLCOverlayPath || ""));
118
+ if (match) return match[1].charAt(0).toUpperCase() + match[1].slice(1);
119
+ }
120
+ return "Variant";
121
+ }
122
+
123
+ function splitVariantTiers(skin) {
124
+ const tiers = skin.questSkinInfo.tiers;
125
+ const parent = typeof skin.id === "number" ? skin.id : null;
126
+ const taken = new Set(tiers.map((tier) => tier?.id));
127
+ const out = [];
128
+ let changed = false;
129
+ let sibling = 0;
130
+ for (const tier of tiers) {
131
+ out.push(tier);
132
+ const augments = tier?.skinAugments?.augments;
133
+ if (!Array.isArray(augments) || augments.length < 2) continue;
134
+ const withOverlays = augments.filter((aug) => Array.isArray(aug?.overlays) && aug.overlays.some((o) => o && o.tileLCOverlayPath));
135
+ const plain = augments.filter((aug) => withOverlays.indexOf(aug) === -1);
136
+ if (withOverlays.length === 0 || plain.length === 0) continue;
137
+ tier.skinAugments = { ...tier.skinAugments, augments: plain };
138
+ for (const aug of withOverlays) {
139
+ let id = parent !== null ? parent + 1000000 * sibling : -(tier.id ?? 0) * 10 - sibling;
140
+ while (taken.has(id)) id += parent !== null ? 1000000 : -1;
141
+ taken.add(id);
142
+ sibling++;
143
+ variantIdRemap.set(id, tier.id);
144
+ out.push({
145
+ ...tier,
146
+ id,
147
+ name: `${tier.name ?? ""} ${variantLabelFromOverlays(aug.overlays)}`.trim(),
148
+ skinAugments: { ...tier.skinAugments, augments: [aug] },
149
+ });
150
+ changed = true;
151
+ }
152
+ }
153
+ if (changed) skin.questSkinInfo.tiers = out;
154
+ return changed;
155
+ }
156
+
157
+ function remapVariantIds(body) {
158
+ if (typeof body !== "string" || variantIdRemap.size === 0) return body;
159
+ let out = body;
160
+ for (const [from, to] of variantIdRemap) {
161
+ out = out.replace(new RegExp(`(["'\\[,:\\s])${from}(?![0-9])`, "g"), `$1${to}`);
162
+ }
163
+ return out;
164
+ }
165
+
166
+ function isShadowEntry(entry) {
167
+ if (!entry || typeof entry !== "object") return false;
168
+ if (Array.isArray(entry.skins)) return typeof entry.id === "number" && entry.id >= 60000;
169
+ return typeof entry.championId === "number" && entry.championId >= 60000 && typeof entry.id === "number";
170
+ }
171
+
172
+ function unlockDeep(node) {
173
+ if (!node || typeof node !== "object") return false;
174
+ let changed = false;
175
+ if (Array.isArray(node)) {
176
+ for (let i = node.length - 1; i >= 0; i--) {
177
+ if (isShadowEntry(node[i])) {
178
+ node.splice(i, 1);
179
+ changed = true;
180
+ }
181
+ }
182
+ }
183
+ if (!Array.isArray(node) && node.questSkinInfo && Array.isArray(node.questSkinInfo.tiers)) {
184
+ changed = splitVariantTiers(node) || changed;
185
+ }
186
+ const augList = node.skinAugments?.augments;
187
+ if (Array.isArray(augList)) {
188
+ for (const aug of augList) {
189
+ if (!aug || typeof aug !== "object") continue;
190
+ if (typeof aug.contentId === "string") augmentIds.add(aug.contentId);
191
+ if (aug.ownership && typeof aug.ownership === "object") {
192
+ changed = unlockOwnership(aug.ownership) || changed;
193
+ } else {
194
+ aug.ownership = synthesizedOwnership();
195
+ changed = true;
196
+ }
197
+ }
198
+ }
199
+ if (node.ownership && typeof node.ownership === "object") {
200
+ changed = unlockOwnership(node.ownership) || changed;
201
+ } else if (typeof node.contentId === "string" && Array.isArray(node.overlays)) {
202
+ node.ownership = synthesizedOwnership();
203
+ changed = true;
204
+ }
205
+ if (node.owned === false) {
206
+ node.owned = true;
207
+ changed = true;
208
+ }
209
+ const values = Array.isArray(node) ? node : Object.values(node);
210
+ for (const value of values) {
211
+ if (value && typeof value === "object") changed = unlockDeep(value) || changed;
212
+ }
213
+ return changed;
214
+ }
215
+
216
+ function unlockAugmentInventory(items) {
217
+ if (!Array.isArray(items)) return false;
218
+ let changed = false;
219
+ const present = new Set();
220
+ for (const item of items) {
221
+ if (!item || typeof item !== "object") continue;
222
+ if (typeof item.uuid === "string") present.add(item.uuid);
223
+ if (item.owned !== true) {
224
+ item.owned = true;
225
+ changed = true;
226
+ }
227
+ if (item.ownershipType !== undefined && item.ownershipType !== "OWNED") {
228
+ item.ownershipType = "OWNED";
229
+ changed = true;
230
+ }
231
+ }
232
+ for (const id of augmentIds) {
233
+ if (present.has(id)) continue;
234
+ items.push({
235
+ uuid: id,
236
+ inventoryType: "SKIN_AUGMENT",
237
+ ownershipType: "OWNED",
238
+ owned: true,
239
+ quantity: 1,
240
+ purchaseDate: FAKE_PURCHASE_DATE,
241
+ });
242
+ changed = true;
243
+ }
244
+ return changed;
245
+ }
246
+
247
+ function ensureAugmentIds() {
248
+ if (augmentIds.size > 0) return Promise.resolve();
249
+ if (harvestPromise) return harvestPromise;
250
+ harvestPromise = (async () => {
251
+ const waits = [...pendingTargetLoads];
252
+ if (waits.length > 0) {
253
+ await Promise.race([
254
+ Promise.allSettled(waits),
255
+ new Promise((resolve) => setTimeout(resolve, HARVEST_WAIT_MS)),
256
+ ]);
257
+ }
258
+ if (augmentIds.size > 0) return;
259
+ try {
260
+ const get = rawFetch || ((...args) => window.fetch(...args));
261
+ let id = summonerId;
262
+ if (id === null) {
263
+ const response = await get(CURRENT_SUMMONER_URI);
264
+ const summoner = await response.json().catch(() => null);
265
+ id = typeof summoner?.summonerId === "number" ? summoner.summonerId : null;
266
+ if (id !== null) summonerId = id;
267
+ }
268
+ if (id === null) return;
269
+ const response = await get(`/lol-champions/v1/inventories/${id}/champions`);
270
+ const json = await response.json().catch(() => null);
271
+ if (json) harvestFromPayload(json);
272
+ } catch {}
273
+ })().finally(() => {
274
+ harvestPromise = null;
275
+ });
276
+ return harvestPromise;
277
+ }
278
+
279
+ let tierObserver = null;
280
+ let tierAppliedForOpen = false;
281
+ let tierDropdown = null;
282
+
283
+ function findGroupingDropdown() {
284
+ for (const dd of document.querySelectorAll("lol-uikit-framed-dropdown")) {
285
+ const labels = [...dd.querySelectorAll(GROUPING_OPTIONS)].map((o) => o.textContent.trim().toLowerCase());
286
+ if (labels.includes("my collection") && labels.includes(TIER_LABEL)) return dd;
287
+ }
288
+ return null;
289
+ }
290
+
291
+ function applyTierGrouping() {
292
+ if (tierDropdown && !tierDropdown.isConnected) {
293
+ tierDropdown = null;
294
+ tierAppliedForOpen = false;
295
+ }
296
+ if (tierAppliedForOpen) return;
297
+ const dd = tierDropdown ?? findGroupingDropdown();
298
+ if (!dd) {
299
+ tierAppliedForOpen = false;
300
+ return;
301
+ }
302
+ tierDropdown = dd;
303
+ const options = [...dd.querySelectorAll(GROUPING_OPTIONS)];
304
+ const tier = options.find((o) => o.textContent.trim().toLowerCase() === TIER_LABEL);
305
+ if (!tier) return;
306
+ const current = options.find((o) => o.hasAttribute("selected"));
307
+ if (current !== tier) tier.click();
308
+ tierAppliedForOpen = true;
309
+ }
310
+
311
+ function startTierGrouping() {
312
+ if (tierObserver) return;
313
+ tierObserver = new MutationObserver(() => {
314
+ try {
315
+ applyTierGrouping();
316
+ } catch (error) {
317
+ logError("tier grouping failed.", error);
318
+ }
319
+ });
320
+ tierObserver.observe(document.documentElement ?? document, { childList: true, subtree: true });
321
+ applyTierGrouping();
322
+ }
323
+
324
+ function stopTierGrouping() {
325
+ tierObserver?.disconnect();
326
+ tierObserver = null;
327
+ tierAppliedForOpen = false;
328
+ tierDropdown = null;
329
+ }
330
+
331
+ function maybeRefreshSummoner() {
332
+ if (summonerId !== null || summonerPending || !isPickerOpen()) return;
333
+ ensureSummonerId();
334
+ }
335
+
336
+ function ensureSummonerId() {
337
+ if (summonerId !== null || summonerPending) return;
338
+ const get = api?.lcu?.get;
339
+ if (!get) return;
340
+ summonerPending = true;
341
+ get(CURRENT_SUMMONER_URI, { ttl: SUMMONER_TTL })
342
+ .then((summoner) => {
343
+ summonerId = typeof summoner?.summonerId === "number" ? summoner.summonerId : null;
344
+ })
345
+ .catch(() => {})
346
+ .finally(() => {
347
+ summonerPending = false;
348
+ });
349
+ }
350
+
351
+ function rewriteWebSocketFrame(text) {
352
+ if (!filterActive || typeof text !== "string") return null;
353
+ const targetFrame = text.indexOf("lol-champions/v1/inventories") !== -1;
354
+ const augmentFrame = text.indexOf("SKIN_AUGMENT") !== -1;
355
+ if (!targetFrame && !augmentFrame) return null;
356
+ if (!isPickerOpen()) return null;
357
+ let frame;
358
+ try {
359
+ frame = JSON.parse(text);
360
+ } catch {
361
+ return null;
362
+ }
363
+ if (!Array.isArray(frame) || frame.length < 3) return null;
364
+ const payload = frame[2];
365
+ if (!payload || typeof payload !== "object" || typeof payload.uri !== "string") return null;
366
+ let changed;
367
+ if (isTargetUrl(payload.uri)) {
368
+ changed = unlockDeep(payload.data);
369
+ } else if (isAugmentUrl(payload.uri)) {
370
+ changed = unlockAugmentInventory(payload.data);
371
+ } else {
372
+ return null;
373
+ }
374
+ if (!changed) return null;
375
+ return JSON.stringify(frame);
376
+ }
377
+
378
+ function wrapMessageListener(listener) {
379
+ if (typeof listener !== "function" || listener.__perfProfileWrap) return listener;
380
+ const wrapped = function (event) {
381
+ if (event && typeof event.data === "string") {
382
+ const rewritten = rewriteWebSocketFrame(event.data);
383
+ if (rewritten !== null) {
384
+ const fresh = new MessageEvent("message", {
385
+ data: rewritten,
386
+ origin: event.origin,
387
+ lastEventId: event.lastEventId,
388
+ ports: event.ports ? [...event.ports] : [],
389
+ source: event.source ?? null,
390
+ });
391
+ return listener.call(this, fresh);
392
+ }
393
+ }
394
+ return listener.call(this, event);
395
+ };
396
+ wrapped.__perfProfileWrap = true;
397
+ wrapped.__perfProfileOriginal = listener;
398
+ return wrapped;
399
+ }
400
+
401
+ function installWebSocketFilter() {
402
+ const proto = window.WebSocket && window.WebSocket.prototype;
403
+ if (!proto || proto.addEventListener.__perfProfileFilter) return;
404
+
405
+ const origAdd = proto.addEventListener;
406
+ const origRemove = proto.removeEventListener;
407
+ const origDispatch = proto.dispatchEvent;
408
+
409
+ proto.addEventListener = function (type, listener, options) {
410
+ return origAdd.call(this, type, type === "message" ? wrapMessageListener(listener) : listener, options);
411
+ };
412
+ proto.removeEventListener = function (type, listener, options) {
413
+ return origRemove.call(this, type, (listener && listener.__perfProfileOriginal) || listener, options);
414
+ };
415
+ proto.dispatchEvent = function (event) {
416
+ if (event && event.type === "message" && typeof event.data === "string") {
417
+ const rewritten = rewriteWebSocketFrame(event.data);
418
+ if (rewritten !== null) {
419
+ return origDispatch.call(
420
+ this,
421
+ new MessageEvent("message", {
422
+ data: rewritten,
423
+ origin: event.origin,
424
+ lastEventId: event.lastEventId,
425
+ ports: event.ports ? [...event.ports] : [],
426
+ source: event.source ?? null,
427
+ }),
428
+ );
429
+ }
430
+ }
431
+ return origDispatch.call(this, event);
432
+ };
433
+
434
+ const descriptor = Object.getOwnPropertyDescriptor(proto, "onmessage");
435
+ if (descriptor && descriptor.set && descriptor.get) {
436
+ Object.defineProperty(proto, "onmessage", {
437
+ configurable: true,
438
+ enumerable: descriptor.enumerable,
439
+ get: descriptor.get,
440
+ set(value) {
441
+ descriptor.set.call(this, typeof value === "function" ? wrapMessageListener(value) : value);
442
+ },
443
+ });
444
+ }
445
+
446
+ proto.addEventListener.__perfProfileFilter = true;
447
+ }
448
+
449
+ function installFetchFilter() {
450
+ const original = window.fetch;
451
+ if (typeof original !== "function" || original.__perfProfileFilter) return;
452
+ rawFetch = (...args) => original.call(window, ...args);
453
+
454
+ const wrapped = function (input, init) {
455
+ const url = typeof input === "string" ? input : (input && input.url) || "";
456
+ const target = isTargetUrl(url);
457
+ const augment = !target && isAugmentUrl(url);
458
+ if (filterActive && pathIdOf(url) !== null) maybeRefreshSummoner();
459
+ if (filterActive && init && typeof init.body === "string") {
460
+ const mapped = remapVariantIds(init.body);
461
+ if (mapped !== init.body) init = { ...init, body: mapped };
462
+ }
463
+ const promise = original.call(this, input, init);
464
+ if (filterActive && target && augmentIds.size === 0) {
465
+ const latch = (async () => {
466
+ try {
467
+ const response = await promise;
468
+ if (response && response.ok && augmentIds.size === 0) {
469
+ const json = await response.clone().json().catch(() => null);
470
+ if (json) harvestFromPayload(json);
471
+ }
472
+ } catch {}
473
+ pendingTargetLoads.delete(latch);
474
+ })();
475
+ pendingTargetLoads.add(latch);
476
+ }
477
+ if (!filterActive || (!target && !augment) || !isPickerOpen()) return promise;
478
+ const ready = augment && augmentIds.size === 0 ? ensureAugmentIds() : Promise.resolve();
479
+ return ready
480
+ .then(() => promise)
481
+ .then((response) => {
482
+ if (!response || !response.ok) return response;
483
+ return response
484
+ .clone()
485
+ .json()
486
+ .catch(() => null)
487
+ .then((json) => {
488
+ if (!json) return response;
489
+ const changed = augment ? unlockAugmentInventory(json) : unlockDeep(json);
490
+ if (!changed) return response;
491
+ return new Response(JSON.stringify(json), {
492
+ status: response.status,
493
+ statusText: response.statusText,
494
+ headers: response.headers,
495
+ });
496
+ });
497
+ });
498
+ };
499
+ wrapped.__perfProfileFilter = true;
500
+ window.fetch = wrapped;
501
+ }
502
+
503
+ function installXhrFilter() {
504
+ const proto = window.XMLHttpRequest && window.XMLHttpRequest.prototype;
505
+ if (!proto || proto.open.__perfProfileFilter) return;
506
+
507
+ const origOpen = proto.open;
508
+ const origSend = proto.send;
509
+ const textDescriptor = Object.getOwnPropertyDescriptor(proto, "responseText");
510
+ const responseDescriptor = Object.getOwnPropertyDescriptor(proto, "response");
511
+
512
+ proto.open = function (method, url, ...rest) {
513
+ this.__perfProfileUrl = typeof url === "string" ? url : "";
514
+ return origOpen.call(this, method, url, ...rest);
515
+ };
516
+ proto.open.__perfProfileFilter = true;
517
+
518
+ proto.send = function (...args) {
519
+ const url = this.__perfProfileUrl || "";
520
+ const target = isTargetUrl(url);
521
+ const augment = !target && isAugmentUrl(url);
522
+ if (filterActive && pathIdOf(url) !== null) maybeRefreshSummoner();
523
+ if (filterActive && typeof args[0] === "string") {
524
+ const mapped = remapVariantIds(args[0]);
525
+ if (mapped !== args[0]) args = [mapped, ...args.slice(1)];
526
+ }
527
+ const xhr = this;
528
+ if (target && filterActive && augmentIds.size === 0 && textDescriptor && textDescriptor.get) {
529
+ const latch = new Promise((resolve) => {
530
+ const finish = () => {
531
+ if (augmentIds.size === 0) {
532
+ try {
533
+ harvestFromPayload(JSON.parse(textDescriptor.get.call(xhr)));
534
+ } catch {}
535
+ }
536
+ pendingTargetLoads.delete(latch);
537
+ resolve();
538
+ };
539
+ xhr.addEventListener("load", finish, { once: true });
540
+ xhr.addEventListener("error", finish, { once: true });
541
+ xhr.addEventListener("abort", finish, { once: true });
542
+ });
543
+ pendingTargetLoads.add(latch);
544
+ }
545
+ if (textDescriptor && textDescriptor.get && (target || augment)) {
546
+ const cache = { raw: null, gate: null, filtered: null };
547
+ const gateOpen = () => filterActive && isPickerOpen();
548
+ const rewriteBody = (json) => (augment ? unlockAugmentInventory(json) : unlockDeep(json));
549
+ const filteredText = () => {
550
+ const raw = textDescriptor.get.call(xhr);
551
+ const gate = gateOpen();
552
+ if (cache.filtered === null || cache.raw !== raw || cache.gate !== gate) {
553
+ cache.raw = raw;
554
+ cache.gate = gate;
555
+ cache.filtered = raw;
556
+ if (gate) {
557
+ try {
558
+ const json = JSON.parse(raw);
559
+ const rewritten = rewriteBody(json) ? JSON.stringify(json) : null;
560
+ if (rewritten !== null) cache.filtered = rewritten;
561
+ } catch {}
562
+ }
563
+ }
564
+ return cache.filtered;
565
+ };
566
+ Object.defineProperty(xhr, "responseText", { configurable: true, get: filteredText });
567
+ if (responseDescriptor && responseDescriptor.get) {
568
+ Object.defineProperty(xhr, "response", {
569
+ configurable: true,
570
+ get() {
571
+ const type = xhr.responseType;
572
+ if (type === "" || type === "text") return filteredText();
573
+ const value = responseDescriptor.get.call(xhr);
574
+ if (type === "json" && gateOpen() && value && typeof value === "object") rewriteBody(value);
575
+ return value;
576
+ },
577
+ });
578
+ }
579
+ }
580
+ if (augment && filterActive && isPickerOpen() && augmentIds.size === 0) {
581
+ const sendArgs = args;
582
+ ensureAugmentIds().finally(() => {
583
+ try {
584
+ origSend.apply(xhr, sendArgs);
585
+ } catch {}
586
+ });
587
+ return;
588
+ }
589
+ return origSend.apply(this, args);
590
+ };
591
+ }
592
+
593
+ function installSourceFilter() {
594
+ try {
595
+ filterActive = readDefaultEnabled();
596
+ installWebSocketFilter();
597
+ installFetchFilter();
598
+ installXhrFilter();
599
+ } catch (error) {
600
+ logError("could not install the source filter.", error);
601
+ }
602
+ }
603
+
604
+ function ensureSourceFilter() {
605
+ try {
606
+ installWebSocketFilter();
607
+ installFetchFilter();
608
+ installXhrFilter();
609
+ } catch (error) {
610
+ logError("could not refresh the source filter.", error);
611
+ }
612
+ }
613
+
614
+ function onEnable(utils) {
615
+ api = utils;
616
+ filterActive = true;
617
+ ensureSourceFilter();
618
+ ensureSummonerId();
619
+ startTierGrouping();
620
+ }
621
+
622
+ function onDisable() {
623
+ filterActive = false;
624
+ stopTierGrouping();
625
+ api = null;
626
+ }
627
+
628
+ export function init() {
629
+ installSourceFilter();
630
+
631
+ const registerWith = (utils) => {
632
+ utils.registerPlugin({
633
+ id: PLUGIN_ID,
634
+ name: "Unlock Profile Skins",
635
+ description: "Unlocks every skin in the Set profile background window.",
636
+ version,
637
+ defaultEnabled: true,
638
+ onEnable,
639
+ onDisable,
640
+ });
641
+ };
642
+
643
+ if (window.PerfectionistUtils) {
644
+ registerWith(window.PerfectionistUtils);
645
+ } else {
646
+ addEventListener("perfectionist:utils-ready", (event) => registerWith(event.detail), { once: true });
647
+ }
648
+ }