perfectionist-plugins 2.0.0 → 2.4.1
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 +123 -121
- package/better-client.js +370 -86
- package/hide-unowned-skins.js +194 -225
- package/package.json +36 -37
- package/perfectionist-utils.js +11 -131
- package/unlock-profile-skins.js +153 -67
- package/tools/betterclient-monitor.js +0 -269
package/hide-unowned-skins.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
export const version = "2.4.
|
|
1
|
+
export const version = "2.4.3";
|
|
2
2
|
|
|
3
3
|
const PLUGIN_ID = "hide-unowned-skins";
|
|
4
4
|
const STYLE_ID = "perfectionist-hide-unowned-skins-style";
|
|
5
5
|
const HIDDEN_CLASS = "perf-unowned-skin-hidden";
|
|
6
6
|
const SKIN_ITEM_SELECTOR = ".skin-selection-item";
|
|
7
7
|
const DOT_SELECTOR = ".skin-selection-indicator-selector";
|
|
8
|
-
const SELECTED_CLASS = "skin-selection-item-selected";
|
|
9
8
|
const SESSION_URI = "/lol-champ-select/v1/session";
|
|
10
9
|
const SELECTION_URI = "/lol-champ-select/v1/session/my-selection";
|
|
11
10
|
const CAROUSEL_URI = "/lol-champ-select/v1/skin-carousel-skins";
|
|
@@ -13,24 +12,83 @@ const CURRENT_SUMMONER_URI = "/lol-summoner/v1/current-summoner";
|
|
|
13
12
|
const SUMMONER_TTL = 10 * 60 * 1000;
|
|
14
13
|
const INVENTORY_TTL = 60 * 1000;
|
|
15
14
|
const REFRESH_DEBOUNCE_MS = 350;
|
|
16
|
-
const REPACK_SETTLE_MS = 120;
|
|
17
15
|
const RETRY_MS = 5000;
|
|
18
16
|
const STATE_KEY = "perfectionist-plugins:state";
|
|
19
17
|
|
|
20
18
|
const CSS = `
|
|
21
19
|
.${HIDDEN_CLASS} { display: none !important; margin: 0 !important; padding: 0 !important; border: none !important; }
|
|
22
|
-
|
|
20
|
+
.perf-native-hidden { display: none !important; }
|
|
23
21
|
`;
|
|
24
22
|
|
|
25
23
|
const OBSERVER_OPTIONS = {
|
|
26
24
|
childList: true,
|
|
27
25
|
subtree: true,
|
|
28
26
|
attributes: true,
|
|
29
|
-
attributeFilter: ["
|
|
27
|
+
attributeFilter: ["src"],
|
|
30
28
|
};
|
|
31
29
|
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
const FILTER_VERSION = "2.4.3";
|
|
31
|
+
const FILTER_STATE_KEY = "__perfSkinFilterState";
|
|
32
|
+
const filterState = (globalThis[FILTER_STATE_KEY] ??= { active: true });
|
|
33
|
+
|
|
34
|
+
let skinShapeKeys = null;
|
|
35
|
+
|
|
36
|
+
function learnSkinShape(list) {
|
|
37
|
+
if (!Array.isArray(list) || list.length === 0) return;
|
|
38
|
+
const keys = new Set(skinShapeKeys || []);
|
|
39
|
+
for (const item of list.slice(0, 4)) {
|
|
40
|
+
if (item && typeof item === "object") for (const key of Object.keys(item)) keys.add(key);
|
|
41
|
+
}
|
|
42
|
+
if (keys.size >= 2) skinShapeKeys = keys;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isSkinLike(item) {
|
|
46
|
+
if (!item || typeof item !== "object" || typeof item.id !== "number") return false;
|
|
47
|
+
if (skinShapeKeys) {
|
|
48
|
+
let hits = 0;
|
|
49
|
+
for (const key of Object.keys(item)) if (skinShapeKeys.has(key)) hits++;
|
|
50
|
+
return hits >= 2;
|
|
51
|
+
}
|
|
52
|
+
return (
|
|
53
|
+
"ownership" in item ||
|
|
54
|
+
"tilePath" in item ||
|
|
55
|
+
"splashPath" in item ||
|
|
56
|
+
"unlocked" in item ||
|
|
57
|
+
Array.isArray(item.chromas)
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isSkinCollection(list) {
|
|
62
|
+
if (!Array.isArray(list) || list.length < 2) return false;
|
|
63
|
+
return isSkinLike(list[0]) && isSkinLike(list[list.length - 1]);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function installArrayFilter() {
|
|
67
|
+
const proto = Array.prototype;
|
|
68
|
+
if (!proto || proto.filter.__perfSkinFilter === FILTER_VERSION) return;
|
|
69
|
+
const nativeFilter = proto.filter;
|
|
70
|
+
const wrapped = function (callback, thisArg) {
|
|
71
|
+
const result = nativeFilter.call(this, callback, thisArg);
|
|
72
|
+
if (!filterState.active || !isSkinCollection(result)) return result;
|
|
73
|
+
let changed = false;
|
|
74
|
+
if (!skinShapeKeys) learnSkinShape(result);
|
|
75
|
+
const kept = nativeFilter.call(result, (skin) => {
|
|
76
|
+
const owned = isOwnedSkin(skin);
|
|
77
|
+
if (!owned) changed = true;
|
|
78
|
+
return owned;
|
|
79
|
+
});
|
|
80
|
+
if (!changed) return result;
|
|
81
|
+
filterStats.array = (filterStats.array || 0) + 1;
|
|
82
|
+
filterStats.source = "array";
|
|
83
|
+
filterStats.lastIn = result.length;
|
|
84
|
+
filterStats.lastOut = kept.length;
|
|
85
|
+
return kept;
|
|
86
|
+
};
|
|
87
|
+
wrapped.__perfSkinFilter = FILTER_VERSION;
|
|
88
|
+
proto.filter = wrapped;
|
|
89
|
+
}
|
|
90
|
+
const filterStats = (globalThis.__perfSkinFilterStats ??= { carousel: 0, source: "", lastIn: 0, lastOut: 0 });
|
|
91
|
+
let rawFetch = (typeof window !== "undefined" && window.fetch && window.fetch.__perfOriginalFetch) || null;
|
|
34
92
|
let clientSkinOrder = [];
|
|
35
93
|
|
|
36
94
|
function readDefaultEnabled() {
|
|
@@ -47,15 +105,31 @@ function isOwnedSkin(skin) {
|
|
|
47
105
|
const id = typeof skin.id === "number" ? skin.id : null;
|
|
48
106
|
if (id === null) return true;
|
|
49
107
|
if (id % 1000 === 0) return true;
|
|
108
|
+
if (ownedSkinIds && allSkinIds && allSkinIds.has(id)) return ownedSkinIds.has(id);
|
|
50
109
|
const ownership = skin.ownership;
|
|
51
110
|
if (ownership && typeof ownership.owned === "boolean") return ownership.owned;
|
|
111
|
+
if (typeof skin.owned === "boolean") return skin.owned;
|
|
112
|
+
if (typeof skin.unlocked === "boolean") return skin.unlocked;
|
|
52
113
|
return true;
|
|
53
114
|
}
|
|
54
115
|
|
|
55
116
|
function filterSkinList(list) {
|
|
56
117
|
if (!Array.isArray(list)) return null;
|
|
57
|
-
|
|
58
|
-
|
|
118
|
+
if (!skinShapeKeys && isSkinCollection(list)) learnSkinShape(list);
|
|
119
|
+
let changed = false;
|
|
120
|
+
const filtered = list
|
|
121
|
+
.map((skin) => {
|
|
122
|
+
if (!skin || !Array.isArray(skin.childSkins)) return skin;
|
|
123
|
+
const childSkins = skin.childSkins.filter(isOwnedSkin);
|
|
124
|
+
if (childSkins.length === skin.childSkins.length) return skin;
|
|
125
|
+
changed = true;
|
|
126
|
+
return Object.assign({}, skin, { childSkins });
|
|
127
|
+
})
|
|
128
|
+
.filter(isOwnedSkin);
|
|
129
|
+
if (filtered.length !== list.length) changed = true;
|
|
130
|
+
filterStats.lastIn = list.length;
|
|
131
|
+
filterStats.lastOut = filtered.length;
|
|
132
|
+
return changed ? filtered : null;
|
|
59
133
|
}
|
|
60
134
|
|
|
61
135
|
function recordClientOrder(list) {
|
|
@@ -66,7 +140,7 @@ function recordClientOrder(list) {
|
|
|
66
140
|
}
|
|
67
141
|
|
|
68
142
|
function rewriteWebSocketFrame(text) {
|
|
69
|
-
if (!
|
|
143
|
+
if (!filterState.active || typeof text !== "string") return null;
|
|
70
144
|
if (text.indexOf("skin-carousel-skins") === -1) return null;
|
|
71
145
|
let frame;
|
|
72
146
|
try {
|
|
@@ -79,6 +153,8 @@ function rewriteWebSocketFrame(text) {
|
|
|
79
153
|
if (!payload || typeof payload !== "object" || typeof payload.uri !== "string") return null;
|
|
80
154
|
if (payload.uri.split("?")[0] !== CAROUSEL_URI) return null;
|
|
81
155
|
const kept = filterSkinList(payload.data);
|
|
156
|
+
filterStats.carousel++;
|
|
157
|
+
filterStats.source = "ws";
|
|
82
158
|
recordClientOrder(kept ?? payload.data);
|
|
83
159
|
if (!kept) return null;
|
|
84
160
|
return JSON.stringify([frame[0], frame[1], Object.assign({}, payload, { data: kept })]);
|
|
@@ -109,7 +185,7 @@ function wrapMessageListener(listener) {
|
|
|
109
185
|
|
|
110
186
|
function installWebSocketFilter() {
|
|
111
187
|
const proto = window.WebSocket && window.WebSocket.prototype;
|
|
112
|
-
if (!proto || proto.addEventListener.__perfSkinFilter) return;
|
|
188
|
+
if (!proto || proto.addEventListener.__perfSkinFilter === FILTER_VERSION) return;
|
|
113
189
|
|
|
114
190
|
const origAdd = proto.addEventListener;
|
|
115
191
|
const origRemove = proto.removeEventListener;
|
|
@@ -152,13 +228,14 @@ function installWebSocketFilter() {
|
|
|
152
228
|
});
|
|
153
229
|
}
|
|
154
230
|
|
|
155
|
-
proto.addEventListener.__perfSkinFilter =
|
|
231
|
+
proto.addEventListener.__perfSkinFilter = FILTER_VERSION;
|
|
156
232
|
}
|
|
157
233
|
|
|
158
234
|
function installFetchFilter() {
|
|
159
235
|
const original = window.fetch;
|
|
160
|
-
if (typeof original !== "function" || original.__perfSkinFilter) return;
|
|
161
|
-
if (!rawFetch) rawFetch = original;
|
|
236
|
+
if (typeof original !== "function" || original.__perfSkinFilter === FILTER_VERSION) return;
|
|
237
|
+
if (!rawFetch) rawFetch = original.__perfOriginalFetch || original;
|
|
238
|
+
globalThis.__perfNativeFetch = rawFetch;
|
|
162
239
|
|
|
163
240
|
const wrapped = function (input, init) {
|
|
164
241
|
const url =
|
|
@@ -170,15 +247,22 @@ function installFetchFilter() {
|
|
|
170
247
|
? input.href
|
|
171
248
|
: "";
|
|
172
249
|
const promise = original.call(this, input, init);
|
|
173
|
-
if (!
|
|
250
|
+
if (!filterState.active || url.split("?")[0].indexOf(CAROUSEL_URI) === -1) return promise;
|
|
174
251
|
return promise.then((response) => {
|
|
175
252
|
if (!response || !response.ok) return response;
|
|
176
|
-
|
|
177
|
-
|
|
253
|
+
let body;
|
|
254
|
+
try {
|
|
255
|
+
body = response.clone();
|
|
256
|
+
} catch {
|
|
257
|
+
return response;
|
|
258
|
+
}
|
|
259
|
+
return body
|
|
178
260
|
.json()
|
|
179
261
|
.catch(() => null)
|
|
180
262
|
.then((json) => {
|
|
181
263
|
const kept = filterSkinList(json);
|
|
264
|
+
filterStats.carousel++;
|
|
265
|
+
filterStats.source = "fetch";
|
|
182
266
|
recordClientOrder(kept ?? json);
|
|
183
267
|
if (!kept) return response;
|
|
184
268
|
return new Response(JSON.stringify(kept), {
|
|
@@ -189,13 +273,14 @@ function installFetchFilter() {
|
|
|
189
273
|
});
|
|
190
274
|
});
|
|
191
275
|
};
|
|
192
|
-
wrapped.__perfSkinFilter =
|
|
276
|
+
wrapped.__perfSkinFilter = FILTER_VERSION;
|
|
277
|
+
wrapped.__perfOriginalFetch = rawFetch;
|
|
193
278
|
window.fetch = wrapped;
|
|
194
279
|
}
|
|
195
280
|
|
|
196
281
|
function installXhrFilter() {
|
|
197
282
|
const proto = window.XMLHttpRequest && window.XMLHttpRequest.prototype;
|
|
198
|
-
if (!proto || proto.open.__perfSkinFilter) return;
|
|
283
|
+
if (!proto || proto.open.__perfSkinFilter === FILTER_VERSION) return;
|
|
199
284
|
|
|
200
285
|
const origOpen = proto.open;
|
|
201
286
|
const origSend = proto.send;
|
|
@@ -206,7 +291,7 @@ function installXhrFilter() {
|
|
|
206
291
|
this.__perfSkinUrl = typeof url === "string" ? url : "";
|
|
207
292
|
return origOpen.call(this, method, url, ...rest);
|
|
208
293
|
};
|
|
209
|
-
proto.open.__perfSkinFilter =
|
|
294
|
+
proto.open.__perfSkinFilter = FILTER_VERSION;
|
|
210
295
|
|
|
211
296
|
proto.send = function (...args) {
|
|
212
297
|
const url = this.__perfSkinUrl || "";
|
|
@@ -215,13 +300,15 @@ function installXhrFilter() {
|
|
|
215
300
|
const cache = { raw: null, filtered: null };
|
|
216
301
|
const filteredText = () => {
|
|
217
302
|
const raw = textDescriptor.get.call(xhr);
|
|
218
|
-
if (!
|
|
303
|
+
if (!filterState.active) return raw;
|
|
219
304
|
if (cache.filtered === null || cache.raw !== raw) {
|
|
220
305
|
cache.raw = raw;
|
|
221
306
|
cache.filtered = raw;
|
|
222
307
|
try {
|
|
223
308
|
const json = JSON.parse(raw);
|
|
224
309
|
const kept = filterSkinList(json);
|
|
310
|
+
filterStats.carousel++;
|
|
311
|
+
filterStats.source = "xhr";
|
|
225
312
|
recordClientOrder(kept ?? json);
|
|
226
313
|
if (kept) cache.filtered = JSON.stringify(kept);
|
|
227
314
|
} catch {
|
|
@@ -245,7 +332,8 @@ function installXhrFilter() {
|
|
|
245
332
|
|
|
246
333
|
function installSourceFilter() {
|
|
247
334
|
try {
|
|
248
|
-
|
|
335
|
+
filterState.active = readDefaultEnabled();
|
|
336
|
+
installArrayFilter();
|
|
249
337
|
installWebSocketFilter();
|
|
250
338
|
installFetchFilter();
|
|
251
339
|
installXhrFilter();
|
|
@@ -256,6 +344,7 @@ function installSourceFilter() {
|
|
|
256
344
|
|
|
257
345
|
function ensureSourceFilter() {
|
|
258
346
|
try {
|
|
347
|
+
installArrayFilter();
|
|
259
348
|
installWebSocketFilter();
|
|
260
349
|
installFetchFilter();
|
|
261
350
|
installXhrFilter();
|
|
@@ -268,10 +357,11 @@ let api = null;
|
|
|
268
357
|
let summonerId = null;
|
|
269
358
|
let ownedSkinIds = null;
|
|
270
359
|
let allSkinIds = null;
|
|
360
|
+
let topLevelOrder = [];
|
|
361
|
+
let ownedTopLevelIds = null;
|
|
271
362
|
let currentChampionId = null;
|
|
272
363
|
let carouselOrder = [];
|
|
273
364
|
let selectedSkinId = null;
|
|
274
|
-
let lastServerSkinId = null;
|
|
275
365
|
let failure = null;
|
|
276
366
|
let requestToken = 0;
|
|
277
367
|
let refreshTimer = null;
|
|
@@ -281,15 +371,8 @@ let selectInFlight = false;
|
|
|
281
371
|
let sessionMisses = 0;
|
|
282
372
|
let championMisses = 0;
|
|
283
373
|
let renderObserver = null;
|
|
284
|
-
const repackOriginals = new Map();
|
|
285
|
-
const lastAppliedLeft = new Map();
|
|
286
374
|
const skinSuffixCache = new Map();
|
|
287
|
-
let repackTimer = null;
|
|
288
375
|
let removeStyle = null;
|
|
289
|
-
let lastSelectedDomIndex = -1;
|
|
290
|
-
let navFixAt = 0;
|
|
291
|
-
let lastSkipSignature = null;
|
|
292
|
-
const widthCache = new Map();
|
|
293
376
|
|
|
294
377
|
function logError(scope, error) {
|
|
295
378
|
console.error(`[Perfectionist] Hide Unowned Skins: ${scope}`, error);
|
|
@@ -301,18 +384,22 @@ function skinSuffixOf(text) {
|
|
|
301
384
|
return /Skins\/Base\//i.test(text) ? 0 : null;
|
|
302
385
|
}
|
|
303
386
|
|
|
387
|
+
const skinIdCache = new WeakMap();
|
|
388
|
+
|
|
304
389
|
function readSkinId(item) {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
390
|
+
const img = item.querySelector("img[src]");
|
|
391
|
+
const src = img ? img.getAttribute("src") || "" : "";
|
|
392
|
+
const art = item.querySelector('[style*="background-image"]');
|
|
393
|
+
const background = art ? art.style.backgroundImage || "" : "";
|
|
394
|
+
const key = src + "|" + background;
|
|
395
|
+
const cached = skinIdCache.get(item);
|
|
396
|
+
if (cached && cached.key === key) return cached.id;
|
|
397
|
+
let suffix = src ? skinSuffixOf(src) : null;
|
|
398
|
+
if (suffix === null && background) suffix = skinSuffixOf(background);
|
|
399
|
+
if (suffix === null) suffix = skinSuffixOf(item.getAttribute("style") || "");
|
|
400
|
+
const id = suffix === null || currentChampionId === null ? null : currentChampionId * 1000 + suffix;
|
|
401
|
+
skinIdCache.set(item, { key, id });
|
|
402
|
+
return id;
|
|
316
403
|
}
|
|
317
404
|
|
|
318
405
|
function hideTargetFor(item) {
|
|
@@ -325,123 +412,8 @@ function hideTargetFor(item) {
|
|
|
325
412
|
return target;
|
|
326
413
|
}
|
|
327
414
|
|
|
328
|
-
function
|
|
329
|
-
|
|
330
|
-
if (item.isConnected) continue;
|
|
331
|
-
repackOriginals.delete(item);
|
|
332
|
-
lastAppliedLeft.delete(item);
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
function guardSelection(items) {
|
|
337
|
-
const selectedIndex = items.findIndex((item) => item.classList.contains(SELECTED_CLASS));
|
|
338
|
-
if (selectedIndex < 0) return false;
|
|
339
|
-
const previous = lastSelectedDomIndex;
|
|
340
|
-
lastSelectedDomIndex = selectedIndex;
|
|
341
|
-
const selected = items[selectedIndex];
|
|
342
|
-
const hidden = selected.classList.contains(HIDDEN_CLASS) || Boolean(selected.parentElement?.classList.contains(HIDDEN_CLASS));
|
|
343
|
-
if (!hidden) {
|
|
344
|
-
lastSkipSignature = null;
|
|
345
|
-
return false;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
const visible = items.map((item, index) => (item.classList.contains(HIDDEN_CLASS) ? -1 : index)).filter((index) => index >= 0);
|
|
349
|
-
if (visible.length === 0) return false;
|
|
350
|
-
|
|
351
|
-
const forward = previous < 0 ? true : selectedIndex >= previous;
|
|
352
|
-
const target = forward
|
|
353
|
-
? (visible.find((index) => index > selectedIndex) ?? visible[0])
|
|
354
|
-
: ([...visible].reverse().find((index) => index < selectedIndex) ?? visible[visible.length - 1]);
|
|
355
|
-
const skinId = readSkinId(items[target]);
|
|
356
|
-
if (skinId === null) return false;
|
|
357
|
-
|
|
358
|
-
const signature = `${selectedIndex}:${skinId}`;
|
|
359
|
-
if (lastSkipSignature === signature) return true;
|
|
360
|
-
const now = Date.now();
|
|
361
|
-
if (now < navFixAt) return true;
|
|
362
|
-
navFixAt = now + 150;
|
|
363
|
-
lastSkipSignature = signature;
|
|
364
|
-
lastSelectedDomIndex = target;
|
|
365
|
-
selectSkin(skinId).catch((error) => logError("could not skip the unowned skin.", error));
|
|
366
|
-
return true;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
function repackVisibleItems(items) {
|
|
370
|
-
pruneRepack();
|
|
371
|
-
const visible = items.filter((item) => !item.classList.contains(HIDDEN_CLASS));
|
|
372
|
-
if (visible.length < 2) {
|
|
373
|
-
restoreRepack();
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
const selectedIndex = items.findIndex((item) => item.classList.contains("skin-selection-item-selected"));
|
|
378
|
-
let anchor = selectedIndex >= 0 ? items[selectedIndex] : null;
|
|
379
|
-
if (!anchor && selectedSkinId !== null) {
|
|
380
|
-
anchor = visible.find((item) => readSkinId(item) === selectedSkinId) ?? null;
|
|
381
|
-
}
|
|
382
|
-
if (!anchor) anchor = visible[Math.floor(visible.length / 2)];
|
|
383
|
-
const anchorDomIndex = items.indexOf(anchor);
|
|
384
|
-
|
|
385
|
-
const widthOf = (item) => {
|
|
386
|
-
const measured = item.getBoundingClientRect().width;
|
|
387
|
-
if (measured > 0) {
|
|
388
|
-
widthCache.set(item, measured);
|
|
389
|
-
return measured;
|
|
390
|
-
}
|
|
391
|
-
return widthCache.get(item) ?? (item === anchor ? 117 : 73);
|
|
392
|
-
};
|
|
393
|
-
const toPx = (item) => {
|
|
394
|
-
const raw = item.style.left || "0";
|
|
395
|
-
if (raw.endsWith("px")) return parseFloat(raw);
|
|
396
|
-
const box = item.parentElement?.getBoundingClientRect().width || 468;
|
|
397
|
-
return (parseFloat(raw) / 100) * box;
|
|
398
|
-
};
|
|
399
|
-
|
|
400
|
-
const anchorLeft = toPx(anchor);
|
|
401
|
-
let rightX = anchorLeft + widthOf(anchor);
|
|
402
|
-
let leftX = anchorLeft;
|
|
403
|
-
|
|
404
|
-
const setLeft = (item, x) => {
|
|
405
|
-
const want = `${x}px`;
|
|
406
|
-
const current = item.style.getPropertyValue("left");
|
|
407
|
-
const priority = item.style.getPropertyPriority("left");
|
|
408
|
-
const last = lastAppliedLeft.get(item);
|
|
409
|
-
const clientMoved = last !== undefined && (current !== last || priority !== "important");
|
|
410
|
-
if (clientMoved || !repackOriginals.has(item)) {
|
|
411
|
-
repackOriginals.set(item, { value: current, priority });
|
|
412
|
-
}
|
|
413
|
-
if (current !== want || priority !== "important") {
|
|
414
|
-
item.style.setProperty("left", want, "important");
|
|
415
|
-
}
|
|
416
|
-
lastAppliedLeft.set(item, want);
|
|
417
|
-
};
|
|
418
|
-
|
|
419
|
-
items.forEach((item, index) => {
|
|
420
|
-
if (item === anchor || item.classList.contains(HIDDEN_CLASS)) return;
|
|
421
|
-
|
|
422
|
-
const isRight = index > anchorDomIndex;
|
|
423
|
-
const w = widthOf(item);
|
|
424
|
-
if (isRight) {
|
|
425
|
-
setLeft(item, rightX);
|
|
426
|
-
rightX += w;
|
|
427
|
-
} else {
|
|
428
|
-
leftX -= w;
|
|
429
|
-
setLeft(item, leftX);
|
|
430
|
-
}
|
|
431
|
-
});
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
function restoreRepack() {
|
|
435
|
-
for (const [item, original] of repackOriginals) {
|
|
436
|
-
if (!item.isConnected) continue;
|
|
437
|
-
const current = item.style.getPropertyValue("left");
|
|
438
|
-
const last = lastAppliedLeft.get(item);
|
|
439
|
-
if (last !== undefined && (current !== last || item.style.getPropertyPriority("left") !== "important")) continue;
|
|
440
|
-
if (original.value) item.style.setProperty("left", original.value, original.priority || undefined);
|
|
441
|
-
else item.style.removeProperty("left");
|
|
442
|
-
}
|
|
443
|
-
repackOriginals.clear();
|
|
444
|
-
lastAppliedLeft.clear();
|
|
415
|
+
function setHidden(element, hide) {
|
|
416
|
+
if (element.classList.contains(HIDDEN_CLASS) !== hide) element.classList.toggle(HIDDEN_CLASS, hide);
|
|
445
417
|
}
|
|
446
418
|
|
|
447
419
|
function applyAll() {
|
|
@@ -451,56 +423,44 @@ function applyAll() {
|
|
|
451
423
|
const skinId = currentChampionId === null ? null : readSkinId(item);
|
|
452
424
|
const hide = ownedSkinIds !== null && skinId !== null && allSkinIds !== null && allSkinIds.has(skinId) && !ownedSkinIds.has(skinId);
|
|
453
425
|
if (hide) hidden++;
|
|
454
|
-
item
|
|
455
|
-
hideTargetFor(item)
|
|
426
|
+
setHidden(item, hide);
|
|
427
|
+
setHidden(hideTargetFor(item), hide);
|
|
456
428
|
});
|
|
457
429
|
|
|
458
430
|
const dots = [...document.querySelectorAll(DOT_SELECTOR)];
|
|
431
|
+
const dotOrder =
|
|
432
|
+
topLevelOrder.length === dots.length
|
|
433
|
+
? topLevelOrder
|
|
434
|
+
: carouselOrder.length === dots.length
|
|
435
|
+
? carouselOrder
|
|
436
|
+
: [];
|
|
459
437
|
let dotsHidden = 0;
|
|
460
|
-
const dotsMapped =
|
|
438
|
+
const dotsMapped = dotOrder.length > 0;
|
|
461
439
|
dots.forEach((dot, index) => {
|
|
462
|
-
const skinId = dotsMapped ?
|
|
463
|
-
const hide =
|
|
440
|
+
const skinId = dotsMapped ? dotOrder[index] : undefined;
|
|
441
|
+
const hide = ownedTopLevelIds !== null && skinId !== undefined && !ownedTopLevelIds.has(skinId);
|
|
464
442
|
if (hide) dotsHidden++;
|
|
465
|
-
dot
|
|
443
|
+
setHidden(dot, hide);
|
|
466
444
|
});
|
|
467
|
-
const dotList = dots[0]?.parentElement;
|
|
468
|
-
if (dotList) {
|
|
469
|
-
if (dotsHidden > 0 && !dotList.hasAttribute("data-perf-dots")) dotList.setAttribute("data-perf-dots", "");
|
|
470
|
-
else if (dotsHidden === 0 && dotList.hasAttribute("data-perf-dots")) dotList.removeAttribute("data-perf-dots");
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
if (ownedSkinIds !== null && hidden > 0) {
|
|
474
|
-
if (guardSelection(items)) runRepack();
|
|
475
|
-
else scheduleRepack();
|
|
476
|
-
} else if (hidden === 0) restoreRepack();
|
|
477
445
|
}
|
|
478
446
|
|
|
479
|
-
function
|
|
480
|
-
|
|
481
|
-
repackTimer = setTimeout(runRepack, REPACK_SETTLE_MS);
|
|
447
|
+
function applyAllGuarded() {
|
|
448
|
+
applyAll();
|
|
482
449
|
}
|
|
483
450
|
|
|
484
|
-
|
|
485
|
-
repackTimer = null;
|
|
486
|
-
renderObserver?.disconnect();
|
|
487
|
-
try {
|
|
488
|
-
const items = [...document.querySelectorAll(SKIN_ITEM_SELECTOR)];
|
|
489
|
-
const hidden = items.some((item) => item.classList.contains(HIDDEN_CLASS));
|
|
490
|
-
if (ownedSkinIds !== null && hidden) repackVisibleItems(items);
|
|
491
|
-
else restoreRepack();
|
|
492
|
-
} finally {
|
|
493
|
-
renderObserver?.observe(document.documentElement ?? document, OBSERVER_OPTIONS);
|
|
494
|
-
}
|
|
495
|
-
}
|
|
451
|
+
let applyQueued = false;
|
|
496
452
|
|
|
497
|
-
function
|
|
498
|
-
|
|
499
|
-
|
|
453
|
+
function scheduleApply() {
|
|
454
|
+
if (applyQueued) return;
|
|
455
|
+
applyQueued = true;
|
|
456
|
+
requestAnimationFrame(() => {
|
|
457
|
+
applyQueued = false;
|
|
458
|
+
ensureSourceFilter();
|
|
459
|
+
const item = document.querySelector(SKIN_ITEM_SELECTOR);
|
|
460
|
+
if (!item && !document.querySelector(DOT_SELECTOR)) return;
|
|
500
461
|
applyAll();
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
}
|
|
462
|
+
if (item) scheduleRefresh();
|
|
463
|
+
});
|
|
504
464
|
}
|
|
505
465
|
|
|
506
466
|
async function loadOwnedSkins(championId) {
|
|
@@ -515,19 +475,36 @@ async function loadOwnedSkins(championId) {
|
|
|
515
475
|
if (!Array.isArray(skins) || skins.length === 0) {
|
|
516
476
|
throw new Error(`no skin inventory returned for champion ${championId}`);
|
|
517
477
|
}
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
478
|
+
const all = new Set();
|
|
479
|
+
const owned = new Set();
|
|
480
|
+
const topLevel = [];
|
|
481
|
+
const ownedTopLevel = new Set();
|
|
482
|
+
for (const skin of skins) {
|
|
483
|
+
if (typeof skin?.id === "number") {
|
|
484
|
+
topLevel.push(skin.id);
|
|
485
|
+
if (skin.ownership?.owned) ownedTopLevel.add(skin.id);
|
|
486
|
+
}
|
|
487
|
+
const variants = [
|
|
488
|
+
skin,
|
|
489
|
+
...(Array.isArray(skin.chromas) ? skin.chromas : []),
|
|
490
|
+
...(Array.isArray(skin.questSkinInfo?.tiers) ? skin.questSkinInfo.tiers : []),
|
|
491
|
+
];
|
|
492
|
+
for (const variant of variants) {
|
|
493
|
+
if (typeof variant?.id !== "number") continue;
|
|
494
|
+
all.add(variant.id);
|
|
495
|
+
if (variant.ownership?.owned) owned.add(variant.id);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
const order = [...all].sort((a, b) => a - b);
|
|
499
|
+
return { owned, order, topLevel, ownedTopLevel };
|
|
523
500
|
}
|
|
524
501
|
|
|
525
502
|
async function loadCarouselOrder() {
|
|
526
|
-
|
|
503
|
+
const preferred = clientSkinOrder.length > 0 ? [...clientSkinOrder] : null;
|
|
527
504
|
try {
|
|
528
505
|
let skins;
|
|
529
|
-
if (rawFetch) {
|
|
530
|
-
let timer
|
|
506
|
+
if (typeof rawFetch === "function") {
|
|
507
|
+
let timer;
|
|
531
508
|
try {
|
|
532
509
|
const response = await Promise.race([
|
|
533
510
|
rawFetch(CAROUSEL_URI, { headers: { Accept: "application/json" } }),
|
|
@@ -542,10 +519,12 @@ async function loadCarouselOrder() {
|
|
|
542
519
|
} else {
|
|
543
520
|
skins = await api.lcu.get(CAROUSEL_URI);
|
|
544
521
|
}
|
|
522
|
+
learnSkinShape(skins);
|
|
523
|
+
if (preferred) return preferred;
|
|
545
524
|
if (!Array.isArray(skins)) return [];
|
|
546
525
|
return skins.map((skin) => skin?.id).filter((id) => typeof id === "number");
|
|
547
526
|
} catch {
|
|
548
|
-
return [];
|
|
527
|
+
return preferred ?? [];
|
|
549
528
|
}
|
|
550
529
|
}
|
|
551
530
|
|
|
@@ -574,6 +553,8 @@ function resetState() {
|
|
|
574
553
|
requestToken++;
|
|
575
554
|
ownedSkinIds = null;
|
|
576
555
|
allSkinIds = null;
|
|
556
|
+
topLevelOrder = [];
|
|
557
|
+
ownedTopLevelIds = null;
|
|
577
558
|
currentChampionId = null;
|
|
578
559
|
selectedSkinId = null;
|
|
579
560
|
carouselOrder = [];
|
|
@@ -588,15 +569,19 @@ function loadChampion(championId) {
|
|
|
588
569
|
currentChampionId = championId;
|
|
589
570
|
ownedSkinIds = null;
|
|
590
571
|
allSkinIds = null;
|
|
572
|
+
topLevelOrder = [];
|
|
573
|
+
ownedTopLevelIds = null;
|
|
591
574
|
failure = null;
|
|
592
575
|
clientSkinOrder = [];
|
|
593
576
|
const token = ++requestToken;
|
|
594
577
|
|
|
595
578
|
Promise.all([loadOwnedSkins(championId), loadCarouselOrder()]).then(
|
|
596
|
-
([{ owned, order: inventoryOrder }, carouselIds]) => {
|
|
579
|
+
([{ owned, order: inventoryOrder, topLevel, ownedTopLevel, entries }, carouselIds]) => {
|
|
597
580
|
if (token !== requestToken) return;
|
|
598
581
|
ownedSkinIds = owned;
|
|
599
582
|
allSkinIds = new Set(inventoryOrder);
|
|
583
|
+
topLevelOrder = topLevel;
|
|
584
|
+
ownedTopLevelIds = ownedTopLevel;
|
|
600
585
|
carouselOrder = carouselIds.length > 0 ? carouselIds : inventoryOrder;
|
|
601
586
|
applyAllGuarded();
|
|
602
587
|
},
|
|
@@ -633,21 +618,15 @@ function updateForSession(session, selection) {
|
|
|
633
618
|
|
|
634
619
|
const knownUnownedSkin = allSkinIds !== null && allSkinIds.has(serverSkinId) && !ownedSkinIds.has(serverSkinId);
|
|
635
620
|
if (!selectInFlight && knownUnownedSkin) {
|
|
636
|
-
const
|
|
637
|
-
const fallback = nextOwnedSkin(ownedSkinIds, serverSkinId ?? 0, direction) ?? nextOwnedSkin(ownedSkinIds, 0, 1);
|
|
621
|
+
const fallback = nextOwnedSkin(ownedTopLevelIds ?? ownedSkinIds, 0, 1);
|
|
638
622
|
if (fallback !== null) {
|
|
639
623
|
selectSkin(fallback).catch((error) => logError("could not correct the selected skin.", error));
|
|
640
624
|
}
|
|
641
625
|
}
|
|
642
|
-
if (!selectInFlight && belongsToChampion && ownedSkinIds.has(serverSkinId)) lastServerSkinId = serverSkinId;
|
|
643
626
|
|
|
644
627
|
applyAllGuarded();
|
|
645
628
|
}
|
|
646
629
|
|
|
647
|
-
function onWindowResize() {
|
|
648
|
-
scheduleRepack();
|
|
649
|
-
}
|
|
650
|
-
|
|
651
630
|
function scheduleRefresh() {
|
|
652
631
|
clearTimeout(refreshTimer);
|
|
653
632
|
refreshTimer = setTimeout(pollSession, REFRESH_DEBOUNCE_MS);
|
|
@@ -683,36 +662,26 @@ async function pollSession() {
|
|
|
683
662
|
|
|
684
663
|
function onEnable(utils) {
|
|
685
664
|
api = utils;
|
|
686
|
-
|
|
665
|
+
filterState.active = true;
|
|
687
666
|
removeStyle = api.addStyle(STYLE_ID, CSS);
|
|
688
667
|
|
|
689
|
-
renderObserver = new MutationObserver(
|
|
690
|
-
ensureSourceFilter();
|
|
691
|
-
const item = document.querySelector(SKIN_ITEM_SELECTOR);
|
|
692
|
-
if (!item && !document.querySelector(DOT_SELECTOR)) return;
|
|
693
|
-
applyAllGuarded();
|
|
694
|
-
if (item) scheduleRefresh();
|
|
695
|
-
});
|
|
668
|
+
renderObserver = new MutationObserver(scheduleApply);
|
|
696
669
|
renderObserver.observe(document.documentElement ?? document, OBSERVER_OPTIONS);
|
|
697
670
|
|
|
698
|
-
window.addEventListener("resize", onWindowResize);
|
|
699
671
|
unsubscribe = api.subscribe(SESSION_URI, onSessionPush);
|
|
700
672
|
pollSession();
|
|
701
673
|
}
|
|
702
674
|
|
|
703
675
|
function onDisable() {
|
|
704
|
-
window.removeEventListener("resize", onWindowResize);
|
|
705
676
|
unsubscribe?.();
|
|
706
677
|
unsubscribe = null;
|
|
707
678
|
clearTimeout(refreshTimer);
|
|
708
679
|
refreshTimer = null;
|
|
709
|
-
clearTimeout(repackTimer);
|
|
710
|
-
repackTimer = null;
|
|
711
680
|
renderObserver?.disconnect();
|
|
712
681
|
removeStyle?.();
|
|
713
682
|
renderObserver = null;
|
|
714
683
|
removeStyle = null;
|
|
715
|
-
|
|
684
|
+
filterState.active = false;
|
|
716
685
|
|
|
717
686
|
resetState();
|
|
718
687
|
document.querySelectorAll(`.${HIDDEN_CLASS}`).forEach((item) => item.classList.remove(HIDDEN_CLASS));
|