perfectionist-plugins 1.6.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 +131 -3
- package/hide-unowned-skins.js +58 -14
- package/package.json +1 -1
- package/perfectionist-utils.js +146 -27
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;
|
|
@@ -9,6 +9,16 @@ const MISSIONS_URI = "/lol-missions/v1/missions";
|
|
|
9
9
|
const MISSIONS_PLAYER_URI = "/lol-missions/v1/player";
|
|
10
10
|
const LOOT_URI = "/lol-loot/v1/player-loot";
|
|
11
11
|
const CLASH_FLAGS_URI = "/lol-clash/v1/simple-state-flags";
|
|
12
|
+
const REWARDS_GRANTS_URI = "/lol-rewards/v1/grants";
|
|
13
|
+
const REWARDS_VIEW_URI = "/lol-rewards/v1/grants/view";
|
|
14
|
+
const LOOT_NOTIF_URI = "/lol-loot/v1/player-loot-notifications";
|
|
15
|
+
const MASTERY_NOTIF_URI = "/lol-champion-mastery/v1/notifications";
|
|
16
|
+
const MASTERY_ACK_URI = "/lol-champion-mastery/v1/notifications/ack";
|
|
17
|
+
const HONOR_NOTICES = [
|
|
18
|
+
{ uri: "/lol-honor-v2/v1/reward-granted", ack: "/lol-honor-v2/v1/reward-granted/ack", present: (d) => Number(d?.quantity ?? -1) >= 0 || Boolean(d?.rewardType) },
|
|
19
|
+
{ uri: "/lol-honor-v2/v1/mutual-honor", ack: "/lol-honor-v2/v1/mutual-honor/ack", present: (d) => (d?.summoners?.length ?? 0) > 0 },
|
|
20
|
+
{ uri: "/lol-honor-v2/v1/level-change", ack: "/lol-honor-v2/v1/level-change/ack", present: (d) => Number(d?.currentState?.level ?? -1) >= 0 },
|
|
21
|
+
];
|
|
12
22
|
const RANKED_NOTIF_URI = "/lol-ranked/v1/notifications";
|
|
13
23
|
const EOS_NOTIF_URI = "/lol-ranked/v1/eos-notifications";
|
|
14
24
|
const NOTICE_LIST_URIS = [
|
|
@@ -29,6 +39,13 @@ const CLASH_BULK_ACKS = [
|
|
|
29
39
|
"/lol-clash/v1/eog-player-update/acknowledge",
|
|
30
40
|
"/lol-clash/v1/game-end/acknowledge",
|
|
31
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";
|
|
32
49
|
const PIP_STYLE_ID = "auto-read-pips-style";
|
|
33
50
|
const PIP_CSS = ".activity-center__tab_label_notification { display: none !important; }";
|
|
34
51
|
|
|
@@ -39,6 +56,7 @@ let sweepRunning = false;
|
|
|
39
56
|
let sweepQueued = false;
|
|
40
57
|
let unsubscribes = [];
|
|
41
58
|
const cooldowns = new Map();
|
|
59
|
+
const markedGrants = new Set();
|
|
42
60
|
|
|
43
61
|
function on(key) {
|
|
44
62
|
try {
|
|
@@ -116,6 +134,49 @@ async function sweepClash() {
|
|
|
116
134
|
}
|
|
117
135
|
}
|
|
118
136
|
|
|
137
|
+
async function sweepRewards() {
|
|
138
|
+
const grants = await getJson(REWARDS_GRANTS_URI).catch(() => null);
|
|
139
|
+
if (Array.isArray(grants) && grants.length && ready("rewards-grants", BULK_COOLDOWN_MS)) {
|
|
140
|
+
const ids = [];
|
|
141
|
+
for (const grant of grants) {
|
|
142
|
+
const id = grant?.grantId ?? grant?.id;
|
|
143
|
+
if (typeof id !== "string" || !id || markedGrants.has(id)) continue;
|
|
144
|
+
ids.push(id);
|
|
145
|
+
}
|
|
146
|
+
if (ids.length) {
|
|
147
|
+
stamp("rewards-grants");
|
|
148
|
+
for (const id of ids) markedGrants.add(id);
|
|
149
|
+
await sendJson("PATCH", REWARDS_VIEW_URI, ids).catch(() => {
|
|
150
|
+
for (const id of ids) markedGrants.delete(id);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const notes = await getJson(LOOT_NOTIF_URI).catch(() => null);
|
|
156
|
+
if (Array.isArray(notes)) {
|
|
157
|
+
for (const note of notes) {
|
|
158
|
+
const id = note?.id ?? note?.lootId;
|
|
159
|
+
if (!id || !ready(`lootnote:${id}`, ITEM_COOLDOWN_MS)) continue;
|
|
160
|
+
stamp(`lootnote:${id}`);
|
|
161
|
+
await sendJson("POST", `${LOOT_NOTIF_URI}/${encodeURIComponent(id)}/acknowledge`).catch(() => {});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const mastery = await getJson(MASTERY_NOTIF_URI).catch(() => null);
|
|
166
|
+
if (Number(mastery?.championId ?? 0) > 0 && ready("mastery", ITEM_COOLDOWN_MS)) {
|
|
167
|
+
stamp("mastery");
|
|
168
|
+
await sendJson("POST", MASTERY_ACK_URI).catch(() => {});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
for (const notice of HONOR_NOTICES) {
|
|
172
|
+
const data = await getJson(notice.uri).catch(() => null);
|
|
173
|
+
const key = `honor:${notice.uri}`;
|
|
174
|
+
if (!data || !notice.present(data) || !ready(key, ITEM_COOLDOWN_MS)) continue;
|
|
175
|
+
stamp(key);
|
|
176
|
+
await sendJson("POST", notice.ack).catch(() => {});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
119
180
|
async function sweepRanked() {
|
|
120
181
|
for (const uri of [RANKED_NOTIF_URI, EOS_NOTIF_URI]) {
|
|
121
182
|
const list = await getJson(uri).catch(() => null);
|
|
@@ -147,6 +208,65 @@ async function sweepNotices() {
|
|
|
147
208
|
}
|
|
148
209
|
}
|
|
149
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
|
+
|
|
150
270
|
function syncPips() {
|
|
151
271
|
const wanted = active && on("pips");
|
|
152
272
|
let style = document.getElementById(PIP_STYLE_ID);
|
|
@@ -176,6 +296,11 @@ async function runSweep() {
|
|
|
176
296
|
try {
|
|
177
297
|
if (on("quests")) await sweepQuests().catch(() => {});
|
|
178
298
|
if (on("loot")) await sweepLootMarks().catch(() => {});
|
|
299
|
+
if (on("rewards")) {
|
|
300
|
+
await sweepRewards().catch(() => {});
|
|
301
|
+
await sweepIdentity().catch(() => {});
|
|
302
|
+
}
|
|
303
|
+
if (on("tft")) await sweepTft().catch(() => {});
|
|
179
304
|
if (on("clash")) await sweepClash().catch(() => {});
|
|
180
305
|
if (on("ranked")) await sweepRanked().catch(() => {});
|
|
181
306
|
if (on("notices")) await sweepNotices().catch(() => {});
|
|
@@ -192,7 +317,7 @@ function onEnable(utils) {
|
|
|
192
317
|
api = utils;
|
|
193
318
|
active = true;
|
|
194
319
|
cooldowns.clear();
|
|
195
|
-
for (const uri of [MISSIONS_URI, LOOT_URI, CLASH_FLAGS_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]) {
|
|
196
321
|
try {
|
|
197
322
|
const off = api.subscribe(uri, scheduleSweep);
|
|
198
323
|
if (off) unsubscribes.push(off);
|
|
@@ -215,6 +340,7 @@ function onDisable() {
|
|
|
215
340
|
}
|
|
216
341
|
unsubscribes = [];
|
|
217
342
|
cooldowns.clear();
|
|
343
|
+
markedGrants.clear();
|
|
218
344
|
api = null;
|
|
219
345
|
syncPips();
|
|
220
346
|
}
|
|
@@ -230,7 +356,7 @@ export function init() {
|
|
|
230
356
|
utils.registerPlugin({
|
|
231
357
|
id: PLUGIN_ID,
|
|
232
358
|
name: "Auto Read",
|
|
233
|
-
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.",
|
|
234
360
|
version,
|
|
235
361
|
defaultEnabled: true,
|
|
236
362
|
options: [
|
|
@@ -242,6 +368,8 @@ export function init() {
|
|
|
242
368
|
items: [
|
|
243
369
|
{ id: "quests", label: "Quests", description: "Quest and mission badges, including the mission button counter.", default: true },
|
|
244
370
|
{ id: "loot", label: "Loot marks", description: "The new mark on freshly earned loot.", default: true },
|
|
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 },
|
|
245
373
|
{ id: "clash", label: "Clash", description: "Clash tab flags and Clash notifications.", default: true },
|
|
246
374
|
{ id: "ranked", label: "Ranked", description: "Ranked, end-of-season and champion mastery notices.", default: true },
|
|
247
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));
|
|
@@ -267,7 +284,7 @@ function getOption(id, key) {
|
|
|
267
284
|
return stored === undefined ? Boolean(fallback) : Boolean(stored);
|
|
268
285
|
}
|
|
269
286
|
|
|
270
|
-
function setOption(id, key, value) {
|
|
287
|
+
function setOption(id, key, value, refresh = true) {
|
|
271
288
|
if (!state.options[id]) state.options[id] = {};
|
|
272
289
|
state.options[id][key] = Boolean(value);
|
|
273
290
|
saveState();
|
|
@@ -277,7 +294,7 @@ function setOption(id, key, value) {
|
|
|
277
294
|
} catch (error) {
|
|
278
295
|
logError(`${entry?.name ?? id} could not apply the option change.`, error);
|
|
279
296
|
}
|
|
280
|
-
refreshPanel();
|
|
297
|
+
if (refresh) refreshPanel();
|
|
281
298
|
}
|
|
282
299
|
|
|
283
300
|
function getPlugins() {
|
|
@@ -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
|
() => {
|
|
@@ -599,13 +623,13 @@ function buildOptionRow(mod, option, disabled, onChange) {
|
|
|
599
623
|
input.setAttribute("aria-label", `${mod.name}: ${option.label}`);
|
|
600
624
|
input.addEventListener("change", () => {
|
|
601
625
|
if (onChange) onChange(input.checked);
|
|
602
|
-
else setOption(mod.id, option.key, input.checked);
|
|
626
|
+
else setOption(mod.id, option.key, input.checked, false);
|
|
603
627
|
});
|
|
604
628
|
row.append(input, el("span", "perf-box"), el("span", "perf-opt-label", option.label));
|
|
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,25 +639,29 @@ 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
|
-
chip.addEventListener("click", () =>
|
|
644
|
+
chip.addEventListener("click", () => {
|
|
645
|
+
setOption(mod.id, key, !getOption(mod.id, key), false);
|
|
646
|
+
chip.classList.toggle("perf-chip--on", getOption(mod.id, key));
|
|
647
|
+
});
|
|
620
648
|
row.append(chip);
|
|
621
649
|
}
|
|
622
650
|
block.append(row);
|
|
623
651
|
return block;
|
|
624
652
|
}
|
|
625
653
|
|
|
626
|
-
function buildOptions(mod) {
|
|
654
|
+
function buildOptions(mod, locked) {
|
|
627
655
|
const options = registry.get(mod.id)?.options ?? [];
|
|
628
656
|
if (options.length === 0) return null;
|
|
629
657
|
const wrap = el("div", "perf-opts");
|
|
630
658
|
for (const option of options) {
|
|
631
659
|
if (option.type === "chips") {
|
|
632
|
-
wrap.append(buildChips(mod, option));
|
|
660
|
+
wrap.append(buildChips(mod, option, locked));
|
|
633
661
|
continue;
|
|
634
662
|
}
|
|
635
663
|
if (!option.children?.length) {
|
|
636
|
-
wrap.append(buildOptionRow(mod, option));
|
|
664
|
+
wrap.append(buildOptionRow(mod, option, Boolean(locked)));
|
|
637
665
|
continue;
|
|
638
666
|
}
|
|
639
667
|
const group = el("div", "perf-opt-group");
|
|
@@ -644,13 +672,13 @@ function buildOptions(mod) {
|
|
|
644
672
|
const kids = el("div", "perf-opt-children");
|
|
645
673
|
const childInputs = [];
|
|
646
674
|
for (const child of option.children) {
|
|
647
|
-
const childRow = buildOptionRow(mod, child, !getOption(mod.id, option.key));
|
|
675
|
+
const childRow = buildOptionRow(mod, child, !getOption(mod.id, option.key) || Boolean(locked));
|
|
648
676
|
childInputs.push(childRow.querySelector("input"));
|
|
649
677
|
kids.append(childRow);
|
|
650
678
|
}
|
|
651
|
-
const master = buildOptionRow(mod, option,
|
|
652
|
-
for (const input of childInputs) input.disabled = !checked;
|
|
653
|
-
setOption(mod.id, option.key, checked);
|
|
679
|
+
const master = buildOptionRow(mod, option, Boolean(locked), (checked) => {
|
|
680
|
+
for (const input of childInputs) input.disabled = !checked || Boolean(locked);
|
|
681
|
+
setOption(mod.id, option.key, checked, false);
|
|
654
682
|
});
|
|
655
683
|
const groupKey = `${mod.id}:${option.key}`;
|
|
656
684
|
const setExpanded = (open) => {
|
|
@@ -671,12 +699,13 @@ function buildOptions(mod) {
|
|
|
671
699
|
return wrap;
|
|
672
700
|
}
|
|
673
701
|
|
|
674
|
-
function buildToggle(mod) {
|
|
702
|
+
function buildToggle(mod, locked) {
|
|
675
703
|
const label = el("label", "perf-toggle");
|
|
676
704
|
label.title = `Enable ${mod.name}`;
|
|
677
705
|
const input = el("input");
|
|
678
706
|
input.type = "checkbox";
|
|
679
707
|
input.checked = isPluginEnabled(mod.id);
|
|
708
|
+
input.disabled = Boolean(locked);
|
|
680
709
|
input.setAttribute("aria-label", `Enable ${mod.name}`);
|
|
681
710
|
input.addEventListener("change", () => setPluginEnabled(mod.id, input.checked));
|
|
682
711
|
label.append(input, el("span", "perf-box"));
|
|
@@ -708,20 +737,90 @@ function buildUninstallButton(mod) {
|
|
|
708
737
|
return button;
|
|
709
738
|
}
|
|
710
739
|
|
|
711
|
-
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) {
|
|
712
809
|
const installed = isInstalled(mod);
|
|
713
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();
|
|
714
813
|
|
|
715
814
|
const info = el("div", "perf-info");
|
|
716
815
|
const name = el("div", "perf-name", mod.name);
|
|
717
816
|
name.append(el("span", "perf-version", `v${registry.get(mod.id)?.version ?? mod.version}`));
|
|
718
817
|
info.append(name);
|
|
719
818
|
if (mod.description) info.append(el("div", "perf-desc", mod.description));
|
|
720
|
-
const optionList = buildOptions(mod);
|
|
819
|
+
const optionList = buildOptions(mod, locked);
|
|
721
820
|
if (optionList) info.append(optionList);
|
|
722
821
|
|
|
723
822
|
const actions = el("div", "perf-actions");
|
|
724
|
-
if (installed) actions.append(buildToggle(mod));
|
|
823
|
+
if (installed) actions.append(buildToggle(mod, locked));
|
|
725
824
|
actions.append(installed ? buildUninstallButton(mod) : buildInstallButton(mod));
|
|
726
825
|
|
|
727
826
|
card.append(info, actions);
|
|
@@ -731,6 +830,8 @@ function buildCard(mod) {
|
|
|
731
830
|
function buildPanel() {
|
|
732
831
|
const panel = el("div");
|
|
733
832
|
panel.id = PANEL_ID;
|
|
833
|
+
panel.append(buildTools());
|
|
834
|
+
|
|
734
835
|
const scroller = el("lol-uikit-scrollable");
|
|
735
836
|
panel.append(scroller);
|
|
736
837
|
|
|
@@ -739,13 +840,31 @@ function buildPanel() {
|
|
|
739
840
|
scroller.append(el("div", "perf-empty", "No plugins are available."));
|
|
740
841
|
return panel;
|
|
741
842
|
}
|
|
742
|
-
|
|
843
|
+
const locked = masterLocked();
|
|
844
|
+
for (const mod of modules) scroller.append(buildCard(mod, locked));
|
|
845
|
+
applySearchFilter(panel);
|
|
743
846
|
return panel;
|
|
744
847
|
}
|
|
745
848
|
|
|
849
|
+
function panelScrollTop(panel) {
|
|
850
|
+
return panel?.querySelector("lol-uikit-scrollable")?.scrollTop ?? 0;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function applyScrollTop(panel, top) {
|
|
854
|
+
if (!top || !panel) return;
|
|
855
|
+
const host = panel.querySelector("lol-uikit-scrollable");
|
|
856
|
+
if (host) host.scrollTop = top;
|
|
857
|
+
}
|
|
858
|
+
|
|
746
859
|
function refreshPanel() {
|
|
747
860
|
const current = document.getElementById(PANEL_ID);
|
|
748
|
-
if (current)
|
|
861
|
+
if (!current) return;
|
|
862
|
+
const top = panelScrollTop(current);
|
|
863
|
+
const next = buildPanel();
|
|
864
|
+
current.replaceWith(next);
|
|
865
|
+
if (!top) return;
|
|
866
|
+
applyScrollTop(next, top);
|
|
867
|
+
requestAnimationFrame(() => applyScrollTop(next, top));
|
|
749
868
|
}
|
|
750
869
|
|
|
751
870
|
function setNavActive(activeNav) {
|