perfectionist-plugins 2.0.0 → 2.4.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.
@@ -1,4 +1,4 @@
1
- export const version = "2.4.0";
1
+ export const version = "2.5.1";
2
2
 
3
3
  const PLUGIN_ID = "hide-unowned-skins";
4
4
  const STYLE_ID = "perfectionist-hide-unowned-skins-style";
@@ -18,7 +18,7 @@ const RETRY_MS = 5000;
18
18
  const STATE_KEY = "perfectionist-plugins:state";
19
19
 
20
20
  const CSS = `
21
- .${HIDDEN_CLASS} { display: none !important; margin: 0 !important; padding: 0 !important; border: none !important; }
21
+ .${HIDDEN_CLASS} { display: none !important; margin: 0 !important; padding: 0 !important; border: none !important; pointer-events: none !important; }
22
22
  [data-perf-dots] { justify-content: center !important; }
23
23
  `;
24
24
 
@@ -162,13 +162,13 @@ function installFetchFilter() {
162
162
 
163
163
  const wrapped = function (input, init) {
164
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
- : "";
165
+ typeof input === "string"
166
+ ? input
167
+ : typeof input?.url === "string"
168
+ ? input.url
169
+ : typeof input?.href === "string"
170
+ ? input.href
171
+ : "";
172
172
  const promise = original.call(this, input, init);
173
173
  if (!filterActive || url.split("?")[0].indexOf(CAROUSEL_URI) === -1) return promise;
174
174
  return promise.then((response) => {
@@ -224,8 +224,7 @@ function installXhrFilter() {
224
224
  const kept = filterSkinList(json);
225
225
  recordClientOrder(kept ?? json);
226
226
  if (kept) cache.filtered = JSON.stringify(kept);
227
- } catch {
228
- }
227
+ } catch {}
229
228
  }
230
229
  return cache.filtered;
231
230
  };
@@ -281,14 +280,16 @@ let selectInFlight = false;
281
280
  let sessionMisses = 0;
282
281
  let championMisses = 0;
283
282
  let renderObserver = null;
284
- const repackOriginals = new Map();
285
- const lastAppliedLeft = new Map();
286
- const skinSuffixCache = new Map();
283
+ let sessionActive = false;
287
284
  let repackTimer = null;
288
285
  let removeStyle = null;
289
286
  let lastSelectedDomIndex = -1;
290
287
  let navFixAt = 0;
291
288
  let lastSkipSignature = null;
289
+ let itemSkinIds = new WeakMap();
290
+ const repackOriginals = new Map();
291
+ const lastAppliedLeft = new Map();
292
+ const skinSuffixCache = new Map();
292
293
  const widthCache = new Map();
293
294
 
294
295
  function logError(scope, error) {
@@ -301,7 +302,8 @@ function skinSuffixOf(text) {
301
302
  return /Skins\/Base\//i.test(text) ? 0 : null;
302
303
  }
303
304
 
304
- function readSkinId(item) {
305
+ function skinIdFromImages(item) {
306
+ if (currentChampionId === null) return null;
305
307
  for (const img of item.querySelectorAll("img[src]")) {
306
308
  const src = img.getAttribute("src");
307
309
  let suffix = skinSuffixCache.get(src);
@@ -315,6 +317,25 @@ function readSkinId(item) {
315
317
  return suffix === null ? null : currentChampionId * 1000 + suffix;
316
318
  }
317
319
 
320
+ function readSkinId(item) {
321
+ return itemSkinIds.get(item) ?? skinIdFromImages(item);
322
+ }
323
+
324
+ function effectiveOrder() {
325
+ return clientSkinOrder.length > 0 ? clientSkinOrder : carouselOrder;
326
+ }
327
+
328
+ function mapCarouselToItems(items) {
329
+ const order = effectiveOrder();
330
+ itemSkinIds = new WeakMap();
331
+ if (order.length === 0 || order.length !== items.length) return false;
332
+ items.forEach((item, index) => {
333
+ const id = order[index];
334
+ if (typeof id === "number") itemSkinIds.set(item, id);
335
+ });
336
+ return true;
337
+ }
338
+
318
339
  function hideTargetFor(item) {
319
340
  let target = item;
320
341
  for (let depth = 0; depth < 2; depth++) {
@@ -366,6 +387,40 @@ function guardSelection(items) {
366
387
  return true;
367
388
  }
368
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
+
369
424
  function repackVisibleItems(items) {
370
425
  pruneRepack();
371
426
  const visible = items.filter((item) => !item.classList.contains(HIDDEN_CLASS));
@@ -374,32 +429,15 @@ function repackVisibleItems(items) {
374
429
  return;
375
430
  }
376
431
 
377
- const selectedIndex = items.findIndex((item) => item.classList.contains("skin-selection-item-selected"));
432
+ const selectedIndex = items.findIndex((item) => item.classList.contains(SELECTED_CLASS));
378
433
  let anchor = selectedIndex >= 0 ? items[selectedIndex] : null;
379
434
  if (!anchor && selectedSkinId !== null) {
380
435
  anchor = visible.find((item) => readSkinId(item) === selectedSkinId) ?? null;
381
436
  }
382
437
  if (!anchor) anchor = visible[Math.floor(visible.length / 2)];
383
438
  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;
439
+ refreshOriginal(anchor);
440
+ const anchorLeft = originalLeftOf(anchor);
403
441
 
404
442
  const setLeft = (item, x) => {
405
443
  const want = `${x}px`;
@@ -416,19 +454,23 @@ function repackVisibleItems(items) {
416
454
  lastAppliedLeft.set(item, want);
417
455
  };
418
456
 
419
- items.forEach((item, index) => {
420
- if (item === anchor || item.classList.contains(HIDDEN_CLASS)) return;
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
+ }
421
465
 
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
- });
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
+ }
432
474
  }
433
475
 
434
476
  function restoreRepack() {
@@ -446,6 +488,7 @@ function restoreRepack() {
446
488
 
447
489
  function applyAll() {
448
490
  const items = [...document.querySelectorAll(SKIN_ITEM_SELECTOR)];
491
+ mapCarouselToItems(items);
449
492
  let hidden = 0;
450
493
  items.forEach((item) => {
451
494
  const skinId = currentChampionId === null ? null : readSkinId(item);
@@ -457,9 +500,10 @@ function applyAll() {
457
500
 
458
501
  const dots = [...document.querySelectorAll(DOT_SELECTOR)];
459
502
  let dotsHidden = 0;
460
- const dotsMapped = carouselOrder.length > 0 && dots.length === carouselOrder.length;
503
+ const dotOrder = effectiveOrder();
504
+ const dotsMapped = dotOrder.length > 0 && dots.length === dotOrder.length;
461
505
  dots.forEach((dot, index) => {
462
- const skinId = dotsMapped ? carouselOrder[index] : undefined;
506
+ const skinId = dotsMapped ? dotOrder[index] : undefined;
463
507
  const hide = ownedSkinIds !== null && skinId !== undefined && allSkinIds !== null && allSkinIds.has(skinId) && !ownedSkinIds.has(skinId);
464
508
  if (hide) dotsHidden++;
465
509
  dot.classList.toggle(HIDDEN_CLASS, hide);
@@ -481,6 +525,27 @@ function scheduleRepack() {
481
525
  repackTimer = setTimeout(runRepack, REPACK_SETTLE_MS);
482
526
  }
483
527
 
528
+ function startRenderObserver() {
529
+ if (!sessionActive || renderObserver || !api) return;
530
+ renderObserver = new MutationObserver(() => {
531
+ ensureSourceFilter();
532
+ const item = document.querySelector(SKIN_ITEM_SELECTOR);
533
+ if (!item && !document.querySelector(DOT_SELECTOR)) return;
534
+ applyAllGuarded();
535
+ if (item) scheduleRefresh();
536
+ });
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
+
484
549
  function runRepack() {
485
550
  repackTimer = null;
486
551
  renderObserver?.disconnect();
@@ -490,7 +555,7 @@ function runRepack() {
490
555
  if (ownedSkinIds !== null && hidden) repackVisibleItems(items);
491
556
  else restoreRepack();
492
557
  } finally {
493
- renderObserver?.observe(document.documentElement ?? document, OBSERVER_OPTIONS);
558
+ startRenderObserver();
494
559
  }
495
560
  }
496
561
 
@@ -499,7 +564,7 @@ function applyAllGuarded() {
499
564
  try {
500
565
  applyAll();
501
566
  } finally {
502
- renderObserver?.observe(document.documentElement ?? document, OBSERVER_OPTIONS);
567
+ startRenderObserver();
503
568
  }
504
569
  }
505
570
 
@@ -656,11 +721,15 @@ function scheduleRefresh() {
656
721
  function onSessionPush(raw) {
657
722
  const msg = raw ?? {};
658
723
  if (msg.eventType === "Delete") {
724
+ sessionActive = false;
725
+ stopRenderObserver();
659
726
  resetState();
660
727
  return;
661
728
  }
662
729
  const s = msg.data ?? msg;
663
730
  if (!s?.myTeam) return;
731
+ sessionActive = true;
732
+ startRenderObserver();
664
733
  updateForSession(s, null);
665
734
  }
666
735
 
@@ -671,11 +740,17 @@ async function pollSession() {
671
740
  try {
672
741
  const session = await api.lcu.get(SESSION_URI);
673
742
  sessionMisses = 0;
743
+ sessionActive = true;
744
+ startRenderObserver();
674
745
  const selection = await api.lcu.get(SELECTION_URI).catch(() => null);
675
746
  updateForSession(session, selection);
676
747
  } catch {
677
748
  sessionMisses++;
678
- if (sessionMisses >= 3) resetState();
749
+ if (sessionMisses >= 3) {
750
+ sessionActive = false;
751
+ stopRenderObserver();
752
+ resetState();
753
+ }
679
754
  } finally {
680
755
  pollInFlight = false;
681
756
  }
@@ -684,17 +759,9 @@ async function pollSession() {
684
759
  function onEnable(utils) {
685
760
  api = utils;
686
761
  filterActive = true;
762
+ sessionActive = false;
687
763
  removeStyle = api.addStyle(STYLE_ID, CSS);
688
764
 
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
- });
696
- renderObserver.observe(document.documentElement ?? document, OBSERVER_OPTIONS);
697
-
698
765
  window.addEventListener("resize", onWindowResize);
699
766
  unsubscribe = api.subscribe(SESSION_URI, onSessionPush);
700
767
  pollSession();
@@ -708,9 +775,9 @@ function onDisable() {
708
775
  refreshTimer = null;
709
776
  clearTimeout(repackTimer);
710
777
  repackTimer = null;
711
- renderObserver?.disconnect();
778
+ sessionActive = false;
779
+ stopRenderObserver();
712
780
  removeStyle?.();
713
- renderObserver = null;
714
781
  removeStyle = null;
715
782
  filterActive = false;
716
783
 
package/package.json CHANGED
@@ -1,37 +1,36 @@
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
- }
1
+ {
2
+ "name": "perfectionist-plugins",
3
+ "version": "2.4.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
+ ],
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/SrKing-9812/Perfectionist-Plugins.git"
29
+ },
30
+ "keywords": [
31
+ "pengu-loader",
32
+ "league-of-legends",
33
+ "plugin"
34
+ ],
35
+ "author": "Sr. King"
36
+ }
@@ -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 = "2.0.0";
9
+ export const version = "2.4.0";
10
10
 
11
11
  const PKG = "perfectionist-plugins";
12
12
  const CDN = "https://cdn.jsdelivr.net/npm";
@@ -26,7 +26,7 @@ const BUNDLED_MODULES = [
26
26
  id: "better-client",
27
27
  file: "better-client.js",
28
28
  name: "BetterClient",
29
- version: "1.2.0",
29
+ version: "1.7.0",
30
30
  description: "Debloats the client: blocks telemetry, merges duplicate requests, caches immutable data, decodes images asynchronously and fixes several bugs.",
31
31
  },
32
32
  {
@@ -40,14 +40,14 @@ const BUNDLED_MODULES = [
40
40
  id: "unlock-profile-skins",
41
41
  file: "unlock-profile-skins.js",
42
42
  name: "Unlock Profile Skins",
43
- version: "1.2.1",
43
+ version: "1.2.4",
44
44
  description: "Unlocks every skin in the Set profile background window.",
45
45
  },
46
46
  {
47
47
  id: "hide-unowned-skins",
48
48
  file: "hide-unowned-skins.js",
49
49
  name: "Hide Unowned Skins",
50
- version: "2.4.0",
50
+ version: "2.5.1",
51
51
  description: "In champion select, only shows the skins you own for the picked champion.",
52
52
  },
53
53
  {
@@ -127,6 +127,9 @@ const STYLE_CSS = `
127
127
  #${PANEL_ID} .perf-chip:hover { color: #f0e6d2; border-color: #c89b3c; }
128
128
  #${PANEL_ID} .perf-chip--on { color: #010a13; background: #c89b3c; border-color: #c89b3c; }
129
129
  #${PANEL_ID} .perf-chip--on:hover { background: #f0e6d2; }
130
+ #${PANEL_ID} .perf-info { margin-top: 11px; padding-top: 8px; border-top: 1px solid rgba(120, 90, 40, 0.35); }
131
+ #${PANEL_ID} .perf-info-line { font-size: 10px; line-height: 16px; letter-spacing: 0.2px; color: #a09b8c;
132
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
130
133
 
131
134
  #${PANEL_ID} .perf-tools { display: flex; align-items: center; gap: 10px; flex: 0 0 auto; margin-bottom: 14px; }
132
135
  #${PANEL_ID} .perf-search { flex: 1 1 auto; min-width: 0; box-sizing: border-box;
@@ -341,6 +344,14 @@ function importModule(url) {
341
344
  return coords?.importModule ? coords.importModule(url) : import(url);
342
345
  }
343
346
 
347
+ // Lets a feature module (BetterClient → Pages Remake) pull its code only when enabled.
348
+ // A local override keeps the dev harness on freshly edited files instead of the CDN copy.
349
+ function loadPluginFile(file) {
350
+ const local = typeof window !== "undefined" ? window.PerfectionistLocalBase : null;
351
+ const url = local ? new URL(file, local).href : moduleUrl(file);
352
+ return importModule(url);
353
+ }
354
+
344
355
  async function installModule(mod) {
345
356
  state.installed[mod.id] = true;
346
357
  delete state.enabled[mod.id];
@@ -636,6 +647,17 @@ function buildOptionRow(mod, option, disabled, onChange) {
636
647
  return row;
637
648
  }
638
649
 
650
+ function buildInfo(option) {
651
+ const block = el("div", "perf-info");
652
+ if (option.label) block.append(el("div", "perf-chips-label", option.label));
653
+ let lines = [];
654
+ try {
655
+ lines = option.lines?.() ?? [];
656
+ } catch {}
657
+ for (const line of lines) block.append(el("div", "perf-info-line", String(line)));
658
+ return block;
659
+ }
660
+
639
661
  function buildChips(mod, option, locked) {
640
662
  const block = el("div", "perf-chip-block");
641
663
  block.title = option.description ?? "";
@@ -658,6 +680,28 @@ function buildChips(mod, option, locked) {
658
680
  return block;
659
681
  }
660
682
 
683
+ function buildAction(option, locked) {
684
+ const button = el("button", "perf-btn", option.label);
685
+ button.type = "button";
686
+ button.title = option.description ?? "";
687
+ button.disabled = Boolean(locked);
688
+ button.addEventListener("click", async () => {
689
+ const original = button.textContent;
690
+ button.disabled = true;
691
+ try {
692
+ await option.onClick?.();
693
+ button.textContent = "Copied";
694
+ } catch {
695
+ button.textContent = "Copy failed";
696
+ }
697
+ setTimeout(() => {
698
+ button.textContent = original;
699
+ button.disabled = Boolean(locked);
700
+ }, 1600);
701
+ });
702
+ return button;
703
+ }
704
+
661
705
  function buildOptions(mod, locked) {
662
706
  const options = registry.get(mod.id)?.options ?? [];
663
707
  if (options.length === 0) return null;
@@ -667,6 +711,14 @@ function buildOptions(mod, locked) {
667
711
  wrap.append(buildChips(mod, option, locked));
668
712
  continue;
669
713
  }
714
+ if (option.type === "info") {
715
+ wrap.append(buildInfo(option));
716
+ continue;
717
+ }
718
+ if (option.type === "action") {
719
+ wrap.append(buildAction(option, locked));
720
+ continue;
721
+ }
670
722
  if (!option.children?.length) {
671
723
  wrap.append(buildOptionRow(mod, option, Boolean(locked)));
672
724
  continue;
@@ -1020,6 +1072,7 @@ const utils = {
1020
1072
  getAvailableModules,
1021
1073
  installModule,
1022
1074
  uninstallModule,
1075
+ loadPluginFile,
1023
1076
  checkForUpdates,
1024
1077
  watch,
1025
1078
  waitFor,