perfectionist-plugins 2.4.6 → 2.4.8

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/auto-accept.js CHANGED
@@ -33,8 +33,8 @@ let attempts = 0;
33
33
  let unsubscribes = [];
34
34
  let removeStyle = null;
35
35
  let domObserver = null;
36
- let domTimer = null;
37
-
36
+ let domTimer = null;
37
+
38
38
  function enabled() {
39
39
  try {
40
40
  return api?.getOption?.(PLUGIN_ID, ACCEPT_KEY, true) === true;
@@ -99,8 +99,8 @@ async function acceptNow() {
99
99
  } finally {
100
100
  sending = false;
101
101
  }
102
- }
103
-
102
+ }
103
+
104
104
  function onPhase(raw) {
105
105
  const phase = unwrap(raw);
106
106
  if (typeof phase !== "string") return;
@@ -148,8 +148,8 @@ function toggle() {
148
148
  syncButton();
149
149
  if (next) acceptNow();
150
150
  else resetReadyCheck();
151
- }
152
-
151
+ }
152
+
153
153
  function buildButton() {
154
154
  const root = document.createElement("div");
155
155
  root.id = ROOT_ID;
@@ -165,8 +165,8 @@ function buildButton() {
165
165
  event.preventDefault();
166
166
  event.stopPropagation();
167
167
  toggle();
168
- });
169
-
168
+ });
169
+
170
170
  root.append(option);
171
171
  return root;
172
172
  }
package/ban-blocker.js CHANGED
@@ -1,4 +1,4 @@
1
- export const version = "3.6.0";
1
+ export const version = "3.6.6";
2
2
 
3
3
  const PLUGIN_ID = "ban-blocker";
4
4
  const STYLE_ID = "perfectionist-ban-blocker-style";
