perfectionist-plugins 2.4.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 +0 -20
- package/better-client.js +796 -1093
- package/hide-unowned-skins.js +205 -303
- package/package.json +1 -1
- package/perfectionist-utils.js +7 -180
package/hide-unowned-skins.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
export const version = "2.
|
|
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
|
-
.${HIDDEN_CLASS} { display: none !important; margin: 0 !important; padding: 0 !important; border: none !important;
|
|
22
|
-
|
|
19
|
+
.${HIDDEN_CLASS} { display: none !important; margin: 0 !important; padding: 0 !important; border: none !important; }
|
|
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,33 +228,41 @@ 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 =
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
242
|
+
typeof input === "string"
|
|
243
|
+
? input
|
|
244
|
+
: typeof input?.url === "string"
|
|
245
|
+
? input.url
|
|
246
|
+
: typeof input?.href === "string"
|
|
247
|
+
? input.href
|
|
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,16 +300,19 @@ 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
|
-
} catch {
|
|
314
|
+
} catch {
|
|
315
|
+
}
|
|
228
316
|
}
|
|
229
317
|
return cache.filtered;
|
|
230
318
|
};
|
|
@@ -244,7 +332,8 @@ function installXhrFilter() {
|
|
|
244
332
|
|
|
245
333
|
function installSourceFilter() {
|
|
246
334
|
try {
|
|
247
|
-
|
|
335
|
+
filterState.active = readDefaultEnabled();
|
|
336
|
+
installArrayFilter();
|
|
248
337
|
installWebSocketFilter();
|
|
249
338
|
installFetchFilter();
|
|
250
339
|
installXhrFilter();
|
|
@@ -255,6 +344,7 @@ function installSourceFilter() {
|
|
|
255
344
|
|
|
256
345
|
function ensureSourceFilter() {
|
|
257
346
|
try {
|
|
347
|
+
installArrayFilter();
|
|
258
348
|
installWebSocketFilter();
|
|
259
349
|
installFetchFilter();
|
|
260
350
|
installXhrFilter();
|
|
@@ -267,10 +357,11 @@ let api = null;
|
|
|
267
357
|
let summonerId = null;
|
|
268
358
|
let ownedSkinIds = null;
|
|
269
359
|
let allSkinIds = null;
|
|
360
|
+
let topLevelOrder = [];
|
|
361
|
+
let ownedTopLevelIds = null;
|
|
270
362
|
let currentChampionId = null;
|
|
271
363
|
let carouselOrder = [];
|
|
272
364
|
let selectedSkinId = null;
|
|
273
|
-
let lastServerSkinId = null;
|
|
274
365
|
let failure = null;
|
|
275
366
|
let requestToken = 0;
|
|
276
367
|
let refreshTimer = null;
|
|
@@ -280,17 +371,8 @@ let selectInFlight = false;
|
|
|
280
371
|
let sessionMisses = 0;
|
|
281
372
|
let championMisses = 0;
|
|
282
373
|
let renderObserver = null;
|
|
283
|
-
let sessionActive = false;
|
|
284
|
-
let repackTimer = null;
|
|
285
|
-
let removeStyle = null;
|
|
286
|
-
let lastSelectedDomIndex = -1;
|
|
287
|
-
let navFixAt = 0;
|
|
288
|
-
let lastSkipSignature = null;
|
|
289
|
-
let itemSkinIds = new WeakMap();
|
|
290
|
-
const repackOriginals = new Map();
|
|
291
|
-
const lastAppliedLeft = new Map();
|
|
292
374
|
const skinSuffixCache = new Map();
|
|
293
|
-
|
|
375
|
+
let removeStyle = null;
|
|
294
376
|
|
|
295
377
|
function logError(scope, error) {
|
|
296
378
|
console.error(`[Perfectionist] Hide Unowned Skins: ${scope}`, error);
|
|
@@ -302,38 +384,22 @@ function skinSuffixOf(text) {
|
|
|
302
384
|
return /Skins\/Base\//i.test(text) ? 0 : null;
|
|
303
385
|
}
|
|
304
386
|
|
|
305
|
-
|
|
306
|
-
if (currentChampionId === null) return null;
|
|
307
|
-
for (const img of item.querySelectorAll("img[src]")) {
|
|
308
|
-
const src = img.getAttribute("src");
|
|
309
|
-
let suffix = skinSuffixCache.get(src);
|
|
310
|
-
if (suffix === undefined) {
|
|
311
|
-
suffix = skinSuffixOf(src);
|
|
312
|
-
skinSuffixCache.set(src, suffix);
|
|
313
|
-
}
|
|
314
|
-
if (suffix !== null) return currentChampionId * 1000 + suffix;
|
|
315
|
-
}
|
|
316
|
-
const suffix = skinSuffixOf(item.innerHTML);
|
|
317
|
-
return suffix === null ? null : currentChampionId * 1000 + suffix;
|
|
318
|
-
}
|
|
387
|
+
const skinIdCache = new WeakMap();
|
|
319
388
|
|
|
320
389
|
function readSkinId(item) {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
if (typeof id === "number") itemSkinIds.set(item, id);
|
|
335
|
-
});
|
|
336
|
-
return true;
|
|
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;
|
|
337
403
|
}
|
|
338
404
|
|
|
339
405
|
function hideTargetFor(item) {
|
|
@@ -346,226 +412,55 @@ function hideTargetFor(item) {
|
|
|
346
412
|
return target;
|
|
347
413
|
}
|
|
348
414
|
|
|
349
|
-
function
|
|
350
|
-
|
|
351
|
-
if (item.isConnected) continue;
|
|
352
|
-
repackOriginals.delete(item);
|
|
353
|
-
lastAppliedLeft.delete(item);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
function guardSelection(items) {
|
|
358
|
-
const selectedIndex = items.findIndex((item) => item.classList.contains(SELECTED_CLASS));
|
|
359
|
-
if (selectedIndex < 0) return false;
|
|
360
|
-
const previous = lastSelectedDomIndex;
|
|
361
|
-
lastSelectedDomIndex = selectedIndex;
|
|
362
|
-
const selected = items[selectedIndex];
|
|
363
|
-
const hidden = selected.classList.contains(HIDDEN_CLASS) || Boolean(selected.parentElement?.classList.contains(HIDDEN_CLASS));
|
|
364
|
-
if (!hidden) {
|
|
365
|
-
lastSkipSignature = null;
|
|
366
|
-
return false;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
const visible = items.map((item, index) => (item.classList.contains(HIDDEN_CLASS) ? -1 : index)).filter((index) => index >= 0);
|
|
370
|
-
if (visible.length === 0) return false;
|
|
371
|
-
|
|
372
|
-
const forward = previous < 0 ? true : selectedIndex >= previous;
|
|
373
|
-
const target = forward
|
|
374
|
-
? (visible.find((index) => index > selectedIndex) ?? visible[0])
|
|
375
|
-
: ([...visible].reverse().find((index) => index < selectedIndex) ?? visible[visible.length - 1]);
|
|
376
|
-
const skinId = readSkinId(items[target]);
|
|
377
|
-
if (skinId === null) return false;
|
|
378
|
-
|
|
379
|
-
const signature = `${selectedIndex}:${skinId}`;
|
|
380
|
-
if (lastSkipSignature === signature) return true;
|
|
381
|
-
const now = Date.now();
|
|
382
|
-
if (now < navFixAt) return true;
|
|
383
|
-
navFixAt = now + 150;
|
|
384
|
-
lastSkipSignature = signature;
|
|
385
|
-
lastSelectedDomIndex = target;
|
|
386
|
-
selectSkin(skinId).catch((error) => logError("could not skip the unowned skin.", error));
|
|
387
|
-
return true;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
function leftToPx(item, raw) {
|
|
391
|
-
if (!raw) return 0;
|
|
392
|
-
if (raw.endsWith("px")) return parseFloat(raw);
|
|
393
|
-
if (raw.endsWith("%")) return (parseFloat(raw) / 100) * (item.parentElement?.offsetWidth || 468);
|
|
394
|
-
return 0;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
function originalLeftOf(item) {
|
|
398
|
-
const original = repackOriginals.get(item);
|
|
399
|
-
return leftToPx(item, original ? original.value : item.style.getPropertyValue("left"));
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
function refreshOriginal(item) {
|
|
403
|
-
const current = item.style.getPropertyValue("left");
|
|
404
|
-
const priority = item.style.getPropertyPriority("left");
|
|
405
|
-
const last = lastAppliedLeft.get(item);
|
|
406
|
-
if (last !== undefined && current === last && priority === "important") return;
|
|
407
|
-
repackOriginals.set(item, { value: current, priority });
|
|
408
|
-
lastAppliedLeft.delete(item);
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
function widthOf(item, anchor) {
|
|
412
|
-
if (item.offsetWidth > 0) {
|
|
413
|
-
widthCache.set(item, item.offsetWidth);
|
|
414
|
-
return item.offsetWidth;
|
|
415
|
-
}
|
|
416
|
-
const measured = item.getBoundingClientRect().width;
|
|
417
|
-
if (measured > 0) {
|
|
418
|
-
widthCache.set(item, measured);
|
|
419
|
-
return measured;
|
|
420
|
-
}
|
|
421
|
-
return widthCache.get(item) ?? (item === anchor ? 117 : 73);
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
function repackVisibleItems(items) {
|
|
425
|
-
pruneRepack();
|
|
426
|
-
const visible = items.filter((item) => !item.classList.contains(HIDDEN_CLASS));
|
|
427
|
-
if (visible.length < 2) {
|
|
428
|
-
restoreRepack();
|
|
429
|
-
return;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
const selectedIndex = items.findIndex((item) => item.classList.contains(SELECTED_CLASS));
|
|
433
|
-
let anchor = selectedIndex >= 0 ? items[selectedIndex] : null;
|
|
434
|
-
if (!anchor && selectedSkinId !== null) {
|
|
435
|
-
anchor = visible.find((item) => readSkinId(item) === selectedSkinId) ?? null;
|
|
436
|
-
}
|
|
437
|
-
if (!anchor) anchor = visible[Math.floor(visible.length / 2)];
|
|
438
|
-
const anchorDomIndex = items.indexOf(anchor);
|
|
439
|
-
refreshOriginal(anchor);
|
|
440
|
-
const anchorLeft = originalLeftOf(anchor);
|
|
441
|
-
|
|
442
|
-
const setLeft = (item, x) => {
|
|
443
|
-
const want = `${x}px`;
|
|
444
|
-
const current = item.style.getPropertyValue("left");
|
|
445
|
-
const priority = item.style.getPropertyPriority("left");
|
|
446
|
-
const last = lastAppliedLeft.get(item);
|
|
447
|
-
const clientMoved = last !== undefined && (current !== last || priority !== "important");
|
|
448
|
-
if (clientMoved || !repackOriginals.has(item)) {
|
|
449
|
-
repackOriginals.set(item, { value: current, priority });
|
|
450
|
-
}
|
|
451
|
-
if (current !== want || priority !== "important") {
|
|
452
|
-
item.style.setProperty("left", want, "important");
|
|
453
|
-
}
|
|
454
|
-
lastAppliedLeft.set(item, want);
|
|
455
|
-
};
|
|
456
|
-
|
|
457
|
-
let rightX = anchorLeft + widthOf(anchor, anchor);
|
|
458
|
-
for (let index = anchorDomIndex + 1; index < items.length; index++) {
|
|
459
|
-
const item = items[index];
|
|
460
|
-
if (item.classList.contains(HIDDEN_CLASS)) continue;
|
|
461
|
-
const width = widthOf(item, anchor);
|
|
462
|
-
setLeft(item, rightX);
|
|
463
|
-
rightX += width;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
let leftX = anchorLeft;
|
|
467
|
-
for (let index = anchorDomIndex - 1; index >= 0; index--) {
|
|
468
|
-
const item = items[index];
|
|
469
|
-
if (item.classList.contains(HIDDEN_CLASS)) continue;
|
|
470
|
-
const width = widthOf(item, anchor);
|
|
471
|
-
leftX -= width;
|
|
472
|
-
setLeft(item, leftX);
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
function restoreRepack() {
|
|
477
|
-
for (const [item, original] of repackOriginals) {
|
|
478
|
-
if (!item.isConnected) continue;
|
|
479
|
-
const current = item.style.getPropertyValue("left");
|
|
480
|
-
const last = lastAppliedLeft.get(item);
|
|
481
|
-
if (last !== undefined && (current !== last || item.style.getPropertyPriority("left") !== "important")) continue;
|
|
482
|
-
if (original.value) item.style.setProperty("left", original.value, original.priority || undefined);
|
|
483
|
-
else item.style.removeProperty("left");
|
|
484
|
-
}
|
|
485
|
-
repackOriginals.clear();
|
|
486
|
-
lastAppliedLeft.clear();
|
|
415
|
+
function setHidden(element, hide) {
|
|
416
|
+
if (element.classList.contains(HIDDEN_CLASS) !== hide) element.classList.toggle(HIDDEN_CLASS, hide);
|
|
487
417
|
}
|
|
488
418
|
|
|
489
419
|
function applyAll() {
|
|
490
420
|
const items = [...document.querySelectorAll(SKIN_ITEM_SELECTOR)];
|
|
491
|
-
mapCarouselToItems(items);
|
|
492
421
|
let hidden = 0;
|
|
493
422
|
items.forEach((item) => {
|
|
494
423
|
const skinId = currentChampionId === null ? null : readSkinId(item);
|
|
495
424
|
const hide = ownedSkinIds !== null && skinId !== null && allSkinIds !== null && allSkinIds.has(skinId) && !ownedSkinIds.has(skinId);
|
|
496
425
|
if (hide) hidden++;
|
|
497
|
-
item
|
|
498
|
-
hideTargetFor(item)
|
|
426
|
+
setHidden(item, hide);
|
|
427
|
+
setHidden(hideTargetFor(item), hide);
|
|
499
428
|
});
|
|
500
429
|
|
|
501
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
|
+
: [];
|
|
502
437
|
let dotsHidden = 0;
|
|
503
|
-
const
|
|
504
|
-
const dotsMapped = dotOrder.length > 0 && dots.length === dotOrder.length;
|
|
438
|
+
const dotsMapped = dotOrder.length > 0;
|
|
505
439
|
dots.forEach((dot, index) => {
|
|
506
440
|
const skinId = dotsMapped ? dotOrder[index] : undefined;
|
|
507
|
-
const hide =
|
|
441
|
+
const hide = ownedTopLevelIds !== null && skinId !== undefined && !ownedTopLevelIds.has(skinId);
|
|
508
442
|
if (hide) dotsHidden++;
|
|
509
|
-
dot
|
|
443
|
+
setHidden(dot, hide);
|
|
510
444
|
});
|
|
511
|
-
const dotList = dots[0]?.parentElement;
|
|
512
|
-
if (dotList) {
|
|
513
|
-
if (dotsHidden > 0 && !dotList.hasAttribute("data-perf-dots")) dotList.setAttribute("data-perf-dots", "");
|
|
514
|
-
else if (dotsHidden === 0 && dotList.hasAttribute("data-perf-dots")) dotList.removeAttribute("data-perf-dots");
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
if (ownedSkinIds !== null && hidden > 0) {
|
|
518
|
-
if (guardSelection(items)) runRepack();
|
|
519
|
-
else scheduleRepack();
|
|
520
|
-
} else if (hidden === 0) restoreRepack();
|
|
521
445
|
}
|
|
522
446
|
|
|
523
|
-
function
|
|
524
|
-
|
|
525
|
-
repackTimer = setTimeout(runRepack, REPACK_SETTLE_MS);
|
|
447
|
+
function applyAllGuarded() {
|
|
448
|
+
applyAll();
|
|
526
449
|
}
|
|
527
450
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
451
|
+
let applyQueued = false;
|
|
452
|
+
|
|
453
|
+
function scheduleApply() {
|
|
454
|
+
if (applyQueued) return;
|
|
455
|
+
applyQueued = true;
|
|
456
|
+
requestAnimationFrame(() => {
|
|
457
|
+
applyQueued = false;
|
|
531
458
|
ensureSourceFilter();
|
|
532
459
|
const item = document.querySelector(SKIN_ITEM_SELECTOR);
|
|
533
460
|
if (!item && !document.querySelector(DOT_SELECTOR)) return;
|
|
534
|
-
|
|
461
|
+
applyAll();
|
|
535
462
|
if (item) scheduleRefresh();
|
|
536
463
|
});
|
|
537
|
-
try {
|
|
538
|
-
renderObserver.observe(document.documentElement ?? document, OBSERVER_OPTIONS);
|
|
539
|
-
} catch {
|
|
540
|
-
renderObserver = null;
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
function stopRenderObserver() {
|
|
545
|
-
renderObserver?.disconnect();
|
|
546
|
-
renderObserver = null;
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
function runRepack() {
|
|
550
|
-
repackTimer = null;
|
|
551
|
-
renderObserver?.disconnect();
|
|
552
|
-
try {
|
|
553
|
-
const items = [...document.querySelectorAll(SKIN_ITEM_SELECTOR)];
|
|
554
|
-
const hidden = items.some((item) => item.classList.contains(HIDDEN_CLASS));
|
|
555
|
-
if (ownedSkinIds !== null && hidden) repackVisibleItems(items);
|
|
556
|
-
else restoreRepack();
|
|
557
|
-
} finally {
|
|
558
|
-
startRenderObserver();
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
function applyAllGuarded() {
|
|
563
|
-
renderObserver?.disconnect();
|
|
564
|
-
try {
|
|
565
|
-
applyAll();
|
|
566
|
-
} finally {
|
|
567
|
-
startRenderObserver();
|
|
568
|
-
}
|
|
569
464
|
}
|
|
570
465
|
|
|
571
466
|
async function loadOwnedSkins(championId) {
|
|
@@ -580,19 +475,36 @@ async function loadOwnedSkins(championId) {
|
|
|
580
475
|
if (!Array.isArray(skins) || skins.length === 0) {
|
|
581
476
|
throw new Error(`no skin inventory returned for champion ${championId}`);
|
|
582
477
|
}
|
|
583
|
-
const
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
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 };
|
|
588
500
|
}
|
|
589
501
|
|
|
590
502
|
async function loadCarouselOrder() {
|
|
591
|
-
|
|
503
|
+
const preferred = clientSkinOrder.length > 0 ? [...clientSkinOrder] : null;
|
|
592
504
|
try {
|
|
593
505
|
let skins;
|
|
594
|
-
if (rawFetch) {
|
|
595
|
-
let timer
|
|
506
|
+
if (typeof rawFetch === "function") {
|
|
507
|
+
let timer;
|
|
596
508
|
try {
|
|
597
509
|
const response = await Promise.race([
|
|
598
510
|
rawFetch(CAROUSEL_URI, { headers: { Accept: "application/json" } }),
|
|
@@ -607,10 +519,12 @@ async function loadCarouselOrder() {
|
|
|
607
519
|
} else {
|
|
608
520
|
skins = await api.lcu.get(CAROUSEL_URI);
|
|
609
521
|
}
|
|
522
|
+
learnSkinShape(skins);
|
|
523
|
+
if (preferred) return preferred;
|
|
610
524
|
if (!Array.isArray(skins)) return [];
|
|
611
525
|
return skins.map((skin) => skin?.id).filter((id) => typeof id === "number");
|
|
612
526
|
} catch {
|
|
613
|
-
return [];
|
|
527
|
+
return preferred ?? [];
|
|
614
528
|
}
|
|
615
529
|
}
|
|
616
530
|
|
|
@@ -639,6 +553,8 @@ function resetState() {
|
|
|
639
553
|
requestToken++;
|
|
640
554
|
ownedSkinIds = null;
|
|
641
555
|
allSkinIds = null;
|
|
556
|
+
topLevelOrder = [];
|
|
557
|
+
ownedTopLevelIds = null;
|
|
642
558
|
currentChampionId = null;
|
|
643
559
|
selectedSkinId = null;
|
|
644
560
|
carouselOrder = [];
|
|
@@ -653,15 +569,19 @@ function loadChampion(championId) {
|
|
|
653
569
|
currentChampionId = championId;
|
|
654
570
|
ownedSkinIds = null;
|
|
655
571
|
allSkinIds = null;
|
|
572
|
+
topLevelOrder = [];
|
|
573
|
+
ownedTopLevelIds = null;
|
|
656
574
|
failure = null;
|
|
657
575
|
clientSkinOrder = [];
|
|
658
576
|
const token = ++requestToken;
|
|
659
577
|
|
|
660
578
|
Promise.all([loadOwnedSkins(championId), loadCarouselOrder()]).then(
|
|
661
|
-
([{ owned, order: inventoryOrder }, carouselIds]) => {
|
|
579
|
+
([{ owned, order: inventoryOrder, topLevel, ownedTopLevel, entries }, carouselIds]) => {
|
|
662
580
|
if (token !== requestToken) return;
|
|
663
581
|
ownedSkinIds = owned;
|
|
664
582
|
allSkinIds = new Set(inventoryOrder);
|
|
583
|
+
topLevelOrder = topLevel;
|
|
584
|
+
ownedTopLevelIds = ownedTopLevel;
|
|
665
585
|
carouselOrder = carouselIds.length > 0 ? carouselIds : inventoryOrder;
|
|
666
586
|
applyAllGuarded();
|
|
667
587
|
},
|
|
@@ -698,21 +618,15 @@ function updateForSession(session, selection) {
|
|
|
698
618
|
|
|
699
619
|
const knownUnownedSkin = allSkinIds !== null && allSkinIds.has(serverSkinId) && !ownedSkinIds.has(serverSkinId);
|
|
700
620
|
if (!selectInFlight && knownUnownedSkin) {
|
|
701
|
-
const
|
|
702
|
-
const fallback = nextOwnedSkin(ownedSkinIds, serverSkinId ?? 0, direction) ?? nextOwnedSkin(ownedSkinIds, 0, 1);
|
|
621
|
+
const fallback = nextOwnedSkin(ownedTopLevelIds ?? ownedSkinIds, 0, 1);
|
|
703
622
|
if (fallback !== null) {
|
|
704
623
|
selectSkin(fallback).catch((error) => logError("could not correct the selected skin.", error));
|
|
705
624
|
}
|
|
706
625
|
}
|
|
707
|
-
if (!selectInFlight && belongsToChampion && ownedSkinIds.has(serverSkinId)) lastServerSkinId = serverSkinId;
|
|
708
626
|
|
|
709
627
|
applyAllGuarded();
|
|
710
628
|
}
|
|
711
629
|
|
|
712
|
-
function onWindowResize() {
|
|
713
|
-
scheduleRepack();
|
|
714
|
-
}
|
|
715
|
-
|
|
716
630
|
function scheduleRefresh() {
|
|
717
631
|
clearTimeout(refreshTimer);
|
|
718
632
|
refreshTimer = setTimeout(pollSession, REFRESH_DEBOUNCE_MS);
|
|
@@ -721,15 +635,11 @@ function scheduleRefresh() {
|
|
|
721
635
|
function onSessionPush(raw) {
|
|
722
636
|
const msg = raw ?? {};
|
|
723
637
|
if (msg.eventType === "Delete") {
|
|
724
|
-
sessionActive = false;
|
|
725
|
-
stopRenderObserver();
|
|
726
638
|
resetState();
|
|
727
639
|
return;
|
|
728
640
|
}
|
|
729
641
|
const s = msg.data ?? msg;
|
|
730
642
|
if (!s?.myTeam) return;
|
|
731
|
-
sessionActive = true;
|
|
732
|
-
startRenderObserver();
|
|
733
643
|
updateForSession(s, null);
|
|
734
644
|
}
|
|
735
645
|
|
|
@@ -740,17 +650,11 @@ async function pollSession() {
|
|
|
740
650
|
try {
|
|
741
651
|
const session = await api.lcu.get(SESSION_URI);
|
|
742
652
|
sessionMisses = 0;
|
|
743
|
-
sessionActive = true;
|
|
744
|
-
startRenderObserver();
|
|
745
653
|
const selection = await api.lcu.get(SELECTION_URI).catch(() => null);
|
|
746
654
|
updateForSession(session, selection);
|
|
747
655
|
} catch {
|
|
748
656
|
sessionMisses++;
|
|
749
|
-
if (sessionMisses >= 3)
|
|
750
|
-
sessionActive = false;
|
|
751
|
-
stopRenderObserver();
|
|
752
|
-
resetState();
|
|
753
|
-
}
|
|
657
|
+
if (sessionMisses >= 3) resetState();
|
|
754
658
|
} finally {
|
|
755
659
|
pollInFlight = false;
|
|
756
660
|
}
|
|
@@ -758,28 +662,26 @@ async function pollSession() {
|
|
|
758
662
|
|
|
759
663
|
function onEnable(utils) {
|
|
760
664
|
api = utils;
|
|
761
|
-
|
|
762
|
-
sessionActive = false;
|
|
665
|
+
filterState.active = true;
|
|
763
666
|
removeStyle = api.addStyle(STYLE_ID, CSS);
|
|
764
667
|
|
|
765
|
-
|
|
668
|
+
renderObserver = new MutationObserver(scheduleApply);
|
|
669
|
+
renderObserver.observe(document.documentElement ?? document, OBSERVER_OPTIONS);
|
|
670
|
+
|
|
766
671
|
unsubscribe = api.subscribe(SESSION_URI, onSessionPush);
|
|
767
672
|
pollSession();
|
|
768
673
|
}
|
|
769
674
|
|
|
770
675
|
function onDisable() {
|
|
771
|
-
window.removeEventListener("resize", onWindowResize);
|
|
772
676
|
unsubscribe?.();
|
|
773
677
|
unsubscribe = null;
|
|
774
678
|
clearTimeout(refreshTimer);
|
|
775
679
|
refreshTimer = null;
|
|
776
|
-
|
|
777
|
-
repackTimer = null;
|
|
778
|
-
sessionActive = false;
|
|
779
|
-
stopRenderObserver();
|
|
680
|
+
renderObserver?.disconnect();
|
|
780
681
|
removeStyle?.();
|
|
682
|
+
renderObserver = null;
|
|
781
683
|
removeStyle = null;
|
|
782
|
-
|
|
684
|
+
filterState.active = false;
|
|
783
685
|
|
|
784
686
|
resetState();
|
|
785
687
|
document.querySelectorAll(`.${HIDDEN_CLASS}`).forEach((item) => item.classList.remove(HIDDEN_CLASS));
|