perfectionist-plugins 2.4.4 → 2.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/auto-accept.js +43 -111
- package/auto-claimer.js +1 -2
- package/auto-read.js +1 -2
- package/ban-blocker.js +1 -2
- package/better-client.js +1 -4
- package/dodge-helper.js +835 -561
- package/hide-unowned-skins.js +2 -9
- package/package.json +40 -40
- package/perfectionist-utils.js +1041 -944
- package/unlock-profile-skins.js +1 -2
package/perfectionist-utils.js
CHANGED
|
@@ -1,945 +1,1042 @@
|
|
|
1
|
-
const READY_EVENT = "perfectionist:utils-ready";
|
|
2
|
-
const STATE_KEY = "perfectionist-plugins:state";
|
|
3
|
-
const NAV_ID = "perfectionist-plugins-nav";
|
|
4
|
-
const PANEL_ID = "perfectionist-plugins-panel";
|
|
5
|
-
const STYLE_ID = "perfectionist-plugins-style";
|
|
6
|
-
const TAB_TITLE = "Perfectionism";
|
|
7
|
-
|
|
8
|
-
export const version = "2.4.0";
|
|
9
|
-
|
|
10
|
-
const PKG = "perfectionist-plugins";
|
|
11
|
-
const CDN = "https://cdn.jsdelivr.net/npm";
|
|
12
|
-
|
|
13
|
-
const BUNDLED_MODULES = [
|
|
14
|
-
{
|
|
15
|
-
id: "perfectionist-utils",
|
|
16
|
-
file: "perfectionist-utils.js",
|
|
17
|
-
name: "Perfectionism Core",
|
|
18
|
-
version,
|
|
19
|
-
description: "The base the other plugins run on. Adds this Perfectionism page to the client settings.",
|
|
20
|
-
core: true,
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
id: "better-client",
|
|
24
|
-
file: "better-client.js",
|
|
25
|
-
name: "BetterClient",
|
|
26
|
-
version: "1.7.0",
|
|
27
|
-
description: "Makes the client lighter and smoother without changing how it looks. Cuts tracking and repeated requests and fixes small bugs. Has an optional Performance mode for slower PCs.",
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
id: "ban-blocker",
|
|
31
|
-
file: "ban-blocker.js",
|
|
32
|
-
name: "Ban Blocker",
|
|
33
|
-
version: "3.6.0",
|
|
34
|
-
description: "Adds a BLOCK button to your teammates in champ select. While a teammate is blocked, the champion they are about to ban is prepicked for you, so their ban can never land on it.",
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
id: "unlock-profile-skins",
|
|
38
|
-
file: "unlock-profile-skins.js",
|
|
39
|
-
name: "Unlock Profile Skins",
|
|
40
|
-
version: "1.2.4",
|
|
41
|
-
description: "Shows every skin as unlocked in the Set profile background window, event skins included, so you can pick any skin as your profile background.",
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
id: "hide-unowned-skins",
|
|
45
|
-
file: "hide-unowned-skins.js",
|
|
46
|
-
name: "Hide Unowned Skins",
|
|
47
|
-
version: "2.5.1",
|
|
48
|
-
description: "In champ select, the skin carousel only shows the skins you own for the champion you picked.",
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
id: "auto-accept",
|
|
52
|
-
file: "auto-accept.js",
|
|
53
|
-
name: "Auto Accept",
|
|
54
|
-
version: "1.1.
|
|
55
|
-
description: "Accepts the match for you when the ready check pops up.
|
|
56
|
-
},
|
|
57
|
-
{
|
|
58
|
-
id: "dodge-helper",
|
|
59
|
-
file: "dodge-helper.js",
|
|
60
|
-
name: "Dodge Helper",
|
|
61
|
-
version: "1.4.
|
|
62
|
-
description: "Adds a button to
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
id: "auto-claimer",
|
|
66
|
-
file: "auto-claimer.js",
|
|
67
|
-
name: "Auto Claimer",
|
|
68
|
-
version: "1.2.0",
|
|
69
|
-
description: "Claims rewards for you as soon as they are ready: event tracks, missions and the TFT Battle Pass. Optional extras can also buy 1 BE shop items and open mystery loot and chests.",
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
id: "auto-read",
|
|
73
|
-
file: "auto-read.js",
|
|
74
|
-
name: "Auto Read",
|
|
75
|
-
version: "1.2.0",
|
|
76
|
-
description: "Marks quests, loot, Clash, TFT and client notices as read for you, so no badge or dot stays lit.",
|
|
77
|
-
},
|
|
78
|
-
];
|
|
79
|
-
|
|
80
|
-
const STYLE_CSS = `
|
|
81
|
-
#${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; }
|
|
82
|
-
#${PANEL_ID} > lol-uikit-scrollable { display: block; flex: 1 1 auto; min-height: 0; }
|
|
83
|
-
#${PANEL_ID} .perf-card { display: flex; align-items: flex-start; justify-content: space-between; gap: 24px;
|
|
84
|
-
padding: 16px 18px; margin-bottom: 12px; box-sizing: border-box;
|
|
85
|
-
background: #010a13; border: 1px solid #785a28; }
|
|
86
|
-
#${PANEL_ID} .perf-card:hover { border-color: #c89b3c; }
|
|
87
|
-
#${PANEL_ID} .perf-card--missing { opacity: 0.75; }
|
|
88
|
-
#${PANEL_ID} .perf-info { min-width: 0; }
|
|
89
|
-
#${PANEL_ID} .perf-name { font-family: "LoL Display", "Times New Roman", Times, Baskerville, Georgia, serif;
|
|
90
|
-
font-size: 15px; font-weight: 700; letter-spacing: 1.05px; text-transform: uppercase; color: #f0e6d2; }
|
|
91
|
-
#${PANEL_ID} .perf-version { font-family: "LoL Body", Arial, sans-serif; font-size: 10px; font-weight: 700;
|
|
92
|
-
letter-spacing: 0.25px; text-transform: none; color: #c89b3c; margin-left: 8px; }
|
|
93
|
-
#${PANEL_ID} .perf-desc { font-size: 12px; letter-spacing: 0.3px; color: #a09b8c; margin-top: 7px; line-height: 1.5; max-width: 560px; }
|
|
94
|
-
#${PANEL_ID} .perf-actions { display: flex; align-items: center; gap: 12px; flex: 0 0 auto; }
|
|
95
|
-
#${PANEL_ID} .perf-toggle { position: relative; display: flex; align-items: center; flex: 0 0 auto; cursor: pointer; }
|
|
96
|
-
#${PANEL_ID} .perf-toggle input { position: absolute; opacity: 0; width: 0; height: 0; margin: 0; }
|
|
97
|
-
#${PANEL_ID} .perf-box { position: relative; width: 18px; height: 18px; box-sizing: border-box;
|
|
98
|
-
border: 1px solid #785a28; background: #0a1428; }
|
|
99
|
-
#${PANEL_ID} .perf-box::after { content: ""; position: absolute; inset: 1px; display: none;
|
|
100
|
-
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2.2 6.4L4.9 9.1L9.8 3.3' fill='none' stroke='%23c89b3c' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 12px 12px no-repeat; }
|
|
101
|
-
#${PANEL_ID} .perf-toggle input:checked + .perf-box::after { display: block; }
|
|
102
|
-
#${PANEL_ID} .perf-toggle:hover .perf-box { border-color: #c89b3c; }
|
|
103
|
-
#${PANEL_ID} .perf-toggle input:focus-visible + .perf-box { outline: 1px solid #c89b3c; outline-offset: 2px; }
|
|
104
|
-
.perf-btn { font-family: "LoL Body", Arial, sans-serif; font-size: 11px; font-weight: 700; line-height: 12px;
|
|
105
|
-
letter-spacing: 1px; text-transform: uppercase; color: #c89b3c; background: #0a1428;
|
|
106
|
-
border: 1px solid #785a28; padding: 8px 16px; cursor: pointer; }
|
|
107
|
-
.perf-btn:hover { color: #f0e6d2; border-color: #c89b3c; background: #1e2328; }
|
|
108
|
-
.perf-btn:disabled { opacity: 0.5; cursor: default; }
|
|
109
|
-
#${PANEL_ID} .perf-uninstall { width: 30px; height: 30px; padding: 0; display: flex; align-items: center; justify-content: center; }
|
|
110
|
-
#${PANEL_ID} .perf-uninstall::after { content: ""; width: 11px; height: 11px;
|
|
111
|
-
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2.5 2.5L9.5 9.5M9.5 2.5L2.5 9.5' fill='none' stroke='%23c89b3c' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E") center / 11px 11px no-repeat; }
|
|
112
|
-
#${PANEL_ID} .perf-empty { font-size: 12px; color: #a09b8c; }
|
|
113
|
-
#${PANEL_ID} .perf-opts { margin-top: 11px; display: flex; flex-direction: column; gap: 8px; }
|
|
114
|
-
#${PANEL_ID} .perf-opt { display: flex; align-items: center; gap: 9px; cursor: pointer; }
|
|
115
|
-
#${PANEL_ID} .perf-opt .perf-opt-label { font-size: 11px; letter-spacing: 0.3px; color: #a09b8c; }
|
|
116
|
-
#${PANEL_ID} .perf-opt:hover .perf-opt-label { color: #f0e6d2; }
|
|
117
|
-
#${PANEL_ID} .perf-opt-group { margin-top: 2px; }
|
|
118
|
-
#${PANEL_ID} .perf-opt-head { display: flex; align-items: center; gap: 9px; }
|
|
119
|
-
#${PANEL_ID} .perf-caret { flex: 0 0 auto; width: 28px; height: 28px; cursor: pointer; display: flex; align-items: center; justify-content: center;
|
|
120
|
-
border: 1px solid transparent; background: transparent; }
|
|
121
|
-
#${PANEL_ID} .perf-caret:hover { border-color: #785a28; background: #0a1428; }
|
|
122
|
-
#${PANEL_ID} .perf-caret::after { content: ""; width: 12px; height: 12px; transform-origin: center;
|
|
123
|
-
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;
|
|
124
|
-
transition: transform 0.15s; }
|
|
125
|
-
#${PANEL_ID} .perf-caret:hover::after { filter: brightness(1.4); }
|
|
126
|
-
#${PANEL_ID} .perf-caret--open::after { transform: rotate(90deg); }
|
|
127
|
-
#${PANEL_ID} .perf-opt-children { margin: 8px 0 2px 25px; padding-left: 12px; display: flex; flex-direction: column; gap: 8px;
|
|
128
|
-
border-left: 1px solid #785a28; }
|
|
129
|
-
#${PANEL_ID} .perf-toggle input:disabled + .perf-box,
|
|
130
|
-
#${PANEL_ID} .perf-toggle input:disabled ~ .perf-opt-label { opacity: 0.45; }
|
|
131
|
-
#${PANEL_ID} .perf-toggle input:disabled ~ .perf-opt-label { cursor: default; }
|
|
132
|
-
#${PANEL_ID} .perf-chip-block { margin-top: 11px; }
|
|
133
|
-
#${PANEL_ID} .perf-chips-label { font-size: 10px; font-weight: 700; letter-spacing: 0.6px; text-transform: uppercase; color: #c89b3c; margin-bottom: 7px; }
|
|
134
|
-
#${PANEL_ID} .perf-chips { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
135
|
-
#${PANEL_ID} .perf-chip { font-family: "LoL Body", Arial, "Helvetica Neue", Helvetica, sans-serif; font-size: 10px; font-weight: 700;
|
|
136
|
-
letter-spacing: 0.5px; text-transform: uppercase; color: #a09b8c; background: #0a1428; border: 1px solid #785a28;
|
|
137
|
-
padding: 6px 11px; cursor: pointer; }
|
|
138
|
-
#${PANEL_ID} .perf-chip:hover { color: #f0e6d2; border-color: #c89b3c; }
|
|
139
|
-
#${PANEL_ID} .perf-chip--on { color: #010a13; background: #c89b3c; border-color: #c89b3c; }
|
|
140
|
-
#${PANEL_ID} .perf-chip--on:hover { background: #f0e6d2; }
|
|
141
|
-
#${PANEL_ID} .perf-tools { display: flex; align-items: center; gap: 10px; flex: 0 0 auto; margin-bottom: 14px; }
|
|
142
|
-
#${PANEL_ID} .perf-search { flex: 1 1 auto; min-width: 0; box-sizing: border-box;
|
|
143
|
-
font-family: "LoL Body", Arial, "Helvetica Neue", Helvetica, sans-serif; font-size: 12px; letter-spacing: 0.3px;
|
|
144
|
-
color: #f0e6d2; background: #0a1428; border: 1px solid #785a28; padding: 9px 12px; outline: none; }
|
|
145
|
-
#${PANEL_ID} .perf-search:focus { border-color: #c89b3c; }
|
|
146
|
-
#${PANEL_ID} .perf-search::placeholder { color: #a09b8c; }
|
|
147
|
-
#${PANEL_ID} .perf-master { flex: 0 0 auto; white-space: nowrap; }
|
|
148
|
-
#${PANEL_ID} .perf-card--locked .perf-opts { pointer-events: none; }
|
|
149
|
-
#${PANEL_ID} .perf-card--locked .perf-actions > .perf-toggle { pointer-events: none; }
|
|
150
|
-
#${PANEL_ID} .perf-card--locked .perf-caret { opacity: 0.45; }
|
|
151
|
-
#${PANEL_ID} .perf-card--locked .perf-chips-label { opacity: 0.45; }
|
|
152
|
-
#${PANEL_ID} .perf-chip:disabled { opacity: 0.45; cursor: default; }
|
|
153
|
-
#${PANEL_ID} .perf-chip:disabled:hover { color: #a09b8c; border-color: #785a28; background: #0a1428; }
|
|
154
|
-
#${PANEL_ID} .perf-chip--on:disabled:hover { color: #010a13; border-color: #c89b3c; background: #c89b3c; }
|
|
155
|
-
`;
|
|
156
|
-
|
|
157
|
-
const registry = new Map();
|
|
158
|
-
const watchers = new Map();
|
|
159
|
-
const requestCache = new Map();
|
|
160
|
-
let socket = null;
|
|
161
|
-
let observer = null;
|
|
162
|
-
let scanScheduled = false;
|
|
163
|
-
|
|
164
|
-
let coords = null;
|
|
165
|
-
const resolved = new Map();
|
|
166
|
-
let
|
|
167
|
-
let
|
|
168
|
-
let
|
|
169
|
-
let
|
|
170
|
-
let
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
return
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
});
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
function
|
|
510
|
-
|
|
511
|
-
if (
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
}
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
const
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
const
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
if (
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
if (
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
const
|
|
821
|
-
if (
|
|
822
|
-
|
|
823
|
-
return;
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
const
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
const
|
|
838
|
-
if (
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
function
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
const
|
|
845
|
-
const
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
);
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1
|
+
const READY_EVENT = "perfectionist:utils-ready";
|
|
2
|
+
const STATE_KEY = "perfectionist-plugins:state";
|
|
3
|
+
const NAV_ID = "perfectionist-plugins-nav";
|
|
4
|
+
const PANEL_ID = "perfectionist-plugins-panel";
|
|
5
|
+
const STYLE_ID = "perfectionist-plugins-style";
|
|
6
|
+
const TAB_TITLE = "Perfectionism";
|
|
7
|
+
|
|
8
|
+
export const version = "2.4.0";
|
|
9
|
+
|
|
10
|
+
const PKG = "perfectionist-plugins";
|
|
11
|
+
const CDN = "https://cdn.jsdelivr.net/npm";
|
|
12
|
+
|
|
13
|
+
const BUNDLED_MODULES = [
|
|
14
|
+
{
|
|
15
|
+
id: "perfectionist-utils",
|
|
16
|
+
file: "perfectionist-utils.js",
|
|
17
|
+
name: "Perfectionism Core",
|
|
18
|
+
version,
|
|
19
|
+
description: "The base the other plugins run on. Adds this Perfectionism page to the client settings.",
|
|
20
|
+
core: true,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: "better-client",
|
|
24
|
+
file: "better-client.js",
|
|
25
|
+
name: "BetterClient",
|
|
26
|
+
version: "1.7.0",
|
|
27
|
+
description: "Makes the client lighter and smoother without changing how it looks. Cuts tracking and repeated requests and fixes small bugs. Has an optional Performance mode for slower PCs.",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: "ban-blocker",
|
|
31
|
+
file: "ban-blocker.js",
|
|
32
|
+
name: "Ban Blocker",
|
|
33
|
+
version: "3.6.0",
|
|
34
|
+
description: "Adds a BLOCK button to your teammates in champ select. While a teammate is blocked, the champion they are about to ban is prepicked for you, so their ban can never land on it.",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: "unlock-profile-skins",
|
|
38
|
+
file: "unlock-profile-skins.js",
|
|
39
|
+
name: "Unlock Profile Skins",
|
|
40
|
+
version: "1.2.4",
|
|
41
|
+
description: "Shows every skin as unlocked in the Set profile background window, event skins included, so you can pick any skin as your profile background.",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: "hide-unowned-skins",
|
|
45
|
+
file: "hide-unowned-skins.js",
|
|
46
|
+
name: "Hide Unowned Skins",
|
|
47
|
+
version: "2.5.1",
|
|
48
|
+
description: "In champ select, the skin carousel only shows the skins you own for the champion you picked.",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "auto-accept",
|
|
52
|
+
file: "auto-accept.js",
|
|
53
|
+
name: "Auto Accept",
|
|
54
|
+
version: "1.1.3",
|
|
55
|
+
description: "Accepts the match for you when the ready check pops up.",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: "dodge-helper",
|
|
59
|
+
file: "dodge-helper.js",
|
|
60
|
+
name: "Dodge Helper",
|
|
61
|
+
version: "1.4.3",
|
|
62
|
+
description: "Adds a DODGE/QUIT button to champ select, showing the LP and queue penalty for the next dodge.",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: "auto-claimer",
|
|
66
|
+
file: "auto-claimer.js",
|
|
67
|
+
name: "Auto Claimer",
|
|
68
|
+
version: "1.2.0",
|
|
69
|
+
description: "Claims rewards for you as soon as they are ready: event tracks, missions and the TFT Battle Pass. Optional extras can also buy 1 BE shop items and open mystery loot and chests.",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: "auto-read",
|
|
73
|
+
file: "auto-read.js",
|
|
74
|
+
name: "Auto Read",
|
|
75
|
+
version: "1.2.0",
|
|
76
|
+
description: "Marks quests, loot, Clash, TFT and client notices as read for you, so no badge or dot stays lit.",
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
const STYLE_CSS = `
|
|
81
|
+
#${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; }
|
|
82
|
+
#${PANEL_ID} > lol-uikit-scrollable { display: block; flex: 1 1 auto; min-height: 0; }
|
|
83
|
+
#${PANEL_ID} .perf-card { display: flex; align-items: flex-start; justify-content: space-between; gap: 24px;
|
|
84
|
+
padding: 16px 18px; margin-bottom: 12px; box-sizing: border-box;
|
|
85
|
+
background: #010a13; border: 1px solid #785a28; }
|
|
86
|
+
#${PANEL_ID} .perf-card:hover { border-color: #c89b3c; }
|
|
87
|
+
#${PANEL_ID} .perf-card--missing { opacity: 0.75; }
|
|
88
|
+
#${PANEL_ID} .perf-info { min-width: 0; }
|
|
89
|
+
#${PANEL_ID} .perf-name { font-family: "LoL Display", "Times New Roman", Times, Baskerville, Georgia, serif;
|
|
90
|
+
font-size: 15px; font-weight: 700; letter-spacing: 1.05px; text-transform: uppercase; color: #f0e6d2; }
|
|
91
|
+
#${PANEL_ID} .perf-version { font-family: "LoL Body", Arial, sans-serif; font-size: 10px; font-weight: 700;
|
|
92
|
+
letter-spacing: 0.25px; text-transform: none; color: #c89b3c; margin-left: 8px; }
|
|
93
|
+
#${PANEL_ID} .perf-desc { font-size: 12px; letter-spacing: 0.3px; color: #a09b8c; margin-top: 7px; line-height: 1.5; max-width: 560px; }
|
|
94
|
+
#${PANEL_ID} .perf-actions { display: flex; align-items: center; gap: 12px; flex: 0 0 auto; }
|
|
95
|
+
#${PANEL_ID} .perf-toggle { position: relative; display: flex; align-items: center; flex: 0 0 auto; cursor: pointer; }
|
|
96
|
+
#${PANEL_ID} .perf-toggle input { position: absolute; opacity: 0; width: 0; height: 0; margin: 0; }
|
|
97
|
+
#${PANEL_ID} .perf-box { position: relative; width: 18px; height: 18px; box-sizing: border-box;
|
|
98
|
+
border: 1px solid #785a28; background: #0a1428; }
|
|
99
|
+
#${PANEL_ID} .perf-box::after { content: ""; position: absolute; inset: 1px; display: none;
|
|
100
|
+
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2.2 6.4L4.9 9.1L9.8 3.3' fill='none' stroke='%23c89b3c' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 12px 12px no-repeat; }
|
|
101
|
+
#${PANEL_ID} .perf-toggle input:checked + .perf-box::after { display: block; }
|
|
102
|
+
#${PANEL_ID} .perf-toggle:hover .perf-box { border-color: #c89b3c; }
|
|
103
|
+
#${PANEL_ID} .perf-toggle input:focus-visible + .perf-box { outline: 1px solid #c89b3c; outline-offset: 2px; }
|
|
104
|
+
.perf-btn { font-family: "LoL Body", Arial, sans-serif; font-size: 11px; font-weight: 700; line-height: 12px;
|
|
105
|
+
letter-spacing: 1px; text-transform: uppercase; color: #c89b3c; background: #0a1428;
|
|
106
|
+
border: 1px solid #785a28; padding: 8px 16px; cursor: pointer; }
|
|
107
|
+
.perf-btn:hover { color: #f0e6d2; border-color: #c89b3c; background: #1e2328; }
|
|
108
|
+
.perf-btn:disabled { opacity: 0.5; cursor: default; }
|
|
109
|
+
#${PANEL_ID} .perf-uninstall { width: 30px; height: 30px; padding: 0; display: flex; align-items: center; justify-content: center; }
|
|
110
|
+
#${PANEL_ID} .perf-uninstall::after { content: ""; width: 11px; height: 11px;
|
|
111
|
+
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2.5 2.5L9.5 9.5M9.5 2.5L2.5 9.5' fill='none' stroke='%23c89b3c' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E") center / 11px 11px no-repeat; }
|
|
112
|
+
#${PANEL_ID} .perf-empty { font-size: 12px; color: #a09b8c; }
|
|
113
|
+
#${PANEL_ID} .perf-opts { margin-top: 11px; display: flex; flex-direction: column; gap: 8px; }
|
|
114
|
+
#${PANEL_ID} .perf-opt { display: flex; align-items: center; gap: 9px; cursor: pointer; }
|
|
115
|
+
#${PANEL_ID} .perf-opt .perf-opt-label { font-size: 11px; letter-spacing: 0.3px; color: #a09b8c; }
|
|
116
|
+
#${PANEL_ID} .perf-opt:hover .perf-opt-label { color: #f0e6d2; }
|
|
117
|
+
#${PANEL_ID} .perf-opt-group { margin-top: 2px; }
|
|
118
|
+
#${PANEL_ID} .perf-opt-head { display: flex; align-items: center; gap: 9px; }
|
|
119
|
+
#${PANEL_ID} .perf-caret { flex: 0 0 auto; width: 28px; height: 28px; cursor: pointer; display: flex; align-items: center; justify-content: center;
|
|
120
|
+
border: 1px solid transparent; background: transparent; }
|
|
121
|
+
#${PANEL_ID} .perf-caret:hover { border-color: #785a28; background: #0a1428; }
|
|
122
|
+
#${PANEL_ID} .perf-caret::after { content: ""; width: 12px; height: 12px; transform-origin: center;
|
|
123
|
+
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;
|
|
124
|
+
transition: transform 0.15s; }
|
|
125
|
+
#${PANEL_ID} .perf-caret:hover::after { filter: brightness(1.4); }
|
|
126
|
+
#${PANEL_ID} .perf-caret--open::after { transform: rotate(90deg); }
|
|
127
|
+
#${PANEL_ID} .perf-opt-children { margin: 8px 0 2px 25px; padding-left: 12px; display: flex; flex-direction: column; gap: 8px;
|
|
128
|
+
border-left: 1px solid #785a28; }
|
|
129
|
+
#${PANEL_ID} .perf-toggle input:disabled + .perf-box,
|
|
130
|
+
#${PANEL_ID} .perf-toggle input:disabled ~ .perf-opt-label { opacity: 0.45; }
|
|
131
|
+
#${PANEL_ID} .perf-toggle input:disabled ~ .perf-opt-label { cursor: default; }
|
|
132
|
+
#${PANEL_ID} .perf-chip-block { margin-top: 11px; }
|
|
133
|
+
#${PANEL_ID} .perf-chips-label { font-size: 10px; font-weight: 700; letter-spacing: 0.6px; text-transform: uppercase; color: #c89b3c; margin-bottom: 7px; }
|
|
134
|
+
#${PANEL_ID} .perf-chips { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
135
|
+
#${PANEL_ID} .perf-chip { font-family: "LoL Body", Arial, "Helvetica Neue", Helvetica, sans-serif; font-size: 10px; font-weight: 700;
|
|
136
|
+
letter-spacing: 0.5px; text-transform: uppercase; color: #a09b8c; background: #0a1428; border: 1px solid #785a28;
|
|
137
|
+
padding: 6px 11px; cursor: pointer; }
|
|
138
|
+
#${PANEL_ID} .perf-chip:hover { color: #f0e6d2; border-color: #c89b3c; }
|
|
139
|
+
#${PANEL_ID} .perf-chip--on { color: #010a13; background: #c89b3c; border-color: #c89b3c; }
|
|
140
|
+
#${PANEL_ID} .perf-chip--on:hover { background: #f0e6d2; }
|
|
141
|
+
#${PANEL_ID} .perf-tools { display: flex; align-items: center; gap: 10px; flex: 0 0 auto; margin-bottom: 14px; }
|
|
142
|
+
#${PANEL_ID} .perf-search { flex: 1 1 auto; min-width: 0; box-sizing: border-box;
|
|
143
|
+
font-family: "LoL Body", Arial, "Helvetica Neue", Helvetica, sans-serif; font-size: 12px; letter-spacing: 0.3px;
|
|
144
|
+
color: #f0e6d2; background: #0a1428; border: 1px solid #785a28; padding: 9px 12px; outline: none; }
|
|
145
|
+
#${PANEL_ID} .perf-search:focus { border-color: #c89b3c; }
|
|
146
|
+
#${PANEL_ID} .perf-search::placeholder { color: #a09b8c; }
|
|
147
|
+
#${PANEL_ID} .perf-master { flex: 0 0 auto; white-space: nowrap; }
|
|
148
|
+
#${PANEL_ID} .perf-card--locked .perf-opts { pointer-events: none; }
|
|
149
|
+
#${PANEL_ID} .perf-card--locked .perf-actions > .perf-toggle { pointer-events: none; }
|
|
150
|
+
#${PANEL_ID} .perf-card--locked .perf-caret { opacity: 0.45; }
|
|
151
|
+
#${PANEL_ID} .perf-card--locked .perf-chips-label { opacity: 0.45; }
|
|
152
|
+
#${PANEL_ID} .perf-chip:disabled { opacity: 0.45; cursor: default; }
|
|
153
|
+
#${PANEL_ID} .perf-chip:disabled:hover { color: #a09b8c; border-color: #785a28; background: #0a1428; }
|
|
154
|
+
#${PANEL_ID} .perf-chip--on:disabled:hover { color: #010a13; border-color: #c89b3c; background: #c89b3c; }
|
|
155
|
+
`;
|
|
156
|
+
|
|
157
|
+
const registry = new Map();
|
|
158
|
+
const watchers = new Map();
|
|
159
|
+
const requestCache = new Map();
|
|
160
|
+
let socket = null;
|
|
161
|
+
let observer = null;
|
|
162
|
+
let scanScheduled = false;
|
|
163
|
+
|
|
164
|
+
let coords = null;
|
|
165
|
+
const resolved = new Map();
|
|
166
|
+
let pendingReload = false;
|
|
167
|
+
let modulesLoaded = false;
|
|
168
|
+
let searchQuery = "";
|
|
169
|
+
let runningVersion = version;
|
|
170
|
+
let coreStarted = false;
|
|
171
|
+
|
|
172
|
+
let state = { installed: {}, enabled: {}, options: {}, masterLocked: false };
|
|
173
|
+
const unloadPending = new Set();
|
|
174
|
+
|
|
175
|
+
let saveBlocked = false;
|
|
176
|
+
|
|
177
|
+
const SAVED_STATE_TRIES = 3;
|
|
178
|
+
const SAVED_STATE_RETRY_MS = 250;
|
|
179
|
+
|
|
180
|
+
function isObject(value) {
|
|
181
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function looksLikeState(saved) {
|
|
185
|
+
return isObject(saved) && (isObject(saved.installed) || isObject(saved.enabled) || isObject(saved.options));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function readSavedState() {
|
|
189
|
+
let saved = null;
|
|
190
|
+
let failed = false;
|
|
191
|
+
for (let attempt = 0; attempt < SAVED_STATE_TRIES; attempt += 1) {
|
|
192
|
+
failed = false;
|
|
193
|
+
try {
|
|
194
|
+
if (typeof DataStore !== "undefined") saved = await DataStore.get(STATE_KEY);
|
|
195
|
+
} catch {
|
|
196
|
+
failed = true;
|
|
197
|
+
}
|
|
198
|
+
if (looksLikeState(saved) || !failed) break;
|
|
199
|
+
await new Promise((resolve) => setTimeout(resolve, SAVED_STATE_RETRY_MS));
|
|
200
|
+
}
|
|
201
|
+
if (!looksLikeState(saved)) {
|
|
202
|
+
try {
|
|
203
|
+
saved = JSON.parse(localStorage.getItem(STATE_KEY) || "null");
|
|
204
|
+
} catch {
|
|
205
|
+
saved = null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return { saved: looksLikeState(saved) ? saved : null, failed };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function normalizeState(saved) {
|
|
212
|
+
const next = {
|
|
213
|
+
installed: isObject(saved?.installed) ? { ...saved.installed } : {},
|
|
214
|
+
enabled: isObject(saved?.enabled) ? { ...saved.enabled } : {},
|
|
215
|
+
options: isObject(saved?.options) ? { ...saved.options } : {},
|
|
216
|
+
masterLocked: saved?.masterLocked === true,
|
|
217
|
+
};
|
|
218
|
+
for (const mod of BUNDLED_MODULES) {
|
|
219
|
+
if (mod.core) continue;
|
|
220
|
+
if (next.installed[mod.id] === undefined && (next.enabled[mod.id] === true || isObject(next.options[mod.id]))) {
|
|
221
|
+
next.installed[mod.id] = true;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return next;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function loadState() {
|
|
228
|
+
const { saved, failed } = await readSavedState();
|
|
229
|
+
state = normalizeState(saved);
|
|
230
|
+
if (!saved && failed) saveBlocked = true;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function saveState() {
|
|
234
|
+
if (saveBlocked) return;
|
|
235
|
+
const record = {
|
|
236
|
+
version: /^\d/.test(runningVersion) ? runningVersion : undefined,
|
|
237
|
+
installed: state.installed,
|
|
238
|
+
enabled: state.enabled,
|
|
239
|
+
options: state.options,
|
|
240
|
+
masterLocked: state.masterLocked === true,
|
|
241
|
+
};
|
|
242
|
+
try {
|
|
243
|
+
localStorage.setItem(STATE_KEY, JSON.stringify(record));
|
|
244
|
+
} catch (error) {
|
|
245
|
+
logError("Could not save plugin state.", error);
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
if (typeof DataStore !== "undefined") DataStore.set(STATE_KEY, record);
|
|
249
|
+
} catch (error) {
|
|
250
|
+
logError("Could not save plugin state.", error);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function logError(scope, error) {
|
|
255
|
+
console.error(`[Perfectionism] ${scope}`, error);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function moduleById(id) {
|
|
259
|
+
return BUNDLED_MODULES.find((m) => m.id === id) ?? null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function isPluginEnabled(id) {
|
|
263
|
+
if (!registry.has(id) && !moduleById(id)) return false;
|
|
264
|
+
if (state.masterLocked === true) return false;
|
|
265
|
+
return state.enabled[id] === true;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function trackUnload(id) {
|
|
269
|
+
if (isPluginEnabled(id)) unloadPending.delete(id);
|
|
270
|
+
else unloadPending.add(id);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function needsReload() {
|
|
274
|
+
return pendingReload || unloadPending.size > 0;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function applyLifecycle(entry, enabled) {
|
|
278
|
+
if (enabled && !entry.active) {
|
|
279
|
+
entry.active = true;
|
|
280
|
+
try {
|
|
281
|
+
entry.onEnable?.(utils);
|
|
282
|
+
} catch (error) {
|
|
283
|
+
entry.active = false;
|
|
284
|
+
logError(`${entry.name} failed to enable.`, error);
|
|
285
|
+
}
|
|
286
|
+
} else if (!enabled && entry.active) {
|
|
287
|
+
entry.active = false;
|
|
288
|
+
try {
|
|
289
|
+
entry.onDisable?.(utils);
|
|
290
|
+
} catch (error) {
|
|
291
|
+
logError(`${entry.name} failed to disable cleanly.`, error);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function registerPlugin(definition) {
|
|
297
|
+
const { id, name, description = "", version = "1.0.0", options = [], onEnable, onDisable, onOptionChange } = definition ?? {};
|
|
298
|
+
if (!id || !name) {
|
|
299
|
+
throw new Error("[Perfectionism] registerPlugin needs an id and a name.");
|
|
300
|
+
}
|
|
301
|
+
if (registry.has(id)) {
|
|
302
|
+
console.warn(`[Perfectionism] ${name} is already registered.`);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const entry = { id, name, description, version, options, onEnable, onDisable, onOptionChange, active: false };
|
|
306
|
+
registry.set(id, entry);
|
|
307
|
+
applyLifecycle(entry, isPluginEnabled(id));
|
|
308
|
+
refreshPanel();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function unregisterPlugin(id) {
|
|
312
|
+
const entry = registry.get(id);
|
|
313
|
+
if (!entry) return;
|
|
314
|
+
applyLifecycle(entry, false);
|
|
315
|
+
registry.delete(id);
|
|
316
|
+
refreshPanel();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function setPluginEnabled(id, enabled) {
|
|
320
|
+
const entry = registry.get(id);
|
|
321
|
+
state.enabled[id] = Boolean(enabled);
|
|
322
|
+
saveState();
|
|
323
|
+
if (entry) {
|
|
324
|
+
applyLifecycle(entry, isPluginEnabled(id));
|
|
325
|
+
trackUnload(id);
|
|
326
|
+
refreshPanel();
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
const mod = moduleById(id);
|
|
330
|
+
if (enabled && state.masterLocked !== true && mod && !mod.core && isInstalled(mod)) {
|
|
331
|
+
loadModule(mod).then(refreshPanel);
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
refreshPanel();
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function findOption(entry, key) {
|
|
339
|
+
for (const option of entry?.options ?? []) {
|
|
340
|
+
if (option.key === key) return option;
|
|
341
|
+
for (const child of option.children ?? []) {
|
|
342
|
+
if (child.key === key) return child;
|
|
343
|
+
}
|
|
344
|
+
for (const item of option.items ?? []) {
|
|
345
|
+
if (`${option.key}.${item.id}` === key) return item;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function getOption(id, key, fallback = false) {
|
|
352
|
+
const entry = registry.get(id);
|
|
353
|
+
const fallbackValue = findOption(entry, key)?.default ?? fallback;
|
|
354
|
+
const stored = state.options?.[id]?.[key];
|
|
355
|
+
return stored === undefined ? Boolean(fallbackValue) : Boolean(stored);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function setOption(id, key, value, refresh = true) {
|
|
359
|
+
if (!state.options[id]) state.options[id] = {};
|
|
360
|
+
state.options[id][key] = Boolean(value);
|
|
361
|
+
saveState();
|
|
362
|
+
const entry = registry.get(id);
|
|
363
|
+
try {
|
|
364
|
+
entry?.onOptionChange?.(key, Boolean(value));
|
|
365
|
+
} catch (error) {
|
|
366
|
+
logError(`${entry?.name ?? id} could not apply the option change.`, error);
|
|
367
|
+
}
|
|
368
|
+
if (refresh) refreshPanel();
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function getPlugins() {
|
|
372
|
+
return [...registry.values()]
|
|
373
|
+
.map(({ id, name, description, version }) => ({ id, name, description, version, enabled: isPluginEnabled(id) }))
|
|
374
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function getAvailableModules() {
|
|
378
|
+
return BUNDLED_MODULES.filter((m) => !m.core);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function isInstalled(mod) {
|
|
382
|
+
const installed = state.installed[mod.id];
|
|
383
|
+
return installed === undefined ? registry.has(mod.id) : Boolean(installed);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function moduleUrl(file) {
|
|
387
|
+
return `${CDN}/${PKG}@${runningVersion}/${file}`;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function importModule(url) {
|
|
391
|
+
return coords?.importModule ? coords.importModule(url) : import(url);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async function installModule(mod) {
|
|
395
|
+
state.installed[mod.id] = true;
|
|
396
|
+
state.enabled[mod.id] = false;
|
|
397
|
+
saveState();
|
|
398
|
+
pendingReload = true;
|
|
399
|
+
refreshPanel();
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async function uninstallModule(mod) {
|
|
403
|
+
if (registry.has(mod.id)) unloadPending.add(mod.id);
|
|
404
|
+
unregisterPlugin(mod.id);
|
|
405
|
+
state.enabled[mod.id] = false;
|
|
406
|
+
state.installed[mod.id] = false;
|
|
407
|
+
saveState();
|
|
408
|
+
refreshPanel();
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function reloadClientNow() {
|
|
412
|
+
if (typeof window.reloadClient === "function") window.reloadClient();
|
|
413
|
+
else window.location.reload();
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async function loadModule(mod) {
|
|
417
|
+
if (resolved.has(mod.id) || registry.has(mod.id)) return;
|
|
418
|
+
try {
|
|
419
|
+
const plugin = await importModule(moduleUrl(mod.file));
|
|
420
|
+
resolved.set(mod.id, plugin);
|
|
421
|
+
plugin.init?.();
|
|
422
|
+
} catch (error) {
|
|
423
|
+
logError(`could not load ${mod.name}.`, error);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async function loadEnabledModules() {
|
|
428
|
+
for (const mod of BUNDLED_MODULES) {
|
|
429
|
+
if (mod.core || !isInstalled(mod) || !isPluginEnabled(mod.id)) continue;
|
|
430
|
+
await loadModule(mod);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async function loadModules() {
|
|
435
|
+
if (modulesLoaded) return;
|
|
436
|
+
modulesLoaded = true;
|
|
437
|
+
await loadEnabledModules();
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function scheduleScan() {
|
|
441
|
+
if (scanScheduled) return;
|
|
442
|
+
scanScheduled = true;
|
|
443
|
+
requestAnimationFrame(() => {
|
|
444
|
+
scanScheduled = false;
|
|
445
|
+
runScan();
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function runScan() {
|
|
450
|
+
for (const [selector, entries] of watchers) {
|
|
451
|
+
let nodes;
|
|
452
|
+
try {
|
|
453
|
+
nodes = document.querySelectorAll(selector);
|
|
454
|
+
} catch (error) {
|
|
455
|
+
logError(`Invalid selector "${selector}".`, error);
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
for (const node of nodes) {
|
|
459
|
+
for (const entry of entries) {
|
|
460
|
+
if (entry.seen.has(node)) continue;
|
|
461
|
+
entry.seen.add(node);
|
|
462
|
+
try {
|
|
463
|
+
entry.callback(node);
|
|
464
|
+
} catch (error) {
|
|
465
|
+
logError(`Watcher for "${selector}" failed.`, error);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function ensureObserver() {
|
|
473
|
+
if (observer) return;
|
|
474
|
+
observer = new MutationObserver(scheduleScan);
|
|
475
|
+
observer.observe(document.documentElement, { childList: true, subtree: true });
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function watch(selector, callback) {
|
|
479
|
+
const entry = { callback, seen: new WeakSet() };
|
|
480
|
+
let entries = watchers.get(selector);
|
|
481
|
+
if (!entries) {
|
|
482
|
+
entries = new Set();
|
|
483
|
+
watchers.set(selector, entries);
|
|
484
|
+
}
|
|
485
|
+
entries.add(entry);
|
|
486
|
+
ensureObserver();
|
|
487
|
+
scheduleScan();
|
|
488
|
+
return () => {
|
|
489
|
+
entries.delete(entry);
|
|
490
|
+
if (entries.size === 0 && watchers.get(selector) === entries) watchers.delete(selector);
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function waitFor(selector, timeout = 15000) {
|
|
495
|
+
return new Promise((resolve, reject) => {
|
|
496
|
+
let unwatch = null;
|
|
497
|
+
const timer = setTimeout(() => {
|
|
498
|
+
unwatch?.();
|
|
499
|
+
reject(new Error(`Timed out waiting for ${selector}`));
|
|
500
|
+
}, timeout);
|
|
501
|
+
unwatch = watch(selector, (element) => {
|
|
502
|
+
clearTimeout(timer);
|
|
503
|
+
unwatch();
|
|
504
|
+
resolve(element);
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function addStyle(name, css) {
|
|
510
|
+
let element = document.getElementById(name);
|
|
511
|
+
if (!element) {
|
|
512
|
+
element = document.createElement("style");
|
|
513
|
+
element.id = name;
|
|
514
|
+
(document.head ?? document.documentElement).appendChild(element);
|
|
515
|
+
}
|
|
516
|
+
element.textContent = css;
|
|
517
|
+
return () => element.remove();
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const LCU_TIMEOUT_MS = 10000;
|
|
521
|
+
|
|
522
|
+
function lcuGet(path, { ttl = 0 } = {}) {
|
|
523
|
+
const now = Date.now();
|
|
524
|
+
const cached = requestCache.get(path);
|
|
525
|
+
if (cached && (cached.pending || cached.expires > now)) return cached.promise;
|
|
526
|
+
|
|
527
|
+
const entry = { pending: true, expires: 0, promise: null };
|
|
528
|
+
const request = fetch(path, { headers: { Accept: "application/json" } }).then(async (response) => {
|
|
529
|
+
if (!response.ok) throw new Error(`LCU ${path} responded ${response.status}`);
|
|
530
|
+
return response.status === 204 ? null : response.json();
|
|
531
|
+
});
|
|
532
|
+
let timer = null;
|
|
533
|
+
const timeout = new Promise((_, reject) => {
|
|
534
|
+
timer = setTimeout(() => reject(new Error(`LCU ${path} timed out`)), LCU_TIMEOUT_MS);
|
|
535
|
+
});
|
|
536
|
+
entry.promise = Promise.race([request, timeout]).finally(() => clearTimeout(timer));
|
|
537
|
+
requestCache.set(path, entry);
|
|
538
|
+
entry.promise.then(
|
|
539
|
+
() => {
|
|
540
|
+
entry.pending = false;
|
|
541
|
+
entry.expires = Date.now() + ttl;
|
|
542
|
+
},
|
|
543
|
+
() => {
|
|
544
|
+
requestCache.delete(path);
|
|
545
|
+
},
|
|
546
|
+
);
|
|
547
|
+
return entry.promise;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
async function lcuSend(method, path, body) {
|
|
551
|
+
const response = await fetch(path, {
|
|
552
|
+
method,
|
|
553
|
+
headers: { Accept: "application/json", "Content-Type": "application/json" },
|
|
554
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
555
|
+
});
|
|
556
|
+
if (!response.ok) throw new Error(`LCU ${method} ${path} responded ${response.status}`);
|
|
557
|
+
return response.status === 204 ? null : response.json();
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function subscribe(uri, listener) {
|
|
561
|
+
if (!socket?.observe) return null;
|
|
562
|
+
const handle = socket.observe(uri, listener);
|
|
563
|
+
return () => handle?.disconnect?.();
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function whenRcpReady(name, timeout = 15000) {
|
|
567
|
+
return new Promise((resolve, reject) => {
|
|
568
|
+
const timer = setTimeout(() => reject(new Error(`Timed out waiting for ${name}`)), timeout);
|
|
569
|
+
const attempt = () => {
|
|
570
|
+
if (window.rcp?.whenReady) {
|
|
571
|
+
window.rcp.whenReady(name).then((api) => {
|
|
572
|
+
clearTimeout(timer);
|
|
573
|
+
resolve(api);
|
|
574
|
+
});
|
|
575
|
+
} else {
|
|
576
|
+
setTimeout(attempt, 250);
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
attempt();
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function el(tag, className, text) {
|
|
584
|
+
const node = document.createElement(tag);
|
|
585
|
+
if (className) node.className = className;
|
|
586
|
+
if (text !== undefined) node.textContent = text;
|
|
587
|
+
return node;
|
|
588
|
+
}
|
|
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, locked) {
|
|
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
|
+
chip.disabled = Boolean(locked);
|
|
619
|
+
if (getOption(mod.id, key)) chip.classList.add("perf-chip--on");
|
|
620
|
+
chip.addEventListener("click", () => {
|
|
621
|
+
setOption(mod.id, key, !getOption(mod.id, key), false);
|
|
622
|
+
chip.classList.toggle("perf-chip--on", getOption(mod.id, key));
|
|
623
|
+
});
|
|
624
|
+
row.append(chip);
|
|
625
|
+
}
|
|
626
|
+
block.append(row);
|
|
627
|
+
return block;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function buildOptions(mod, locked) {
|
|
631
|
+
const options = registry.get(mod.id)?.options ?? [];
|
|
632
|
+
if (options.length === 0) return null;
|
|
633
|
+
const wrap = el("div", "perf-opts");
|
|
634
|
+
for (const option of options) {
|
|
635
|
+
if (option.type === "chips") {
|
|
636
|
+
wrap.append(buildChips(mod, option, locked));
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
if (!option.children?.length) {
|
|
640
|
+
wrap.append(buildOptionRow(mod, option, Boolean(locked)));
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
const group = el("div", "perf-opt-group");
|
|
644
|
+
const head = el("div", "perf-opt-head");
|
|
645
|
+
const caret = el("span", "perf-caret");
|
|
646
|
+
caret.setAttribute("role", "button");
|
|
647
|
+
caret.setAttribute("aria-label", `Show ${mod.name} options`);
|
|
648
|
+
const kids = el("div", "perf-opt-children");
|
|
649
|
+
const childInputs = [];
|
|
650
|
+
for (const child of option.children) {
|
|
651
|
+
const childRow = buildOptionRow(mod, child, !getOption(mod.id, option.key) || Boolean(locked));
|
|
652
|
+
childInputs.push(childRow.querySelector("input"));
|
|
653
|
+
kids.append(childRow);
|
|
654
|
+
}
|
|
655
|
+
const master = buildOptionRow(mod, option, Boolean(locked), (checked) => {
|
|
656
|
+
for (const input of childInputs) input.disabled = !checked || Boolean(locked);
|
|
657
|
+
setOption(mod.id, option.key, checked, false);
|
|
658
|
+
});
|
|
659
|
+
const groupKey = `${mod.id}:${option.key}`;
|
|
660
|
+
const setExpanded = (open) => {
|
|
661
|
+
optionGroupState.set(groupKey, open);
|
|
662
|
+
kids.style.display = open ? "" : "none";
|
|
663
|
+
caret.classList.toggle("perf-caret--open", open);
|
|
664
|
+
};
|
|
665
|
+
setExpanded(optionGroupState.get(groupKey) ?? false);
|
|
666
|
+
caret.addEventListener("click", (event) => {
|
|
667
|
+
event.preventDefault();
|
|
668
|
+
event.stopPropagation();
|
|
669
|
+
setExpanded(!(optionGroupState.get(groupKey) ?? false));
|
|
670
|
+
});
|
|
671
|
+
head.append(master, caret);
|
|
672
|
+
group.append(head, kids);
|
|
673
|
+
wrap.append(group);
|
|
674
|
+
}
|
|
675
|
+
return wrap;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function buildToggle(mod, locked) {
|
|
679
|
+
const label = el("label", "perf-toggle");
|
|
680
|
+
label.title = `Enable ${mod.name}`;
|
|
681
|
+
const input = el("input");
|
|
682
|
+
input.type = "checkbox";
|
|
683
|
+
input.checked = isPluginEnabled(mod.id);
|
|
684
|
+
input.disabled = Boolean(locked);
|
|
685
|
+
input.setAttribute("aria-label", `Enable ${mod.name}`);
|
|
686
|
+
input.addEventListener("change", () => setPluginEnabled(mod.id, input.checked));
|
|
687
|
+
label.append(input, el("span", "perf-box"));
|
|
688
|
+
return label;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function buildInstallButton(mod) {
|
|
692
|
+
const button = el("button", "perf-btn perf-install", "Install");
|
|
693
|
+
button.title = `Install ${mod.name}`;
|
|
694
|
+
button.addEventListener("click", async () => {
|
|
695
|
+
button.disabled = true;
|
|
696
|
+
button.textContent = "Installing";
|
|
697
|
+
try {
|
|
698
|
+
await installModule(mod);
|
|
699
|
+
} catch (error) {
|
|
700
|
+
logError(`could not install ${mod.name}.`, error);
|
|
701
|
+
button.disabled = false;
|
|
702
|
+
button.textContent = "Install";
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
return button;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function buildUninstallButton(mod) {
|
|
709
|
+
const button = el("button", "perf-btn perf-uninstall");
|
|
710
|
+
button.title = `Uninstall ${mod.name}`;
|
|
711
|
+
button.setAttribute("aria-label", `Uninstall ${mod.name}`);
|
|
712
|
+
button.addEventListener("click", () => uninstallModule(mod).catch((error) => logError(`could not uninstall ${mod.name}.`, error)));
|
|
713
|
+
return button;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function installedModules() {
|
|
717
|
+
return getAvailableModules().filter(isInstalled);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function masterLocked() {
|
|
721
|
+
return installedModules().length > 0 && state.masterLocked === true;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function disableAllPlugins() {
|
|
725
|
+
state.masterLocked = true;
|
|
726
|
+
saveState();
|
|
727
|
+
for (const entry of registry.values()) {
|
|
728
|
+
applyLifecycle(entry, false);
|
|
729
|
+
trackUnload(entry.id);
|
|
730
|
+
}
|
|
731
|
+
refreshPanel();
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function enableAllPlugins() {
|
|
735
|
+
state.masterLocked = false;
|
|
736
|
+
saveState();
|
|
737
|
+
for (const entry of registry.values()) {
|
|
738
|
+
applyLifecycle(entry, isPluginEnabled(entry.id));
|
|
739
|
+
trackUnload(entry.id);
|
|
740
|
+
}
|
|
741
|
+
loadEnabledModules().then(refreshPanel);
|
|
742
|
+
refreshPanel();
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function applySearchFilter(panel = document.getElementById(PANEL_ID)) {
|
|
746
|
+
if (!panel) return;
|
|
747
|
+
let shown = 0;
|
|
748
|
+
for (const card of panel.querySelectorAll(".perf-card")) {
|
|
749
|
+
const match = !searchQuery || (card.dataset.search ?? "").includes(searchQuery);
|
|
750
|
+
card.style.display = match ? "" : "none";
|
|
751
|
+
if (match) shown += 1;
|
|
752
|
+
}
|
|
753
|
+
const scroller = panel.querySelector("lol-uikit-scrollable");
|
|
754
|
+
let empty = panel.querySelector(".perf-no-match");
|
|
755
|
+
if (shown === 0 && getAvailableModules().length > 0) {
|
|
756
|
+
if (!empty && scroller) {
|
|
757
|
+
empty = el("div", "perf-empty perf-no-match", "No plugins match your search.");
|
|
758
|
+
scroller.append(empty);
|
|
759
|
+
}
|
|
760
|
+
} else if (empty) {
|
|
761
|
+
empty.remove();
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function buildTools() {
|
|
766
|
+
const tools = el("div", "perf-tools");
|
|
767
|
+
|
|
768
|
+
const search = el("input", "perf-search");
|
|
769
|
+
search.type = "search";
|
|
770
|
+
search.placeholder = "Search plugins...";
|
|
771
|
+
search.value = searchQuery;
|
|
772
|
+
search.setAttribute("aria-label", "Search plugins");
|
|
773
|
+
search.addEventListener("input", () => {
|
|
774
|
+
searchQuery = search.value.trim().toLowerCase();
|
|
775
|
+
applySearchFilter();
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
const master = el("button", "perf-btn perf-master", masterLocked() ? "Enable All" : "Disable All");
|
|
779
|
+
master.type = "button";
|
|
780
|
+
master.disabled = installedModules().length === 0;
|
|
781
|
+
master.addEventListener("click", () => {
|
|
782
|
+
if (masterLocked()) enableAllPlugins();
|
|
783
|
+
else disableAllPlugins();
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
tools.append(search, master);
|
|
787
|
+
return tools;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function buildCard(mod, locked) {
|
|
791
|
+
const installed = isInstalled(mod);
|
|
792
|
+
const card = el("div", installed ? "perf-card" : "perf-card perf-card--missing");
|
|
793
|
+
if (locked) card.classList.add("perf-card--locked");
|
|
794
|
+
card.dataset.search = `${mod.name} ${mod.description}`.toLowerCase();
|
|
795
|
+
|
|
796
|
+
const info = el("div", "perf-info");
|
|
797
|
+
const name = el("div", "perf-name", mod.name);
|
|
798
|
+
name.append(el("span", "perf-version", `v${registry.get(mod.id)?.version ?? mod.version}`));
|
|
799
|
+
info.append(name);
|
|
800
|
+
if (mod.description) info.append(el("div", "perf-desc", mod.description));
|
|
801
|
+
const optionList = buildOptions(mod, locked);
|
|
802
|
+
if (optionList) info.append(optionList);
|
|
803
|
+
|
|
804
|
+
const actions = el("div", "perf-actions");
|
|
805
|
+
if (installed) actions.append(buildToggle(mod, locked));
|
|
806
|
+
actions.append(installed ? buildUninstallButton(mod) : buildInstallButton(mod));
|
|
807
|
+
|
|
808
|
+
card.append(info, actions);
|
|
809
|
+
return card;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function buildPanel() {
|
|
813
|
+
const panel = el("div");
|
|
814
|
+
panel.id = PANEL_ID;
|
|
815
|
+
panel.append(buildTools());
|
|
816
|
+
|
|
817
|
+
const scroller = el("lol-uikit-scrollable");
|
|
818
|
+
panel.append(scroller);
|
|
819
|
+
|
|
820
|
+
const modules = getAvailableModules();
|
|
821
|
+
if (modules.length === 0) {
|
|
822
|
+
scroller.append(el("div", "perf-empty", "No plugins are available."));
|
|
823
|
+
return panel;
|
|
824
|
+
}
|
|
825
|
+
const locked = masterLocked();
|
|
826
|
+
for (const mod of modules) scroller.append(buildCard(mod, locked));
|
|
827
|
+
applySearchFilter(panel);
|
|
828
|
+
return panel;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function panelScrollTop(panel) {
|
|
832
|
+
return panel?.querySelector("lol-uikit-scrollable")?.scrollTop ?? 0;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function applyScrollTop(panel, top) {
|
|
836
|
+
if (!top || !panel) return;
|
|
837
|
+
const host = panel.querySelector("lol-uikit-scrollable");
|
|
838
|
+
if (host) host.scrollTop = top;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function refreshPanel() {
|
|
842
|
+
const current = document.getElementById(PANEL_ID);
|
|
843
|
+
if (!current) return;
|
|
844
|
+
const top = panelScrollTop(current);
|
|
845
|
+
const next = buildPanel();
|
|
846
|
+
current.replaceWith(next);
|
|
847
|
+
if (!top) return;
|
|
848
|
+
applyScrollTop(next, top);
|
|
849
|
+
requestAnimationFrame(() => applyScrollTop(next, top));
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function setNavActive(activeNav) {
|
|
853
|
+
for (const nav of document.querySelectorAll(".lol-settings-nav")) {
|
|
854
|
+
if (nav === activeNav) {
|
|
855
|
+
nav.setAttribute("active", "true");
|
|
856
|
+
} else if (nav.hasAttribute("active")) {
|
|
857
|
+
nav.removeAttribute("active");
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
let panelRequested = false;
|
|
863
|
+
let syncTimer = null;
|
|
864
|
+
|
|
865
|
+
function startSync() {
|
|
866
|
+
if (syncTimer === null) syncTimer = setInterval(keepPanelInSync, 300);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function stopSync() {
|
|
870
|
+
if (syncTimer === null) return;
|
|
871
|
+
clearInterval(syncTimer);
|
|
872
|
+
syncTimer = null;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function openPanel() {
|
|
876
|
+
panelRequested = true;
|
|
877
|
+
startSync();
|
|
878
|
+
const content = document.querySelector(".lol-settings-content");
|
|
879
|
+
if (!content) return;
|
|
880
|
+
|
|
881
|
+
const options = content.querySelector(".lol-settings-options");
|
|
882
|
+
if (options) options.style.display = "none";
|
|
883
|
+
|
|
884
|
+
const existing = document.getElementById(PANEL_ID);
|
|
885
|
+
if (existing) existing.remove();
|
|
886
|
+
content.append(buildPanel());
|
|
887
|
+
|
|
888
|
+
setNavActive(document.getElementById(NAV_ID));
|
|
889
|
+
const currentTitle = document.querySelector(".lol-settings-title-current");
|
|
890
|
+
if (currentTitle) currentTitle.textContent = TAB_TITLE.toUpperCase();
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function closePanel() {
|
|
894
|
+
panelRequested = false;
|
|
895
|
+
const panel = document.getElementById(PANEL_ID);
|
|
896
|
+
if (!panel) return;
|
|
897
|
+
panel.remove();
|
|
898
|
+
const options = document.querySelector(".lol-settings-content .lol-settings-options");
|
|
899
|
+
if (options) options.style.display = "";
|
|
900
|
+
setNavActive(null);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function keepPanelInSync() {
|
|
904
|
+
const content = document.querySelector(".lol-settings-content");
|
|
905
|
+
if (!content) {
|
|
906
|
+
panelRequested = false;
|
|
907
|
+
stopSync();
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
if (!panelRequested) {
|
|
911
|
+
const staleNav = document.getElementById(NAV_ID);
|
|
912
|
+
if (staleNav && staleNav.hasAttribute("active")) staleNav.removeAttribute("active");
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
const panel = document.getElementById(PANEL_ID);
|
|
917
|
+
if (!panel || panel.parentElement !== content) {
|
|
918
|
+
openPanel();
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
const options = content.querySelector(".lol-settings-options");
|
|
923
|
+
if (options && options.style.display !== "none") options.style.display = "none";
|
|
924
|
+
|
|
925
|
+
const nav = document.getElementById(NAV_ID);
|
|
926
|
+
if (nav) {
|
|
927
|
+
for (const other of document.querySelectorAll(".lol-settings-nav")) {
|
|
928
|
+
if (other !== nav && other.hasAttribute("active")) other.removeAttribute("active");
|
|
929
|
+
}
|
|
930
|
+
if (nav.getAttribute("active") !== "true") nav.setAttribute("active", "true");
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
const title = document.querySelector(".lol-settings-title-current");
|
|
934
|
+
if (title && title.textContent !== TAB_TITLE.toUpperCase()) title.textContent = TAB_TITLE.toUpperCase();
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function ensureSettingsTab(titleNode) {
|
|
938
|
+
startSync();
|
|
939
|
+
if (document.getElementById(NAV_ID)) return;
|
|
940
|
+
|
|
941
|
+
const scope = titleNode.closest("lol-uikit-scrollable") ?? titleNode.parentElement;
|
|
942
|
+
const firstTitle = scope?.querySelector(".lol-settings-nav-title");
|
|
943
|
+
if (!firstTitle) return;
|
|
944
|
+
|
|
945
|
+
const groupTitle = el("div", "lol-settings-nav-title", "Plugins");
|
|
946
|
+
groupTitle.dataset.perfectionist = "title";
|
|
947
|
+
|
|
948
|
+
const item = document.createElement("lol-uikit-navigation-item");
|
|
949
|
+
item.id = NAV_ID;
|
|
950
|
+
item.className = "lol-settings-nav";
|
|
951
|
+
item.setAttribute("name", "perfectionist-plugins");
|
|
952
|
+
item.append(el("div", null, TAB_TITLE.toUpperCase()));
|
|
953
|
+
|
|
954
|
+
const bar = document.createElement("lol-uikit-navigation-bar");
|
|
955
|
+
bar.setAttribute("direction", "down");
|
|
956
|
+
bar.setAttribute("type", "tabbed");
|
|
957
|
+
bar.dataset.perfectionist = "bar";
|
|
958
|
+
bar.append(item);
|
|
959
|
+
|
|
960
|
+
firstTitle.before(groupTitle, bar);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function isDoneButton(target) {
|
|
964
|
+
const button = target.closest("button, lol-uikit-flat-button, [role=button]");
|
|
965
|
+
return !!button && /^\s*done\s*$/i.test(button.textContent ?? "");
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function installSettingsTab() {
|
|
969
|
+
addStyle(STYLE_ID, STYLE_CSS);
|
|
970
|
+
|
|
971
|
+
document.addEventListener(
|
|
972
|
+
"click",
|
|
973
|
+
(event) => {
|
|
974
|
+
const target = event.target;
|
|
975
|
+
if (!(target instanceof Element)) return;
|
|
976
|
+
if (needsReload() && isDoneButton(target)) {
|
|
977
|
+
pendingReload = false;
|
|
978
|
+
unloadPending.clear();
|
|
979
|
+
reloadClientNow();
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
if (target.closest(`#${NAV_ID}`)) {
|
|
983
|
+
event.stopPropagation();
|
|
984
|
+
openPanel();
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
const nativeNav = target.closest(".lol-settings-nav");
|
|
988
|
+
if (nativeNav && document.getElementById(PANEL_ID)) {
|
|
989
|
+
closePanel();
|
|
990
|
+
const label = nativeNav.textContent.trim();
|
|
991
|
+
setTimeout(() => {
|
|
992
|
+
const title = document.querySelector(".lol-settings-title-current");
|
|
993
|
+
if (title && label && !document.getElementById(PANEL_ID)) title.textContent = label;
|
|
994
|
+
}, 0);
|
|
995
|
+
}
|
|
996
|
+
},
|
|
997
|
+
true,
|
|
998
|
+
);
|
|
999
|
+
|
|
1000
|
+
watch(".lol-settings-nav-title", ensureSettingsTab);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
const utils = {
|
|
1004
|
+
version,
|
|
1005
|
+
registerPlugin,
|
|
1006
|
+
unregisterPlugin,
|
|
1007
|
+
setPluginEnabled,
|
|
1008
|
+
isPluginEnabled,
|
|
1009
|
+
getPlugins,
|
|
1010
|
+
getOption,
|
|
1011
|
+
setOption,
|
|
1012
|
+
getAvailableModules,
|
|
1013
|
+
installModule,
|
|
1014
|
+
uninstallModule,
|
|
1015
|
+
watch,
|
|
1016
|
+
waitFor,
|
|
1017
|
+
addStyle,
|
|
1018
|
+
lcu: { get: lcuGet, send: lcuSend },
|
|
1019
|
+
subscribe,
|
|
1020
|
+
whenRcpReady,
|
|
1021
|
+
};
|
|
1022
|
+
|
|
1023
|
+
export async function init(context, coordsProvided) {
|
|
1024
|
+
if (coreStarted || window.PerfectionistUtils) return;
|
|
1025
|
+
coreStarted = true;
|
|
1026
|
+
coords = coordsProvided ?? null;
|
|
1027
|
+
runningVersion = coords?.version || version;
|
|
1028
|
+
|
|
1029
|
+
await loadState();
|
|
1030
|
+
saveState();
|
|
1031
|
+
|
|
1032
|
+
socket = context?.socket ?? null;
|
|
1033
|
+
window.PerfectionistUtils = utils;
|
|
1034
|
+
installSettingsTab();
|
|
1035
|
+
window.dispatchEvent(new CustomEvent(READY_EVENT, { detail: utils }));
|
|
1036
|
+
|
|
1037
|
+
try {
|
|
1038
|
+
await loadModules();
|
|
1039
|
+
} catch (error) {
|
|
1040
|
+
logError("startup failed.", error);
|
|
1041
|
+
}
|
|
945
1042
|
}
|