perfectionist-plugins 1.0.3 → 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 +13 -0
- package/ban-blocker.js +107 -30
- package/hide-unowned-skins.js +51 -9
- package/package.json +5 -3
- package/perfectionist-utils.js +14 -3
- package/unlock-profile-skins.js +648 -0
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> </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
|
package/ban-blocker.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const version = "3.4.
|
|
1
|
+
export const version = "3.4.2";
|
|
2
2
|
|
|
3
3
|
const PLUGIN_ID = "ban-blocker";
|
|
4
4
|
const STYLE_ID = "perfectionist-ban-blocker-style";
|
|
@@ -13,6 +13,7 @@ const FAIL_COOLDOWN_MAX_MS = 30000;
|
|
|
13
13
|
const POLL_MS = 40;
|
|
14
14
|
const ENSURE_MS = 300;
|
|
15
15
|
const DETECT_RETRY_MS = 2000;
|
|
16
|
+
const GONE_RESET_MS = 1500;
|
|
16
17
|
|
|
17
18
|
const BAN_GLYPH_SVG =
|
|
18
19
|
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'>" +
|
|
@@ -58,11 +59,14 @@ let ensureTimer = null;
|
|
|
58
59
|
let detectTimer = null;
|
|
59
60
|
let refreshTimer = null;
|
|
60
61
|
let pumpTimer = null;
|
|
62
|
+
let goneTimer = null;
|
|
61
63
|
let observer = null;
|
|
62
64
|
let pollInFlight = false;
|
|
63
65
|
let uiScheduled = false;
|
|
64
66
|
|
|
65
67
|
const enabled = new Map();
|
|
68
|
+
const alias = new Map();
|
|
69
|
+
const fetchedNames = new Set();
|
|
66
70
|
const playerNames = new Map();
|
|
67
71
|
const cellToPlayer = new Map();
|
|
68
72
|
const buttons = new Map();
|
|
@@ -96,13 +100,62 @@ function isPicked(members, champ) {
|
|
|
96
100
|
return (members ?? []).some((p) => p.championId === champ && p.completed);
|
|
97
101
|
}
|
|
98
102
|
|
|
103
|
+
function canonOf(id) {
|
|
104
|
+
return (id != null && alias.get(id)) || id;
|
|
105
|
+
}
|
|
106
|
+
|
|
99
107
|
function isBlocked(id) {
|
|
100
|
-
return enabled.get(id) === true;
|
|
108
|
+
return enabled.get(canonOf(id)) === true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function isWeakKey(k) {
|
|
112
|
+
return typeof k === "string" && k.startsWith("cell:");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function payloadName(p) {
|
|
116
|
+
return norm(`${p?.gameName ?? ""}${p?.tagLine ?? ""}`) || norm(p?.summonerName) || "";
|
|
101
117
|
}
|
|
102
118
|
|
|
103
|
-
function
|
|
119
|
+
function keyOf(p) {
|
|
104
120
|
if (typeof p === "number") return p;
|
|
105
|
-
|
|
121
|
+
const strong = p?.summonerId || p?.puuid;
|
|
122
|
+
if (strong) return strong;
|
|
123
|
+
const n = payloadName(p);
|
|
124
|
+
if (n) return `name:${n}`;
|
|
125
|
+
return p?.cellId != null ? `cell:${p.cellId}` : null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function linkPlayer(p, rowName, cellId) {
|
|
129
|
+
const strong = typeof p === "number" ? p : p?.summonerId || p?.puuid;
|
|
130
|
+
const pn = typeof p === "number" ? "" : payloadName(p);
|
|
131
|
+
const rn = norm(rowName);
|
|
132
|
+
const keys = [];
|
|
133
|
+
if (strong) keys.push(strong);
|
|
134
|
+
if (pn) keys.push(`name:${pn}`);
|
|
135
|
+
if (rn && rn !== pn) keys.push(`name:${rn}`);
|
|
136
|
+
const weak = cellId != null ? `cell:${cellId}` : null;
|
|
137
|
+
|
|
138
|
+
let root = null;
|
|
139
|
+
for (const k of keys) {
|
|
140
|
+
const r = alias.get(k);
|
|
141
|
+
if (r != null) {
|
|
142
|
+
root = r;
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (root == null && keys.length) {
|
|
147
|
+
root = keys[0];
|
|
148
|
+
const prev = weak ? alias.get(weak) : null;
|
|
149
|
+
if (prev != null && isWeakKey(prev) && enabled.has(prev)) {
|
|
150
|
+
if (!enabled.has(root)) enabled.set(root, enabled.get(prev));
|
|
151
|
+
enabled.delete(prev);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (root == null) root = (weak && alias.get(weak)) || weak;
|
|
155
|
+
if (root == null) return null;
|
|
156
|
+
for (const k of keys) alias.set(k, root);
|
|
157
|
+
if (weak) alias.set(weak, root);
|
|
158
|
+
return root;
|
|
106
159
|
}
|
|
107
160
|
|
|
108
161
|
function banPending(s) {
|
|
@@ -149,7 +202,10 @@ function decide(s) {
|
|
|
149
202
|
const me = s?.localPlayerCellId;
|
|
150
203
|
|
|
151
204
|
cellToPlayer.clear();
|
|
152
|
-
for (const p of m)
|
|
205
|
+
for (const p of m) {
|
|
206
|
+
const row = rows.find((r) => r.slotId === p.cellId);
|
|
207
|
+
cellToPlayer.set(p.cellId, linkPlayer(p, row?.name, p.cellId));
|
|
208
|
+
}
|
|
153
209
|
|
|
154
210
|
let myPick = null;
|
|
155
211
|
for (const a of actions) {
|
|
@@ -160,7 +216,7 @@ function decide(s) {
|
|
|
160
216
|
}
|
|
161
217
|
myActionId = myPick ? myPick.id : null;
|
|
162
218
|
|
|
163
|
-
const blockedCells = new Set(m.filter((p) => isBlocked(
|
|
219
|
+
const blockedCells = new Set(m.filter((p) => isBlocked(cellToPlayer.get(p.cellId))).map((p) => p.cellId));
|
|
164
220
|
|
|
165
221
|
let threat = 0;
|
|
166
222
|
for (const a of actions) {
|
|
@@ -193,6 +249,15 @@ function onSessionGone() {
|
|
|
193
249
|
goneAway = true;
|
|
194
250
|
uiVisible = false;
|
|
195
251
|
removeButtons();
|
|
252
|
+
if (!goneTimer) {
|
|
253
|
+
goneTimer = setTimeout(() => {
|
|
254
|
+
goneTimer = null;
|
|
255
|
+
enabled.clear();
|
|
256
|
+
alias.clear();
|
|
257
|
+
playerNames.clear();
|
|
258
|
+
fetchedNames.clear();
|
|
259
|
+
}, GONE_RESET_MS);
|
|
260
|
+
}
|
|
196
261
|
}
|
|
197
262
|
|
|
198
263
|
function onSession(raw) {
|
|
@@ -208,6 +273,8 @@ function onSession(raw) {
|
|
|
208
273
|
return;
|
|
209
274
|
}
|
|
210
275
|
if (!s?.myTeam) return;
|
|
276
|
+
clearTimeout(goneTimer);
|
|
277
|
+
goneTimer = null;
|
|
211
278
|
goneAway = false;
|
|
212
279
|
lastSession = s;
|
|
213
280
|
|
|
@@ -256,29 +323,27 @@ function getRows() {
|
|
|
256
323
|
}
|
|
257
324
|
|
|
258
325
|
function ensureName(cell) {
|
|
259
|
-
const
|
|
260
|
-
if (
|
|
261
|
-
playerNames.set(id, []);
|
|
326
|
+
const strong = cell?.summonerId || cell?.puuid;
|
|
327
|
+
if (!strong || fetchedNames.has(strong)) return;
|
|
262
328
|
const get = api?.lcu?.get;
|
|
263
|
-
if (!get)
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
}
|
|
267
|
-
const byPuuid = typeof id === "string" && id.length > 20;
|
|
268
|
-
const path = byPuuid ? `/lol-summoner/v2/summoners/puuid/${id}` : `/lol-summoner/v1/summoners/${id}`;
|
|
329
|
+
if (!get) return;
|
|
330
|
+
fetchedNames.add(strong);
|
|
331
|
+
const byPuuid = typeof strong === "string" && strong.length > 20;
|
|
332
|
+
const path = byPuuid ? `/lol-summoner/v2/summoners/puuid/${strong}` : `/lol-summoner/v1/summoners/${strong}`;
|
|
269
333
|
get(path)
|
|
270
334
|
.then((data) => {
|
|
271
335
|
const names = [norm(data?.displayName), norm(data?.gameName)].filter(Boolean);
|
|
272
|
-
if (names.length)
|
|
273
|
-
|
|
336
|
+
if (!names.length) return;
|
|
337
|
+
const root = canonOf(strong);
|
|
338
|
+
playerNames.set(root, [...new Set([...(playerNames.get(root) ?? []), ...names])]);
|
|
274
339
|
})
|
|
275
|
-
.catch(() =>
|
|
340
|
+
.catch(() => fetchedNames.delete(strong));
|
|
276
341
|
}
|
|
277
342
|
|
|
278
343
|
function nameMatches(rowName, id) {
|
|
279
344
|
const rn = norm(rowName);
|
|
280
345
|
if (!rn) return false;
|
|
281
|
-
return (playerNames.get(id) ?? []).some(
|
|
346
|
+
return (playerNames.get(canonOf(id)) ?? []).some(
|
|
282
347
|
(n) => n === rn || (n.length >= 3 && rn.length >= 3 && (n.startsWith(rn) || rn.startsWith(n))),
|
|
283
348
|
);
|
|
284
349
|
}
|
|
@@ -292,8 +357,13 @@ function bindRowsToTeam(session) {
|
|
|
292
357
|
for (const r of list) {
|
|
293
358
|
const cell =
|
|
294
359
|
(r.slotId != null ? cells.find((c) => c.cellId === r.slotId) : null) ??
|
|
295
|
-
cells.find((c) => nameMatches(r.name,
|
|
296
|
-
|
|
360
|
+
cells.find((c) => nameMatches(r.name, keyOf(c)));
|
|
361
|
+
if (cell) {
|
|
362
|
+
r.player = linkPlayer(cell, r.name, cell.cellId);
|
|
363
|
+
cellToPlayer.set(cell.cellId, r.player);
|
|
364
|
+
} else {
|
|
365
|
+
r.player = null;
|
|
366
|
+
}
|
|
297
367
|
}
|
|
298
368
|
}
|
|
299
369
|
|
|
@@ -303,14 +373,18 @@ function stopEvent(e) {
|
|
|
303
373
|
e.preventDefault();
|
|
304
374
|
}
|
|
305
375
|
|
|
376
|
+
function playerForBadge(btn) {
|
|
377
|
+
const rowEl = btn.closest(".summoner-object");
|
|
378
|
+
const r = rowEl ? rows.find((x) => x.el === rowEl) : rows[Number(btn.dataset.slot)];
|
|
379
|
+
const byCell = btn.dataset.cell ? cellToPlayer.get(Number(btn.dataset.cell)) ?? null : null;
|
|
380
|
+
return canonOf(byCell ?? r?.player ?? null);
|
|
381
|
+
}
|
|
382
|
+
|
|
306
383
|
function toggle(btn) {
|
|
307
|
-
let id =
|
|
308
|
-
if (id == null) {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
const rowEl = btn.closest(".summoner-object");
|
|
312
|
-
const r = list.find((x) => x.el === rowEl) ?? list[Number(btn.dataset.slot)];
|
|
313
|
-
id = r?.player ?? null;
|
|
384
|
+
let id = playerForBadge(btn);
|
|
385
|
+
if (id == null && lastSession) {
|
|
386
|
+
bindRowsToTeam(lastSession);
|
|
387
|
+
id = playerForBadge(btn);
|
|
314
388
|
}
|
|
315
389
|
if (id == null) return;
|
|
316
390
|
const myCell = lastSession?.localPlayerCellId;
|
|
@@ -386,7 +460,7 @@ function ensureButtons() {
|
|
|
386
460
|
function renderButtons() {
|
|
387
461
|
const hasSession = !!lastSession?.myTeam;
|
|
388
462
|
for (const [, btn] of buttons) {
|
|
389
|
-
const id =
|
|
463
|
+
const id = playerForBadge(btn);
|
|
390
464
|
const on = id != null && isBlocked(id);
|
|
391
465
|
btn.classList.toggle("on", on);
|
|
392
466
|
btn.classList.toggle("unresolved", id == null && hasSession);
|
|
@@ -505,7 +579,8 @@ function onDisable() {
|
|
|
505
579
|
clearTimeout(refreshTimer);
|
|
506
580
|
clearTimeout(detectTimer);
|
|
507
581
|
clearTimeout(pumpTimer);
|
|
508
|
-
|
|
582
|
+
clearTimeout(goneTimer);
|
|
583
|
+
pollTimer = ensureTimer = refreshTimer = detectTimer = pumpTimer = goneTimer = null;
|
|
509
584
|
observer?.disconnect();
|
|
510
585
|
observer = null;
|
|
511
586
|
|
|
@@ -518,6 +593,8 @@ function onDisable() {
|
|
|
518
593
|
cellToPlayer.clear();
|
|
519
594
|
enabled.clear();
|
|
520
595
|
playerNames.clear();
|
|
596
|
+
alias.clear();
|
|
597
|
+
fetchedNames.clear();
|
|
521
598
|
lastSession = null;
|
|
522
599
|
rows = [];
|
|
523
600
|
removeButtons();
|
package/hide-unowned-skins.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const version = "2.
|
|
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
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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)
|
|
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
|
-
|
|
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)
|
|
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
|
|
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",
|
package/perfectionist-utils.js
CHANGED
|
@@ -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
|
|
9
|
+
export const version = "1.1.0";
|
|
10
10
|
|
|
11
11
|
const PKG = "perfectionist-plugins";
|
|
12
12
|
const CDN = "https://cdn.jsdelivr.net/npm";
|
|
@@ -26,7 +26,7 @@ const BUNDLED_MODULES = [
|
|
|
26
26
|
id: "ban-blocker",
|
|
27
27
|
file: "ban-blocker.js",
|
|
28
28
|
name: "Ban Blocker",
|
|
29
|
-
version: "3.4.
|
|
29
|
+
version: "3.4.2",
|
|
30
30
|
description: "During bans, blocking a teammate instantly claims the champion they hover to ban.",
|
|
31
31
|
},
|
|
32
32
|
{
|
|
@@ -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 = `
|
|
@@ -123,11 +130,15 @@ function saveState() {
|
|
|
123
130
|
enabled: state.enabled,
|
|
124
131
|
};
|
|
125
132
|
try {
|
|
126
|
-
if (typeof DataStore !== "undefined") DataStore.set(STATE_KEY, record);
|
|
127
133
|
localStorage.setItem(STATE_KEY, JSON.stringify(record));
|
|
128
134
|
} catch (error) {
|
|
129
135
|
console.error("[Perfectionism] Could not save plugin state.", error);
|
|
130
136
|
}
|
|
137
|
+
try {
|
|
138
|
+
if (typeof DataStore !== "undefined") DataStore.set(STATE_KEY, record);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
console.error("[Perfectionism] Could not save plugin state.", error);
|
|
141
|
+
}
|
|
131
142
|
}
|
|
132
143
|
|
|
133
144
|
function logError(scope, error) {
|
|
@@ -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
|
+
}
|