perfectionist-plugins 1.4.1 → 1.8.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-claimer.js +59 -28
- package/auto-read.js +320 -0
- package/package.json +5 -3
- package/perfectionist-utils.js +150 -18
package/auto-claimer.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const PLUGIN_ID = "auto-claimer";
|
|
2
|
-
const version = "1.
|
|
2
|
+
const version = "1.2.0";
|
|
3
3
|
|
|
4
4
|
const SWEEP_DEBOUNCE_MS = 400;
|
|
5
5
|
const CLAIM_COOLDOWN_MS = 5 * 60 * 1000;
|
|
@@ -29,7 +29,7 @@ let shopQueued = false;
|
|
|
29
29
|
let unsubscribes = [];
|
|
30
30
|
let sourceInstalled = false;
|
|
31
31
|
const cooldowns = new Map();
|
|
32
|
-
const
|
|
32
|
+
const recipes = new Map();
|
|
33
33
|
|
|
34
34
|
function on(key) {
|
|
35
35
|
try {
|
|
@@ -113,20 +113,26 @@ async function sweepTftPasses() {
|
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
function
|
|
117
|
-
if (!
|
|
118
|
-
const promise = getJson(`/lol-loot/v1/
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
116
|
+
function recipeFor(lootId) {
|
|
117
|
+
if (!recipes.has(lootId)) {
|
|
118
|
+
const promise = getJson(`/lol-loot/v1/recipes/initial-item/${encodeURIComponent(lootId)}`)
|
|
119
|
+
.then((list) => (Array.isArray(list) ? (list.find((entry) => entry?.type === "OPEN" && entry.recipeName) ?? null) : null))
|
|
120
|
+
.catch(() => {
|
|
121
|
+
recipes.delete(lootId);
|
|
122
|
+
return null;
|
|
123
|
+
});
|
|
124
|
+
recipes.set(lootId, promise);
|
|
123
125
|
}
|
|
124
|
-
return
|
|
126
|
+
return recipes.get(lootId);
|
|
125
127
|
}
|
|
126
128
|
|
|
127
129
|
async function sweepLoot() {
|
|
128
130
|
const loot = await getJson(LOOT_URI);
|
|
129
131
|
if (!Array.isArray(loot)) return;
|
|
132
|
+
const owned = new Map();
|
|
133
|
+
for (const item of loot) {
|
|
134
|
+
if (item?.lootId) owned.set(item.lootId, item.count ?? 0);
|
|
135
|
+
}
|
|
130
136
|
let actions = 0;
|
|
131
137
|
for (const item of loot) {
|
|
132
138
|
if (actions >= MAX_ACTIONS_PER_SWEEP) break;
|
|
@@ -134,15 +140,32 @@ async function sweepLoot() {
|
|
|
134
140
|
const count = item?.count ?? 0;
|
|
135
141
|
if (!lootId || count <= 0 || /^(CURRENCY|MATERIAL)_/.test(lootId)) continue;
|
|
136
142
|
if (!ready(`loot:${lootId}`, CLAIM_COOLDOWN_MS)) continue;
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
143
|
+
const recipe = await recipeFor(lootId);
|
|
144
|
+
if (!recipe) continue;
|
|
145
|
+
const isChest = item.type === "CHEST" || /CHEST/i.test(lootId) || /CHEST/i.test(recipe.displayCategories ?? "");
|
|
146
|
+
if (!on("openLoot") || !(isChest ? on("openChests") : on("openMystery"))) continue;
|
|
147
|
+
const slots = Array.isArray(recipe.slots) ? recipe.slots : [];
|
|
148
|
+
const body = [];
|
|
149
|
+
let repeat = count;
|
|
150
|
+
let blocked = false;
|
|
151
|
+
for (const slot of slots) {
|
|
152
|
+
const candidates = Array.isArray(slot?.lootIds) ? slot.lootIds : [];
|
|
153
|
+
const match = candidates.find((id) => (owned.get(id) ?? 0) > 0);
|
|
154
|
+
if (!match) {
|
|
155
|
+
blocked = true;
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
body.push(match);
|
|
159
|
+
repeat = Math.min(repeat, Math.floor((owned.get(match) ?? 0) / Math.max(1, slot.quantity ?? 1)));
|
|
160
|
+
}
|
|
161
|
+
if (blocked || body.length === 0 || repeat <= 0) continue;
|
|
162
|
+
repeat = Math.min(repeat, 25);
|
|
142
163
|
stamp(`loot:${lootId}`);
|
|
143
164
|
actions++;
|
|
144
|
-
|
|
145
|
-
|
|
165
|
+
for (let i = 0; i < body.length; i++) {
|
|
166
|
+
owned.set(body[i], (owned.get(body[i]) ?? 0) - repeat * (slots[i].quantity ?? 1));
|
|
167
|
+
}
|
|
168
|
+
await sendJson("POST", `/lol-loot/v1/recipes/${encodeURIComponent(recipe.recipeName)}/craft?repeat=${repeat}`, body).catch(() => {});
|
|
146
169
|
}
|
|
147
170
|
}
|
|
148
171
|
|
|
@@ -221,7 +244,7 @@ async function runClaims() {
|
|
|
221
244
|
await sweepEvents().catch(() => {});
|
|
222
245
|
await sweepMissions().catch(() => {});
|
|
223
246
|
await sweepTftPasses().catch(() => {});
|
|
224
|
-
if (on("openLoot") || on("openChests")) await sweepLoot().catch(() => {});
|
|
247
|
+
if (on("openLoot") && (on("openMystery") || on("openChests"))) await sweepLoot().catch(() => {});
|
|
225
248
|
} finally {
|
|
226
249
|
claimRunning = false;
|
|
227
250
|
if (claimQueued) {
|
|
@@ -279,7 +302,7 @@ function onEnable(utils) {
|
|
|
279
302
|
api = utils;
|
|
280
303
|
active = true;
|
|
281
304
|
cooldowns.clear();
|
|
282
|
-
|
|
305
|
+
recipes.clear();
|
|
283
306
|
const claimPush = () => scheduleClaims();
|
|
284
307
|
const shopPush = () => {
|
|
285
308
|
if (on("buy1Be")) scheduleShop();
|
|
@@ -317,7 +340,7 @@ function onDisable() {
|
|
|
317
340
|
}
|
|
318
341
|
unsubscribes = [];
|
|
319
342
|
cooldowns.clear();
|
|
320
|
-
|
|
343
|
+
recipes.clear();
|
|
321
344
|
api = null;
|
|
322
345
|
}
|
|
323
346
|
|
|
@@ -349,15 +372,23 @@ export function init() {
|
|
|
349
372
|
},
|
|
350
373
|
{
|
|
351
374
|
key: "openLoot",
|
|
352
|
-
label: "Auto-open loot
|
|
353
|
-
description: "
|
|
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.",
|
|
375
|
+
label: "Auto-open loot",
|
|
376
|
+
description: "Master switch for opening loot automatically. Choose what to include below.",
|
|
360
377
|
default: false,
|
|
378
|
+
children: [
|
|
379
|
+
{
|
|
380
|
+
key: "openMystery",
|
|
381
|
+
label: "Include mystery items",
|
|
382
|
+
description: "Mystery icons, mystery ward skins, mystery emotes, mystery skins and other random-reward items.",
|
|
383
|
+
default: false,
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
key: "openChests",
|
|
387
|
+
label: "Include Hextech Chests",
|
|
388
|
+
description: "Hextech Chests, opened with your keys when one is required.",
|
|
389
|
+
default: false,
|
|
390
|
+
},
|
|
391
|
+
],
|
|
361
392
|
},
|
|
362
393
|
],
|
|
363
394
|
onEnable,
|
package/auto-read.js
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
const PLUGIN_ID = "auto-read";
|
|
2
|
+
const version = "1.1.0";
|
|
3
|
+
|
|
4
|
+
const SWEEP_DEBOUNCE_MS = 500;
|
|
5
|
+
const ITEM_COOLDOWN_MS = 10 * 60 * 1000;
|
|
6
|
+
const BULK_COOLDOWN_MS = 60 * 1000;
|
|
7
|
+
|
|
8
|
+
const MISSIONS_URI = "/lol-missions/v1/missions";
|
|
9
|
+
const MISSIONS_PLAYER_URI = "/lol-missions/v1/player";
|
|
10
|
+
const LOOT_URI = "/lol-loot/v1/player-loot";
|
|
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
|
+
];
|
|
22
|
+
const RANKED_NOTIF_URI = "/lol-ranked/v1/notifications";
|
|
23
|
+
const EOS_NOTIF_URI = "/lol-ranked/v1/eos-notifications";
|
|
24
|
+
const NOTICE_LIST_URIS = [
|
|
25
|
+
"/player-notifications/v1/notifications",
|
|
26
|
+
"/lol-patch/v1/notifications",
|
|
27
|
+
"/patcher/v1/notifications",
|
|
28
|
+
"/lol-leaver-buster/v1/notifications",
|
|
29
|
+
"/lol-lobby/v2/notifications",
|
|
30
|
+
];
|
|
31
|
+
const NOTICE_BULK_DELETES = [
|
|
32
|
+
"/lol-gameflow/v1/early-exit-notifications/eog",
|
|
33
|
+
"/lol-gameflow/v1/early-exit-notifications/missions",
|
|
34
|
+
"/lol-statstones/v1/vignette-notifications",
|
|
35
|
+
"/lol-player-behavior/v1/code-of-conduct-notification",
|
|
36
|
+
];
|
|
37
|
+
const CLASH_BULK_ACKS = [
|
|
38
|
+
"/lol-clash/v1/notifications/acknowledge",
|
|
39
|
+
"/lol-clash/v1/eog-player-update/acknowledge",
|
|
40
|
+
"/lol-clash/v1/game-end/acknowledge",
|
|
41
|
+
];
|
|
42
|
+
const PIP_STYLE_ID = "auto-read-pips-style";
|
|
43
|
+
const PIP_CSS = ".activity-center__tab_label_notification { display: none !important; }";
|
|
44
|
+
|
|
45
|
+
let api = null;
|
|
46
|
+
let active = false;
|
|
47
|
+
let sweepTimer = null;
|
|
48
|
+
let sweepRunning = false;
|
|
49
|
+
let sweepQueued = false;
|
|
50
|
+
let unsubscribes = [];
|
|
51
|
+
const cooldowns = new Map();
|
|
52
|
+
const markedGrants = new Set();
|
|
53
|
+
|
|
54
|
+
function on(key) {
|
|
55
|
+
try {
|
|
56
|
+
return api?.getOption?.(PLUGIN_ID, `read.${key}`) === true;
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function ready(key, wait) {
|
|
63
|
+
return (cooldowns.get(key) ?? 0) + wait <= Date.now();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function stamp(key) {
|
|
67
|
+
cooldowns.set(key, Date.now());
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getJson(path) {
|
|
71
|
+
return fetch(path, { headers: { Accept: "application/json" } }).then((response) => {
|
|
72
|
+
if (!response.ok) throw new Error(`${path} responded ${response.status}`);
|
|
73
|
+
return response.json();
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function sendJson(method, path, body) {
|
|
78
|
+
return fetch(path, {
|
|
79
|
+
method,
|
|
80
|
+
headers: { Accept: "application/json", "Content-Type": "application/json" },
|
|
81
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
82
|
+
}).then((response) => {
|
|
83
|
+
if (!response.ok) throw new Error(`${method} ${path} responded ${response.status}`);
|
|
84
|
+
return response.status === 204 ? null : response.json().catch(() => null);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function sweepQuests() {
|
|
89
|
+
const missions = await getJson(MISSIONS_URI);
|
|
90
|
+
if (!Array.isArray(missions)) return;
|
|
91
|
+
const ids = [];
|
|
92
|
+
for (const mission of missions) {
|
|
93
|
+
if (!mission?.id || mission.viewed !== false) continue;
|
|
94
|
+
if (!ready(`mission:${mission.id}`, ITEM_COOLDOWN_MS)) continue;
|
|
95
|
+
stamp(`mission:${mission.id}`);
|
|
96
|
+
ids.push(mission.id);
|
|
97
|
+
}
|
|
98
|
+
for (let i = 0; i < ids.length; i += 100) {
|
|
99
|
+
await sendJson("PUT", MISSIONS_PLAYER_URI, { missionIds: ids.slice(i, i + 100) }).catch(() => {});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function sweepLootMarks() {
|
|
104
|
+
const loot = await getJson(LOOT_URI);
|
|
105
|
+
if (!Array.isArray(loot)) return;
|
|
106
|
+
for (const item of loot) {
|
|
107
|
+
const id = item?.lootId;
|
|
108
|
+
if (!id || item.isNew !== true || !ready(`lootmark:${id}`, ITEM_COOLDOWN_MS)) continue;
|
|
109
|
+
stamp(`lootmark:${id}`);
|
|
110
|
+
await sendJson("DELETE", `${LOOT_URI}/${encodeURIComponent(id)}/new-notification`).catch(() => {});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function sweepClash() {
|
|
115
|
+
const flags = await getJson(CLASH_FLAGS_URI).catch(() => null);
|
|
116
|
+
if (Array.isArray(flags)) {
|
|
117
|
+
for (const flag of flags) {
|
|
118
|
+
if (!flag?.id || flag.status === "ACKNOWLEDGED" || !ready(`clashflag:${flag.id}`, ITEM_COOLDOWN_MS)) continue;
|
|
119
|
+
stamp(`clashflag:${flag.id}`);
|
|
120
|
+
await sendJson("POST", `${CLASH_FLAGS_URI}/${encodeURIComponent(flag.id)}/acknowledge`).catch(() => {});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (!ready("clash-bulk", BULK_COOLDOWN_MS)) return;
|
|
124
|
+
stamp("clash-bulk");
|
|
125
|
+
for (const path of CLASH_BULK_ACKS) {
|
|
126
|
+
await sendJson("POST", path).catch(() => {});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function sweepRewards() {
|
|
131
|
+
const grants = await getJson(REWARDS_GRANTS_URI).catch(() => null);
|
|
132
|
+
if (Array.isArray(grants) && grants.length && ready("rewards-grants", BULK_COOLDOWN_MS)) {
|
|
133
|
+
const ids = [];
|
|
134
|
+
for (const grant of grants) {
|
|
135
|
+
const id = grant?.grantId ?? grant?.id;
|
|
136
|
+
if (typeof id !== "string" || !id || markedGrants.has(id)) continue;
|
|
137
|
+
ids.push(id);
|
|
138
|
+
}
|
|
139
|
+
if (ids.length) {
|
|
140
|
+
stamp("rewards-grants");
|
|
141
|
+
for (const id of ids) markedGrants.add(id);
|
|
142
|
+
await sendJson("PATCH", REWARDS_VIEW_URI, ids).catch(() => {
|
|
143
|
+
for (const id of ids) markedGrants.delete(id);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const notes = await getJson(LOOT_NOTIF_URI).catch(() => null);
|
|
149
|
+
if (Array.isArray(notes)) {
|
|
150
|
+
for (const note of notes) {
|
|
151
|
+
const id = note?.id ?? note?.lootId;
|
|
152
|
+
if (!id || !ready(`lootnote:${id}`, ITEM_COOLDOWN_MS)) continue;
|
|
153
|
+
stamp(`lootnote:${id}`);
|
|
154
|
+
await sendJson("POST", `${LOOT_NOTIF_URI}/${encodeURIComponent(id)}/acknowledge`).catch(() => {});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const mastery = await getJson(MASTERY_NOTIF_URI).catch(() => null);
|
|
159
|
+
if (Number(mastery?.championId ?? 0) > 0 && ready("mastery", ITEM_COOLDOWN_MS)) {
|
|
160
|
+
stamp("mastery");
|
|
161
|
+
await sendJson("POST", MASTERY_ACK_URI).catch(() => {});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
for (const notice of HONOR_NOTICES) {
|
|
165
|
+
const data = await getJson(notice.uri).catch(() => null);
|
|
166
|
+
const key = `honor:${notice.uri}`;
|
|
167
|
+
if (!data || !notice.present(data) || !ready(key, ITEM_COOLDOWN_MS)) continue;
|
|
168
|
+
stamp(key);
|
|
169
|
+
await sendJson("POST", notice.ack).catch(() => {});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function sweepRanked() {
|
|
174
|
+
for (const uri of [RANKED_NOTIF_URI, EOS_NOTIF_URI]) {
|
|
175
|
+
const list = await getJson(uri).catch(() => null);
|
|
176
|
+
if (!Array.isArray(list)) continue;
|
|
177
|
+
for (const note of list) {
|
|
178
|
+
const id = note?.id ?? note?.notificationId;
|
|
179
|
+
if (id == null || !ready(`ranked:${id}`, ITEM_COOLDOWN_MS)) continue;
|
|
180
|
+
stamp(`ranked:${id}`);
|
|
181
|
+
await sendJson("POST", `${uri}/${encodeURIComponent(id)}/acknowledge`).catch(() => {});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function sweepNotices() {
|
|
187
|
+
for (const uri of NOTICE_LIST_URIS) {
|
|
188
|
+
const list = await getJson(uri).catch(() => null);
|
|
189
|
+
if (!Array.isArray(list)) continue;
|
|
190
|
+
for (const note of list) {
|
|
191
|
+
const id = note?.id ?? note?.notificationId;
|
|
192
|
+
if (id == null || !ready(`notice:${id}`, ITEM_COOLDOWN_MS)) continue;
|
|
193
|
+
stamp(`notice:${id}`);
|
|
194
|
+
await sendJson("DELETE", `${uri}/${encodeURIComponent(id)}`).catch(() => {});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (!ready("notice-bulk", BULK_COOLDOWN_MS)) return;
|
|
198
|
+
stamp("notice-bulk");
|
|
199
|
+
for (const path of NOTICE_BULK_DELETES) {
|
|
200
|
+
await sendJson("DELETE", path).catch(() => {});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function syncPips() {
|
|
205
|
+
const wanted = active && on("pips");
|
|
206
|
+
let style = document.getElementById(PIP_STYLE_ID);
|
|
207
|
+
if (wanted && !style) {
|
|
208
|
+
style = document.createElement("style");
|
|
209
|
+
style.id = PIP_STYLE_ID;
|
|
210
|
+
style.textContent = PIP_CSS;
|
|
211
|
+
document.head.appendChild(style);
|
|
212
|
+
} else if (!wanted && style) {
|
|
213
|
+
style.remove();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function scheduleSweep() {
|
|
218
|
+
if (!active) return;
|
|
219
|
+
clearTimeout(sweepTimer);
|
|
220
|
+
sweepTimer = setTimeout(runSweep, SWEEP_DEBOUNCE_MS);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function runSweep() {
|
|
224
|
+
if (!active) return;
|
|
225
|
+
if (sweepRunning) {
|
|
226
|
+
sweepQueued = true;
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
sweepRunning = true;
|
|
230
|
+
try {
|
|
231
|
+
if (on("quests")) await sweepQuests().catch(() => {});
|
|
232
|
+
if (on("loot")) await sweepLootMarks().catch(() => {});
|
|
233
|
+
if (on("rewards")) await sweepRewards().catch(() => {});
|
|
234
|
+
if (on("clash")) await sweepClash().catch(() => {});
|
|
235
|
+
if (on("ranked")) await sweepRanked().catch(() => {});
|
|
236
|
+
if (on("notices")) await sweepNotices().catch(() => {});
|
|
237
|
+
} finally {
|
|
238
|
+
sweepRunning = false;
|
|
239
|
+
if (sweepQueued) {
|
|
240
|
+
sweepQueued = false;
|
|
241
|
+
scheduleSweep();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function onEnable(utils) {
|
|
247
|
+
api = utils;
|
|
248
|
+
active = true;
|
|
249
|
+
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]) {
|
|
251
|
+
try {
|
|
252
|
+
const off = api.subscribe(uri, scheduleSweep);
|
|
253
|
+
if (off) unsubscribes.push(off);
|
|
254
|
+
} catch {}
|
|
255
|
+
}
|
|
256
|
+
scheduleSweep();
|
|
257
|
+
syncPips();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function onDisable() {
|
|
261
|
+
active = false;
|
|
262
|
+
clearTimeout(sweepTimer);
|
|
263
|
+
sweepTimer = null;
|
|
264
|
+
sweepRunning = false;
|
|
265
|
+
sweepQueued = false;
|
|
266
|
+
for (const off of unsubscribes) {
|
|
267
|
+
try {
|
|
268
|
+
off();
|
|
269
|
+
} catch {}
|
|
270
|
+
}
|
|
271
|
+
unsubscribes = [];
|
|
272
|
+
cooldowns.clear();
|
|
273
|
+
markedGrants.clear();
|
|
274
|
+
api = null;
|
|
275
|
+
syncPips();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function onOptionChange() {
|
|
279
|
+
if (!active) return;
|
|
280
|
+
scheduleSweep();
|
|
281
|
+
syncPips();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function init() {
|
|
285
|
+
const registerWith = (utils) => {
|
|
286
|
+
utils.registerPlugin({
|
|
287
|
+
id: PLUGIN_ID,
|
|
288
|
+
name: "Auto Read",
|
|
289
|
+
description: "Automatically marks quests, loot, Clash and client notices as read so no badge or dot stays lit.",
|
|
290
|
+
version,
|
|
291
|
+
defaultEnabled: true,
|
|
292
|
+
options: [
|
|
293
|
+
{
|
|
294
|
+
type: "chips",
|
|
295
|
+
key: "read",
|
|
296
|
+
label: "Mark as read",
|
|
297
|
+
description: "Choose what gets marked as read automatically.",
|
|
298
|
+
items: [
|
|
299
|
+
{ id: "quests", label: "Quests", description: "Quest and mission badges, including the mission button counter.", default: true },
|
|
300
|
+
{ id: "loot", label: "Loot marks", description: "The new mark on freshly earned loot.", default: true },
|
|
301
|
+
{ id: "rewards", label: "Rewards", description: "The dot on your avatar for new tokens, titles and banners, plus mastery, honor and loot notifications.", default: true },
|
|
302
|
+
{ id: "clash", label: "Clash", description: "Clash tab flags and Clash notifications.", default: true },
|
|
303
|
+
{ id: "ranked", label: "Ranked", description: "Ranked, end-of-season and champion mastery notices.", default: true },
|
|
304
|
+
{ id: "notices", label: "Notices", description: "Patch, patcher, leaver buster, lobby, behavior and gameflow notices.", default: true },
|
|
305
|
+
{ id: "pips", label: "Tab pips", description: "Hides the new pips on activity tabs.", default: true },
|
|
306
|
+
],
|
|
307
|
+
},
|
|
308
|
+
],
|
|
309
|
+
onEnable,
|
|
310
|
+
onDisable,
|
|
311
|
+
onOptionChange,
|
|
312
|
+
});
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
if (window.PerfectionistUtils) {
|
|
316
|
+
registerWith(window.PerfectionistUtils);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
addEventListener("perfectionist:utils-ready", (e) => registerWith(e.detail ?? window.PerfectionistUtils), { once: true });
|
|
320
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "perfectionist-plugins",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "Small plugins that fix the annoying parts of League of Legends champ select. Built for Pengu Loader.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "perfectionist-utils.js",
|
|
@@ -10,14 +10,16 @@
|
|
|
10
10
|
"./ban-blocker.js": "./ban-blocker.js",
|
|
11
11
|
"./hide-unowned-skins.js": "./hide-unowned-skins.js",
|
|
12
12
|
"./unlock-profile-skins.js": "./unlock-profile-skins.js",
|
|
13
|
-
"./auto-claimer.js": "./auto-claimer.js"
|
|
13
|
+
"./auto-claimer.js": "./auto-claimer.js",
|
|
14
|
+
"./auto-read.js": "./auto-read.js"
|
|
14
15
|
},
|
|
15
16
|
"files": [
|
|
16
17
|
"perfectionist-utils.js",
|
|
17
18
|
"ban-blocker.js",
|
|
18
19
|
"hide-unowned-skins.js",
|
|
19
20
|
"unlock-profile-skins.js",
|
|
20
|
-
"auto-claimer.js"
|
|
21
|
+
"auto-claimer.js",
|
|
22
|
+
"auto-read.js"
|
|
21
23
|
],
|
|
22
24
|
"repository": {
|
|
23
25
|
"type": "git",
|
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.7.0";
|
|
10
10
|
|
|
11
11
|
const PKG = "perfectionist-plugins";
|
|
12
12
|
const CDN = "https://cdn.jsdelivr.net/npm";
|
|
@@ -47,13 +47,21 @@ const BUNDLED_MODULES = [
|
|
|
47
47
|
id: "auto-claimer",
|
|
48
48
|
file: "auto-claimer.js",
|
|
49
49
|
name: "Auto Claimer",
|
|
50
|
-
version: "1.
|
|
50
|
+
version: "1.2.0",
|
|
51
51
|
description: "Automatically claims battle pass, event and mission rewards the moment they become claimable.",
|
|
52
52
|
},
|
|
53
|
+
{
|
|
54
|
+
id: "auto-read",
|
|
55
|
+
file: "auto-read.js",
|
|
56
|
+
name: "Auto Read",
|
|
57
|
+
version: "1.1.0",
|
|
58
|
+
description: "Automatically marks quests, loot, Clash and client notices as read so no badge or dot stays lit.",
|
|
59
|
+
},
|
|
53
60
|
];
|
|
54
61
|
|
|
55
62
|
const STYLE_CSS = `
|
|
56
|
-
#${PANEL_ID} { padding:
|
|
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: 100%; }
|
|
57
65
|
#${PANEL_ID} .perf-card { display: flex; align-items: flex-start; justify-content: space-between; gap: 24px;
|
|
58
66
|
padding: 16px 18px; margin-bottom: 12px; box-sizing: border-box;
|
|
59
67
|
background: #010a13; border: 1px solid #785a28; }
|
|
@@ -88,6 +96,30 @@ const STYLE_CSS = `
|
|
|
88
96
|
#${PANEL_ID} .perf-opt { display: flex; align-items: center; gap: 9px; cursor: pointer; }
|
|
89
97
|
#${PANEL_ID} .perf-opt .perf-opt-label { font-size: 11px; letter-spacing: 0.3px; color: #a09b8c; }
|
|
90
98
|
#${PANEL_ID} .perf-opt:hover .perf-opt-label { color: #f0e6d2; }
|
|
99
|
+
#${PANEL_ID} .perf-opt-group { margin-top: 2px; }
|
|
100
|
+
#${PANEL_ID} .perf-opt-head { display: flex; align-items: center; gap: 9px; }
|
|
101
|
+
#${PANEL_ID} .perf-caret { flex: 0 0 auto; width: 28px; height: 28px; cursor: pointer; display: flex; align-items: center; justify-content: center;
|
|
102
|
+
border: 1px solid transparent; background: transparent; }
|
|
103
|
+
#${PANEL_ID} .perf-caret:hover { border-color: #785a28; background: #0a1428; }
|
|
104
|
+
#${PANEL_ID} .perf-caret::after { content: ""; width: 12px; height: 12px; transform-origin: center;
|
|
105
|
+
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath d='M2 1l4 3-4 3z' fill='%23c89b3c'/%3E%3C/svg%3E") center / 12px 12px no-repeat;
|
|
106
|
+
transition: transform 0.15s; }
|
|
107
|
+
#${PANEL_ID} .perf-caret:hover::after { filter: brightness(1.4); }
|
|
108
|
+
#${PANEL_ID} .perf-caret--open::after { transform: rotate(90deg); }
|
|
109
|
+
#${PANEL_ID} .perf-opt-children { margin: 8px 0 2px 25px; padding-left: 12px; display: flex; flex-direction: column; gap: 8px;
|
|
110
|
+
border-left: 1px solid #785a28; }
|
|
111
|
+
#${PANEL_ID} .perf-toggle input:disabled + .perf-box,
|
|
112
|
+
#${PANEL_ID} .perf-toggle input:disabled ~ .perf-opt-label { opacity: 0.45; }
|
|
113
|
+
#${PANEL_ID} .perf-toggle input:disabled ~ .perf-opt-label { cursor: default; }
|
|
114
|
+
#${PANEL_ID} .perf-chip-block { margin-top: 11px; }
|
|
115
|
+
#${PANEL_ID} .perf-chips-label { font-size: 10px; font-weight: 700; letter-spacing: 0.6px; text-transform: uppercase; color: #c89b3c; margin-bottom: 7px; }
|
|
116
|
+
#${PANEL_ID} .perf-chips { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
117
|
+
#${PANEL_ID} .perf-chip { font-family: "LoL Body", Arial, "Helvetica Neue", Helvetica, sans-serif; font-size: 10px; font-weight: 700;
|
|
118
|
+
letter-spacing: 0.5px; text-transform: uppercase; color: #a09b8c; background: #0a1428; border: 1px solid #785a28;
|
|
119
|
+
padding: 6px 11px; cursor: pointer; }
|
|
120
|
+
#${PANEL_ID} .perf-chip:hover { color: #f0e6d2; border-color: #c89b3c; }
|
|
121
|
+
#${PANEL_ID} .perf-chip--on { color: #010a13; background: #c89b3c; border-color: #c89b3c; }
|
|
122
|
+
#${PANEL_ID} .perf-chip--on:hover { background: #f0e6d2; }
|
|
91
123
|
|
|
92
124
|
.perf-update { position: fixed; inset: 0; z-index: 100000; display: flex; align-items: center;
|
|
93
125
|
justify-content: center; background: rgba(1, 10, 19, 0.72); }
|
|
@@ -215,14 +247,27 @@ function setPluginEnabled(id, enabled) {
|
|
|
215
247
|
return true;
|
|
216
248
|
}
|
|
217
249
|
|
|
250
|
+
function findOption(entry, key) {
|
|
251
|
+
for (const option of entry?.options ?? []) {
|
|
252
|
+
if (option.key === key) return option;
|
|
253
|
+
for (const child of option.children ?? []) {
|
|
254
|
+
if (child.key === key) return child;
|
|
255
|
+
}
|
|
256
|
+
for (const item of option.items ?? []) {
|
|
257
|
+
if (`${option.key}.${item.id}` === key) return item;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
|
|
218
263
|
function getOption(id, key) {
|
|
219
264
|
const entry = registry.get(id);
|
|
220
|
-
const fallback = entry
|
|
265
|
+
const fallback = findOption(entry, key)?.default;
|
|
221
266
|
const stored = state.options?.[id]?.[key];
|
|
222
267
|
return stored === undefined ? Boolean(fallback) : Boolean(stored);
|
|
223
268
|
}
|
|
224
269
|
|
|
225
|
-
function setOption(id, key, value) {
|
|
270
|
+
function setOption(id, key, value, refresh = true) {
|
|
226
271
|
if (!state.options[id]) state.options[id] = {};
|
|
227
272
|
state.options[id][key] = Boolean(value);
|
|
228
273
|
saveState();
|
|
@@ -232,7 +277,7 @@ function setOption(id, key, value) {
|
|
|
232
277
|
} catch (error) {
|
|
233
278
|
logError(`${entry?.name ?? id} could not apply the option change.`, error);
|
|
234
279
|
}
|
|
235
|
-
refreshPanel();
|
|
280
|
+
if (refresh) refreshPanel();
|
|
236
281
|
}
|
|
237
282
|
|
|
238
283
|
function getPlugins() {
|
|
@@ -542,20 +587,89 @@ function el(tag, className, text) {
|
|
|
542
587
|
return node;
|
|
543
588
|
}
|
|
544
589
|
|
|
590
|
+
const optionGroupState = new Map();
|
|
591
|
+
|
|
592
|
+
function buildOptionRow(mod, option, disabled, onChange) {
|
|
593
|
+
const row = el("label", "perf-opt perf-toggle");
|
|
594
|
+
row.title = option.description ?? "";
|
|
595
|
+
const input = el("input");
|
|
596
|
+
input.type = "checkbox";
|
|
597
|
+
input.checked = getOption(mod.id, option.key);
|
|
598
|
+
input.disabled = Boolean(disabled);
|
|
599
|
+
input.setAttribute("aria-label", `${mod.name}: ${option.label}`);
|
|
600
|
+
input.addEventListener("change", () => {
|
|
601
|
+
if (onChange) onChange(input.checked);
|
|
602
|
+
else setOption(mod.id, option.key, input.checked, false);
|
|
603
|
+
});
|
|
604
|
+
row.append(input, el("span", "perf-box"), el("span", "perf-opt-label", option.label));
|
|
605
|
+
return row;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function buildChips(mod, option) {
|
|
609
|
+
const block = el("div", "perf-chip-block");
|
|
610
|
+
block.title = option.description ?? "";
|
|
611
|
+
if (option.label) block.append(el("div", "perf-chips-label", option.label));
|
|
612
|
+
const row = el("div", "perf-chips");
|
|
613
|
+
for (const item of option.items ?? []) {
|
|
614
|
+
const key = `${option.key}.${item.id}`;
|
|
615
|
+
const chip = el("button", "perf-chip", item.label);
|
|
616
|
+
chip.type = "button";
|
|
617
|
+
chip.title = item.description ?? "";
|
|
618
|
+
if (getOption(mod.id, key)) chip.classList.add("perf-chip--on");
|
|
619
|
+
chip.addEventListener("click", () => {
|
|
620
|
+
setOption(mod.id, key, !getOption(mod.id, key), false);
|
|
621
|
+
chip.classList.toggle("perf-chip--on", getOption(mod.id, key));
|
|
622
|
+
});
|
|
623
|
+
row.append(chip);
|
|
624
|
+
}
|
|
625
|
+
block.append(row);
|
|
626
|
+
return block;
|
|
627
|
+
}
|
|
628
|
+
|
|
545
629
|
function buildOptions(mod) {
|
|
546
630
|
const options = registry.get(mod.id)?.options ?? [];
|
|
547
631
|
if (options.length === 0) return null;
|
|
548
632
|
const wrap = el("div", "perf-opts");
|
|
549
633
|
for (const option of options) {
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
634
|
+
if (option.type === "chips") {
|
|
635
|
+
wrap.append(buildChips(mod, option));
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
if (!option.children?.length) {
|
|
639
|
+
wrap.append(buildOptionRow(mod, option));
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
const group = el("div", "perf-opt-group");
|
|
643
|
+
const head = el("div", "perf-opt-head");
|
|
644
|
+
const caret = el("span", "perf-caret");
|
|
645
|
+
caret.setAttribute("role", "button");
|
|
646
|
+
caret.setAttribute("aria-label", `Show ${mod.name} options`);
|
|
647
|
+
const kids = el("div", "perf-opt-children");
|
|
648
|
+
const childInputs = [];
|
|
649
|
+
for (const child of option.children) {
|
|
650
|
+
const childRow = buildOptionRow(mod, child, !getOption(mod.id, option.key));
|
|
651
|
+
childInputs.push(childRow.querySelector("input"));
|
|
652
|
+
kids.append(childRow);
|
|
653
|
+
}
|
|
654
|
+
const master = buildOptionRow(mod, option, false, (checked) => {
|
|
655
|
+
for (const input of childInputs) input.disabled = !checked;
|
|
656
|
+
setOption(mod.id, option.key, checked, false);
|
|
657
|
+
});
|
|
658
|
+
const groupKey = `${mod.id}:${option.key}`;
|
|
659
|
+
const setExpanded = (open) => {
|
|
660
|
+
optionGroupState.set(groupKey, open);
|
|
661
|
+
kids.style.display = open ? "" : "none";
|
|
662
|
+
caret.classList.toggle("perf-caret--open", open);
|
|
663
|
+
};
|
|
664
|
+
setExpanded(optionGroupState.get(groupKey) ?? false);
|
|
665
|
+
caret.addEventListener("click", (event) => {
|
|
666
|
+
event.preventDefault();
|
|
667
|
+
event.stopPropagation();
|
|
668
|
+
setExpanded(!(optionGroupState.get(groupKey) ?? false));
|
|
669
|
+
});
|
|
670
|
+
head.append(master, caret);
|
|
671
|
+
group.append(head, kids);
|
|
672
|
+
wrap.append(group);
|
|
559
673
|
}
|
|
560
674
|
return wrap;
|
|
561
675
|
}
|
|
@@ -620,19 +734,37 @@ function buildCard(mod) {
|
|
|
620
734
|
function buildPanel() {
|
|
621
735
|
const panel = el("div");
|
|
622
736
|
panel.id = PANEL_ID;
|
|
737
|
+
const scroller = el("lol-uikit-scrollable");
|
|
738
|
+
panel.append(scroller);
|
|
623
739
|
|
|
624
740
|
const modules = getAvailableModules();
|
|
625
741
|
if (modules.length === 0) {
|
|
626
|
-
|
|
742
|
+
scroller.append(el("div", "perf-empty", "No plugins are available."));
|
|
627
743
|
return panel;
|
|
628
744
|
}
|
|
629
|
-
for (const mod of modules)
|
|
745
|
+
for (const mod of modules) scroller.append(buildCard(mod));
|
|
630
746
|
return panel;
|
|
631
747
|
}
|
|
632
748
|
|
|
749
|
+
function panelScrollTop(panel) {
|
|
750
|
+
return panel?.querySelector("lol-uikit-scrollable")?.scrollTop ?? 0;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function applyScrollTop(panel, top) {
|
|
754
|
+
if (!top || !panel) return;
|
|
755
|
+
const host = panel.querySelector("lol-uikit-scrollable");
|
|
756
|
+
if (host) host.scrollTop = top;
|
|
757
|
+
}
|
|
758
|
+
|
|
633
759
|
function refreshPanel() {
|
|
634
760
|
const current = document.getElementById(PANEL_ID);
|
|
635
|
-
if (current)
|
|
761
|
+
if (!current) return;
|
|
762
|
+
const top = panelScrollTop(current);
|
|
763
|
+
const next = buildPanel();
|
|
764
|
+
current.replaceWith(next);
|
|
765
|
+
if (!top) return;
|
|
766
|
+
applyScrollTop(next, top);
|
|
767
|
+
requestAnimationFrame(() => applyScrollTop(next, top));
|
|
636
768
|
}
|
|
637
769
|
|
|
638
770
|
function setNavActive(activeNav) {
|