perfectionist-plugins 1.8.0 → 2.0.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 +22 -0
- package/auto-read.js +75 -4
- package/better-client.js +512 -0
- package/hide-unowned-skins.js +112 -18
- package/package.json +37 -34
- package/perfectionist-utils.js +131 -27
- package/tools/betterclient-monitor.js +269 -0
package/hide-unowned-skins.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
export const version = "2.
|
|
1
|
+
export const version = "2.4.0";
|
|
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";
|
|
8
9
|
const SESSION_URI = "/lol-champ-select/v1/session";
|
|
9
10
|
const SELECTION_URI = "/lol-champ-select/v1/session/my-selection";
|
|
10
11
|
const CAROUSEL_URI = "/lol-champ-select/v1/skin-carousel-skins";
|
|
@@ -25,7 +26,7 @@ const OBSERVER_OPTIONS = {
|
|
|
25
26
|
childList: true,
|
|
26
27
|
subtree: true,
|
|
27
28
|
attributes: true,
|
|
28
|
-
attributeFilter: ["class", "style"],
|
|
29
|
+
attributeFilter: ["class", "style", "src"],
|
|
29
30
|
};
|
|
30
31
|
|
|
31
32
|
let filterActive = true;
|
|
@@ -160,7 +161,14 @@ function installFetchFilter() {
|
|
|
160
161
|
if (!rawFetch) rawFetch = original;
|
|
161
162
|
|
|
162
163
|
const wrapped = function (input, init) {
|
|
163
|
-
const url =
|
|
164
|
+
const url =
|
|
165
|
+
typeof input === "string"
|
|
166
|
+
? input
|
|
167
|
+
: typeof input?.url === "string"
|
|
168
|
+
? input.url
|
|
169
|
+
: typeof input?.href === "string"
|
|
170
|
+
? input.href
|
|
171
|
+
: "";
|
|
164
172
|
const promise = original.call(this, input, init);
|
|
165
173
|
if (!filterActive || url.split("?")[0].indexOf(CAROUSEL_URI) === -1) return promise;
|
|
166
174
|
return promise.then((response) => {
|
|
@@ -263,6 +271,7 @@ let allSkinIds = null;
|
|
|
263
271
|
let currentChampionId = null;
|
|
264
272
|
let carouselOrder = [];
|
|
265
273
|
let selectedSkinId = null;
|
|
274
|
+
let lastServerSkinId = null;
|
|
266
275
|
let failure = null;
|
|
267
276
|
let requestToken = 0;
|
|
268
277
|
let refreshTimer = null;
|
|
@@ -273,16 +282,21 @@ let sessionMisses = 0;
|
|
|
273
282
|
let championMisses = 0;
|
|
274
283
|
let renderObserver = null;
|
|
275
284
|
const repackOriginals = new Map();
|
|
285
|
+
const lastAppliedLeft = new Map();
|
|
276
286
|
const skinSuffixCache = new Map();
|
|
277
287
|
let repackTimer = null;
|
|
278
288
|
let removeStyle = null;
|
|
289
|
+
let lastSelectedDomIndex = -1;
|
|
290
|
+
let navFixAt = 0;
|
|
291
|
+
let lastSkipSignature = null;
|
|
292
|
+
const widthCache = new Map();
|
|
279
293
|
|
|
280
294
|
function logError(scope, error) {
|
|
281
295
|
console.error(`[Perfectionist] Hide Unowned Skins: ${scope}`, error);
|
|
282
296
|
}
|
|
283
297
|
|
|
284
298
|
function skinSuffixOf(text) {
|
|
285
|
-
const numbered = text.match(/Skins\/Skin(\d+)\//i) ?? text.match(/
|
|
299
|
+
const numbered = text.match(/Skins\/Skin(\d+)\//i) ?? text.match(/skin(\d+)\//i);
|
|
286
300
|
if (numbered) return Number(numbered[1]);
|
|
287
301
|
return /Skins\/Base\//i.test(text) ? 0 : null;
|
|
288
302
|
}
|
|
@@ -311,7 +325,49 @@ function hideTargetFor(item) {
|
|
|
311
325
|
return target;
|
|
312
326
|
}
|
|
313
327
|
|
|
328
|
+
function pruneRepack() {
|
|
329
|
+
for (const item of [...repackOriginals.keys()]) {
|
|
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
|
+
|
|
314
369
|
function repackVisibleItems(items) {
|
|
370
|
+
pruneRepack();
|
|
315
371
|
const visible = items.filter((item) => !item.classList.contains(HIDDEN_CLASS));
|
|
316
372
|
if (visible.length < 2) {
|
|
317
373
|
restoreRepack();
|
|
@@ -326,7 +382,14 @@ function repackVisibleItems(items) {
|
|
|
326
382
|
if (!anchor) anchor = visible[Math.floor(visible.length / 2)];
|
|
327
383
|
const anchorDomIndex = items.indexOf(anchor);
|
|
328
384
|
|
|
329
|
-
const widthOf = (item) =>
|
|
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
|
+
};
|
|
330
393
|
const toPx = (item) => {
|
|
331
394
|
const raw = item.style.left || "0";
|
|
332
395
|
if (raw.endsWith("px")) return parseFloat(raw);
|
|
@@ -340,15 +403,17 @@ function repackVisibleItems(items) {
|
|
|
340
403
|
|
|
341
404
|
const setLeft = (item, x) => {
|
|
342
405
|
const want = `${x}px`;
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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") {
|
|
350
414
|
item.style.setProperty("left", want, "important");
|
|
351
415
|
}
|
|
416
|
+
lastAppliedLeft.set(item, want);
|
|
352
417
|
};
|
|
353
418
|
|
|
354
419
|
items.forEach((item, index) => {
|
|
@@ -367,12 +432,16 @@ function repackVisibleItems(items) {
|
|
|
367
432
|
}
|
|
368
433
|
|
|
369
434
|
function restoreRepack() {
|
|
370
|
-
if (repackOriginals.size === 0) return;
|
|
371
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;
|
|
372
440
|
if (original.value) item.style.setProperty("left", original.value, original.priority || undefined);
|
|
373
441
|
else item.style.removeProperty("left");
|
|
374
442
|
}
|
|
375
443
|
repackOriginals.clear();
|
|
444
|
+
lastAppliedLeft.clear();
|
|
376
445
|
}
|
|
377
446
|
|
|
378
447
|
function applyAll() {
|
|
@@ -401,8 +470,10 @@ function applyAll() {
|
|
|
401
470
|
else if (dotsHidden === 0 && dotList.hasAttribute("data-perf-dots")) dotList.removeAttribute("data-perf-dots");
|
|
402
471
|
}
|
|
403
472
|
|
|
404
|
-
if (ownedSkinIds !== null && hidden > 0)
|
|
405
|
-
|
|
473
|
+
if (ownedSkinIds !== null && hidden > 0) {
|
|
474
|
+
if (guardSelection(items)) runRepack();
|
|
475
|
+
else scheduleRepack();
|
|
476
|
+
} else if (hidden === 0) restoreRepack();
|
|
406
477
|
}
|
|
407
478
|
|
|
408
479
|
function scheduleRepack() {
|
|
@@ -456,8 +527,18 @@ async function loadCarouselOrder() {
|
|
|
456
527
|
try {
|
|
457
528
|
let skins;
|
|
458
529
|
if (rawFetch) {
|
|
459
|
-
|
|
460
|
-
|
|
530
|
+
let timer = null;
|
|
531
|
+
try {
|
|
532
|
+
const response = await Promise.race([
|
|
533
|
+
rawFetch(CAROUSEL_URI, { headers: { Accept: "application/json" } }),
|
|
534
|
+
new Promise((_, reject) => {
|
|
535
|
+
timer = setTimeout(() => reject(new Error("carousel request timed out")), 8000);
|
|
536
|
+
}),
|
|
537
|
+
]);
|
|
538
|
+
skins = response.ok ? await response.json() : null;
|
|
539
|
+
} finally {
|
|
540
|
+
clearTimeout(timer);
|
|
541
|
+
}
|
|
461
542
|
} else {
|
|
462
543
|
skins = await api.lcu.get(CAROUSEL_URI);
|
|
463
544
|
}
|
|
@@ -478,9 +559,13 @@ function nextOwnedSkin(owned, fromId, direction) {
|
|
|
478
559
|
async function selectSkin(skinId) {
|
|
479
560
|
selectedSkinId = skinId;
|
|
480
561
|
selectInFlight = true;
|
|
562
|
+
const guard = setTimeout(() => {
|
|
563
|
+
selectInFlight = false;
|
|
564
|
+
}, 8000);
|
|
481
565
|
try {
|
|
482
566
|
await api.lcu.send("PATCH", SELECTION_URI, { selectedSkinId: skinId });
|
|
483
567
|
} finally {
|
|
568
|
+
clearTimeout(guard);
|
|
484
569
|
selectInFlight = false;
|
|
485
570
|
}
|
|
486
571
|
}
|
|
@@ -548,15 +633,21 @@ function updateForSession(session, selection) {
|
|
|
548
633
|
|
|
549
634
|
const knownUnownedSkin = allSkinIds !== null && allSkinIds.has(serverSkinId) && !ownedSkinIds.has(serverSkinId);
|
|
550
635
|
if (!selectInFlight && knownUnownedSkin) {
|
|
551
|
-
const
|
|
636
|
+
const direction = lastServerSkinId !== null && serverSkinId !== null && serverSkinId < lastServerSkinId ? -1 : 1;
|
|
637
|
+
const fallback = nextOwnedSkin(ownedSkinIds, serverSkinId ?? 0, direction) ?? nextOwnedSkin(ownedSkinIds, 0, 1);
|
|
552
638
|
if (fallback !== null) {
|
|
553
639
|
selectSkin(fallback).catch((error) => logError("could not correct the selected skin.", error));
|
|
554
640
|
}
|
|
555
641
|
}
|
|
642
|
+
if (!selectInFlight && belongsToChampion && ownedSkinIds.has(serverSkinId)) lastServerSkinId = serverSkinId;
|
|
556
643
|
|
|
557
644
|
applyAllGuarded();
|
|
558
645
|
}
|
|
559
646
|
|
|
647
|
+
function onWindowResize() {
|
|
648
|
+
scheduleRepack();
|
|
649
|
+
}
|
|
650
|
+
|
|
560
651
|
function scheduleRefresh() {
|
|
561
652
|
clearTimeout(refreshTimer);
|
|
562
653
|
refreshTimer = setTimeout(pollSession, REFRESH_DEBOUNCE_MS);
|
|
@@ -596,6 +687,7 @@ function onEnable(utils) {
|
|
|
596
687
|
removeStyle = api.addStyle(STYLE_ID, CSS);
|
|
597
688
|
|
|
598
689
|
renderObserver = new MutationObserver(() => {
|
|
690
|
+
ensureSourceFilter();
|
|
599
691
|
const item = document.querySelector(SKIN_ITEM_SELECTOR);
|
|
600
692
|
if (!item && !document.querySelector(DOT_SELECTOR)) return;
|
|
601
693
|
applyAllGuarded();
|
|
@@ -603,11 +695,13 @@ function onEnable(utils) {
|
|
|
603
695
|
});
|
|
604
696
|
renderObserver.observe(document.documentElement ?? document, OBSERVER_OPTIONS);
|
|
605
697
|
|
|
698
|
+
window.addEventListener("resize", onWindowResize);
|
|
606
699
|
unsubscribe = api.subscribe(SESSION_URI, onSessionPush);
|
|
607
700
|
pollSession();
|
|
608
701
|
}
|
|
609
702
|
|
|
610
703
|
function onDisable() {
|
|
704
|
+
window.removeEventListener("resize", onWindowResize);
|
|
611
705
|
unsubscribe?.();
|
|
612
706
|
unsubscribe = null;
|
|
613
707
|
clearTimeout(refreshTimer);
|
package/package.json
CHANGED
|
@@ -1,34 +1,37 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "perfectionist-plugins",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Small plugins that fix the annoying parts of League of Legends champ select. Built for Pengu Loader.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "perfectionist-utils.js",
|
|
7
|
-
"exports": {
|
|
8
|
-
".": "./perfectionist-utils.js",
|
|
9
|
-
"./perfectionist-utils.js": "./perfectionist-utils.js",
|
|
10
|
-
"./ban-blocker.js": "./ban-blocker.js",
|
|
11
|
-
"./hide-unowned-skins.js": "./hide-unowned-skins.js",
|
|
12
|
-
"./unlock-profile-skins.js": "./unlock-profile-skins.js",
|
|
13
|
-
"./auto-claimer.js": "./auto-claimer.js",
|
|
14
|
-
"./auto-read.js": "./auto-read.js"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"auto-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "perfectionist-plugins",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Small plugins that fix the annoying parts of League of Legends champ select. Built for Pengu Loader.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "perfectionist-utils.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./perfectionist-utils.js",
|
|
9
|
+
"./perfectionist-utils.js": "./perfectionist-utils.js",
|
|
10
|
+
"./ban-blocker.js": "./ban-blocker.js",
|
|
11
|
+
"./hide-unowned-skins.js": "./hide-unowned-skins.js",
|
|
12
|
+
"./unlock-profile-skins.js": "./unlock-profile-skins.js",
|
|
13
|
+
"./auto-claimer.js": "./auto-claimer.js",
|
|
14
|
+
"./auto-read.js": "./auto-read.js",
|
|
15
|
+
"./better-client.js": "./better-client.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"perfectionist-utils.js",
|
|
19
|
+
"ban-blocker.js",
|
|
20
|
+
"hide-unowned-skins.js",
|
|
21
|
+
"unlock-profile-skins.js",
|
|
22
|
+
"auto-claimer.js",
|
|
23
|
+
"auto-read.js",
|
|
24
|
+
"better-client.js",
|
|
25
|
+
"tools/betterclient-monitor.js"
|
|
26
|
+
],
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/SrKing-9812/Perfectionist-Plugins.git"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"pengu-loader",
|
|
33
|
+
"league-of-legends",
|
|
34
|
+
"plugin"
|
|
35
|
+
],
|
|
36
|
+
"author": "Sr. King"
|
|
37
|
+
}
|
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 = "
|
|
9
|
+
export const version = "2.0.0";
|
|
10
10
|
|
|
11
11
|
const PKG = "perfectionist-plugins";
|
|
12
12
|
const CDN = "https://cdn.jsdelivr.net/npm";
|
|
@@ -22,6 +22,13 @@ const BUNDLED_MODULES = [
|
|
|
22
22
|
description: "Shared core for the Perfectionism plugins.",
|
|
23
23
|
core: true,
|
|
24
24
|
},
|
|
25
|
+
{
|
|
26
|
+
id: "better-client",
|
|
27
|
+
file: "better-client.js",
|
|
28
|
+
name: "BetterClient",
|
|
29
|
+
version: "1.2.0",
|
|
30
|
+
description: "Debloats the client: blocks telemetry, merges duplicate requests, caches immutable data, decodes images asynchronously and fixes several bugs.",
|
|
31
|
+
},
|
|
25
32
|
{
|
|
26
33
|
id: "ban-blocker",
|
|
27
34
|
file: "ban-blocker.js",
|
|
@@ -29,13 +36,6 @@ const BUNDLED_MODULES = [
|
|
|
29
36
|
version: "3.6.0",
|
|
30
37
|
description: "During bans, blocking a teammate instantly claims the champion they hover to ban.",
|
|
31
38
|
},
|
|
32
|
-
{
|
|
33
|
-
id: "hide-unowned-skins",
|
|
34
|
-
file: "hide-unowned-skins.js",
|
|
35
|
-
name: "Hide Unowned Skins",
|
|
36
|
-
version: "2.3.2",
|
|
37
|
-
description: "In champion select, only shows the skins you own for the picked champion.",
|
|
38
|
-
},
|
|
39
39
|
{
|
|
40
40
|
id: "unlock-profile-skins",
|
|
41
41
|
file: "unlock-profile-skins.js",
|
|
@@ -43,6 +43,13 @@ const BUNDLED_MODULES = [
|
|
|
43
43
|
version: "1.2.1",
|
|
44
44
|
description: "Unlocks every skin in the Set profile background window.",
|
|
45
45
|
},
|
|
46
|
+
{
|
|
47
|
+
id: "hide-unowned-skins",
|
|
48
|
+
file: "hide-unowned-skins.js",
|
|
49
|
+
name: "Hide Unowned Skins",
|
|
50
|
+
version: "2.4.0",
|
|
51
|
+
description: "In champion select, only shows the skins you own for the picked champion.",
|
|
52
|
+
},
|
|
46
53
|
{
|
|
47
54
|
id: "auto-claimer",
|
|
48
55
|
file: "auto-claimer.js",
|
|
@@ -54,14 +61,14 @@ const BUNDLED_MODULES = [
|
|
|
54
61
|
id: "auto-read",
|
|
55
62
|
file: "auto-read.js",
|
|
56
63
|
name: "Auto Read",
|
|
57
|
-
version: "1.
|
|
58
|
-
description: "Automatically marks quests, loot, Clash and client notices as read so no badge or dot stays lit.",
|
|
64
|
+
version: "1.2.0",
|
|
65
|
+
description: "Automatically marks quests, loot, Clash, TFT and client notices as read so no badge or dot stays lit.",
|
|
59
66
|
},
|
|
60
67
|
];
|
|
61
68
|
|
|
62
69
|
const STYLE_CSS = `
|
|
63
|
-
#${PANEL_ID} { overflow-x: hidden; padding: 15px 20px; position: relative; width: calc(100% - 40px); box-sizing: content-box; color: #f0e6d2; font-family: "LoL Body", Arial, "Helvetica Neue", Helvetica, sans-serif; }
|
|
64
|
-
#${PANEL_ID} > lol-uikit-scrollable { display: block; height:
|
|
70
|
+
#${PANEL_ID} { display: flex; flex-direction: column; overflow-x: hidden; padding: 15px 20px; position: relative; width: calc(100% - 40px); box-sizing: content-box; color: #f0e6d2; font-family: "LoL Body", Arial, "Helvetica Neue", Helvetica, sans-serif; }
|
|
71
|
+
#${PANEL_ID} > lol-uikit-scrollable { display: block; flex: 1 1 auto; min-height: 0; }
|
|
65
72
|
#${PANEL_ID} .perf-card { display: flex; align-items: flex-start; justify-content: space-between; gap: 24px;
|
|
66
73
|
padding: 16px 18px; margin-bottom: 12px; box-sizing: border-box;
|
|
67
74
|
background: #010a13; border: 1px solid #785a28; }
|
|
@@ -121,6 +128,21 @@ const STYLE_CSS = `
|
|
|
121
128
|
#${PANEL_ID} .perf-chip--on { color: #010a13; background: #c89b3c; border-color: #c89b3c; }
|
|
122
129
|
#${PANEL_ID} .perf-chip--on:hover { background: #f0e6d2; }
|
|
123
130
|
|
|
131
|
+
#${PANEL_ID} .perf-tools { display: flex; align-items: center; gap: 10px; flex: 0 0 auto; margin-bottom: 14px; }
|
|
132
|
+
#${PANEL_ID} .perf-search { flex: 1 1 auto; min-width: 0; box-sizing: border-box;
|
|
133
|
+
font-family: "LoL Body", Arial, "Helvetica Neue", Helvetica, sans-serif; font-size: 12px; letter-spacing: 0.3px;
|
|
134
|
+
color: #f0e6d2; background: #0a1428; border: 1px solid #785a28; padding: 9px 12px; outline: none; }
|
|
135
|
+
#${PANEL_ID} .perf-search:focus { border-color: #c89b3c; }
|
|
136
|
+
#${PANEL_ID} .perf-search::placeholder { color: #a09b8c; }
|
|
137
|
+
#${PANEL_ID} .perf-master { flex: 0 0 auto; white-space: nowrap; }
|
|
138
|
+
#${PANEL_ID} .perf-card--locked .perf-opts { pointer-events: none; }
|
|
139
|
+
#${PANEL_ID} .perf-card--locked .perf-actions > .perf-toggle { pointer-events: none; }
|
|
140
|
+
#${PANEL_ID} .perf-card--locked .perf-caret { opacity: 0.45; }
|
|
141
|
+
#${PANEL_ID} .perf-card--locked .perf-chips-label { opacity: 0.45; }
|
|
142
|
+
#${PANEL_ID} .perf-chip:disabled { opacity: 0.45; cursor: default; }
|
|
143
|
+
#${PANEL_ID} .perf-chip:disabled:hover { color: #a09b8c; border-color: #785a28; background: #0a1428; }
|
|
144
|
+
#${PANEL_ID} .perf-chip--on:disabled:hover { color: #010a13; border-color: #c89b3c; background: #c89b3c; }
|
|
145
|
+
|
|
124
146
|
.perf-update { position: fixed; inset: 0; z-index: 100000; display: flex; align-items: center;
|
|
125
147
|
justify-content: center; background: rgba(1, 10, 19, 0.72); }
|
|
126
148
|
.perf-update-box { width: 430px; box-sizing: border-box; padding: 22px 26px;
|
|
@@ -145,9 +167,10 @@ let coords = null;
|
|
|
145
167
|
const resolved = new Map();
|
|
146
168
|
let availableModules = BUNDLED_MODULES;
|
|
147
169
|
let pendingReload = false;
|
|
170
|
+
let searchQuery = "";
|
|
148
171
|
let runningVersion = version;
|
|
149
172
|
|
|
150
|
-
let state = { installed: {}, enabled: {}, options: {} };
|
|
173
|
+
let state = { installed: {}, enabled: {}, options: {}, masterLocked: false };
|
|
151
174
|
|
|
152
175
|
async function loadState() {
|
|
153
176
|
let saved = null;
|
|
@@ -160,7 +183,7 @@ async function loadState() {
|
|
|
160
183
|
saved = null;
|
|
161
184
|
}
|
|
162
185
|
if (saved?.installed && typeof saved.installed === "object") {
|
|
163
|
-
state = { installed: saved.installed, enabled: saved.enabled ?? {}, options: saved.options ?? {} };
|
|
186
|
+
state = { installed: saved.installed, enabled: saved.enabled ?? {}, options: saved.options ?? {}, masterLocked: saved.masterLocked === true };
|
|
164
187
|
return;
|
|
165
188
|
}
|
|
166
189
|
saveState();
|
|
@@ -172,6 +195,7 @@ function saveState() {
|
|
|
172
195
|
installed: state.installed,
|
|
173
196
|
enabled: state.enabled,
|
|
174
197
|
options: state.options,
|
|
198
|
+
masterLocked: state.masterLocked === true,
|
|
175
199
|
};
|
|
176
200
|
try {
|
|
177
201
|
localStorage.setItem(STATE_KEY, JSON.stringify(record));
|
|
@@ -524,16 +548,23 @@ function addStyle(name, css) {
|
|
|
524
548
|
return () => element.remove();
|
|
525
549
|
}
|
|
526
550
|
|
|
551
|
+
const LCU_TIMEOUT_MS = 10000;
|
|
552
|
+
|
|
527
553
|
function lcuGet(path, { ttl = 0 } = {}) {
|
|
528
554
|
const now = Date.now();
|
|
529
555
|
const cached = requestCache.get(path);
|
|
530
556
|
if (cached && (cached.pending || cached.expires > now)) return cached.promise;
|
|
531
557
|
|
|
532
558
|
const entry = { pending: true, expires: 0, promise: null };
|
|
533
|
-
|
|
559
|
+
const request = fetch(path, { headers: { Accept: "application/json" } }).then(async (response) => {
|
|
534
560
|
if (!response.ok) throw new Error(`LCU ${path} responded ${response.status}`);
|
|
535
561
|
return response.status === 204 ? null : response.json();
|
|
536
562
|
});
|
|
563
|
+
let timer = null;
|
|
564
|
+
const timeout = new Promise((_, reject) => {
|
|
565
|
+
timer = setTimeout(() => reject(new Error(`LCU ${path} timed out`)), LCU_TIMEOUT_MS);
|
|
566
|
+
});
|
|
567
|
+
entry.promise = Promise.race([request, timeout]).finally(() => clearTimeout(timer));
|
|
537
568
|
requestCache.set(path, entry);
|
|
538
569
|
entry.promise.then(
|
|
539
570
|
() => {
|
|
@@ -605,7 +636,7 @@ function buildOptionRow(mod, option, disabled, onChange) {
|
|
|
605
636
|
return row;
|
|
606
637
|
}
|
|
607
638
|
|
|
608
|
-
function buildChips(mod, option) {
|
|
639
|
+
function buildChips(mod, option, locked) {
|
|
609
640
|
const block = el("div", "perf-chip-block");
|
|
610
641
|
block.title = option.description ?? "";
|
|
611
642
|
if (option.label) block.append(el("div", "perf-chips-label", option.label));
|
|
@@ -615,6 +646,7 @@ function buildChips(mod, option) {
|
|
|
615
646
|
const chip = el("button", "perf-chip", item.label);
|
|
616
647
|
chip.type = "button";
|
|
617
648
|
chip.title = item.description ?? "";
|
|
649
|
+
chip.disabled = Boolean(locked);
|
|
618
650
|
if (getOption(mod.id, key)) chip.classList.add("perf-chip--on");
|
|
619
651
|
chip.addEventListener("click", () => {
|
|
620
652
|
setOption(mod.id, key, !getOption(mod.id, key), false);
|
|
@@ -626,17 +658,17 @@ function buildChips(mod, option) {
|
|
|
626
658
|
return block;
|
|
627
659
|
}
|
|
628
660
|
|
|
629
|
-
function buildOptions(mod) {
|
|
661
|
+
function buildOptions(mod, locked) {
|
|
630
662
|
const options = registry.get(mod.id)?.options ?? [];
|
|
631
663
|
if (options.length === 0) return null;
|
|
632
664
|
const wrap = el("div", "perf-opts");
|
|
633
665
|
for (const option of options) {
|
|
634
666
|
if (option.type === "chips") {
|
|
635
|
-
wrap.append(buildChips(mod, option));
|
|
667
|
+
wrap.append(buildChips(mod, option, locked));
|
|
636
668
|
continue;
|
|
637
669
|
}
|
|
638
670
|
if (!option.children?.length) {
|
|
639
|
-
wrap.append(buildOptionRow(mod, option));
|
|
671
|
+
wrap.append(buildOptionRow(mod, option, Boolean(locked)));
|
|
640
672
|
continue;
|
|
641
673
|
}
|
|
642
674
|
const group = el("div", "perf-opt-group");
|
|
@@ -647,12 +679,12 @@ function buildOptions(mod) {
|
|
|
647
679
|
const kids = el("div", "perf-opt-children");
|
|
648
680
|
const childInputs = [];
|
|
649
681
|
for (const child of option.children) {
|
|
650
|
-
const childRow = buildOptionRow(mod, child, !getOption(mod.id, option.key));
|
|
682
|
+
const childRow = buildOptionRow(mod, child, !getOption(mod.id, option.key) || Boolean(locked));
|
|
651
683
|
childInputs.push(childRow.querySelector("input"));
|
|
652
684
|
kids.append(childRow);
|
|
653
685
|
}
|
|
654
|
-
const master = buildOptionRow(mod, option,
|
|
655
|
-
for (const input of childInputs) input.disabled = !checked;
|
|
686
|
+
const master = buildOptionRow(mod, option, Boolean(locked), (checked) => {
|
|
687
|
+
for (const input of childInputs) input.disabled = !checked || Boolean(locked);
|
|
656
688
|
setOption(mod.id, option.key, checked, false);
|
|
657
689
|
});
|
|
658
690
|
const groupKey = `${mod.id}:${option.key}`;
|
|
@@ -674,12 +706,13 @@ function buildOptions(mod) {
|
|
|
674
706
|
return wrap;
|
|
675
707
|
}
|
|
676
708
|
|
|
677
|
-
function buildToggle(mod) {
|
|
709
|
+
function buildToggle(mod, locked) {
|
|
678
710
|
const label = el("label", "perf-toggle");
|
|
679
711
|
label.title = `Enable ${mod.name}`;
|
|
680
712
|
const input = el("input");
|
|
681
713
|
input.type = "checkbox";
|
|
682
714
|
input.checked = isPluginEnabled(mod.id);
|
|
715
|
+
input.disabled = Boolean(locked);
|
|
683
716
|
input.setAttribute("aria-label", `Enable ${mod.name}`);
|
|
684
717
|
input.addEventListener("change", () => setPluginEnabled(mod.id, input.checked));
|
|
685
718
|
label.append(input, el("span", "perf-box"));
|
|
@@ -711,20 +744,87 @@ function buildUninstallButton(mod) {
|
|
|
711
744
|
return button;
|
|
712
745
|
}
|
|
713
746
|
|
|
714
|
-
function
|
|
747
|
+
function installedModules() {
|
|
748
|
+
return getAvailableModules().filter(isInstalled);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function masterLocked() {
|
|
752
|
+
return installedModules().length > 0 && state.masterLocked === true;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function disableAllPlugins() {
|
|
756
|
+
state.masterLocked = true;
|
|
757
|
+
saveState();
|
|
758
|
+
refreshPanel();
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function enableAllPlugins() {
|
|
762
|
+
state.masterLocked = false;
|
|
763
|
+
saveState();
|
|
764
|
+
refreshPanel();
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function applySearchFilter(panel = document.getElementById(PANEL_ID)) {
|
|
768
|
+
if (!panel) return;
|
|
769
|
+
let shown = 0;
|
|
770
|
+
for (const card of panel.querySelectorAll(".perf-card")) {
|
|
771
|
+
const match = !searchQuery || (card.dataset.search ?? "").includes(searchQuery);
|
|
772
|
+
card.style.display = match ? "" : "none";
|
|
773
|
+
if (match) shown += 1;
|
|
774
|
+
}
|
|
775
|
+
const scroller = panel.querySelector("lol-uikit-scrollable");
|
|
776
|
+
let empty = panel.querySelector(".perf-no-match");
|
|
777
|
+
if (shown === 0 && getAvailableModules().length > 0) {
|
|
778
|
+
if (!empty && scroller) {
|
|
779
|
+
empty = el("div", "perf-empty perf-no-match", "No plugins match your search.");
|
|
780
|
+
scroller.append(empty);
|
|
781
|
+
}
|
|
782
|
+
} else if (empty) {
|
|
783
|
+
empty.remove();
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function buildTools() {
|
|
788
|
+
const tools = el("div", "perf-tools");
|
|
789
|
+
|
|
790
|
+
const search = el("input", "perf-search");
|
|
791
|
+
search.type = "search";
|
|
792
|
+
search.placeholder = "Search plugins...";
|
|
793
|
+
search.value = searchQuery;
|
|
794
|
+
search.setAttribute("aria-label", "Search plugins");
|
|
795
|
+
search.addEventListener("input", () => {
|
|
796
|
+
searchQuery = search.value.trim().toLowerCase();
|
|
797
|
+
applySearchFilter();
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
const master = el("button", "perf-btn perf-master", masterLocked() ? "Enable All" : "Disable All");
|
|
801
|
+
master.type = "button";
|
|
802
|
+
master.disabled = installedModules().length === 0;
|
|
803
|
+
master.addEventListener("click", () => {
|
|
804
|
+
if (masterLocked()) enableAllPlugins();
|
|
805
|
+
else disableAllPlugins();
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
tools.append(search, master);
|
|
809
|
+
return tools;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function buildCard(mod, locked) {
|
|
715
813
|
const installed = isInstalled(mod);
|
|
716
814
|
const card = el("div", installed ? "perf-card" : "perf-card perf-card--missing");
|
|
815
|
+
if (locked) card.classList.add("perf-card--locked");
|
|
816
|
+
card.dataset.search = `${mod.name} ${mod.description}`.toLowerCase();
|
|
717
817
|
|
|
718
818
|
const info = el("div", "perf-info");
|
|
719
819
|
const name = el("div", "perf-name", mod.name);
|
|
720
820
|
name.append(el("span", "perf-version", `v${registry.get(mod.id)?.version ?? mod.version}`));
|
|
721
821
|
info.append(name);
|
|
722
822
|
if (mod.description) info.append(el("div", "perf-desc", mod.description));
|
|
723
|
-
const optionList = buildOptions(mod);
|
|
823
|
+
const optionList = buildOptions(mod, locked);
|
|
724
824
|
if (optionList) info.append(optionList);
|
|
725
825
|
|
|
726
826
|
const actions = el("div", "perf-actions");
|
|
727
|
-
if (installed) actions.append(buildToggle(mod));
|
|
827
|
+
if (installed) actions.append(buildToggle(mod, locked));
|
|
728
828
|
actions.append(installed ? buildUninstallButton(mod) : buildInstallButton(mod));
|
|
729
829
|
|
|
730
830
|
card.append(info, actions);
|
|
@@ -734,6 +834,8 @@ function buildCard(mod) {
|
|
|
734
834
|
function buildPanel() {
|
|
735
835
|
const panel = el("div");
|
|
736
836
|
panel.id = PANEL_ID;
|
|
837
|
+
panel.append(buildTools());
|
|
838
|
+
|
|
737
839
|
const scroller = el("lol-uikit-scrollable");
|
|
738
840
|
panel.append(scroller);
|
|
739
841
|
|
|
@@ -742,7 +844,9 @@ function buildPanel() {
|
|
|
742
844
|
scroller.append(el("div", "perf-empty", "No plugins are available."));
|
|
743
845
|
return panel;
|
|
744
846
|
}
|
|
745
|
-
|
|
847
|
+
const locked = masterLocked();
|
|
848
|
+
for (const mod of modules) scroller.append(buildCard(mod, locked));
|
|
849
|
+
applySearchFilter(panel);
|
|
746
850
|
return panel;
|
|
747
851
|
}
|
|
748
852
|
|