perfectionist-plugins 1.4.1 → 1.6.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 +263 -0
- package/package.json +5 -3
- package/perfectionist-utils.js +128 -15
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,263 @@
|
|
|
1
|
+
const PLUGIN_ID = "auto-read";
|
|
2
|
+
const version = "1.0.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 RANKED_NOTIF_URI = "/lol-ranked/v1/notifications";
|
|
13
|
+
const EOS_NOTIF_URI = "/lol-ranked/v1/eos-notifications";
|
|
14
|
+
const NOTICE_LIST_URIS = [
|
|
15
|
+
"/player-notifications/v1/notifications",
|
|
16
|
+
"/lol-patch/v1/notifications",
|
|
17
|
+
"/patcher/v1/notifications",
|
|
18
|
+
"/lol-leaver-buster/v1/notifications",
|
|
19
|
+
"/lol-lobby/v2/notifications",
|
|
20
|
+
];
|
|
21
|
+
const NOTICE_BULK_DELETES = [
|
|
22
|
+
"/lol-gameflow/v1/early-exit-notifications/eog",
|
|
23
|
+
"/lol-gameflow/v1/early-exit-notifications/missions",
|
|
24
|
+
"/lol-statstones/v1/vignette-notifications",
|
|
25
|
+
"/lol-player-behavior/v1/code-of-conduct-notification",
|
|
26
|
+
];
|
|
27
|
+
const CLASH_BULK_ACKS = [
|
|
28
|
+
"/lol-clash/v1/notifications/acknowledge",
|
|
29
|
+
"/lol-clash/v1/eog-player-update/acknowledge",
|
|
30
|
+
"/lol-clash/v1/game-end/acknowledge",
|
|
31
|
+
];
|
|
32
|
+
const PIP_STYLE_ID = "auto-read-pips-style";
|
|
33
|
+
const PIP_CSS = ".activity-center__tab_label_notification { display: none !important; }";
|
|
34
|
+
|
|
35
|
+
let api = null;
|
|
36
|
+
let active = false;
|
|
37
|
+
let sweepTimer = null;
|
|
38
|
+
let sweepRunning = false;
|
|
39
|
+
let sweepQueued = false;
|
|
40
|
+
let unsubscribes = [];
|
|
41
|
+
const cooldowns = new Map();
|
|
42
|
+
|
|
43
|
+
function on(key) {
|
|
44
|
+
try {
|
|
45
|
+
return api?.getOption?.(PLUGIN_ID, `read.${key}`) === true;
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function ready(key, wait) {
|
|
52
|
+
return (cooldowns.get(key) ?? 0) + wait <= Date.now();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function stamp(key) {
|
|
56
|
+
cooldowns.set(key, Date.now());
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getJson(path) {
|
|
60
|
+
return fetch(path, { headers: { Accept: "application/json" } }).then((response) => {
|
|
61
|
+
if (!response.ok) throw new Error(`${path} responded ${response.status}`);
|
|
62
|
+
return response.json();
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function sendJson(method, path, body) {
|
|
67
|
+
return fetch(path, {
|
|
68
|
+
method,
|
|
69
|
+
headers: { Accept: "application/json", "Content-Type": "application/json" },
|
|
70
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
71
|
+
}).then((response) => {
|
|
72
|
+
if (!response.ok) throw new Error(`${method} ${path} responded ${response.status}`);
|
|
73
|
+
return response.status === 204 ? null : response.json().catch(() => null);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function sweepQuests() {
|
|
78
|
+
const missions = await getJson(MISSIONS_URI);
|
|
79
|
+
if (!Array.isArray(missions)) return;
|
|
80
|
+
const ids = [];
|
|
81
|
+
for (const mission of missions) {
|
|
82
|
+
if (!mission?.id || mission.viewed !== false) continue;
|
|
83
|
+
if (!ready(`mission:${mission.id}`, ITEM_COOLDOWN_MS)) continue;
|
|
84
|
+
stamp(`mission:${mission.id}`);
|
|
85
|
+
ids.push(mission.id);
|
|
86
|
+
}
|
|
87
|
+
for (let i = 0; i < ids.length; i += 100) {
|
|
88
|
+
await sendJson("PUT", MISSIONS_PLAYER_URI, { missionIds: ids.slice(i, i + 100) }).catch(() => {});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function sweepLootMarks() {
|
|
93
|
+
const loot = await getJson(LOOT_URI);
|
|
94
|
+
if (!Array.isArray(loot)) return;
|
|
95
|
+
for (const item of loot) {
|
|
96
|
+
const id = item?.lootId;
|
|
97
|
+
if (!id || item.isNew !== true || !ready(`lootmark:${id}`, ITEM_COOLDOWN_MS)) continue;
|
|
98
|
+
stamp(`lootmark:${id}`);
|
|
99
|
+
await sendJson("DELETE", `${LOOT_URI}/${encodeURIComponent(id)}/new-notification`).catch(() => {});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function sweepClash() {
|
|
104
|
+
const flags = await getJson(CLASH_FLAGS_URI).catch(() => null);
|
|
105
|
+
if (Array.isArray(flags)) {
|
|
106
|
+
for (const flag of flags) {
|
|
107
|
+
if (!flag?.id || flag.status === "ACKNOWLEDGED" || !ready(`clashflag:${flag.id}`, ITEM_COOLDOWN_MS)) continue;
|
|
108
|
+
stamp(`clashflag:${flag.id}`);
|
|
109
|
+
await sendJson("POST", `${CLASH_FLAGS_URI}/${encodeURIComponent(flag.id)}/acknowledge`).catch(() => {});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (!ready("clash-bulk", BULK_COOLDOWN_MS)) return;
|
|
113
|
+
stamp("clash-bulk");
|
|
114
|
+
for (const path of CLASH_BULK_ACKS) {
|
|
115
|
+
await sendJson("POST", path).catch(() => {});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function sweepRanked() {
|
|
120
|
+
for (const uri of [RANKED_NOTIF_URI, EOS_NOTIF_URI]) {
|
|
121
|
+
const list = await getJson(uri).catch(() => null);
|
|
122
|
+
if (!Array.isArray(list)) continue;
|
|
123
|
+
for (const note of list) {
|
|
124
|
+
const id = note?.id ?? note?.notificationId;
|
|
125
|
+
if (id == null || !ready(`ranked:${id}`, ITEM_COOLDOWN_MS)) continue;
|
|
126
|
+
stamp(`ranked:${id}`);
|
|
127
|
+
await sendJson("POST", `${uri}/${encodeURIComponent(id)}/acknowledge`).catch(() => {});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function sweepNotices() {
|
|
133
|
+
for (const uri of NOTICE_LIST_URIS) {
|
|
134
|
+
const list = await getJson(uri).catch(() => null);
|
|
135
|
+
if (!Array.isArray(list)) continue;
|
|
136
|
+
for (const note of list) {
|
|
137
|
+
const id = note?.id ?? note?.notificationId;
|
|
138
|
+
if (id == null || !ready(`notice:${id}`, ITEM_COOLDOWN_MS)) continue;
|
|
139
|
+
stamp(`notice:${id}`);
|
|
140
|
+
await sendJson("DELETE", `${uri}/${encodeURIComponent(id)}`).catch(() => {});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (!ready("notice-bulk", BULK_COOLDOWN_MS)) return;
|
|
144
|
+
stamp("notice-bulk");
|
|
145
|
+
for (const path of NOTICE_BULK_DELETES) {
|
|
146
|
+
await sendJson("DELETE", path).catch(() => {});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function syncPips() {
|
|
151
|
+
const wanted = active && on("pips");
|
|
152
|
+
let style = document.getElementById(PIP_STYLE_ID);
|
|
153
|
+
if (wanted && !style) {
|
|
154
|
+
style = document.createElement("style");
|
|
155
|
+
style.id = PIP_STYLE_ID;
|
|
156
|
+
style.textContent = PIP_CSS;
|
|
157
|
+
document.head.appendChild(style);
|
|
158
|
+
} else if (!wanted && style) {
|
|
159
|
+
style.remove();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function scheduleSweep() {
|
|
164
|
+
if (!active) return;
|
|
165
|
+
clearTimeout(sweepTimer);
|
|
166
|
+
sweepTimer = setTimeout(runSweep, SWEEP_DEBOUNCE_MS);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function runSweep() {
|
|
170
|
+
if (!active) return;
|
|
171
|
+
if (sweepRunning) {
|
|
172
|
+
sweepQueued = true;
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
sweepRunning = true;
|
|
176
|
+
try {
|
|
177
|
+
if (on("quests")) await sweepQuests().catch(() => {});
|
|
178
|
+
if (on("loot")) await sweepLootMarks().catch(() => {});
|
|
179
|
+
if (on("clash")) await sweepClash().catch(() => {});
|
|
180
|
+
if (on("ranked")) await sweepRanked().catch(() => {});
|
|
181
|
+
if (on("notices")) await sweepNotices().catch(() => {});
|
|
182
|
+
} finally {
|
|
183
|
+
sweepRunning = false;
|
|
184
|
+
if (sweepQueued) {
|
|
185
|
+
sweepQueued = false;
|
|
186
|
+
scheduleSweep();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function onEnable(utils) {
|
|
192
|
+
api = utils;
|
|
193
|
+
active = true;
|
|
194
|
+
cooldowns.clear();
|
|
195
|
+
for (const uri of [MISSIONS_URI, LOOT_URI, CLASH_FLAGS_URI, RANKED_NOTIF_URI, EOS_NOTIF_URI, ...NOTICE_LIST_URIS]) {
|
|
196
|
+
try {
|
|
197
|
+
const off = api.subscribe(uri, scheduleSweep);
|
|
198
|
+
if (off) unsubscribes.push(off);
|
|
199
|
+
} catch {}
|
|
200
|
+
}
|
|
201
|
+
scheduleSweep();
|
|
202
|
+
syncPips();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function onDisable() {
|
|
206
|
+
active = false;
|
|
207
|
+
clearTimeout(sweepTimer);
|
|
208
|
+
sweepTimer = null;
|
|
209
|
+
sweepRunning = false;
|
|
210
|
+
sweepQueued = false;
|
|
211
|
+
for (const off of unsubscribes) {
|
|
212
|
+
try {
|
|
213
|
+
off();
|
|
214
|
+
} catch {}
|
|
215
|
+
}
|
|
216
|
+
unsubscribes = [];
|
|
217
|
+
cooldowns.clear();
|
|
218
|
+
api = null;
|
|
219
|
+
syncPips();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function onOptionChange() {
|
|
223
|
+
if (!active) return;
|
|
224
|
+
scheduleSweep();
|
|
225
|
+
syncPips();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function init() {
|
|
229
|
+
const registerWith = (utils) => {
|
|
230
|
+
utils.registerPlugin({
|
|
231
|
+
id: PLUGIN_ID,
|
|
232
|
+
name: "Auto Read",
|
|
233
|
+
description: "Automatically marks quests, loot, Clash and client notices as read so no badge or dot stays lit.",
|
|
234
|
+
version,
|
|
235
|
+
defaultEnabled: true,
|
|
236
|
+
options: [
|
|
237
|
+
{
|
|
238
|
+
type: "chips",
|
|
239
|
+
key: "read",
|
|
240
|
+
label: "Mark as read",
|
|
241
|
+
description: "Choose what gets marked as read automatically.",
|
|
242
|
+
items: [
|
|
243
|
+
{ id: "quests", label: "Quests", description: "Quest and mission badges, including the mission button counter.", default: true },
|
|
244
|
+
{ id: "loot", label: "Loot marks", description: "The new mark on freshly earned loot.", default: true },
|
|
245
|
+
{ id: "clash", label: "Clash", description: "Clash tab flags and Clash notifications.", default: true },
|
|
246
|
+
{ id: "ranked", label: "Ranked", description: "Ranked, end-of-season and champion mastery notices.", default: true },
|
|
247
|
+
{ id: "notices", label: "Notices", description: "Patch, patcher, leaver buster, lobby, behavior and gameflow notices.", default: true },
|
|
248
|
+
{ id: "pips", label: "Tab pips", description: "Hides the new pips on activity tabs.", default: true },
|
|
249
|
+
],
|
|
250
|
+
},
|
|
251
|
+
],
|
|
252
|
+
onEnable,
|
|
253
|
+
onDisable,
|
|
254
|
+
onOptionChange,
|
|
255
|
+
});
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
if (window.PerfectionistUtils) {
|
|
259
|
+
registerWith(window.PerfectionistUtils);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
addEventListener("perfectionist:utils-ready", (e) => registerWith(e.detail ?? window.PerfectionistUtils), { once: true });
|
|
263
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "perfectionist-plugins",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.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.6.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.0.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,9 +247,22 @@ 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
|
}
|
|
@@ -542,20 +587,86 @@ 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);
|
|
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", () => setOption(mod.id, key, !getOption(mod.id, key)));
|
|
620
|
+
row.append(chip);
|
|
621
|
+
}
|
|
622
|
+
block.append(row);
|
|
623
|
+
return block;
|
|
624
|
+
}
|
|
625
|
+
|
|
545
626
|
function buildOptions(mod) {
|
|
546
627
|
const options = registry.get(mod.id)?.options ?? [];
|
|
547
628
|
if (options.length === 0) return null;
|
|
548
629
|
const wrap = el("div", "perf-opts");
|
|
549
630
|
for (const option of options) {
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
631
|
+
if (option.type === "chips") {
|
|
632
|
+
wrap.append(buildChips(mod, option));
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
if (!option.children?.length) {
|
|
636
|
+
wrap.append(buildOptionRow(mod, option));
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
const group = el("div", "perf-opt-group");
|
|
640
|
+
const head = el("div", "perf-opt-head");
|
|
641
|
+
const caret = el("span", "perf-caret");
|
|
642
|
+
caret.setAttribute("role", "button");
|
|
643
|
+
caret.setAttribute("aria-label", `Show ${mod.name} options`);
|
|
644
|
+
const kids = el("div", "perf-opt-children");
|
|
645
|
+
const childInputs = [];
|
|
646
|
+
for (const child of option.children) {
|
|
647
|
+
const childRow = buildOptionRow(mod, child, !getOption(mod.id, option.key));
|
|
648
|
+
childInputs.push(childRow.querySelector("input"));
|
|
649
|
+
kids.append(childRow);
|
|
650
|
+
}
|
|
651
|
+
const master = buildOptionRow(mod, option, false, (checked) => {
|
|
652
|
+
for (const input of childInputs) input.disabled = !checked;
|
|
653
|
+
setOption(mod.id, option.key, checked);
|
|
654
|
+
});
|
|
655
|
+
const groupKey = `${mod.id}:${option.key}`;
|
|
656
|
+
const setExpanded = (open) => {
|
|
657
|
+
optionGroupState.set(groupKey, open);
|
|
658
|
+
kids.style.display = open ? "" : "none";
|
|
659
|
+
caret.classList.toggle("perf-caret--open", open);
|
|
660
|
+
};
|
|
661
|
+
setExpanded(optionGroupState.get(groupKey) ?? false);
|
|
662
|
+
caret.addEventListener("click", (event) => {
|
|
663
|
+
event.preventDefault();
|
|
664
|
+
event.stopPropagation();
|
|
665
|
+
setExpanded(!(optionGroupState.get(groupKey) ?? false));
|
|
666
|
+
});
|
|
667
|
+
head.append(master, caret);
|
|
668
|
+
group.append(head, kids);
|
|
669
|
+
wrap.append(group);
|
|
559
670
|
}
|
|
560
671
|
return wrap;
|
|
561
672
|
}
|
|
@@ -620,13 +731,15 @@ function buildCard(mod) {
|
|
|
620
731
|
function buildPanel() {
|
|
621
732
|
const panel = el("div");
|
|
622
733
|
panel.id = PANEL_ID;
|
|
734
|
+
const scroller = el("lol-uikit-scrollable");
|
|
735
|
+
panel.append(scroller);
|
|
623
736
|
|
|
624
737
|
const modules = getAvailableModules();
|
|
625
738
|
if (modules.length === 0) {
|
|
626
|
-
|
|
739
|
+
scroller.append(el("div", "perf-empty", "No plugins are available."));
|
|
627
740
|
return panel;
|
|
628
741
|
}
|
|
629
|
-
for (const mod of modules)
|
|
742
|
+
for (const mod of modules) scroller.append(buildCard(mod));
|
|
630
743
|
return panel;
|
|
631
744
|
}
|
|
632
745
|
|