perfectionist-plugins 2.4.7 → 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/ban-blocker.js CHANGED
@@ -1,4 +1,4 @@
1
- export const version = "3.6.2";
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,6 +66,7 @@ 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();
@@ -232,7 +234,9 @@ function decide(s) {
232
234
  }
233
235
  myActionId = myPick ? myPick.id : null;
234
236
 
235
- 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
+ );
236
240
 
237
241
  let threat = 0;
238
242
  for (const a of actions) {
@@ -264,6 +268,7 @@ function onSessionGone() {
264
268
  lastSession = null;
265
269
  goneAway = true;
266
270
  uiVisible = false;
271
+ blockedCellIds.clear();
267
272
  swapSeen.clear();
268
273
  removeButtons();
269
274
  if (!goneTimer) {
@@ -378,22 +383,29 @@ function bindRowsToTeam(session) {
378
383
  const selfIdx = list.findIndex((r) => isSelfRow(r));
379
384
  const selfCellIdx =
380
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;
381
395
  let pick;
382
- if (selfIdx >= 0 && selfCellIdx >= 0) {
383
- const selfSlot = list[selfIdx].slotId;
384
- if (selfSlot != null && selfSlot === cells[selfCellIdx].cellId) {
385
- pick = (r) => (r.slotId != null ? cells.find((c) => c.cellId === r.slotId) : null);
386
- } else if (selfIdx === selfCellIdx) {
387
- pick = (r) => (r.slotId != null ? cells[r.slotId] : null) ?? cells[r.index];
388
- } else {
389
- pick = () => null;
390
- }
396
+ if (slotMapComplete) {
397
+ pick = (r) => bySlot[r.index] ?? null;
391
398
  } else {
392
- 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;
393
401
  }
394
402
 
395
403
  for (const r of list) {
396
- 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)));
397
409
  if (cell) {
398
410
  r.cell = cell.cellId;
399
411
  r.player = linkPlayer(cell, r.name, cell.cellId);
@@ -410,23 +422,56 @@ function stopEvent(e) {
410
422
  e.preventDefault();
411
423
  }
412
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
+
413
448
  function playerForBadge(btn) {
414
449
  const rowEl = btn.closest(".summoner-object");
415
450
  const r = rowEl ? rows.find((x) => x.el === rowEl) : rows[Number(btn.dataset.slot)];
416
- 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;
417
459
  return canonOf(byCell ?? r?.player ?? null);
418
460
  }
419
461
 
420
462
  function toggle(btn) {
421
- let id = playerForBadge(btn);
422
- if (id == null && lastSession) {
463
+ if (lastSession) {
423
464
  bindRowsToTeam(lastSession);
424
- id = playerForBadge(btn);
465
+ decide(lastSession);
425
466
  }
426
- if (id == null) return;
467
+ const cell = badgeCell(btn);
427
468
  const myCell = lastSession?.localPlayerCellId;
428
- if (myCell != null && btn.dataset.cell && Number(btn.dataset.cell) === myCell) return;
429
- 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);
430
475
  renderButtons();
431
476
  if (lastSession) {
432
477
  decide(lastSession);
@@ -461,7 +506,12 @@ function isSelfRow(r) {
461
506
  }
462
507
 
463
508
  function rowKey(r) {
464
- return String(r.slotId ?? r.cell ?? r.index);
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}`;
465
515
  }
466
516
 
467
517
  function nativeSwapIn(circle) {
@@ -471,23 +521,25 @@ function nativeSwapIn(circle) {
471
521
 
472
522
  function ensureButtons() {
473
523
  const list = getRows();
524
+ const active = new Set();
474
525
  for (const r of list) {
475
526
  if (!r.circle || !r.circle.isConnected) r.circle = r.el.querySelector(".champion-icon-container");
476
527
  if (!r.circle || !r.circle.isConnected) continue;
477
- const key = rowKey(r);
478
- let btn = buttons.get(r.index);
528
+ const swapKey = rowKey(r);
529
+ const key = buttonKey(r);
530
+ let btn = buttons.get(key);
479
531
  if (isSelfRow(r)) {
480
532
  if (btn) {
481
533
  btn.remove();
482
- buttons.delete(r.index);
534
+ buttons.delete(key);
483
535
  }
484
536
  continue;
485
537
  }
486
- if (nativeSwapIn(r.circle)) swapSeen.add(key);
487
- if (!swapSeen.has(key)) {
538
+ if (nativeSwapIn(r.circle)) swapSeen.add(swapKey);
539
+ if (!swapSeen.has(swapKey)) {
488
540
  if (btn) {
489
541
  btn.remove();
490
- buttons.delete(r.index);
542
+ buttons.delete(key);
491
543
  }
492
544
  continue;
493
545
  }
@@ -496,16 +548,17 @@ function ensureButtons() {
496
548
  for (const orphan of r.circle.querySelectorAll(`:scope > .${BTN_CLASS}`)) orphan.remove();
497
549
  btn = buildBadge(r.index);
498
550
  r.circle.appendChild(btn);
499
- buttons.set(r.index, btn);
551
+ buttons.set(key, btn);
500
552
  }
553
+ active.add(key);
501
554
  btn.dataset.slot = String(r.index);
502
- const cell = r.cell ?? r.slotId;
555
+ const cell = cellForRow(r);
503
556
  btn.dataset.cell = cell == null ? "" : String(cell);
504
557
  }
505
- for (const [slot, btn] of [...buttons]) {
506
- if (!list[slot] || !btn.isConnected) {
558
+ for (const [key, btn] of [...buttons]) {
559
+ if (!active.has(key) || !btn.isConnected) {
507
560
  btn.remove();
508
- buttons.delete(slot);
561
+ buttons.delete(key);
509
562
  }
510
563
  }
511
564
  }
@@ -514,7 +567,8 @@ function renderButtons() {
514
567
  const hasSession = !!lastSession?.myTeam;
515
568
  for (const [, btn] of buttons) {
516
569
  const id = playerForBadge(btn);
517
- const on = id != null && isBlocked(id);
570
+ const cell = badgeCell(btn);
571
+ const on = (cell != null && blockedCellIds.has(cell)) || (id != null && isBlocked(id));
518
572
  btn.classList.toggle("on", on);
519
573
  btn.classList.toggle("unresolved", id == null && hasSession);
520
574
  const title = on ? "Blocked — click to allow their bans" : "Block this teammate's bans";
@@ -640,6 +694,7 @@ function onDisable() {
640
694
  failCount.clear();
641
695
  cellToPlayer.clear();
642
696
  enabled.clear();
697
+ blockedCellIds.clear();
643
698
  playerNames.clear();
644
699
  alias.clear();
645
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) => {
@@ -109,7 +109,7 @@ function isOwnedSkin(skin) {
109
109
  }
110
110
 
111
111
  function filterSkinList(list) {
112
- if (!Array.isArray(list)) return null;
112
+ if (!Array.isArray(list) || currentChampionId === null) return null;
113
113
  if (!skinShapeKeys && isSkinCollection(list)) learnSkinShape(list);
114
114
  let changed = false;
115
115
  const filtered = list
@@ -133,7 +133,7 @@ function recordClientOrder(list) {
133
133
  }
134
134
 
135
135
  function rewriteWebSocketFrame(text) {
136
- if (!filterState.active || typeof text !== "string") return null;
136
+ if (!filterState.active || currentChampionId === null || typeof text !== "string") return null;
137
137
  if (text.indexOf("skin-carousel-skins") === -1) return null;
138
138
  let frame;
139
139
  try {
@@ -237,7 +237,7 @@ function installFetchFilter() {
237
237
  ? input.href
238
238
  : "";
239
239
  const promise = original.call(this, input, init);
240
- 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;
241
241
  return promise.then((response) => {
242
242
  if (!response || !response.ok) return response;
243
243
  let body;
@@ -288,7 +288,7 @@ function installXhrFilter() {
288
288
  const cache = { raw: null, filtered: null };
289
289
  const filteredText = () => {
290
290
  const raw = textDescriptor.get.call(xhr);
291
- if (!filterState.active) return raw;
291
+ if (!filterState.active || currentChampionId === null) return raw;
292
292
  if (cache.filtered === null || cache.raw !== raw) {
293
293
  cache.raw = raw;
294
294
  cache.filtered = raw;
@@ -436,6 +436,7 @@ function scheduleApply() {
436
436
  requestAnimationFrame(() => {
437
437
  applyQueued = false;
438
438
  ensureSourceFilter();
439
+ if (currentChampionId === null) return;
439
440
  const item = document.querySelector(SKIN_ITEM_SELECTOR);
440
441
  if (!item && !document.querySelector(DOT_SELECTOR)) return;
441
442
  applyAll();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perfectionist-plugins",
3
- "version": "2.4.7",
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",
@@ -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.2",
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.4.3",
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
 
@@ -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
  }