@@ -28,7 +28,8 @@ const CSS = `
28
28
  right: -2px !important;
29
29
  left: auto !important;
30
30
  bottom: auto !important;
31
- z-index: 2 !important;
31
+ z-index: 50 !important;
32
+ pointer-events: auto !important;
32
33
  }
33
34
  .swap-button-component.${BTN_CLASS} .icon {
34
35
  -webkit-mask-image: url("${BAN_GLYPH}") !important;
@@ -65,11 +66,13 @@ let uiScheduled = false;
65
66
  let lastDetect = -Infinity;
66
67
 
67
68
  const enabled = new Map();
69
+ const blockedCellIds = new Set();
68
70
  const alias = new Map();
69
71
  const fetchedNames = new Set();
70
72
  const playerNames = new Map();
71
73
  const cellToPlayer = new Map();
72
74
  const buttons = new Map();
75
+ const swapSeen = new Set();
73
76
  const failedUntil = new Map();
74
77
  const failCount = new Map();
75
78
  let rows = [];
@@ -159,10 +162,19 @@ function linkPlayer(p, rowName, cellId) {
159
162
  return root;
160
163
  }
161
164
 
165
+ function pickStarted(s) {
166
+ const actions = s?.actions?.flat?.() ?? [];
167
+ return actions.some((a) => a.type === "pick" && (a.isInProgress || a.completed));
168
+ }
169
+
162
170
  function banPending(s) {
163
- const bans = (s?.actions?.flat?.() ?? []).filter((a) => a.type === "ban");
171
+ const actions = s?.actions?.flat?.() ?? [];
172
+ const bans = actions.filter((a) => a.type === "ban");
164
173
  if (!bans.length) return true;
165
- return bans.some((a) => !a.completed);
174
+ if (bans.some((a) => !a.completed)) return true;
175
+ const phase = s?.timer?.phase;
176
+ if (phase && phase !== "BAN_PICK") return false;
177
+ return !pickStarted(s);
166
178
  }
167
179
 
168
180
  function pump() {
@@ -222,7 +234,9 @@ function decide(s) {
222
234
  }
223
235
  myActionId = myPick ? myPick.id : null;
224
236
 
225
- const blockedCells = new Set(m.filter((p) => isBlocked(cellToPlayer.get(p.cellId))).map((p) => p.cellId));
237
+ const blockedCells = new Set(
238
+ m.filter((p) => blockedCellIds.has(p.cellId) || isBlocked(cellToPlayer.get(p.cellId))).map((p) => p.cellId),
239
+ );
226
240
 
227
241
  let threat = 0;
228
242
  for (const a of actions) {
@@ -254,6 +268,8 @@ function onSessionGone() {
254
268
  lastSession = null;
255
269
  goneAway = true;
256
270
  uiVisible = false;
271
+ blockedCellIds.clear();
272
+ swapSeen.clear();
257
273
  removeButtons();
258
274
  if (!goneTimer) {
259
275
  goneTimer = setTimeout(() => {
@@ -367,22 +383,29 @@ function bindRowsToTeam(session) {
367
383
  const selfIdx = list.findIndex((r) => isSelfRow(r));
368
384
  const selfCellIdx =
369
385
  session?.localPlayerCellId != null ? cells.findIndex((c) => c.cellId === session.localPlayerCellId) : -1;
386
+
387
+ // The DOM can label allied rows 0-4 while the session uses cell IDs 5-9 (red side).
388
+ // If every row still has a unique matching cell, slot IDs are reliable; otherwise
389
+ // align by the local row's position in myTeam, then fall back to row order.
390
+ const bySlot = list.map((r) =>
391
+ r.slotId != null ? cells.find((c) => c.cellId === r.slotId) ?? null : null,
392
+ );
393
+ const slotMapComplete =
394
+ bySlot.every(Boolean) && new Set(bySlot.map((c) => c.cellId)).size === list.length;
370
395
  let pick;
371
- if (selfIdx >= 0 && selfCellIdx >= 0) {
372
- const selfSlot = list[selfIdx].slotId;
373
- if (selfSlot != null && selfSlot === cells[selfCellIdx].cellId) {
374
- pick = (r) => (r.slotId != null ? cells.find((c) => c.cellId === r.slotId) : null);
375
- } else if (selfIdx === selfCellIdx) {
376
- pick = (r) => (r.slotId != null ? cells[r.slotId] : null) ?? cells[r.index];
377
- } else {
378
- pick = () => null;
379
- }
396
+ if (slotMapComplete) {
397
+ pick = (r) => bySlot[r.index] ?? null;
380
398
  } else {
381
- pick = (r) => (r.slotId != null ? cells.find((c) => c.cellId === r.slotId) : null);
399
+ const shift = selfIdx >= 0 && selfCellIdx >= 0 ? selfCellIdx - selfIdx : 0;
400
+ pick = (r) => cells[r.index + shift] ?? null;
382
401
  }
383
402
 
384
403
  for (const r of list) {
385
- const cell = pick(r) ?? cells.find((c) => nameMatches(r.name, keyOf(c)));
404
+ const rowName = norm(r.name);
405
+ const cell =
406
+ pick(r) ??
407
+ cells.find((c) => rowName && (norm(c.summonerName) === rowName || payloadName(c) === rowName)) ??
408
+ cells.find((c) => nameMatches(r.name, keyOf(c)));
386
409
  if (cell) {
387
410
  r.cell = cell.cellId;
388
411
  r.player = linkPlayer(cell, r.name, cell.cellId);
@@ -399,23 +422,56 @@ function stopEvent(e) {
399
422
  e.preventDefault();
400
423
  }
401
424
 
425
+ function cellForRow(r) {
426
+ if (!r) return null;
427
+ if (r.cell != null) return r.cell;
428
+ const cells = lastSession?.myTeam ?? [];
429
+ if (!cells.length) return r.slotId ?? null;
430
+ const direct = r.slotId != null ? cells.find((c) => c.cellId === r.slotId) : null;
431
+ if (direct) return direct.cellId;
432
+ const selfIdx = rows.findIndex((x) => isSelfRow(x));
433
+ const selfCellIdx =
434
+ lastSession?.localPlayerCellId != null
435
+ ? cells.findIndex((c) => c.cellId === lastSession.localPlayerCellId)
436
+ : -1;
437
+ const shift = selfIdx >= 0 && selfCellIdx >= 0 ? selfCellIdx - selfIdx : 0;
438
+ return cells[r.index + shift]?.cellId ?? cells[r.index]?.cellId ?? null;
439
+ }
440
+
441
+ function badgeCell(btn) {
442
+ const rowEl = btn.closest(".summoner-object");
443
+ const r = rowEl ? rows.find((x) => x.el === rowEl) : rows[Number(btn.dataset.slot)];
444
+ const cell = cellForRow(r) ?? (btn.dataset.cell ? Number(btn.dataset.cell) : null);
445
+ return cell == null ? null : Number(cell);
446
+ }
447
+
402
448
  function playerForBadge(btn) {
403
449
  const rowEl = btn.closest(".summoner-object");
404
450
  const r = rowEl ? rows.find((x) => x.el === rowEl) : rows[Number(btn.dataset.slot)];
405
- const byCell = btn.dataset.cell ? cellToPlayer.get(Number(btn.dataset.cell)) ?? null : null;
451
+ const cell = badgeCell(btn);
452
+ const member = cell == null ? null : (lastSession?.myTeam ?? []).find((p) => p.cellId === cell);
453
+ if (member) {
454
+ const id = linkPlayer(member, r?.name, cell);
455
+ cellToPlayer.set(cell, id);
456
+ return canonOf(id);
457
+ }
458
+ const byCell = cell == null ? null : cellToPlayer.get(cell) ?? null;
406
459
  return canonOf(byCell ?? r?.player ?? null);
407
460
  }
408
461
 
409
462
  function toggle(btn) {
410
- let id = playerForBadge(btn);
411
- if (id == null && lastSession) {
463
+ if (lastSession) {
412
464
  bindRowsToTeam(lastSession);
413
- id = playerForBadge(btn);
465
+ decide(lastSession);
414
466
  }
415
- if (id == null) return;
467
+ const cell = badgeCell(btn);
416
468
  const myCell = lastSession?.localPlayerCellId;
417
- if (myCell != null && btn.dataset.cell && Number(btn.dataset.cell) === myCell) return;
418
- enabled.set(id, !isBlocked(id));
469
+ if (cell == null || (myCell != null && cell === myCell)) return;
470
+ const next = !blockedCellIds.has(cell);
471
+ if (next) blockedCellIds.add(cell);
472
+ else blockedCellIds.delete(cell);
473
+ const id = playerForBadge(btn);
474
+ if (id != null) enabled.set(id, next);
419
475
  renderButtons();
420
476
  if (lastSession) {
421
477
  decide(lastSession);
@@ -449,19 +505,41 @@ function isSelfRow(r) {
449
505
  return lastSession?.localPlayerCellId != null && (r.cell ?? r.slotId) === lastSession.localPlayerCellId;
450
506
  }
451
507
 
508
+ function rowKey(r) {
509
+ return String(r.cell ?? r.slotId ?? r.index);
510
+ }
511
+
512
+ function buttonKey(r) {
513
+ const cell = r.cell ?? r.slotId;
514
+ return cell == null ? `row:${r.index}` : `cell:${cell}`;
515
+ }
516
+
452
517
  function nativeSwapIn(circle) {
453
- return !!circle.querySelector(".swap-button-component:not([data-bb])");
518
+ if (!circle) return false;
519
+ return [...circle.querySelectorAll(".swap-button-component")].some((el) => el.dataset.bb !== "1");
454
520
  }
455
521
 
456
522
  function ensureButtons() {
457
523
  const list = getRows();
524
+ const active = new Set();
458
525
  for (const r of list) {
526
+ if (!r.circle || !r.circle.isConnected) r.circle = r.el.querySelector(".champion-icon-container");
459
527
  if (!r.circle || !r.circle.isConnected) continue;
460
- let btn = buttons.get(r.index);
461
- if (isSelfRow(r) || !nativeSwapIn(r.circle)) {
528
+ const swapKey = rowKey(r);
529
+ const key = buttonKey(r);
530
+ let btn = buttons.get(key);
531
+ if (isSelfRow(r)) {
532
+ if (btn) {
533
+ btn.remove();
534
+ buttons.delete(key);
535
+ }
536
+ continue;
537
+ }
538
+ if (nativeSwapIn(r.circle)) swapSeen.add(swapKey);
539
+ if (!swapSeen.has(swapKey)) {
462
540
  if (btn) {
463
541
  btn.remove();
464
- buttons.delete(r.index);
542
+ buttons.delete(key);
465
543
  }
466
544
  continue;
467
545
  }
@@ -470,16 +548,17 @@ function ensureButtons() {
470
548
  for (const orphan of r.circle.querySelectorAll(`:scope > .${BTN_CLASS}`)) orphan.remove();
471
549
  btn = buildBadge(r.index);
472
550
  r.circle.appendChild(btn);
473
- buttons.set(r.index, btn);
551
+ buttons.set(key, btn);
474
552
  }
553
+ active.add(key);
475
554
  btn.dataset.slot = String(r.index);
476
- const cell = r.cell ?? r.slotId;
555
+ const cell = cellForRow(r);
477
556
  btn.dataset.cell = cell == null ? "" : String(cell);
478
557
  }
479
- for (const [slot, btn] of [...buttons]) {
480
- if (!list[slot] || !btn.isConnected) {
558
+ for (const [key, btn] of [...buttons]) {
559
+ if (!active.has(key) || !btn.isConnected) {
481
560
  btn.remove();
482
- buttons.delete(slot);
561
+ buttons.delete(key);
483
562
  }
484
563
  }
485
564
  }
@@ -488,7 +567,8 @@ function renderButtons() {
488
567
  const hasSession = !!lastSession?.myTeam;
489
568
  for (const [, btn] of buttons) {
490
569
  const id = playerForBadge(btn);
491
- const on = id != null && isBlocked(id);
570
+ const cell = badgeCell(btn);
571
+ const on = (cell != null && blockedCellIds.has(cell)) || (id != null && isBlocked(id));
492
572
  btn.classList.toggle("on", on);
493
573
  btn.classList.toggle("unresolved", id == null && hasSession);
494
574
  const title = on ? "Blocked — click to allow their bans" : "Block this teammate's bans";
@@ -531,6 +611,7 @@ function update() {
531
611
  renderButtons();
532
612
  } else if (uiVisible) {
533
613
  uiVisible = false;
614
+ swapSeen.clear();
534
615
  removeButtons();
535
616
  }
536
617
  }
@@ -605,6 +686,7 @@ function onDisable() {
605
686
  observer = null;
606
687
 
607
688
  uiVisible = false;
689
+ swapSeen.clear();
608
690
  goneAway = true;
609
691
  target = undefined;
610
692
  sent = 0;
@@ -612,6 +694,7 @@ function onDisable() {
612
694
  failCount.clear();
613
695
  cellToPlayer.clear();
614
696
  enabled.clear();
697
+ blockedCellIds.clear();
615
698
  playerNames.clear();
616
699
  alias.clear();
617
700
  fetchedNames.clear();
package/dodge-helper.js CHANGED
@@ -1,5 +1,5 @@
1
1
  const PLUGIN_ID = "dodge-helper";
2
- export const version = "1.4.3";
2
+ export const version = "1.4.14";
3
3
 
4
4
  const PHASE_URI = "/lol-gameflow/v1/gameflow-phase";
5
5
  const SESSION_URI = "/lol-gameflow/v1/session";
@@ -7,8 +7,8 @@ const CHAMP_SELECT_URI = "/lol-champ-select/v1/session";
7
7
  const TIMER_URI = "/lol-champ-select/v1/session/timer";
8
8
  const SUMMONER_URI = "/lol-summoner/v1/current-summoner";
9
9
  const RANKED_STATS_URI = "/lol-ranked/v1/current-ranked-stats";
10
- const DODGE_URI = "/lol-gameflow/v1/session/dodge";
11
- const QUIT_URI = "/lol-lobby-team-builder/champ-select/v1/session/quit";
10
+ const QUIT_V2_URI = '/lol-login/v1/session/invoke?destination=lcdsServiceProxy&method=call&args=["","teambuilder-draft","quitV2",""]';
11
+ const CUSTOM_CANCEL_URI = "/lol-lobby/v1/lobby/custom/cancel-champ-select";
12
12
  const STORE_KEY = "perfectionist:dodge-helper:accounts:v1";
13
13
  const PENDING_KEY = "perfectionist:dodge-helper:pending:v1";
14
14
  const DAY_MS = 24 * 60 * 60 * 1000;
@@ -43,15 +43,30 @@ const GAME_PHASES = new Set([
43
43
 
44
44
  const CSS = `
