perfectionist-plugins 1.3.0 → 1.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.
package/README.md CHANGED
@@ -53,6 +53,20 @@ Built for [Pengu Loader](https://pengu.lol)
53
53
  Browse the full list and pick any skin you like as your profile background.
54
54
  </td>
55
55
  </tr>
56
+ <tr>
57
+ <td>&nbsp;</td>
58
+ </tr>
59
+ <tr>
60
+ <th align="left">🎁 Auto Claimer</th>
61
+ </tr>
62
+ <tr>
63
+ <td>
64
+ Claims every reward for you the moment it becomes claimable — event tracks, missions and the TFT Battle Pass.
65
+ <br><br>
66
+ Three optional extras, all <b>off by default</b>: auto-buy shop items Riot lists for 1 BE,
67
+ auto-open mystery loot (icons, ward skins), and auto-open chests with keys.
68
+ </td>
69
+ </tr>
56
70
  </table>
57
71
 
58
72
  ## Install
@@ -0,0 +1,374 @@
1
+ const PLUGIN_ID = "auto-claimer";
2
+ const version = "1.0.0";
3
+
4
+ const SWEEP_DEBOUNCE_MS = 400;
5
+ const CLAIM_COOLDOWN_MS = 5 * 60 * 1000;
6
+ const BUY_COOLDOWN_MS = 30 * 60 * 1000;
7
+ const SHOP_THROTTLE_MS = 60 * 1000;
8
+ const BUY_FALLBACK_MS = 15 * 60 * 1000;
9
+ const MAX_ACTIONS_PER_SWEEP = 25;
10
+
11
+ const EVENTS_URI = "/lol-event-hub/v1/events";
12
+ const MISSIONS_URI = "/lol-missions/v1/missions";
13
+ const TFT_PASS_URI = "/lol-tft-pass/v1/battle-pass";
14
+ const TFT_PASSES_URI = "/lol-tft-pass/v1/active-passes";
15
+ const LOOT_URI = "/lol-loot/v1/player-loot";
16
+ const STORE_READY_URI = "/lol-store/v1/store-ready";
17
+ const CATALOG_URI = "/lol-store/v1/catalog";
18
+ const BUY_URI = "/lol-purchase-widget/v2/purchaseItems";
19
+
20
+ let api = null;
21
+ let active = false;
22
+ let claimTimer = null;
23
+ let claimRunning = false;
24
+ let claimQueued = false;
25
+ let shopDebounce = null;
26
+ let shopInterval = null;
27
+ let shopRunning = false;
28
+ let shopQueued = false;
29
+ let unsubscribes = [];
30
+ let sourceInstalled = false;
31
+ const cooldowns = new Map();
32
+ const menus = new Map();
33
+
34
+ function on(key) {
35
+ try {
36
+ return api?.getOption?.(PLUGIN_ID, key) === true;
37
+ } catch {
38
+ return false;
39
+ }
40
+ }
41
+
42
+ function ready(key, wait) {
43
+ return (cooldowns.get(key) ?? 0) + wait <= Date.now();
44
+ }
45
+
46
+ function stamp(key) {
47
+ cooldowns.set(key, Date.now());
48
+ }
49
+
50
+ function getJson(path) {
51
+ return fetch(path, { headers: { Accept: "application/json" } }).then((response) => {
52
+ if (!response.ok) throw new Error(`${path} responded ${response.status}`);
53
+ return response.json();
54
+ });
55
+ }
56
+
57
+ function sendJson(method, path, body) {
58
+ return fetch(path, {
59
+ method,
60
+ headers: { Accept: "application/json", "Content-Type": "application/json" },
61
+ body: body === undefined ? undefined : JSON.stringify(body),
62
+ }).then((response) => {
63
+ if (!response.ok) throw new Error(`${method} ${path} responded ${response.status}`);
64
+ return response.status === 204 ? null : response.json().catch(() => null);
65
+ });
66
+ }
67
+
68
+ async function sweepEvents() {
69
+ const events = await getJson(EVENTS_URI);
70
+ if (!Array.isArray(events)) return;
71
+ for (const entry of events) {
72
+ const id = entry?.eventId;
73
+ const unclaimed = entry?.eventInfo?.unclaimedRewardCount ?? 0;
74
+ if (!id || unclaimed <= 0 || !ready(`event:${id}`, CLAIM_COOLDOWN_MS)) continue;
75
+ stamp(`event:${id}`);
76
+ await sendJson("POST", `${EVENTS_URI}/${id}/reward-track/claim-all`).catch(() => {});
77
+ }
78
+ }
79
+
80
+ async function sweepMissions() {
81
+ const missions = await getJson(MISSIONS_URI);
82
+ if (!Array.isArray(missions)) return;
83
+ for (const mission of missions) {
84
+ if (mission?.status !== "SELECT_REWARDS") continue;
85
+ const id = mission.id;
86
+ if (!id || !ready(`mission:${id}`, CLAIM_COOLDOWN_MS)) continue;
87
+ const rewards = Array.isArray(mission.rewards) ? mission.rewards : [];
88
+ const groups = [...new Set(rewards.map((reward) => reward?.rewardGroup).filter((group) => group != null && group !== ""))];
89
+ if (groups.length === 0) continue;
90
+ if (groups.length > 1 && (mission.rewardStrategy?.selectMaxGroupCount ?? 0) > 0) continue;
91
+ stamp(`mission:${id}`);
92
+ await sendJson("PUT", `/lol-missions/v1/player/${id}`, { rewardGroups: [groups[0]] }).catch(() => {});
93
+ }
94
+ }
95
+
96
+ async function sweepTftPasses() {
97
+ const seen = new Set();
98
+ for (const uri of [TFT_PASS_URI, TFT_PASSES_URI]) {
99
+ const data = await getJson(uri).catch(() => null);
100
+ const passes = Array.isArray(data) ? data : data ? [data] : [];
101
+ for (const pass of passes) {
102
+ const passId = pass?.info?.passId;
103
+ if (!passId || seen.has(passId)) continue;
104
+ seen.add(passId);
105
+ const milestones = [...(Array.isArray(pass.milestones) ? pass.milestones : []), ...(Array.isArray(pass.bonuses) ? pass.bonuses : [])];
106
+ const claimable = milestones.some(
107
+ (m) => m && !m.isLocked && !m.isClaimRequestPending && (m.status === "REWARDABLE" || m.status === "CLAIMABLE"),
108
+ );
109
+ if (!claimable || !ready(`tftpass:${passId}`, CLAIM_COOLDOWN_MS)) continue;
110
+ stamp(`tftpass:${passId}`);
111
+ await sendJson("PUT", `/lol-tft-pass/v1/pass/${passId}/milestone/claimAllRewards`).catch(() => {});
112
+ }
113
+ }
114
+ }
115
+
116
+ function menuFor(lootId) {
117
+ if (!menus.has(lootId)) {
118
+ const promise = getJson(`/lol-loot/v1/player-loot/${encodeURIComponent(lootId)}/context-menu`).catch(() => {
119
+ menus.delete(lootId);
120
+ return null;
121
+ });
122
+ menus.set(lootId, promise);
123
+ }
124
+ return menus.get(lootId);
125
+ }
126
+
127
+ async function sweepLoot() {
128
+ const loot = await getJson(LOOT_URI);
129
+ if (!Array.isArray(loot)) return;
130
+ let actions = 0;
131
+ for (const item of loot) {
132
+ if (actions >= MAX_ACTIONS_PER_SWEEP) break;
133
+ const lootId = item?.lootId;
134
+ const count = item?.count ?? 0;
135
+ if (!lootId || count <= 0 || /^(CURRENCY|MATERIAL)_/.test(lootId)) continue;
136
+ if (!ready(`loot:${lootId}`, CLAIM_COOLDOWN_MS)) continue;
137
+ const menu = await menuFor(lootId);
138
+ const open = (Array.isArray(menu) ? menu : []).find((action) => action?.actionType === "OPEN" && action.name && action.enabled !== false);
139
+ if (!open) continue;
140
+ const needsKey = (open.requiredOthersCount ?? 0) > 0;
141
+ if (needsKey ? !on("openChests") : !on("openLoot")) continue;
142
+ stamp(`loot:${lootId}`);
143
+ actions++;
144
+ const repeat = Math.min(count, 25);
145
+ await sendJson("POST", `/lol-loot/v1/recipes/${encodeURIComponent(open.name)}/craft?repeat=${repeat}`, [lootId]).catch(() => {});
146
+ }
147
+ }
148
+
149
+ async function isOwned(inventoryType, itemId) {
150
+ if (!inventoryType || itemId == null) return null;
151
+ const list = await getJson(`/lol-inventory/v2/inventory/${encodeURIComponent(inventoryType)}`).catch(() => null);
152
+ if (!Array.isArray(list)) return null;
153
+ return list.some((entry) => entry?.itemId === itemId);
154
+ }
155
+
156
+ async function runShop() {
157
+ if (!active || shopRunning) {
158
+ if (shopRunning) shopQueued = true;
159
+ return;
160
+ }
161
+ if (!on("buy1Be") || !ready("shop-catalog", SHOP_THROTTLE_MS)) return;
162
+ shopRunning = true;
163
+ stamp("shop-catalog");
164
+ try {
165
+ const catalog = await getJson(CATALOG_URI);
166
+ if (!Array.isArray(catalog)) return;
167
+ const cheap = catalog.filter(
168
+ (item) =>
169
+ item?.active === true &&
170
+ Array.isArray(item.prices) &&
171
+ item.prices.some((price) => price?.cost === 1 && price?.currency === "IP"),
172
+ );
173
+ for (const item of cheap) {
174
+ const key = `buy:${item.inventoryType}:${item.itemId}`;
175
+ if (!ready(key, BUY_COOLDOWN_MS)) continue;
176
+ const owned = await isOwned(item.inventoryType, item.itemId);
177
+ if (owned === null) continue;
178
+ stamp(key);
179
+ if (owned) continue;
180
+ await sendJson("POST", BUY_URI, {
181
+ items: [
182
+ {
183
+ itemKey: { itemId: item.itemId, inventoryType: item.inventoryType },
184
+ purchaseCurrencyInfo: { currencyType: "IP", price: 1, purchasable: true },
185
+ source: "store",
186
+ featuredSection: "",
187
+ quantity: 1,
188
+ },
189
+ ],
190
+ }).catch(() => {});
191
+ }
192
+ } finally {
193
+ shopRunning = false;
194
+ if (shopQueued) {
195
+ shopQueued = false;
196
+ scheduleShop();
197
+ }
198
+ }
199
+ }
200
+
201
+ function scheduleClaims() {
202
+ if (!active) return;
203
+ clearTimeout(claimTimer);
204
+ claimTimer = setTimeout(runClaims, SWEEP_DEBOUNCE_MS);
205
+ }
206
+
207
+ function scheduleShop() {
208
+ if (!active || !on("buy1Be")) return;
209
+ clearTimeout(shopDebounce);
210
+ shopDebounce = setTimeout(runShop, SWEEP_DEBOUNCE_MS);
211
+ }
212
+
213
+ async function runClaims() {
214
+ if (!active) return;
215
+ if (claimRunning) {
216
+ claimQueued = true;
217
+ return;
218
+ }
219
+ claimRunning = true;
220
+ try {
221
+ await sweepEvents().catch(() => {});
222
+ await sweepMissions().catch(() => {});
223
+ await sweepTftPasses().catch(() => {});
224
+ if (on("openLoot") || on("openChests")) await sweepLoot().catch(() => {});
225
+ } finally {
226
+ claimRunning = false;
227
+ if (claimQueued) {
228
+ claimQueued = false;
229
+ scheduleClaims();
230
+ }
231
+ }
232
+ }
233
+
234
+ function syncShopTimer() {
235
+ if (active && on("buy1Be")) {
236
+ if (!shopInterval) shopInterval = setInterval(scheduleShop, BUY_FALLBACK_MS);
237
+ scheduleShop();
238
+ return;
239
+ }
240
+ clearInterval(shopInterval);
241
+ clearTimeout(shopDebounce);
242
+ shopInterval = null;
243
+ }
244
+
245
+ function noteTraffic(url) {
246
+ if (!active || typeof url !== "string") return;
247
+ if (url.includes("/lol-store/v1/catalog")) scheduleShop();
248
+ }
249
+
250
+ function installSourceWatch() {
251
+ if (sourceInstalled) return;
252
+ sourceInstalled = true;
253
+ const original = window.fetch;
254
+ if (typeof original === "function" && !original.__perfClaimWatch) {
255
+ const wrapped = function (input, init) {
256
+ noteTraffic(typeof input === "string" ? input : (input && input.url) || "");
257
+ return original.call(this, input, init);
258
+ };
259
+ wrapped.__perfClaimWatch = true;
260
+ window.fetch = wrapped;
261
+ }
262
+ const proto = window.XMLHttpRequest && window.XMLHttpRequest.prototype;
263
+ if (proto && !proto.open.__perfClaimWatch) {
264
+ const origOpen = proto.open;
265
+ const origSend = proto.send;
266
+ proto.open = function (method, url, ...rest) {
267
+ this.__perfClaimUrl = typeof url === "string" ? url : "";
268
+ return origOpen.call(this, method, url, ...rest);
269
+ };
270
+ proto.open.__perfClaimWatch = true;
271
+ proto.send = function (...args) {
272
+ noteTraffic(this.__perfClaimUrl || "");
273
+ return origSend.apply(this, args);
274
+ };
275
+ }
276
+ }
277
+
278
+ function onEnable(utils) {
279
+ api = utils;
280
+ active = true;
281
+ cooldowns.clear();
282
+ menus.clear();
283
+ const claimPush = () => scheduleClaims();
284
+ const shopPush = () => {
285
+ if (on("buy1Be")) scheduleShop();
286
+ };
287
+ for (const uri of [EVENTS_URI, MISSIONS_URI, TFT_PASS_URI, TFT_PASSES_URI, LOOT_URI]) {
288
+ try {
289
+ const off = api.subscribe(uri, claimPush);
290
+ if (off) unsubscribes.push(off);
291
+ } catch {}
292
+ }
293
+ try {
294
+ const off = api.subscribe(STORE_READY_URI, shopPush);
295
+ if (off) unsubscribes.push(off);
296
+ } catch {}
297
+ scheduleClaims();
298
+ syncShopTimer();
299
+ }
300
+
301
+ function onDisable() {
302
+ active = false;
303
+ clearTimeout(claimTimer);
304
+ clearTimeout(shopDebounce);
305
+ clearInterval(shopInterval);
306
+ claimTimer = null;
307
+ shopDebounce = null;
308
+ shopInterval = null;
309
+ claimRunning = false;
310
+ claimQueued = false;
311
+ shopRunning = false;
312
+ shopQueued = false;
313
+ for (const off of unsubscribes) {
314
+ try {
315
+ off();
316
+ } catch {}
317
+ }
318
+ unsubscribes = [];
319
+ cooldowns.clear();
320
+ menus.clear();
321
+ api = null;
322
+ }
323
+
324
+ function onOptionChange(key) {
325
+ if (!active) return;
326
+ if (key === "buy1Be") {
327
+ syncShopTimer();
328
+ return;
329
+ }
330
+ scheduleClaims();
331
+ }
332
+
333
+ export function init() {
334
+ installSourceWatch();
335
+
336
+ const registerWith = (utils) => {
337
+ utils.registerPlugin({
338
+ id: PLUGIN_ID,
339
+ name: "Auto Claimer",
340
+ description: "Automatically claims battle pass, event and mission rewards the moment they become claimable.",
341
+ version,
342
+ defaultEnabled: true,
343
+ options: [
344
+ {
345
+ key: "buy1Be",
346
+ label: "Auto-buy 1 BE shop items",
347
+ description: "Instantly buys items Riot lists in the shop for 1 BE (wards, icons, emotes, bundles).",
348
+ default: false,
349
+ },
350
+ {
351
+ key: "openLoot",
352
+ label: "Auto-open loot (mystery icons, ward skins, ...)",
353
+ description: "Opens free mystery loot the moment you get it. Never touches items that need a key.",
354
+ default: false,
355
+ },
356
+ {
357
+ key: "openChests",
358
+ label: "Auto-open chests with keys",
359
+ description: "Opens chest + key loot automatically while you have keys.",
360
+ default: false,
361
+ },
362
+ ],
363
+ onEnable,
364
+ onDisable,
365
+ onOptionChange,
366
+ });
367
+ };
368
+
369
+ if (window.PerfectionistUtils) {
370
+ registerWith(window.PerfectionistUtils);
371
+ return;
372
+ }
373
+ addEventListener("perfectionist:utils-ready", (e) => registerWith(e.detail ?? window.PerfectionistUtils), { once: true });
374
+ }
package/ban-blocker.js CHANGED
@@ -1,4 +1,4 @@
1
- export const version = "3.5.0";
1
+ export const version = "3.6.0";
2
2
 
3
3
  const PLUGIN_ID = "ban-blocker";
4
4
  const STYLE_ID = "perfectionist-ban-blocker-style";
@@ -118,7 +118,7 @@ function payloadName(p) {
118
118
 
119
119
  function keyOf(p) {
120
120
  if (typeof p === "number") return p;
121
- const strong = p?.summonerId || p?.puuid;
121
+ const strong = p?.summonerId || p?.puuid || p?.obfuscatedPuuid || p?.obfuscatedSummonerId;
122
122
  if (strong) return strong;
123
123
  const n = payloadName(p);
124
124
  if (n) return `name:${n}`;
@@ -126,7 +126,8 @@ function keyOf(p) {
126
126
  }
127
127
 
128
128
  function linkPlayer(p, rowName, cellId) {
129
- const strong = typeof p === "number" ? p : p?.summonerId || p?.puuid;
129
+ const strong =
130
+ typeof p === "number" ? p : p?.summonerId || p?.puuid || p?.obfuscatedPuuid || p?.obfuscatedSummonerId;
130
131
  const pn = typeof p === "number" ? "" : payloadName(p);
131
132
  const rn = norm(rowName);
132
133
  const keys = [];
@@ -208,7 +209,7 @@ function decide(s) {
208
209
 
209
210
  cellToPlayer.clear();
210
211
  for (const p of m) {
211
- const row = rows.find((r) => r.slotId === p.cellId);
212
+ const row = rows.find((r) => (r.cell ?? r.slotId) === p.cellId);
212
213
  cellToPlayer.set(p.cellId, linkPlayer(p, row?.name, p.cellId));
213
214
  }
214
215
 
@@ -310,7 +311,8 @@ function detectRows() {
310
311
  return found.map((r, i) => ({
311
312
  ...r,
312
313
  index: i,
313
- player: rows[i]?.player ?? null,
314
+ player: rows[i]?.el === r.el ? (rows[i]?.player ?? null) : null,
315
+ cell: rows[i]?.el === r.el ? (rows[i]?.cell ?? null) : null,
314
316
  }));
315
317
  } catch (err) {
316
318
  logError("row detection failed.", err);
@@ -362,11 +364,27 @@ function bindRowsToTeam(session) {
362
364
  if (!list.length || !cells.length) return;
363
365
  for (const c of cells) ensureName(c);
364
366
 
367
+ const selfIdx = list.findIndex((r) => isSelfRow(r));
368
+ const selfCellIdx =
369
+ session?.localPlayerCellId != null ? cells.findIndex((c) => c.cellId === session.localPlayerCellId) : -1;
370
+ 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
+ }
380
+ } else {
381
+ pick = (r) => (r.slotId != null ? cells.find((c) => c.cellId === r.slotId) : null);
382
+ }
383
+
365
384
  for (const r of list) {
366
- const cell =
367
- (r.slotId != null ? cells.find((c) => c.cellId === r.slotId) : null) ??
368
- cells.find((c) => nameMatches(r.name, keyOf(c)));
385
+ const cell = pick(r) ?? cells.find((c) => nameMatches(r.name, keyOf(c)));
369
386
  if (cell) {
387
+ r.cell = cell.cellId;
370
388
  r.player = linkPlayer(cell, r.name, cell.cellId);
371
389
  cellToPlayer.set(cell.cellId, r.player);
372
390
  } else {
@@ -428,7 +446,7 @@ function buildBadge(slot) {
428
446
 
429
447
  function isSelfRow(r) {
430
448
  if (r.el.classList.contains("is-self")) return true;
431
- return lastSession?.localPlayerCellId != null && r.slotId === lastSession.localPlayerCellId;
449
+ return lastSession?.localPlayerCellId != null && (r.cell ?? r.slotId) === lastSession.localPlayerCellId;
432
450
  }
433
451
 
434
452
  function nativeSwapIn(circle) {
@@ -455,7 +473,8 @@ function ensureButtons() {
455
473
  buttons.set(r.index, btn);
456
474
  }
457
475
  btn.dataset.slot = String(r.index);
458
- btn.dataset.cell = r.slotId == null ? "" : String(r.slotId);
476
+ const cell = r.cell ?? r.slotId;
477
+ btn.dataset.cell = cell == null ? "" : String(cell);
459
478
  }
460
479
  for (const [slot, btn] of [...buttons]) {
461
480
  if (!list[slot] || !btn.isConnected) {
@@ -1,4 +1,4 @@
1
- export const version = "2.3.0";
1
+ export const version = "2.3.1";
2
2
 
3
3
  const PLUGIN_ID = "hide-unowned-skins";
4
4
  const STYLE_ID = "perfectionist-hide-unowned-skins-style";
@@ -591,8 +591,10 @@ function onEnable(utils) {
591
591
  removeStyle = api.addStyle(STYLE_ID, CSS);
592
592
 
593
593
  renderObserver = new MutationObserver(() => {
594
+ const item = document.querySelector(SKIN_ITEM_SELECTOR);
595
+ if (!item && !document.querySelector(DOT_SELECTOR)) return;
594
596
  applyAllGuarded();
595
- if (document.querySelector(SKIN_ITEM_SELECTOR)) scheduleRefresh();
597
+ if (item) scheduleRefresh();
596
598
  });
597
599
  renderObserver.observe(document.documentElement ?? document, OBSERVER_OPTIONS);
598
600
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perfectionist-plugins",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
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",
@@ -9,13 +9,15 @@
9
9
  "./perfectionist-utils.js": "./perfectionist-utils.js",
10
10
  "./ban-blocker.js": "./ban-blocker.js",
11
11
  "./hide-unowned-skins.js": "./hide-unowned-skins.js",
12
- "./unlock-profile-skins.js": "./unlock-profile-skins.js"
12
+ "./unlock-profile-skins.js": "./unlock-profile-skins.js",
13
+ "./auto-claimer.js": "./auto-claimer.js"
13
14
  },
14
15
  "files": [
15
16
  "perfectionist-utils.js",
16
17
  "ban-blocker.js",
17
18
  "hide-unowned-skins.js",
18
- "unlock-profile-skins.js"
19
+ "unlock-profile-skins.js",
20
+ "auto-claimer.js"
19
21
  ],
20
22
  "repository": {
21
23
  "type": "git",
@@ -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 = "1.1.0";
9
+ export const version = "1.2.0";
10
10
 
11
11
  const PKG = "perfectionist-plugins";
12
12
  const CDN = "https://cdn.jsdelivr.net/npm";
@@ -26,23 +26,30 @@ const BUNDLED_MODULES = [
26
26
  id: "ban-blocker",
27
27
  file: "ban-blocker.js",
28
28
  name: "Ban Blocker",
29
- version: "3.5.0",
29
+ version: "3.6.0",
30
30
  description: "During bans, blocking a teammate instantly claims the champion they hover to ban.",
31
31
  },
32
32
  {
33
33
  id: "hide-unowned-skins",
34
34
  file: "hide-unowned-skins.js",
35
35
  name: "Hide Unowned Skins",
36
- version: "2.3.0",
36
+ version: "2.3.1",
37
37
  description: "In champion select, only shows the skins you own for the picked champion.",
38
38
  },
39
39
  {
40
40
  id: "unlock-profile-skins",
41
41
  file: "unlock-profile-skins.js",
42
42
  name: "Unlock Profile Skins",
43
- version: "1.2.0",
43
+ version: "1.2.1",
44
44
  description: "Unlocks every skin in the Set profile background window.",
45
45
  },
46
+ {
47
+ id: "auto-claimer",
48
+ file: "auto-claimer.js",
49
+ name: "Auto Claimer",
50
+ version: "1.0.0",
51
+ description: "Automatically claims battle pass, event and mission rewards the moment they become claimable.",
52
+ },
46
53
  ];
47
54
 
48
55
  const STYLE_CSS = `
@@ -77,6 +84,10 @@ const STYLE_CSS = `
77
84
  #${PANEL_ID} .perf-uninstall::after { content: ""; width: 11px; height: 11px;
78
85
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2.5 2.5L9.5 9.5M9.5 2.5L2.5 9.5' fill='none' stroke='%23c89b3c' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E") center / 11px 11px no-repeat; }
79
86
  #${PANEL_ID} .perf-empty { font-size: 12px; color: #a09b8c; }
87
+ #${PANEL_ID} .perf-opts { margin-top: 11px; display: flex; flex-direction: column; gap: 8px; }
88
+ #${PANEL_ID} .perf-opt { display: flex; align-items: center; gap: 9px; cursor: pointer; }
89
+ #${PANEL_ID} .perf-opt .perf-opt-label { font-size: 11px; letter-spacing: 0.3px; color: #a09b8c; }
90
+ #${PANEL_ID} .perf-opt:hover .perf-opt-label { color: #f0e6d2; }
80
91
 
81
92
  .perf-update { position: fixed; inset: 0; z-index: 100000; display: flex; align-items: center;
82
93
  justify-content: center; background: rgba(1, 10, 19, 0.72); }
@@ -104,7 +115,7 @@ let availableModules = BUNDLED_MODULES;
104
115
  let pendingReload = false;
105
116
  let runningVersion = version;
106
117
 
107
- let state = { installed: {}, enabled: {} };
118
+ let state = { installed: {}, enabled: {}, options: {} };
108
119
 
109
120
  async function loadState() {
110
121
  let saved = null;
@@ -117,7 +128,7 @@ async function loadState() {
117
128
  saved = null;
118
129
  }
119
130
  if (saved?.installed && typeof saved.installed === "object") {
120
- state = { installed: saved.installed, enabled: saved.enabled ?? {} };
131
+ state = { installed: saved.installed, enabled: saved.enabled ?? {}, options: saved.options ?? {} };
121
132
  return;
122
133
  }
123
134
  saveState();
@@ -128,6 +139,7 @@ function saveState() {
128
139
  version: /^\d/.test(runningVersion) ? runningVersion : undefined,
129
140
  installed: state.installed,
130
141
  enabled: state.enabled,
142
+ options: state.options,
131
143
  };
132
144
  try {
133
145
  localStorage.setItem(STATE_KEY, JSON.stringify(record));
@@ -171,7 +183,7 @@ function applyLifecycle(entry, enabled) {
171
183
  }
172
184
 
173
185
  function registerPlugin(definition) {
174
- const { id, name, description = "", version = "1.0.0", defaultEnabled = true, onEnable, onDisable } = definition ?? {};
186
+ const { id, name, description = "", version = "1.0.0", defaultEnabled = true, options = [], onEnable, onDisable, onOptionChange } = definition ?? {};
175
187
  if (!id || !name) {
176
188
  throw new Error("[Perfectionism] registerPlugin needs an id and a name.");
177
189
  }
@@ -179,7 +191,7 @@ function registerPlugin(definition) {
179
191
  console.warn(`[Perfectionism] ${name} is already registered.`);
180
192
  return;
181
193
  }
182
- const entry = { id, name, description, version, defaultEnabled, onEnable, onDisable, active: false };
194
+ const entry = { id, name, description, version, defaultEnabled, options, onEnable, onDisable, onOptionChange, active: false };
183
195
  registry.set(id, entry);
184
196
  applyLifecycle(entry, isPluginEnabled(id));
185
197
  refreshPanel();
@@ -203,6 +215,26 @@ function setPluginEnabled(id, enabled) {
203
215
  return true;
204
216
  }
205
217
 
218
+ function getOption(id, key) {
219
+ const entry = registry.get(id);
220
+ const fallback = entry?.options?.find((option) => option.key === key)?.default;
221
+ const stored = state.options?.[id]?.[key];
222
+ return stored === undefined ? Boolean(fallback) : Boolean(stored);
223
+ }
224
+
225
+ function setOption(id, key, value) {
226
+ if (!state.options[id]) state.options[id] = {};
227
+ state.options[id][key] = Boolean(value);
228
+ saveState();
229
+ const entry = registry.get(id);
230
+ try {
231
+ entry?.onOptionChange?.(key, Boolean(value));
232
+ } catch (error) {
233
+ logError(`${entry?.name ?? id} could not apply the option change.`, error);
234
+ }
235
+ refreshPanel();
236
+ }
237
+
206
238
  function getPlugins() {
207
239
  return [...registry.values()]
208
240
  .map(({ id, name, description, version }) => ({ id, name, description, version, enabled: isPluginEnabled(id) }))
@@ -510,6 +542,24 @@ function el(tag, className, text) {
510
542
  return node;
511
543
  }
512
544
 
545
+ function buildOptions(mod) {
546
+ const options = registry.get(mod.id)?.options ?? [];
547
+ if (options.length === 0) return null;
548
+ const wrap = el("div", "perf-opts");
549
+ for (const option of options) {
550
+ const row = el("label", "perf-opt perf-toggle");
551
+ row.title = option.description ?? "";
552
+ const input = el("input");
553
+ input.type = "checkbox";
554
+ input.checked = getOption(mod.id, option.key);
555
+ input.setAttribute("aria-label", `${mod.name}: ${option.label}`);
556
+ input.addEventListener("change", () => setOption(mod.id, option.key, input.checked));
557
+ row.append(input, el("span", "perf-box"), el("span", "perf-opt-label", option.label));
558
+ wrap.append(row);
559
+ }
560
+ return wrap;
561
+ }
562
+
513
563
  function buildToggle(mod) {
514
564
  const label = el("label", "perf-toggle");
515
565
  label.title = `Enable ${mod.name}`;
@@ -556,6 +606,8 @@ function buildCard(mod) {
556
606
  name.append(el("span", "perf-version", `v${registry.get(mod.id)?.version ?? mod.version}`));
557
607
  info.append(name);
558
608
  if (mod.description) info.append(el("div", "perf-desc", mod.description));
609
+ const optionList = buildOptions(mod);
610
+ if (optionList) info.append(optionList);
559
611
 
560
612
  const actions = el("div", "perf-actions");
561
613
  if (installed) actions.append(buildToggle(mod));
@@ -727,6 +779,8 @@ const utils = {
727
779
  setPluginEnabled,
728
780
  isPluginEnabled,
729
781
  getPlugins,
782
+ getOption,
783
+ setOption,
730
784
  getAvailableModules,
731
785
  installModule,
732
786
  uninstallModule,
@@ -1,4 +1,4 @@
1
- export const version = "1.2.0";
1
+ export const version = "1.2.1";
2
2
 
3
3
  const PLUGIN_ID = "unlock-profile-skins";
4
4
  const PICKER_BUTTON_SELECTOR = ".style-profile-skin-picker-button";
@@ -294,6 +294,7 @@ function applyTierGrouping() {
294
294
  tierAppliedForOpen = false;
295
295
  }
296
296
  if (tierAppliedForOpen) return;
297
+ if (!isPickerOpen()) return;
297
298
  const dd = tierDropdown ?? findGroupingDropdown();
298
299
  if (!dd) {
299
300
  tierAppliedForOpen = false;