perfectionist-plugins 1.2.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 +14 -0
- package/auto-claimer.js +374 -0
- package/ban-blocker.js +54 -33
- package/hide-unowned-skins.js +4 -2
- package/package.json +32 -30
- package/perfectionist-utils.js +62 -8
- package/unlock-profile-skins.js +2 -1
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> </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
|
package/auto-claimer.js
ADDED
|
@@ -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.
|
|
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";
|
|
@@ -8,11 +8,11 @@ const SESSION_URI = "/lol-champ-select/v1/session";
|
|
|
8
8
|
const RESEND_AFTER_MS = 150;
|
|
9
9
|
const RESCHEDULE_MS = RESEND_AFTER_MS + 20;
|
|
10
10
|
const MAX_IN_FLIGHT = 3;
|
|
11
|
-
const FAIL_COOLDOWN_MS =
|
|
12
|
-
const FAIL_COOLDOWN_MAX_MS =
|
|
13
|
-
const POLL_MS =
|
|
11
|
+
const FAIL_COOLDOWN_MS = 400;
|
|
12
|
+
const FAIL_COOLDOWN_MAX_MS = 3000;
|
|
13
|
+
const POLL_MS = 500;
|
|
14
14
|
const ENSURE_MS = 300;
|
|
15
|
-
const DETECT_RETRY_MS =
|
|
15
|
+
const DETECT_RETRY_MS = 250;
|
|
16
16
|
const GONE_RESET_MS = 1500;
|
|
17
17
|
|
|
18
18
|
const BAN_GLYPH_SVG =
|
|
@@ -56,13 +56,13 @@ let removeStyle = null;
|
|
|
56
56
|
let unsubscribe = null;
|
|
57
57
|
let pollTimer = null;
|
|
58
58
|
let ensureTimer = null;
|
|
59
|
-
let detectTimer = null;
|
|
60
59
|
let refreshTimer = null;
|
|
61
60
|
let pumpTimer = null;
|
|
62
61
|
let goneTimer = null;
|
|
63
62
|
let observer = null;
|
|
64
63
|
let pollInFlight = false;
|
|
65
64
|
let uiScheduled = false;
|
|
65
|
+
let lastDetect = -Infinity;
|
|
66
66
|
|
|
67
67
|
const enabled = new Map();
|
|
68
68
|
const alias = new Map();
|
|
@@ -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 =
|
|
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 = [];
|
|
@@ -172,24 +173,29 @@ function pump() {
|
|
|
172
173
|
}
|
|
173
174
|
|
|
174
175
|
function fire(id, champ) {
|
|
175
|
-
|
|
176
|
-
if (!send || id == null) return;
|
|
176
|
+
if (id == null) return;
|
|
177
177
|
sent = champ;
|
|
178
178
|
sentAt = performance.now();
|
|
179
179
|
inFlight++;
|
|
180
180
|
clearTimeout(pumpTimer);
|
|
181
181
|
pumpTimer = setTimeout(pump, RESCHEDULE_MS);
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
})
|
|
186
|
-
|
|
182
|
+
fetch(`${SESSION_URI}/actions/${id}`, {
|
|
183
|
+
method: "PATCH",
|
|
184
|
+
headers: { "Content-Type": "application/json" },
|
|
185
|
+
body: JSON.stringify({ championId: champ }),
|
|
186
|
+
})
|
|
187
|
+
.then((r) => {
|
|
188
|
+
if (r.ok) {
|
|
189
|
+
failCount.delete(champ);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
187
192
|
const n = failCount.get(champ) ?? 0;
|
|
188
193
|
failCount.set(champ, n + 1);
|
|
189
194
|
failedUntil.set(champ, performance.now() + Math.min(FAIL_COOLDOWN_MS * 2 ** n, FAIL_COOLDOWN_MAX_MS));
|
|
190
195
|
if (target === champ) target = undefined;
|
|
191
196
|
if (sent === champ) sent = 0;
|
|
192
197
|
})
|
|
198
|
+
.catch(() => {})
|
|
193
199
|
.finally(() => {
|
|
194
200
|
inFlight--;
|
|
195
201
|
pump();
|
|
@@ -203,7 +209,7 @@ function decide(s) {
|
|
|
203
209
|
|
|
204
210
|
cellToPlayer.clear();
|
|
205
211
|
for (const p of m) {
|
|
206
|
-
const row = rows.find((r) => r.slotId === p.cellId);
|
|
212
|
+
const row = rows.find((r) => (r.cell ?? r.slotId) === p.cellId);
|
|
207
213
|
cellToPlayer.set(p.cellId, linkPlayer(p, row?.name, p.cellId));
|
|
208
214
|
}
|
|
209
215
|
|
|
@@ -305,7 +311,8 @@ function detectRows() {
|
|
|
305
311
|
return found.map((r, i) => ({
|
|
306
312
|
...r,
|
|
307
313
|
index: i,
|
|
308
|
-
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,
|
|
309
316
|
}));
|
|
310
317
|
} catch (err) {
|
|
311
318
|
logError("row detection failed.", err);
|
|
@@ -314,11 +321,14 @@ function detectRows() {
|
|
|
314
321
|
}
|
|
315
322
|
|
|
316
323
|
function getRows() {
|
|
324
|
+
const connected = rows.length > 0 && rows.every((r) => r.el.isConnected);
|
|
325
|
+
if (connected) return rows;
|
|
326
|
+
const now = performance.now();
|
|
327
|
+
if (rows.length === 0 && now - lastDetect < DETECT_RETRY_MS) return rows;
|
|
328
|
+
lastDetect = now;
|
|
317
329
|
const found = detectRows();
|
|
318
330
|
if (found.length) rows = found;
|
|
319
331
|
else if (!document.querySelector("div.summoner-object")) rows = [];
|
|
320
|
-
clearTimeout(detectTimer);
|
|
321
|
-
detectTimer = setTimeout(getRows, DETECT_RETRY_MS);
|
|
322
332
|
return rows;
|
|
323
333
|
}
|
|
324
334
|
|
|
@@ -354,11 +364,27 @@ function bindRowsToTeam(session) {
|
|
|
354
364
|
if (!list.length || !cells.length) return;
|
|
355
365
|
for (const c of cells) ensureName(c);
|
|
356
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
|
+
|
|
357
384
|
for (const r of list) {
|
|
358
|
-
const cell =
|
|
359
|
-
(r.slotId != null ? cells.find((c) => c.cellId === r.slotId) : null) ??
|
|
360
|
-
cells.find((c) => nameMatches(r.name, keyOf(c)));
|
|
385
|
+
const cell = pick(r) ?? cells.find((c) => nameMatches(r.name, keyOf(c)));
|
|
361
386
|
if (cell) {
|
|
387
|
+
r.cell = cell.cellId;
|
|
362
388
|
r.player = linkPlayer(cell, r.name, cell.cellId);
|
|
363
389
|
cellToPlayer.set(cell.cellId, r.player);
|
|
364
390
|
} else {
|
|
@@ -420,7 +446,7 @@ function buildBadge(slot) {
|
|
|
420
446
|
|
|
421
447
|
function isSelfRow(r) {
|
|
422
448
|
if (r.el.classList.contains("is-self")) return true;
|
|
423
|
-
return lastSession?.localPlayerCellId != null && r.slotId === lastSession.localPlayerCellId;
|
|
449
|
+
return lastSession?.localPlayerCellId != null && (r.cell ?? r.slotId) === lastSession.localPlayerCellId;
|
|
424
450
|
}
|
|
425
451
|
|
|
426
452
|
function nativeSwapIn(circle) {
|
|
@@ -447,7 +473,8 @@ function ensureButtons() {
|
|
|
447
473
|
buttons.set(r.index, btn);
|
|
448
474
|
}
|
|
449
475
|
btn.dataset.slot = String(r.index);
|
|
450
|
-
|
|
476
|
+
const cell = r.cell ?? r.slotId;
|
|
477
|
+
btn.dataset.cell = cell == null ? "" : String(cell);
|
|
451
478
|
}
|
|
452
479
|
for (const [slot, btn] of [...buttons]) {
|
|
453
480
|
if (!list[slot] || !btn.isConnected) {
|
|
@@ -536,13 +563,7 @@ function onEnable(utils) {
|
|
|
536
563
|
}, ENSURE_MS);
|
|
537
564
|
|
|
538
565
|
try {
|
|
539
|
-
observer = new MutationObserver(() =>
|
|
540
|
-
try {
|
|
541
|
-
update();
|
|
542
|
-
} catch (err) {
|
|
543
|
-
logError("ui update failed.", err);
|
|
544
|
-
}
|
|
545
|
-
});
|
|
566
|
+
observer = new MutationObserver(() => scheduleUi());
|
|
546
567
|
observer.observe(document.body, { childList: true, subtree: true });
|
|
547
568
|
} catch (err) {
|
|
548
569
|
logError("observer setup failed.", err);
|
|
@@ -577,10 +598,9 @@ function onDisable() {
|
|
|
577
598
|
clearInterval(pollTimer);
|
|
578
599
|
clearInterval(ensureTimer);
|
|
579
600
|
clearTimeout(refreshTimer);
|
|
580
|
-
clearTimeout(detectTimer);
|
|
581
601
|
clearTimeout(pumpTimer);
|
|
582
602
|
clearTimeout(goneTimer);
|
|
583
|
-
pollTimer = ensureTimer = refreshTimer =
|
|
603
|
+
pollTimer = ensureTimer = refreshTimer = pumpTimer = goneTimer = null;
|
|
584
604
|
observer?.disconnect();
|
|
585
605
|
observer = null;
|
|
586
606
|
|
|
@@ -597,6 +617,7 @@ function onDisable() {
|
|
|
597
617
|
fetchedNames.clear();
|
|
598
618
|
lastSession = null;
|
|
599
619
|
rows = [];
|
|
620
|
+
lastDetect = -Infinity;
|
|
600
621
|
removeButtons();
|
|
601
622
|
|
|
602
623
|
removeStyle?.();
|
package/hide-unowned-skins.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const version = "2.3.
|
|
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 (
|
|
597
|
+
if (item) scheduleRefresh();
|
|
596
598
|
});
|
|
597
599
|
renderObserver.observe(document.documentElement ?? document, OBSERVER_OPTIONS);
|
|
598
600
|
|
package/package.json
CHANGED
|
@@ -1,30 +1,32 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "perfectionist-plugins",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Small plugins that fix the annoying parts of League of Legends champ select. Built for Pengu Loader.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "perfectionist-utils.js",
|
|
7
|
-
"exports": {
|
|
8
|
-
".": "./perfectionist-utils.js",
|
|
9
|
-
"./perfectionist-utils.js": "./perfectionist-utils.js",
|
|
10
|
-
"./ban-blocker.js": "./ban-blocker.js",
|
|
11
|
-
"./hide-unowned-skins.js": "./hide-unowned-skins.js",
|
|
12
|
-
"./unlock-profile-skins.js": "./unlock-profile-skins.js"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "perfectionist-plugins",
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"description": "Small plugins that fix the annoying parts of League of Legends champ select. Built for Pengu Loader.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "perfectionist-utils.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./perfectionist-utils.js",
|
|
9
|
+
"./perfectionist-utils.js": "./perfectionist-utils.js",
|
|
10
|
+
"./ban-blocker.js": "./ban-blocker.js",
|
|
11
|
+
"./hide-unowned-skins.js": "./hide-unowned-skins.js",
|
|
12
|
+
"./unlock-profile-skins.js": "./unlock-profile-skins.js",
|
|
13
|
+
"./auto-claimer.js": "./auto-claimer.js"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"perfectionist-utils.js",
|
|
17
|
+
"ban-blocker.js",
|
|
18
|
+
"hide-unowned-skins.js",
|
|
19
|
+
"unlock-profile-skins.js",
|
|
20
|
+
"auto-claimer.js"
|
|
21
|
+
],
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/SrKing-9812/Perfectionist-Plugins.git"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"pengu-loader",
|
|
28
|
+
"league-of-legends",
|
|
29
|
+
"plugin"
|
|
30
|
+
],
|
|
31
|
+
"author": "Sr. King"
|
|
32
|
+
}
|
package/perfectionist-utils.js
CHANGED
|
@@ -6,7 +6,7 @@ const PANEL_ID = "perfectionist-plugins-panel";
|
|
|
6
6
|
const STYLE_ID = "perfectionist-plugins-style";
|
|
7
7
|
const TAB_TITLE = "Perfectionism";
|
|
8
8
|
|
|
9
|
-
export const version = "1.
|
|
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.
|
|
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.
|
|
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.
|
|
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,
|
package/unlock-profile-skins.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const version = "1.2.
|
|
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;
|