45
45
  #${LAYER_ID} {
46
- position: fixed !important; inset: 0 !important; z-index: 99999 !important;
46
+ position: fixed !important; inset: 0 !important; z-index: 100 !important;
47
47
  pointer-events: none !important;
48
48
  }
49
49
  #${ROOT_ID} {
50
50
  position: fixed !important; display: flex !important; align-items: flex-end !important;
51
- justify-content: center !important; width: 125px !important; height: 32px !important;
51
+ justify-content: center !important; width: 114px !important; height: 32px !important;
52
52
  pointer-events: auto !important; z-index: 1 !important;
53
53
  }
54
- #${ROOT_ID} lol-uikit-flat-button { width: 125px !important; height: 32px !important; }
54
+ #${ROOT_ID} .perf-dodge-button {
55
+ width: 100% !important; height: 32px !important; box-sizing: border-box !important;
56
+ appearance: none !important; -webkit-appearance: none !important; border-radius: 0 !important;
57
+ padding: 0 !important; border: 1px solid #785a28 !important;
58
+ background: linear-gradient(to bottom, #1e2328 0%, #11161c 55%, #0a0e12 100%) !important;
59
+ color: #cdbe91 !important; font-family: var(--font-display, "Beaufort for LOL", serif) !important;
60
+ font-size: 13px !important; font-weight: 700 !important; letter-spacing: 1px !important;
61
+ text-transform: uppercase !important; cursor: pointer !important;
62
+ text-shadow: 0 1px 2px #000 !important;
63
+ box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.55), inset 0 1px 0 rgba(240, 230, 210, 0.12) !important;
64
+ }
65
+ #${ROOT_ID} .perf-dodge-button:hover {
66
+ color: #f0e6d2 !important; border-color: #c89b3c !important;
67
+ background: linear-gradient(to bottom, #2a3038 0%, #192027 55%, #0e141a 100%) !important;
68
+ }
69
+ #${ROOT_ID} .perf-dodge-button:disabled { opacity: 0.6 !important; cursor: default !important; }
55
70
  #${ROOT_ID} .perf-dodge-cost {
