perfectionist-plugins 1.8.0 → 1.9.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/auto-read.js +75 -4
- package/hide-unowned-skins.js +58 -14
- package/package.json +1 -1
- package/perfectionist-utils.js +121 -21
package/auto-read.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const PLUGIN_ID = "auto-read";
|
|
2
|
-
const version = "1.
|
|
2
|
+
const version = "1.2.0";
|
|
3
3
|
|
|
4
4
|
const SWEEP_DEBOUNCE_MS = 500;
|
|
5
5
|
const ITEM_COOLDOWN_MS = 10 * 60 * 1000;
|
|
@@ -39,6 +39,13 @@ const CLASH_BULK_ACKS = [
|
|
|
39
39
|
"/lol-clash/v1/eog-player-update/acknowledge",
|
|
40
40
|
"/lol-clash/v1/game-end/acknowledge",
|
|
41
41
|
];
|
|
42
|
+
const IDENTITY_NOTICE_TYPES = ["ACHIEVEMENT_TITLE", "SUMMONER_ICON", "REGALIA_BANNER"];
|
|
43
|
+
const IDENTITY_ACK_URI = "/lol-inventory/v1/notification/acknowledge";
|
|
44
|
+
const TOKENS_PREFS_URI = "/lol-settings/v2/account/LCUPreferences/lol-customizer-tokens";
|
|
45
|
+
const LEVELUP_PREFS_URI = "/lol-settings/v2/account/LCUPreferences/lol-challenges-latest-level-up";
|
|
46
|
+
const TFT_BATTLE_PASS_URI = "/lol-tft-pass/v1/battle-pass";
|
|
47
|
+
const TFT_HOME_HUB_URI = "/lol-tft/v1/tft/homeHub";
|
|
48
|
+
const TFT_PREFS_URI = "/lol-settings/v2/account/LCUPreferences/lol-tft";
|
|
42
49
|
const PIP_STYLE_ID = "auto-read-pips-style";
|
|
43
50
|
const PIP_CSS = ".activity-center__tab_label_notification { display: none !important; }";
|
|
44
51
|
|
|
@@ -201,6 +208,65 @@ async function sweepNotices() {
|
|
|
201
208
|
}
|
|
202
209
|
}
|
|
203
210
|
|
|
211
|
+
async function sweepIdentity() {
|
|
212
|
+
for (const type of IDENTITY_NOTICE_TYPES) {
|
|
213
|
+
const list = await getJson(`/lol-inventory/v1/notifications/${type}`).catch(() => null);
|
|
214
|
+
if (!Array.isArray(list)) continue;
|
|
215
|
+
for (const note of list) {
|
|
216
|
+
const id = note?.id;
|
|
217
|
+
if (id == null || note.acknowledged === true) continue;
|
|
218
|
+
if (!ready(`invnote:${id}`, ITEM_COOLDOWN_MS)) continue;
|
|
219
|
+
stamp(`invnote:${id}`);
|
|
220
|
+
await sendJson("POST", IDENTITY_ACK_URI, { id }).catch(() => {});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (!ready("tokens-visit", BULK_COOLDOWN_MS)) return;
|
|
224
|
+
const [tokens, levelUp] = await Promise.all([
|
|
225
|
+
getJson(TOKENS_PREFS_URI).catch(() => null),
|
|
226
|
+
getJson(LEVELUP_PREFS_URI).catch(() => null),
|
|
227
|
+
]);
|
|
228
|
+
const lastVisit = Number(tokens?.data?.lastVisitTime ?? 0);
|
|
229
|
+
const lastLevelUp = Number(levelUp?.data?.lastLevelUpTime ?? 0);
|
|
230
|
+
if (!lastVisit || !(lastLevelUp > lastVisit)) return;
|
|
231
|
+
stamp("tokens-visit");
|
|
232
|
+
await sendJson("PUT", TOKENS_PREFS_URI, { schemaVersion: 1, data: { lastVisitTime: Date.now() } }).catch(() => {});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function sweepTft() {
|
|
236
|
+
const bp = await getJson(TFT_BATTLE_PASS_URI).catch(() => null);
|
|
237
|
+
if (bp && Array.isArray(bp.milestones) && Array.isArray(bp.bonuses)) {
|
|
238
|
+
const list = [...bp.milestones, ...bp.bonuses];
|
|
239
|
+
const claimable = list.some((m) => m && m.status === "CLAIMABLE" && !m.isLocked);
|
|
240
|
+
const pending = list.some((m) => m && m.isClaimRequestPending);
|
|
241
|
+
const passId = bp?.info?.passId;
|
|
242
|
+
if (claimable && !pending && passId && ready("tft-claim", BULK_COOLDOWN_MS)) {
|
|
243
|
+
stamp("tft-claim");
|
|
244
|
+
await sendJson("PUT", `/lol-tft-pass/v1/pass/${encodeURIComponent(passId)}/milestone/claimAllRewards`).catch(() => {});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (!ready("tft-promos", BULK_COOLDOWN_MS)) return;
|
|
248
|
+
const [hub, prefs] = await Promise.all([
|
|
249
|
+
getJson(TFT_HOME_HUB_URI).catch(() => null),
|
|
250
|
+
getJson(TFT_PREFS_URI).catch(() => null),
|
|
251
|
+
]);
|
|
252
|
+
const store = hub?.storePromoOfferIds;
|
|
253
|
+
const battle = hub?.battlePassOfferIds;
|
|
254
|
+
const tact = hub?.tacticianPromoOfferIds;
|
|
255
|
+
if (!hub?.enabled || !Array.isArray(store) || !Array.isArray(battle) || !Array.isArray(tact)) return;
|
|
256
|
+
const target = { storeOfferIds: [battle[0], store[0]], tacticianOfferIds: tact };
|
|
257
|
+
const seen = prefs?.data?.seenOfferIds;
|
|
258
|
+
const same =
|
|
259
|
+
Array.isArray(seen?.storeOfferIds) &&
|
|
260
|
+
seen.storeOfferIds.length === target.storeOfferIds.length &&
|
|
261
|
+
seen.storeOfferIds.every((value, index) => value === target.storeOfferIds[index]) &&
|
|
262
|
+
Array.isArray(seen?.tacticianOfferIds) &&
|
|
263
|
+
seen.tacticianOfferIds.length === target.tacticianOfferIds.length &&
|
|
264
|
+
seen.tacticianOfferIds.every((value, index) => value === target.tacticianOfferIds[index]);
|
|
265
|
+
if (same) return;
|
|
266
|
+
stamp("tft-promos");
|
|
267
|
+
await sendJson("PATCH", TFT_PREFS_URI, { data: { seenOfferIds: target }, schemaVersion: 1 }).catch(() => {});
|
|
268
|
+
}
|
|
269
|
+
|
|
204
270
|
function syncPips() {
|
|
205
271
|
const wanted = active && on("pips");
|
|
206
272
|
let style = document.getElementById(PIP_STYLE_ID);
|
|
@@ -230,7 +296,11 @@ async function runSweep() {
|
|
|
230
296
|
try {
|
|
231
297
|
if (on("quests")) await sweepQuests().catch(() => {});
|
|
232
298
|
if (on("loot")) await sweepLootMarks().catch(() => {});
|
|
233
|
-
if (on("rewards"))
|
|
299
|
+
if (on("rewards")) {
|
|
300
|
+
await sweepRewards().catch(() => {});
|
|
301
|
+
await sweepIdentity().catch(() => {});
|
|
302
|
+
}
|
|
303
|
+
if (on("tft")) await sweepTft().catch(() => {});
|
|
234
304
|
if (on("clash")) await sweepClash().catch(() => {});
|
|
235
305
|
if (on("ranked")) await sweepRanked().catch(() => {});
|
|
236
306
|
if (on("notices")) await sweepNotices().catch(() => {});
|
|
@@ -247,7 +317,7 @@ function onEnable(utils) {
|
|
|
247
317
|
api = utils;
|
|
248
318
|
active = true;
|
|
249
319
|
cooldowns.clear();
|
|
250
|
-
for (const uri of [MISSIONS_URI, LOOT_URI, CLASH_FLAGS_URI, REWARDS_GRANTS_URI, LOOT_NOTIF_URI, MASTERY_NOTIF_URI, ...HONOR_NOTICES.map((n) => n.uri), RANKED_NOTIF_URI, EOS_NOTIF_URI, ...NOTICE_LIST_URIS]) {
|
|
320
|
+
for (const uri of [MISSIONS_URI, LOOT_URI, CLASH_FLAGS_URI, REWARDS_GRANTS_URI, LOOT_NOTIF_URI, MASTERY_NOTIF_URI, ...HONOR_NOTICES.map((n) => n.uri), RANKED_NOTIF_URI, EOS_NOTIF_URI, ...NOTICE_LIST_URIS, ...IDENTITY_NOTICE_TYPES.map((type) => `/lol-inventory/v1/notifications/${type}`), TOKENS_PREFS_URI, LEVELUP_PREFS_URI, TFT_BATTLE_PASS_URI, TFT_HOME_HUB_URI, TFT_PREFS_URI]) {
|
|
251
321
|
try {
|
|
252
322
|
const off = api.subscribe(uri, scheduleSweep);
|
|
253
323
|
if (off) unsubscribes.push(off);
|
|
@@ -286,7 +356,7 @@ export function init() {
|
|
|
286
356
|
utils.registerPlugin({
|
|
287
357
|
id: PLUGIN_ID,
|
|
288
358
|
name: "Auto Read",
|
|
289
|
-
description: "Automatically marks quests, loot, Clash and client notices as read so no badge or dot stays lit.",
|
|
359
|
+
description: "Automatically marks quests, loot, Clash, TFT and client notices as read so no badge or dot stays lit.",
|
|
290
360
|
version,
|
|
291
361
|
defaultEnabled: true,
|
|
292
362
|
options: [
|
|
@@ -299,6 +369,7 @@ export function init() {
|
|
|
299
369
|
{ id: "quests", label: "Quests", description: "Quest and mission badges, including the mission button counter.", default: true },
|
|
300
370
|
{ id: "loot", label: "Loot marks", description: "The new mark on freshly earned loot.", default: true },
|
|
301
371
|
{ id: "rewards", label: "Rewards", description: "The dot on your avatar for new tokens, titles and banners, plus mastery, honor and loot notifications.", default: true },
|
|
372
|
+
{ id: "tft", label: "TFT tab", description: "Claims claimable TFT pass rewards and marks TFT promo offers as seen so the TFT tab dot stays off.", default: true },
|
|
302
373
|
{ id: "clash", label: "Clash", description: "Clash tab flags and Clash notifications.", default: true },
|
|
303
374
|
{ id: "ranked", label: "Ranked", description: "Ranked, end-of-season and champion mastery notices.", default: true },
|
|
304
375
|
{ id: "notices", label: "Notices", description: "Patch, patcher, leaver buster, lobby, behavior and gameflow notices.", default: true },
|
package/hide-unowned-skins.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const version = "2.
|
|
1
|
+
export const version = "2.4.0";
|
|
2
2
|
|
|
3
3
|
const PLUGIN_ID = "hide-unowned-skins";
|
|
4
4
|
const STYLE_ID = "perfectionist-hide-unowned-skins-style";
|
|
@@ -25,7 +25,7 @@ const OBSERVER_OPTIONS = {
|
|
|
25
25
|
childList: true,
|
|
26
26
|
subtree: true,
|
|
27
27
|
attributes: true,
|
|
28
|
-
attributeFilter: ["class", "style"],
|
|
28
|
+
attributeFilter: ["class", "style", "src"],
|
|
29
29
|
};
|
|
30
30
|
|
|
31
31
|
let filterActive = true;
|
|
@@ -160,7 +160,14 @@ function installFetchFilter() {
|
|
|
160
160
|
if (!rawFetch) rawFetch = original;
|
|
161
161
|
|
|
162
162
|
const wrapped = function (input, init) {
|
|
163
|
-
const url =
|
|
163
|
+
const url =
|
|
164
|
+
typeof input === "string"
|
|
165
|
+
? input
|
|
166
|
+
: typeof input?.url === "string"
|
|
167
|
+
? input.url
|
|
168
|
+
: typeof input?.href === "string"
|
|
169
|
+
? input.href
|
|
170
|
+
: "";
|
|
164
171
|
const promise = original.call(this, input, init);
|
|
165
172
|
if (!filterActive || url.split("?")[0].indexOf(CAROUSEL_URI) === -1) return promise;
|
|
166
173
|
return promise.then((response) => {
|
|
@@ -273,6 +280,7 @@ let sessionMisses = 0;
|
|
|
273
280
|
let championMisses = 0;
|
|
274
281
|
let renderObserver = null;
|
|
275
282
|
const repackOriginals = new Map();
|
|
283
|
+
const lastAppliedLeft = new Map();
|
|
276
284
|
const skinSuffixCache = new Map();
|
|
277
285
|
let repackTimer = null;
|
|
278
286
|
let removeStyle = null;
|
|
@@ -282,7 +290,7 @@ function logError(scope, error) {
|
|
|
282
290
|
}
|
|
283
291
|
|
|
284
292
|
function skinSuffixOf(text) {
|
|
285
|
-
const numbered = text.match(/Skins\/Skin(\d+)\//i) ?? text.match(/
|
|
293
|
+
const numbered = text.match(/Skins\/Skin(\d+)\//i) ?? text.match(/skin(\d+)\//i);
|
|
286
294
|
if (numbered) return Number(numbered[1]);
|
|
287
295
|
return /Skins\/Base\//i.test(text) ? 0 : null;
|
|
288
296
|
}
|
|
@@ -311,7 +319,16 @@ function hideTargetFor(item) {
|
|
|
311
319
|
return target;
|
|
312
320
|
}
|
|
313
321
|
|
|
322
|
+
function pruneRepack() {
|
|
323
|
+
for (const item of [...repackOriginals.keys()]) {
|
|
324
|
+
if (item.isConnected) continue;
|
|
325
|
+
repackOriginals.delete(item);
|
|
326
|
+
lastAppliedLeft.delete(item);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
314
330
|
function repackVisibleItems(items) {
|
|
331
|
+
pruneRepack();
|
|
315
332
|
const visible = items.filter((item) => !item.classList.contains(HIDDEN_CLASS));
|
|
316
333
|
if (visible.length < 2) {
|
|
317
334
|
restoreRepack();
|
|
@@ -340,15 +357,17 @@ function repackVisibleItems(items) {
|
|
|
340
357
|
|
|
341
358
|
const setLeft = (item, x) => {
|
|
342
359
|
const want = `${x}px`;
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
360
|
+
const current = item.style.getPropertyValue("left");
|
|
361
|
+
const priority = item.style.getPropertyPriority("left");
|
|
362
|
+
const last = lastAppliedLeft.get(item);
|
|
363
|
+
const clientMoved = last !== undefined && (current !== last || priority !== "important");
|
|
364
|
+
if (clientMoved || !repackOriginals.has(item)) {
|
|
365
|
+
repackOriginals.set(item, { value: current, priority });
|
|
366
|
+
}
|
|
367
|
+
if (current !== want || priority !== "important") {
|
|
350
368
|
item.style.setProperty("left", want, "important");
|
|
351
369
|
}
|
|
370
|
+
lastAppliedLeft.set(item, want);
|
|
352
371
|
};
|
|
353
372
|
|
|
354
373
|
items.forEach((item, index) => {
|
|
@@ -367,12 +386,16 @@ function repackVisibleItems(items) {
|
|
|
367
386
|
}
|
|
368
387
|
|
|
369
388
|
function restoreRepack() {
|
|
370
|
-
if (repackOriginals.size === 0) return;
|
|
371
389
|
for (const [item, original] of repackOriginals) {
|
|
390
|
+
if (!item.isConnected) continue;
|
|
391
|
+
const current = item.style.getPropertyValue("left");
|
|
392
|
+
const last = lastAppliedLeft.get(item);
|
|
393
|
+
if (last !== undefined && (current !== last || item.style.getPropertyPriority("left") !== "important")) continue;
|
|
372
394
|
if (original.value) item.style.setProperty("left", original.value, original.priority || undefined);
|
|
373
395
|
else item.style.removeProperty("left");
|
|
374
396
|
}
|
|
375
397
|
repackOriginals.clear();
|
|
398
|
+
lastAppliedLeft.clear();
|
|
376
399
|
}
|
|
377
400
|
|
|
378
401
|
function applyAll() {
|
|
@@ -456,8 +479,18 @@ async function loadCarouselOrder() {
|
|
|
456
479
|
try {
|
|
457
480
|
let skins;
|
|
458
481
|
if (rawFetch) {
|
|
459
|
-
|
|
460
|
-
|
|
482
|
+
let timer = null;
|
|
483
|
+
try {
|
|
484
|
+
const response = await Promise.race([
|
|
485
|
+
rawFetch(CAROUSEL_URI, { headers: { Accept: "application/json" } }),
|
|
486
|
+
new Promise((_, reject) => {
|
|
487
|
+
timer = setTimeout(() => reject(new Error("carousel request timed out")), 8000);
|
|
488
|
+
}),
|
|
489
|
+
]);
|
|
490
|
+
skins = response.ok ? await response.json() : null;
|
|
491
|
+
} finally {
|
|
492
|
+
clearTimeout(timer);
|
|
493
|
+
}
|
|
461
494
|
} else {
|
|
462
495
|
skins = await api.lcu.get(CAROUSEL_URI);
|
|
463
496
|
}
|
|
@@ -478,9 +511,13 @@ function nextOwnedSkin(owned, fromId, direction) {
|
|
|
478
511
|
async function selectSkin(skinId) {
|
|
479
512
|
selectedSkinId = skinId;
|
|
480
513
|
selectInFlight = true;
|
|
514
|
+
const guard = setTimeout(() => {
|
|
515
|
+
selectInFlight = false;
|
|
516
|
+
}, 8000);
|
|
481
517
|
try {
|
|
482
518
|
await api.lcu.send("PATCH", SELECTION_URI, { selectedSkinId: skinId });
|
|
483
519
|
} finally {
|
|
520
|
+
clearTimeout(guard);
|
|
484
521
|
selectInFlight = false;
|
|
485
522
|
}
|
|
486
523
|
}
|
|
@@ -557,6 +594,10 @@ function updateForSession(session, selection) {
|
|
|
557
594
|
applyAllGuarded();
|
|
558
595
|
}
|
|
559
596
|
|
|
597
|
+
function onWindowResize() {
|
|
598
|
+
scheduleRepack();
|
|
599
|
+
}
|
|
600
|
+
|
|
560
601
|
function scheduleRefresh() {
|
|
561
602
|
clearTimeout(refreshTimer);
|
|
562
603
|
refreshTimer = setTimeout(pollSession, REFRESH_DEBOUNCE_MS);
|
|
@@ -596,6 +637,7 @@ function onEnable(utils) {
|
|
|
596
637
|
removeStyle = api.addStyle(STYLE_ID, CSS);
|
|
597
638
|
|
|
598
639
|
renderObserver = new MutationObserver(() => {
|
|
640
|
+
ensureSourceFilter();
|
|
599
641
|
const item = document.querySelector(SKIN_ITEM_SELECTOR);
|
|
600
642
|
if (!item && !document.querySelector(DOT_SELECTOR)) return;
|
|
601
643
|
applyAllGuarded();
|
|
@@ -603,11 +645,13 @@ function onEnable(utils) {
|
|
|
603
645
|
});
|
|
604
646
|
renderObserver.observe(document.documentElement ?? document, OBSERVER_OPTIONS);
|
|
605
647
|
|
|
648
|
+
window.addEventListener("resize", onWindowResize);
|
|
606
649
|
unsubscribe = api.subscribe(SESSION_URI, onSessionPush);
|
|
607
650
|
pollSession();
|
|
608
651
|
}
|
|
609
652
|
|
|
610
653
|
function onDisable() {
|
|
654
|
+
window.removeEventListener("resize", onWindowResize);
|
|
611
655
|
unsubscribe?.();
|
|
612
656
|
unsubscribe = null;
|
|
613
657
|
clearTimeout(refreshTimer);
|
package/package.json
CHANGED
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.8.0";
|
|
10
10
|
|
|
11
11
|
const PKG = "perfectionist-plugins";
|
|
12
12
|
const CDN = "https://cdn.jsdelivr.net/npm";
|
|
@@ -33,7 +33,7 @@ const BUNDLED_MODULES = [
|
|
|
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.4.0",
|
|
37
37
|
description: "In champion select, only shows the skins you own for the picked champion.",
|
|
38
38
|
},
|
|
39
39
|
{
|
|
@@ -54,14 +54,14 @@ const BUNDLED_MODULES = [
|
|
|
54
54
|
id: "auto-read",
|
|
55
55
|
file: "auto-read.js",
|
|
56
56
|
name: "Auto Read",
|
|
57
|
-
version: "1.
|
|
58
|
-
description: "Automatically marks quests, loot, Clash and client notices as read so no badge or dot stays lit.",
|
|
57
|
+
version: "1.2.0",
|
|
58
|
+
description: "Automatically marks quests, loot, Clash, TFT and client notices as read so no badge or dot stays lit.",
|
|
59
59
|
},
|
|
60
60
|
];
|
|
61
61
|
|
|
62
62
|
const STYLE_CSS = `
|
|
63
|
-
#${PANEL_ID} { overflow-x: hidden; padding: 15px 20px; position: relative; width: calc(100% - 40px); box-sizing: content-box; color: #f0e6d2; font-family: "LoL Body", Arial, "Helvetica Neue", Helvetica, sans-serif; }
|
|
64
|
-
#${PANEL_ID} > lol-uikit-scrollable { display: block; height:
|
|
63
|
+
#${PANEL_ID} { display: flex; flex-direction: column; overflow-x: hidden; padding: 15px 20px; position: relative; width: calc(100% - 40px); box-sizing: content-box; color: #f0e6d2; font-family: "LoL Body", Arial, "Helvetica Neue", Helvetica, sans-serif; }
|
|
64
|
+
#${PANEL_ID} > lol-uikit-scrollable { display: block; flex: 1 1 auto; min-height: 0; }
|
|
65
65
|
#${PANEL_ID} .perf-card { display: flex; align-items: flex-start; justify-content: space-between; gap: 24px;
|
|
66
66
|
padding: 16px 18px; margin-bottom: 12px; box-sizing: border-box;
|
|
67
67
|
background: #010a13; border: 1px solid #785a28; }
|
|
@@ -121,6 +121,21 @@ const STYLE_CSS = `
|
|
|
121
121
|
#${PANEL_ID} .perf-chip--on { color: #010a13; background: #c89b3c; border-color: #c89b3c; }
|
|
122
122
|
#${PANEL_ID} .perf-chip--on:hover { background: #f0e6d2; }
|
|
123
123
|
|
|
124
|
+
#${PANEL_ID} .perf-tools { display: flex; align-items: center; gap: 10px; flex: 0 0 auto; margin-bottom: 14px; }
|
|
125
|
+
#${PANEL_ID} .perf-search { flex: 1 1 auto; min-width: 0; box-sizing: border-box;
|
|
126
|
+
font-family: "LoL Body", Arial, "Helvetica Neue", Helvetica, sans-serif; font-size: 12px; letter-spacing: 0.3px;
|
|
127
|
+
color: #f0e6d2; background: #0a1428; border: 1px solid #785a28; padding: 9px 12px; outline: none; }
|
|
128
|
+
#${PANEL_ID} .perf-search:focus { border-color: #c89b3c; }
|
|
129
|
+
#${PANEL_ID} .perf-search::placeholder { color: #a09b8c; }
|
|
130
|
+
#${PANEL_ID} .perf-master { flex: 0 0 auto; white-space: nowrap; }
|
|
131
|
+
#${PANEL_ID} .perf-card--locked .perf-opts { pointer-events: none; }
|
|
132
|
+
#${PANEL_ID} .perf-card--locked .perf-actions > .perf-toggle { pointer-events: none; }
|
|
133
|
+
#${PANEL_ID} .perf-card--locked .perf-caret { opacity: 0.45; }
|
|
134
|
+
#${PANEL_ID} .perf-card--locked .perf-chips-label { opacity: 0.45; }
|
|
135
|
+
#${PANEL_ID} .perf-chip:disabled { opacity: 0.45; cursor: default; }
|
|
136
|
+
#${PANEL_ID} .perf-chip:disabled:hover { color: #a09b8c; border-color: #785a28; background: #0a1428; }
|
|
137
|
+
#${PANEL_ID} .perf-chip--on:disabled:hover { color: #010a13; border-color: #c89b3c; background: #c89b3c; }
|
|
138
|
+
|
|
124
139
|
.perf-update { position: fixed; inset: 0; z-index: 100000; display: flex; align-items: center;
|
|
125
140
|
justify-content: center; background: rgba(1, 10, 19, 0.72); }
|
|
126
141
|
.perf-update-box { width: 430px; box-sizing: border-box; padding: 22px 26px;
|
|
@@ -145,9 +160,10 @@ let coords = null;
|
|
|
145
160
|
const resolved = new Map();
|
|
146
161
|
let availableModules = BUNDLED_MODULES;
|
|
147
162
|
let pendingReload = false;
|
|
163
|
+
let searchQuery = "";
|
|
148
164
|
let runningVersion = version;
|
|
149
165
|
|
|
150
|
-
let state = { installed: {}, enabled: {}, options: {} };
|
|
166
|
+
let state = { installed: {}, enabled: {}, options: {}, masterLocked: false };
|
|
151
167
|
|
|
152
168
|
async function loadState() {
|
|
153
169
|
let saved = null;
|
|
@@ -160,7 +176,7 @@ async function loadState() {
|
|
|
160
176
|
saved = null;
|
|
161
177
|
}
|
|
162
178
|
if (saved?.installed && typeof saved.installed === "object") {
|
|
163
|
-
state = { installed: saved.installed, enabled: saved.enabled ?? {}, options: saved.options ?? {} };
|
|
179
|
+
state = { installed: saved.installed, enabled: saved.enabled ?? {}, options: saved.options ?? {}, masterLocked: saved.masterLocked === true };
|
|
164
180
|
return;
|
|
165
181
|
}
|
|
166
182
|
saveState();
|
|
@@ -172,6 +188,7 @@ function saveState() {
|
|
|
172
188
|
installed: state.installed,
|
|
173
189
|
enabled: state.enabled,
|
|
174
190
|
options: state.options,
|
|
191
|
+
masterLocked: state.masterLocked === true,
|
|
175
192
|
};
|
|
176
193
|
try {
|
|
177
194
|
localStorage.setItem(STATE_KEY, JSON.stringify(record));
|
|
@@ -524,16 +541,23 @@ function addStyle(name, css) {
|
|
|
524
541
|
return () => element.remove();
|
|
525
542
|
}
|
|
526
543
|
|
|
544
|
+
const LCU_TIMEOUT_MS = 10000;
|
|
545
|
+
|
|
527
546
|
function lcuGet(path, { ttl = 0 } = {}) {
|
|
528
547
|
const now = Date.now();
|
|
529
548
|
const cached = requestCache.get(path);
|
|
530
549
|
if (cached && (cached.pending || cached.expires > now)) return cached.promise;
|
|
531
550
|
|
|
532
551
|
const entry = { pending: true, expires: 0, promise: null };
|
|
533
|
-
|
|
552
|
+
const request = fetch(path, { headers: { Accept: "application/json" } }).then(async (response) => {
|
|
534
553
|
if (!response.ok) throw new Error(`LCU ${path} responded ${response.status}`);
|
|
535
554
|
return response.status === 204 ? null : response.json();
|
|
536
555
|
});
|
|
556
|
+
let timer = null;
|
|
557
|
+
const timeout = new Promise((_, reject) => {
|
|
558
|
+
timer = setTimeout(() => reject(new Error(`LCU ${path} timed out`)), LCU_TIMEOUT_MS);
|
|
559
|
+
});
|
|
560
|
+
entry.promise = Promise.race([request, timeout]).finally(() => clearTimeout(timer));
|
|
537
561
|
requestCache.set(path, entry);
|
|
538
562
|
entry.promise.then(
|
|
539
563
|
() => {
|
|
@@ -605,7 +629,7 @@ function buildOptionRow(mod, option, disabled, onChange) {
|
|
|
605
629
|
return row;
|
|
606
630
|
}
|
|
607
631
|
|
|
608
|
-
function buildChips(mod, option) {
|
|
632
|
+
function buildChips(mod, option, locked) {
|
|
609
633
|
const block = el("div", "perf-chip-block");
|
|
610
634
|
block.title = option.description ?? "";
|
|
611
635
|
if (option.label) block.append(el("div", "perf-chips-label", option.label));
|
|
@@ -615,6 +639,7 @@ function buildChips(mod, option) {
|
|
|
615
639
|
const chip = el("button", "perf-chip", item.label);
|
|
616
640
|
chip.type = "button";
|
|
617
641
|
chip.title = item.description ?? "";
|
|
642
|
+
chip.disabled = Boolean(locked);
|
|
618
643
|
if (getOption(mod.id, key)) chip.classList.add("perf-chip--on");
|
|
619
644
|
chip.addEventListener("click", () => {
|
|
620
645
|
setOption(mod.id, key, !getOption(mod.id, key), false);
|
|
@@ -626,17 +651,17 @@ function buildChips(mod, option) {
|
|
|
626
651
|
return block;
|
|
627
652
|
}
|
|
628
653
|
|
|
629
|
-
function buildOptions(mod) {
|
|
654
|
+
function buildOptions(mod, locked) {
|
|
630
655
|
const options = registry.get(mod.id)?.options ?? [];
|
|
631
656
|
if (options.length === 0) return null;
|
|
632
657
|
const wrap = el("div", "perf-opts");
|
|
633
658
|
for (const option of options) {
|
|
634
659
|
if (option.type === "chips") {
|
|
635
|
-
wrap.append(buildChips(mod, option));
|
|
660
|
+
wrap.append(buildChips(mod, option, locked));
|
|
636
661
|
continue;
|
|
637
662
|
}
|
|
638
663
|
if (!option.children?.length) {
|
|
639
|
-
wrap.append(buildOptionRow(mod, option));
|
|
664
|
+
wrap.append(buildOptionRow(mod, option, Boolean(locked)));
|
|
640
665
|
continue;
|
|
641
666
|
}
|
|
642
667
|
const group = el("div", "perf-opt-group");
|
|
@@ -647,12 +672,12 @@ function buildOptions(mod) {
|
|
|
647
672
|
const kids = el("div", "perf-opt-children");
|
|
648
673
|
const childInputs = [];
|
|
649
674
|
for (const child of option.children) {
|
|
650
|
-
const childRow = buildOptionRow(mod, child, !getOption(mod.id, option.key));
|
|
675
|
+
const childRow = buildOptionRow(mod, child, !getOption(mod.id, option.key) || Boolean(locked));
|
|
651
676
|
childInputs.push(childRow.querySelector("input"));
|
|
652
677
|
kids.append(childRow);
|
|
653
678
|
}
|
|
654
|
-
const master = buildOptionRow(mod, option,
|
|
655
|
-
for (const input of childInputs) input.disabled = !checked;
|
|
679
|
+
const master = buildOptionRow(mod, option, Boolean(locked), (checked) => {
|
|
680
|
+
for (const input of childInputs) input.disabled = !checked || Boolean(locked);
|
|
656
681
|
setOption(mod.id, option.key, checked, false);
|
|
657
682
|
});
|
|
658
683
|
const groupKey = `${mod.id}:${option.key}`;
|
|
@@ -674,12 +699,13 @@ function buildOptions(mod) {
|
|
|
674
699
|
return wrap;
|
|
675
700
|
}
|
|
676
701
|
|
|
677
|
-
function buildToggle(mod) {
|
|
702
|
+
function buildToggle(mod, locked) {
|
|
678
703
|
const label = el("label", "perf-toggle");
|
|
679
704
|
label.title = `Enable ${mod.name}`;
|
|
680
705
|
const input = el("input");
|
|
681
706
|
input.type = "checkbox";
|
|
682
707
|
input.checked = isPluginEnabled(mod.id);
|
|
708
|
+
input.disabled = Boolean(locked);
|
|
683
709
|
input.setAttribute("aria-label", `Enable ${mod.name}`);
|
|
684
710
|
input.addEventListener("change", () => setPluginEnabled(mod.id, input.checked));
|
|
685
711
|
label.append(input, el("span", "perf-box"));
|
|
@@ -711,20 +737,90 @@ function buildUninstallButton(mod) {
|
|
|
711
737
|
return button;
|
|
712
738
|
}
|
|
713
739
|
|
|
714
|
-
function
|
|
740
|
+
function installedModules() {
|
|
741
|
+
return getAvailableModules().filter(isInstalled);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// The master switch is a UI lock only: it greys out every card's settings and
|
|
745
|
+
// toggle (same treatment as a disabled parent's child options) while each
|
|
746
|
+
// plugin keeps its own enabled/disabled state untouched.
|
|
747
|
+
function masterLocked() {
|
|
748
|
+
return installedModules().length > 0 && state.masterLocked === true;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function disableAllPlugins() {
|
|
752
|
+
state.masterLocked = true;
|
|
753
|
+
saveState();
|
|
754
|
+
refreshPanel();
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function enableAllPlugins() {
|
|
758
|
+
state.masterLocked = false;
|
|
759
|
+
saveState();
|
|
760
|
+
refreshPanel();
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function applySearchFilter(panel = document.getElementById(PANEL_ID)) {
|
|
764
|
+
if (!panel) return;
|
|
765
|
+
let shown = 0;
|
|
766
|
+
for (const card of panel.querySelectorAll(".perf-card")) {
|
|
767
|
+
const match = !searchQuery || (card.dataset.search ?? "").includes(searchQuery);
|
|
768
|
+
card.style.display = match ? "" : "none";
|
|
769
|
+
if (match) shown += 1;
|
|
770
|
+
}
|
|
771
|
+
const scroller = panel.querySelector("lol-uikit-scrollable");
|
|
772
|
+
let empty = panel.querySelector(".perf-no-match");
|
|
773
|
+
if (shown === 0 && getAvailableModules().length > 0) {
|
|
774
|
+
if (!empty && scroller) {
|
|
775
|
+
empty = el("div", "perf-empty perf-no-match", "No plugins match your search.");
|
|
776
|
+
scroller.append(empty);
|
|
777
|
+
}
|
|
778
|
+
} else if (empty) {
|
|
779
|
+
empty.remove();
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function buildTools() {
|
|
784
|
+
const tools = el("div", "perf-tools");
|
|
785
|
+
|
|
786
|
+
const search = el("input", "perf-search");
|
|
787
|
+
search.type = "search";
|
|
788
|
+
search.placeholder = "Search plugins...";
|
|
789
|
+
search.value = searchQuery;
|
|
790
|
+
search.setAttribute("aria-label", "Search plugins");
|
|
791
|
+
search.addEventListener("input", () => {
|
|
792
|
+
searchQuery = search.value.trim().toLowerCase();
|
|
793
|
+
applySearchFilter();
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
const master = el("button", "perf-btn perf-master", masterLocked() ? "Enable All" : "Disable All");
|
|
797
|
+
master.type = "button";
|
|
798
|
+
master.disabled = installedModules().length === 0;
|
|
799
|
+
master.addEventListener("click", () => {
|
|
800
|
+
if (masterLocked()) enableAllPlugins();
|
|
801
|
+
else disableAllPlugins();
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
tools.append(search, master);
|
|
805
|
+
return tools;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function buildCard(mod, locked) {
|
|
715
809
|
const installed = isInstalled(mod);
|
|
716
810
|
const card = el("div", installed ? "perf-card" : "perf-card perf-card--missing");
|
|
811
|
+
if (locked) card.classList.add("perf-card--locked");
|
|
812
|
+
card.dataset.search = `${mod.name} ${mod.description}`.toLowerCase();
|
|
717
813
|
|
|
718
814
|
const info = el("div", "perf-info");
|
|
719
815
|
const name = el("div", "perf-name", mod.name);
|
|
720
816
|
name.append(el("span", "perf-version", `v${registry.get(mod.id)?.version ?? mod.version}`));
|
|
721
817
|
info.append(name);
|
|
722
818
|
if (mod.description) info.append(el("div", "perf-desc", mod.description));
|
|
723
|
-
const optionList = buildOptions(mod);
|
|
819
|
+
const optionList = buildOptions(mod, locked);
|
|
724
820
|
if (optionList) info.append(optionList);
|
|
725
821
|
|
|
726
822
|
const actions = el("div", "perf-actions");
|
|
727
|
-
if (installed) actions.append(buildToggle(mod));
|
|
823
|
+
if (installed) actions.append(buildToggle(mod, locked));
|
|
728
824
|
actions.append(installed ? buildUninstallButton(mod) : buildInstallButton(mod));
|
|
729
825
|
|
|
730
826
|
card.append(info, actions);
|
|
@@ -734,6 +830,8 @@ function buildCard(mod) {
|
|
|
734
830
|
function buildPanel() {
|
|
735
831
|
const panel = el("div");
|
|
736
832
|
panel.id = PANEL_ID;
|
|
833
|
+
panel.append(buildTools());
|
|
834
|
+
|
|
737
835
|
const scroller = el("lol-uikit-scrollable");
|
|
738
836
|
panel.append(scroller);
|
|
739
837
|
|
|
@@ -742,7 +840,9 @@ function buildPanel() {
|
|
|
742
840
|
scroller.append(el("div", "perf-empty", "No plugins are available."));
|
|
743
841
|
return panel;
|
|
744
842
|
}
|
|
745
|
-
|
|
843
|
+
const locked = masterLocked();
|
|
844
|
+
for (const mod of modules) scroller.append(buildCard(mod, locked));
|
|
845
|
+
applySearchFilter(panel);
|
|
746
846
|
return panel;
|
|
747
847
|
}
|
|
748
848
|
|