56
71
  position: absolute !important; left: 50% !important; bottom: calc(100% + 4px) !important;
57
72
  transform: translateX(-50%) !important; box-sizing: border-box !important;
@@ -399,24 +414,20 @@ function build() {
399
414
  const cost = document.createElement("div");
400
415
  cost.id = COST_ID;
401
416
  cost.className = "perf-dodge-cost";
402
- const button = document.createElement("lol-uikit-flat-button");
417
+ const button = document.createElement("button");
418
+ button.type = "button";
403
419
  button.id = BUTTON_ID;
420
+ button.className = "perf-dodge-button";
421
+ button.textContent = "DODGE";
404
422
  bindButton(button);
405
423
  root.append(cost, button);
406
424
  return root;
407
425
  }
408
426
 
409
427
  function bindButton(button) {
410
- if (!button) return;
411
- if (!button.__perfClickBound) {
412
- button.__perfClickBound = true;
413
- button.addEventListener("click", onButtonClick, true);
414
- }
415
- const inner = button.shadowRoot?.querySelector(".lol-uikit-flat-button");
416
- if (inner && !inner.__perfClickBound) {
417
- inner.__perfClickBound = true;
418
- inner.addEventListener("click", onButtonClick);
419
- }
428
+ if (!button || button.__perfClickBound) return;
429
+ button.__perfClickBound = true;
430
+ button.addEventListener("click", onButtonClick);
420
431
  }
421
432
 
422
433
  function onButtonClick(event) {
@@ -470,19 +481,30 @@ function positionRoot() {
470
481
  const root = document.getElementById(ROOT_ID);
471
482
  if (!root) return;
472
483
  const footer = visibleFooter();
473
- const width = root.offsetWidth || 125;
474
484
  const height = root.offsetHeight || 32;
475
- if (footer && footer.left > width + 24) {
476
- root.style.left = `${Math.round(footer.left - width - 10)}px`;
477
- root.style.top = `${Math.round(Math.min(window.innerHeight - height - 8, Math.max(8, footer.bottom - height)))}px`;
478
- root.style.right = "auto";
485
+ if (footer) {
486
+ const footerElement = document.querySelector(FOOTER_SELECTOR);
487
+ const chat = footerElement?.querySelector("lol-social-chat-toggle-button");
488
+ const voice = footerElement?.querySelector(".champ-select-voice-button-wrapper");
489
+ const chatRect = chat?.getBoundingClientRect();
490
+ const voiceRect = voice?.getBoundingClientRect();
491
+ const left = chatRect?.left;
492
+ const rightEdge = voiceRect?.right ?? footer.right;
493
+ const width = left && rightEdge > left ? Math.round(rightEdge - left) : 114;
494
+ const right = Math.round(Math.max(8, window.innerWidth - rightEdge));
495
+ const top = Math.round(Math.max(8, footer.top - height - 8));
496
+ root.style.width = `${width}px`;
497
+ root.style.left = "auto";
498
+ root.style.right = `${right}px`;
499
+ root.style.top = `${top}px`;
479
500
  root.style.bottom = "auto";
480
501
  return;
481
502
  }
503
+ root.style.width = "114px";
482
504
  root.style.left = "auto";
483
505
  root.style.top = "auto";
484
- root.style.right = "145px";
485
- root.style.bottom = "21px";
506
+ root.style.right = "20px";
507
+ root.style.bottom = "76px";
486
508
  }
487
509
 
488
510
  function schedulePosition() {
@@ -698,14 +720,30 @@ function handleTimer(raw) {
698
720
  syncTimer(raw);
699
721
  }
700
722
 
723
+ async function requestDodge(uri) {
724
+ const options = { method: "POST" };
725
+ if (uri === QUIT_V2_URI) options.body = '["","teambuilder-draft","quitV2",""]';
726
+ const response = await fetch(uri, options);
727
+ if (!response.ok) throw new Error(`LCU ${uri} responded ${response.status}`);
728
+ return response;
729
+ }
730
+
701
731
  async function quitOrDodge() {
702
732
  if (!active || dodging || !state.queueId || isCustomQueue() || state.showQuitButton) return;
703
733
  dodging = true;
704
734
  render();
705
- const ranked = RANKED_QUEUES.has(Number(state.queueId));
706
- const uri = ranked ? DODGE_URI : QUIT_URI;
707
735
  try {
708
- await api.lcu.send("POST", uri);
736
+ let sent = false;
737
+ let lastError = null;
738
+ for (const uri of [QUIT_V2_URI, CUSTOM_CANCEL_URI]) {
739
+ try {
740
+ await requestDodge(uri);
741
+ sent = true;
742
+ } catch (error) {
743
+ lastError = error;
744
+ }
745
+ }
746
+ if (!sent) throw lastError ?? new Error("No dodge endpoint accepted the request");
709
747
  await recordDodge();
710
748
  } catch (error) {
711
749
  console.warn("[Dodge Helper] quit failed:", error);
@@ -832,4 +870,4 @@ export function init() {
832
870
  addEventListener("perfectionist:utils-ready", (event) => registerWith(event.detail ?? window.PerfectionistUtils), {
833
871
  once: true,
834
872
  });
835
- }
873
+ }
@@ -1,4 +1,4 @@
1
- export const version = "2.4.3";
1
+ export const version = "2.4.5";
2
2
 
3
3
  const PLUGIN_ID = "hide-unowned-skins";
4
4
  const STYLE_ID = "perfectionist-hide-unowned-skins-style";
@@ -27,7 +27,7 @@ const OBSERVER_OPTIONS = {
27
27
  attributeFilter: ["src"],
28
28
  };
29
29
 
30
- const FILTER_VERSION = "2.4.3";
30
+ const FILTER_VERSION = "2.4.5";
31
31
  const FILTER_STATE_KEY = "__perfSkinFilterState";
32
32
  const filterState = (globalThis[FILTER_STATE_KEY] ??= { active: true });
33
33
 
@@ -69,7 +69,7 @@ function installArrayFilter() {
69
69
  const nativeFilter = proto.filter;
70
70
  const wrapped = function (callback, thisArg) {
71
71
  const result = nativeFilter.call(this, callback, thisArg);
72
- if (!filterState.active || !isSkinCollection(result)) return result;
72
+ if (!filterState.active || currentChampionId === null || !isSkinCollection(result)) return result;
73
73
  let changed = false;
74
74
  if (!skinShapeKeys) learnSkinShape(result);
75
75
  const kept = nativeFilter.call(result, (skin) => {
@@ -78,16 +78,11 @@ function installArrayFilter() {
78
78
  return owned;
79
79
  });
80
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
81
  return kept;
86
82
  };
87
83
  wrapped.__perfSkinFilter = FILTER_VERSION;
88
84
  proto.filter = wrapped;
89
85
  }
90
- const filterStats = (globalThis.__perfSkinFilterStats ??= { carousel: 0, source: "", lastIn: 0, lastOut: 0 });
91
86
  let rawFetch = (typeof window !== "undefined" && window.fetch && window.fetch.__perfOriginalFetch) || null;
92
87
  let clientSkinOrder = [];
93
88
 
@@ -114,7 +109,7 @@ function isOwnedSkin(skin) {
114
109
  }
115
110
 
116
111
  function filterSkinList(list) {
117
- if (!Array.isArray(list)) return null;
112
+ if (!Array.isArray(list) || currentChampionId === null) return null;
118
113
  if (!skinShapeKeys && isSkinCollection(list)) learnSkinShape(list);
119
114
  let changed = false;
120
115
  const filtered = list
@@ -127,8 +122,6 @@ function filterSkinList(list) {
127
122
  })
128
123
  .filter(isOwnedSkin);
129
124
  if (filtered.length !== list.length) changed = true;
130
- filterStats.lastIn = list.length;
131
- filterStats.lastOut = filtered.length;
132
125
  return changed ? filtered : null;
133
126
  }
134
127
 
@@ -140,7 +133,7 @@ function recordClientOrder(list) {
140
133
  }
141
134
 
142
135
  function rewriteWebSocketFrame(text) {
143
- if (!filterState.active || typeof text !== "string") return null;
136
+ if (!filterState.active || currentChampionId === null || typeof text !== "string") return null;
144
137
  if (text.indexOf("skin-carousel-skins") === -1) return null;
145
138
  let frame;
146
139
  try {
@@ -153,8 +146,6 @@ function rewriteWebSocketFrame(text) {
153
146
  if (!payload || typeof payload !== "object" || typeof payload.uri !== "string") return null;
154
147
  if (payload.uri.split("?")[0] !== CAROUSEL_URI) return null;
155
148
  const kept = filterSkinList(payload.data);
156
- filterStats.carousel++;
157
- filterStats.source = "ws";
158
149
  recordClientOrder(kept ?? payload.data);
159
150
  if (!kept) return null;
160
151
  return JSON.stringify([frame[0], frame[1], Object.assign({}, payload, { data: kept })]);
@@ -235,7 +226,6 @@ function installFetchFilter() {
235
226
  const original = window.fetch;
236
227
  if (typeof original !== "function" || original.__perfSkinFilter === FILTER_VERSION) return;
237
228
  if (!rawFetch) rawFetch = original.__perfOriginalFetch || original;
238
- globalThis.__perfNativeFetch = rawFetch;
239
229
 
240
230
  const wrapped = function (input, init) {
241
231
  const url =
@@ -247,7 +237,7 @@ function installFetchFilter() {
247
237
  ? input.href
248
238
  : "";
249
239
  const promise = original.call(this, input, init);
250
- if (!filterState.active || url.split("?")[0].indexOf(CAROUSEL_URI) === -1) return promise;
240
+ if (!filterState.active || currentChampionId === null || url.split("?")[0].indexOf(CAROUSEL_URI) === -1) return promise;
251
241
  return promise.then((response) => {
252
242
  if (!response || !response.ok) return response;
253
243
  let body;
@@ -261,8 +251,6 @@ function installFetchFilter() {
261
251
  .catch(() => null)
262
252
  .then((json) => {
263
253
  const kept = filterSkinList(json);
264
- filterStats.carousel++;
265
- filterStats.source = "fetch";
266
254
  recordClientOrder(kept ?? json);
267
255
  if (!kept) return response;
268
256
  return new Response(JSON.stringify(kept), {
@@ -300,15 +288,13 @@ function installXhrFilter() {
300
288
  const cache = { raw: null, filtered: null };
301
289
  const filteredText = () => {
302
290
  const raw = textDescriptor.get.call(xhr);
303
- if (!filterState.active) return raw;
291
+ if (!filterState.active || currentChampionId === null) return raw;
304
292
  if (cache.filtered === null || cache.raw !== raw) {
305
293
  cache.raw = raw;
306
294
  cache.filtered = raw;
307
295
  try {
308
296
  const json = JSON.parse(raw);
309
297
  const kept = filterSkinList(json);
310
- filterStats.carousel++;
311
- filterStats.source = "xhr";
312
298
  recordClientOrder(kept ?? json);
313
299
  if (kept) cache.filtered = JSON.stringify(kept);
314
300
  } catch {
@@ -442,10 +428,6 @@ function applyAll() {
442
428
  });
443
429
  }
444
430
 
445
- function applyAllGuarded() {
446
- applyAll();
447
- }
448
-
449
431
  let applyQueued = false;
450
432
 
451
433
  function scheduleApply() {
@@ -454,6 +436,7 @@ function scheduleApply() {
454
436
  requestAnimationFrame(() => {
455
437
  applyQueued = false;
456
438
  ensureSourceFilter();
439
+ if (currentChampionId === null) return;
457
440
  const item = document.querySelector(SKIN_ITEM_SELECTOR);
458
441
  if (!item && !document.querySelector(DOT_SELECTOR)) return;
459
442
  applyAll();
@@ -558,7 +541,7 @@ function resetState() {
558
541
  failure = null;
559
542
  sessionMisses = 0;
560
543
  championMisses = 0;
561
- applyAllGuarded();
544
+ applyAll();
562
545
  }
563
546
 
564
547
  function loadChampion(championId) {
@@ -579,7 +562,7 @@ function loadChampion(championId) {
579
562
  topLevelOrder = topLevel;
580
563
  ownedTopLevelIds = ownedTopLevel;
581
564
  carouselOrder = carouselIds.length > 0 ? carouselIds : inventoryOrder;
582
- applyAllGuarded();
565
+ applyAll();
583
566
  },
584
567
  (error) => {
585
568
  if (token !== requestToken) return;
@@ -618,7 +601,7 @@ function updateForSession(session, selection) {
618
601
  }
619
602
  }
620
603
 
621
- applyAllGuarded();
604
+ applyAll();
622
605
  }
623
606
 
624
607
  function scheduleRefresh() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perfectionist-plugins",
3
- "version": "2.4.6",
3
+ "version": "2.4.8",
4
4
  "description": "Small plugins that fix the annoying parts of League of Legends champ select. Built for Pengu Loader.",
5
5
  "type": "module",
6
6
  "main": "perfectionist-utils.js",
@@ -5,7 +5,7 @@ const PANEL_ID = "perfectionist-plugins-panel";
5
5
  const STYLE_ID = "perfectionist-plugins-style";
6
6
  const TAB_TITLE = "Perfectionism";
7
7
 
8
- export const version = "2.4.0";
8
+ export const version = "2.4.6";
9
9
 
10
10
  const PKG = "perfectionist-plugins";
11
11
  const CDN = "https://cdn.jsdelivr.net/npm";
@@ -30,21 +30,21 @@ const BUNDLED_MODULES = [
30
30
  id: "ban-blocker",
31
31
  file: "ban-blocker.js",
32
32
  name: "Ban Blocker",
33
- version: "3.6.0",
33
+ version: "3.6.6",
34
34
  description: "Adds a BLOCK button to your teammates in champ select. While a teammate is blocked, the champion they are about to ban is prepicked for you, so their ban can never land on it.",
35
35
  },
36
36
  {
37
37
  id: "unlock-profile-skins",
38
38
  file: "unlock-profile-skins.js",
39
39
  name: "Unlock Profile Skins",
40
- version: "1.2.4",
40
+ version: "1.2.6",
41
41
  description: "Shows every skin as unlocked in the Set profile background window, event skins included, so you can pick any skin as your profile background.",
42
42
  },
43
43
  {
44
44
  id: "hide-unowned-skins",
45
45
  file: "hide-unowned-skins.js",
46
46
  name: "Hide Unowned Skins",
47
- version: "2.5.1",
47
+ version: "2.4.5",
48
48
  description: "In champ select, the skin carousel only shows the skins you own for the champion you picked.",
49
49
  },
50
50
  {
@@ -58,7 +58,7 @@ const BUNDLED_MODULES = [
58
58
  id: "dodge-helper",
59
59
  file: "dodge-helper.js",
60
60
  name: "Dodge Helper",
61
- version: "1.4.3",
61
+ version: "1.4.13",
62
62
  description: "Adds a DODGE/QUIT button to champ select, showing the LP and queue penalty for the next dodge.",
63
63
  },
64
64
  {
@@ -384,6 +384,7 @@ function isInstalled(mod) {
384
384
  }
385
385
 
386
386
  function moduleUrl(file) {
387
+ if (coords?.moduleBase) return new URL(file, coords.moduleBase).href;
387
388
  return `${CDN}/${PKG}@${runningVersion}/${file}`;
388
389
  }
389
390
 
@@ -849,10 +850,23 @@ function refreshPanel() {
849
850
  requestAnimationFrame(() => applyScrollTop(next, top));
850
851
  }
851
852
 
853
+ function settingsNavScope() {
854
+ const title =
855
+ document.querySelector(".lol-settings-nav-title:not([data-perfectionist])") ??
856
+ document.querySelector(".lol-settings-nav-title");
857
+ return title?.closest("lol-uikit-scrollable") ?? title?.parentElement ?? null;
858
+ }
859
+
860
+ function navItems() {
861
+ const scope = settingsNavScope();
862
+ if (!scope) return [];
863
+ return [...scope.querySelectorAll('lol-uikit-navigation-item, [class*="settings-nav"]')];
864
+ }
865
+
852
866
  function setNavActive(activeNav) {
853
- for (const nav of document.querySelectorAll(".lol-settings-nav")) {
867
+ for (const nav of navItems()) {
854
868
  if (nav === activeNav) {
855
- nav.setAttribute("active", "true");
869
+ if (nav.getAttribute("active") !== "true") nav.setAttribute("active", "true");
856
870
  } else if (nav.hasAttribute("active")) {
857
871
  nav.removeAttribute("active");
858
872
  }
@@ -861,6 +875,26 @@ function setNavActive(activeNav) {
861
875
 
862
876
  let panelRequested = false;
863
877
  let syncTimer = null;
878
+ let navObserver = null;
879
+ let nativeActiveBeforeOpen = null;
880
+
881
+ function suppressNativeNavActive() {
882
+ const own = document.getElementById(NAV_ID);
883
+ if (!own) return;
884
+ if (!panelRequested) {
885
+ if (own.hasAttribute("active")) own.removeAttribute("active");
886
+ return;
887
+ }
888
+ for (const nav of navItems()) {
889
+ if (nav !== own && nav.hasAttribute("active")) nav.removeAttribute("active");
890
+ }
891
+ }
892
+
893
+ function watchNavActive() {
894
+ if (navObserver || !document.body) return;
895
+ navObserver = new MutationObserver(suppressNativeNavActive);
896
+ navObserver.observe(document.body, { subtree: true, attributes: true, attributeFilter: ["active"] });
897
+ }
864
898
 
865
899
  function startSync() {
866
900
  if (syncTimer === null) syncTimer = setInterval(keepPanelInSync, 300);
@@ -885,7 +919,11 @@ function openPanel() {
885
919
  if (existing) existing.remove();
886
920
  content.append(buildPanel());
887
921
 
888
- setNavActive(document.getElementById(NAV_ID));
922
+ const ownNav = document.getElementById(NAV_ID);
923
+ if (!nativeActiveBeforeOpen) {
924
+ nativeActiveBeforeOpen = navItems().find((nav) => nav !== ownNav && nav.hasAttribute("active")) ?? null;
925
+ }
926
+ setNavActive(ownNav);
889
927
  const currentTitle = document.querySelector(".lol-settings-title-current");
890
928
  if (currentTitle) currentTitle.textContent = TAB_TITLE.toUpperCase();
891
929
  }
@@ -897,7 +935,9 @@ function closePanel() {
897
935
  panel.remove();
898
936
  const options = document.querySelector(".lol-settings-content .lol-settings-options");
899
937
  if (options) options.style.display = "";
900
- setNavActive(null);
938
+ document.getElementById(NAV_ID)?.removeAttribute("active");
939
+ if (nativeActiveBeforeOpen?.isConnected) nativeActiveBeforeOpen.setAttribute("active", "true");
940
+ nativeActiveBeforeOpen = null;
901
941
  }
902
942
 
903
943
  function keepPanelInSync() {
@@ -905,6 +945,7 @@ function keepPanelInSync() {
905
945
  if (!content) {
906
946
  panelRequested = false;
907
947
  stopSync();
948
+ document.getElementById(NAV_ID)?.removeAttribute("active");
908
949
  return;
909
950
  }
910
951
  if (!panelRequested) {
@@ -923,12 +964,7 @@ function keepPanelInSync() {
923
964
  if (options && options.style.display !== "none") options.style.display = "none";
924
965
 
925
966
  const nav = document.getElementById(NAV_ID);
926
- if (nav) {
927
- for (const other of document.querySelectorAll(".lol-settings-nav")) {
928
- if (other !== nav && other.hasAttribute("active")) other.removeAttribute("active");
929
- }
930
- if (nav.getAttribute("active") !== "true") nav.setAttribute("active", "true");
931
- }
967
+ if (nav) setNavActive(nav);
932
968
 
933
969
  const title = document.querySelector(".lol-settings-title-current");
934
970
  if (title && title.textContent !== TAB_TITLE.toUpperCase()) title.textContent = TAB_TITLE.toUpperCase();
@@ -967,6 +1003,7 @@ function isDoneButton(target) {
967
1003
 
968
1004
  function installSettingsTab() {
969
1005
  addStyle(STYLE_ID, STYLE_CSS);
1006
+ watchNavActive();
970
1007
 
971
1008
  document.addEventListener(
972
1009
  "click",
@@ -1,4 +1,4 @@
1
- export const version = "1.2.4";
1
+ export const version = "1.2.6";
2
2
 
3
3
  const PLUGIN_ID = "unlock-profile-skins";
4
4
  const PICKER_BUTTON_SELECTOR = ".style-profile-skin-picker-button";
@@ -39,8 +39,39 @@ function logError(scope, error) {
39
39
  console.error(`[Perfectionist] Unlock Profile Skins: ${scope}`, error);
40
40
  }
41
41
 
42
+ const PICKER_ARM_MS = 5000;
43
+ let pickerArmUntil = 0;
44
+ let pickerArmTimer = null;
45
+
42
46
  function isPickerOpen() {
43
- return document.querySelector(PICKER_BUTTON_SELECTOR) !== null;
47
+ for (const dialog of document.querySelectorAll("lol-uikit-dialog-frame")) {
48
+ const rect = dialog.getBoundingClientRect();
49
+ if (rect.width <= 0 || rect.height <= 0) continue;
50
+ if (/set profile background/i.test(dialog.textContent || "")) return true;
51
+ }
52
+ return false;
53
+ }
54
+
55
+ function shouldFilter() {
56
+ return filterActive && (isPickerOpen() || Date.now() < pickerArmUntil);
57
+ }
58
+
59
+ function armPicker() {
60
+ pickerArmUntil = Date.now() + PICKER_ARM_MS;
61
+ if (pickerArmTimer !== null) clearTimeout(pickerArmTimer);
62
+ const waitForPicker = () => {
63
+ if (isPickerOpen()) {
64
+ pickerArmTimer = null;
65
+ startTierGrouping();
66
+ return;
67
+ }
68
+ if (Date.now() >= pickerArmUntil) {
69
+ pickerArmTimer = null;
70
+ return;
71
+ }
72
+ pickerArmTimer = setTimeout(waitForPicker, 100);
73
+ };
74
+ pickerArmTimer = setTimeout(waitForPicker, 50);
44
75
  }
45
76
 
46
77
  function cleanPath(url) {
@@ -386,7 +417,7 @@ function ensureSummonerId() {
386
417
  });
387
418
  }
388
419
  function rewriteInventoryText(raw) {
389
- if (typeof raw !== "string") return raw;
420
+ if (!shouldFilter() || typeof raw !== "string") return raw;
390
421
  if (inventoryTextCache.has(raw)) {
391
422
  const cached = inventoryTextCache.get(raw);
392
423
  inventoryTextCache.delete(raw);
@@ -406,7 +437,7 @@ function rewriteInventoryText(raw) {
406
437
  }
407
438
 
408
439
  function rewriteWebSocketFrame(text) {
409
- if (!filterActive || typeof text !== "string") return null;
440
+ if (!shouldFilter() || typeof text !== "string") return null;
410
441
  const targetFrame = text.indexOf("lol-champions/v1/inventories") !== -1;
411
442
  const augmentFrame = text.indexOf("SKIN_AUGMENT") !== -1;
412
443
  if (!targetFrame && !augmentFrame) return null;
@@ -512,12 +543,12 @@ function installFetchFilter() {
512
543
  const inventory = pathIdOf(url) !== null;
513
544
  const augmentRequest = isAugmentUrl(url);
514
545
  let options = init;
515
- if (filterActive && options && typeof options.body === "string") {
546
+ if (shouldFilter() && options && typeof options.body === "string") {
516
547
  const mapped = remapVariantIds(options.body);
517
548
  if (mapped !== options.body) options = { ...options, body: mapped };
518
549
  }
519
550
  const promise = original.call(this, input, options);
520
- if (!filterActive || (!inventory && !augmentRequest)) return promise;
551
+ if (!shouldFilter() || (!inventory && !augmentRequest)) return promise;
521
552
  if (inventory) maybeRefreshSummoner();
522
553
 
523
554
  return whenSummonerKnown(SUMMONER_WAIT_MS).then(() => {
@@ -594,13 +625,13 @@ function installXhrFilter() {
594
625
  const url = this.__perfProfileUrl || "";
595
626
  const target = isTargetUrl(url);
596
627
  const augment = !target && isAugmentUrl(url);
597
- if (filterActive && pathIdOf(url) !== null) maybeRefreshSummoner();
598
- if (filterActive && typeof args[0] === "string") {
628
+ if (shouldFilter() && pathIdOf(url) !== null) maybeRefreshSummoner();
629
+ if (shouldFilter() && typeof args[0] === "string") {
599
630
  const mapped = remapVariantIds(args[0]);
600
631
  if (mapped !== args[0]) args = [mapped, ...args.slice(1)];
601
632
  }
602
633
  const xhr = this;
603
- if (target && filterActive && augmentIds.size === 0 && textDescriptor && textDescriptor.get) {
634
+ if (target && shouldFilter() && augmentIds.size === 0 && textDescriptor && textDescriptor.get) {
604
635
  const latch = new Promise((resolve) => {
605
636
  const finish = () => {
606
637
  if (augmentIds.size === 0) {
@@ -619,7 +650,7 @@ function installXhrFilter() {
619
650
  }
620
651
  if (textDescriptor && textDescriptor.get && (pathIdOf(url) !== null || augment)) {
621
652
  const cache = { raw: null, gate: null, filtered: null };
622
- const gateOpen = () => filterActive && (augment || isTargetUrl(url));
653
+ const gateOpen = () => shouldFilter() && (augment || isTargetUrl(url));
623
654
  const rewriteBody = (json) => (augment ? unlockAugmentInventory(json) : unlockDeep(json));
624
655
  const filteredText = () => {
625
656
  const gate = gateOpen();
@@ -653,7 +684,7 @@ function installXhrFilter() {
653
684
  });
654
685
  }
655
686
  }
656
- if (augment && filterActive && augmentIds.size === 0) {
687
+ if (augment && shouldFilter() && augmentIds.size === 0) {
657
688
  const sendArgs = args;
658
689
  ensureAugmentIds().finally(() => {
659
690
  try {
@@ -693,11 +724,10 @@ function onEnable(utils) {
693
724
  filterActive = true;
694
725
  ensureSourceFilter();
695
726
  ensureSummonerId();
696
- tierArmListener = () => {
697
- if (tierObserver) return;
698
- setTimeout(() => {
699
- if (tierArmListener && isPickerOpen()) startTierGrouping();
700
- }, 0);
727
+ tierArmListener = (event) => {
728
+ const target = event.target instanceof Element ? event.target.closest(PICKER_BUTTON_SELECTOR) : null;
729
+ if (!target) return;
730
+ armPicker();
701
731
  };
702
732
  document.addEventListener("click", tierArmListener, true);
703
733
  if (isPickerOpen()) startTierGrouping();
@@ -708,6 +738,9 @@ function onDisable() {
708
738
  inventoryTextCache.clear();
709
739
  if (tierArmListener) document.removeEventListener("click", tierArmListener, true);
710
740
  tierArmListener = null;
741
+ if (pickerArmTimer !== null) clearTimeout(pickerArmTimer);
742
+ pickerArmTimer = null;
743
+ pickerArmUntil = 0;
711
744
  stopTierGrouping();
712
745
  api = null;
713
746
  }