perfectionist-plugins 1.0.0 → 1.0.1
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/ban-blocker.js +552 -617
- package/hide-unowned-skins.js +578 -640
- package/package.json +1 -1
- package/perfectionist-utils.js +754 -819
package/perfectionist-utils.js
CHANGED
|
@@ -1,819 +1,754 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
{
|
|
26
|
-
id: "
|
|
27
|
-
file:
|
|
28
|
-
name: "
|
|
29
|
-
version,
|
|
30
|
-
description: "
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
#${PANEL_ID}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
#${PANEL_ID} .perf-
|
|
56
|
-
#${PANEL_ID} .perf-
|
|
57
|
-
#${PANEL_ID} .perf-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
#${PANEL_ID} .perf-
|
|
63
|
-
#${PANEL_ID} .perf-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
#${PANEL_ID} .perf-
|
|
71
|
-
|
|
72
|
-
#${PANEL_ID} .perf-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
.perf-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
.perf-update {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
let
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
let
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
return
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function
|
|
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
|
-
const
|
|
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
|
-
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
-
const
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
return
|
|
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
|
-
const
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
function
|
|
592
|
-
|
|
593
|
-
const
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
const
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
panel.
|
|
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
|
-
if (
|
|
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
|
-
if (
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
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
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
return;
|
|
756
|
-
}
|
|
757
|
-
const nativeNav = target.closest(".lol-settings-nav");
|
|
758
|
-
if (nativeNav && document.getElementById(PANEL_ID)) {
|
|
759
|
-
closePanel();
|
|
760
|
-
// Ember does not refresh the breadcrumb after our panel replaced it, so set it to the clicked category.
|
|
761
|
-
const label = nativeNav.textContent.trim();
|
|
762
|
-
setTimeout(() => {
|
|
763
|
-
const title = document.querySelector(".lol-settings-title-current");
|
|
764
|
-
if (title && label && !document.getElementById(PANEL_ID)) title.textContent = label;
|
|
765
|
-
}, 0);
|
|
766
|
-
}
|
|
767
|
-
},
|
|
768
|
-
true,
|
|
769
|
-
);
|
|
770
|
-
|
|
771
|
-
watch(".lol-settings-nav-title", ensureSettingsTab);
|
|
772
|
-
setInterval(keepPanelInSync, 300);
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
/* ---------- Bootstrap ---------- */
|
|
776
|
-
|
|
777
|
-
const utils = {
|
|
778
|
-
version,
|
|
779
|
-
registerPlugin,
|
|
780
|
-
unregisterPlugin,
|
|
781
|
-
setPluginEnabled,
|
|
782
|
-
isPluginEnabled,
|
|
783
|
-
getPlugins,
|
|
784
|
-
getAvailableModules,
|
|
785
|
-
installModule,
|
|
786
|
-
uninstallModule,
|
|
787
|
-
checkForUpdates,
|
|
788
|
-
watch,
|
|
789
|
-
waitFor,
|
|
790
|
-
addStyle,
|
|
791
|
-
lcu: { get: lcuGet, send: lcuSend },
|
|
792
|
-
subscribe,
|
|
793
|
-
whenRcpReady,
|
|
794
|
-
};
|
|
795
|
-
|
|
796
|
-
/**
|
|
797
|
-
* Pengu entry point. Loading the file a second time is a no-op. Plugins can wait for the utils with:
|
|
798
|
-
* window.PerfectionistUtils ?? await new Promise(r => addEventListener("perfectionist:utils-ready", () => r(window.PerfectionistUtils), { once: true }))
|
|
799
|
-
*/
|
|
800
|
-
export async function init(context, coordsProvided) {
|
|
801
|
-
if (window.PerfectionistUtils) return;
|
|
802
|
-
coords = coordsProvided ?? null;
|
|
803
|
-
runningVersion = coords?.version || version;
|
|
804
|
-
|
|
805
|
-
await loadState();
|
|
806
|
-
saveState(); // pin this version for the loader's next import
|
|
807
|
-
|
|
808
|
-
socket = context?.socket ?? null;
|
|
809
|
-
window.PerfectionistUtils = utils;
|
|
810
|
-
installSettingsTab();
|
|
811
|
-
window.dispatchEvent(new CustomEvent(READY_EVENT, { detail: utils }));
|
|
812
|
-
|
|
813
|
-
try {
|
|
814
|
-
await loadModules();
|
|
815
|
-
checkForUpdates(); // runs in the background; it restarts the client when it installs an update
|
|
816
|
-
} catch (error) {
|
|
817
|
-
logError("startup failed.", error);
|
|
818
|
-
}
|
|
819
|
-
}
|
|
1
|
+
const READY_EVENT = "perfectionist:utils-ready";
|
|
2
|
+
const STATE_KEY = "perfectionist-plugins:state";
|
|
3
|
+
const CACHE_KEY = "perfectionist-plugins:cache";
|
|
4
|
+
const NAV_ID = "perfectionist-plugins-nav";
|
|
5
|
+
const PANEL_ID = "perfectionist-plugins-panel";
|
|
6
|
+
const STYLE_ID = "perfectionist-plugins-style";
|
|
7
|
+
const TAB_TITLE = "Perfectionism";
|
|
8
|
+
|
|
9
|
+
export const version = "1.0.1";
|
|
10
|
+
|
|
11
|
+
const PKG = "perfectionist-plugins";
|
|
12
|
+
const CDN = "https://cdn.jsdelivr.net/npm";
|
|
13
|
+
const REGISTRY_URL = `https://registry.npmjs.org/${PKG}/latest`;
|
|
14
|
+
const CORE_FILE = "perfectionist-utils.js";
|
|
15
|
+
|
|
16
|
+
const BUNDLED_MODULES = [
|
|
17
|
+
{
|
|
18
|
+
id: "perfectionist-utils",
|
|
19
|
+
file: CORE_FILE,
|
|
20
|
+
name: "Perfectionism Core",
|
|
21
|
+
version,
|
|
22
|
+
description: "Shared core for the Perfectionism plugins.",
|
|
23
|
+
core: true,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: "ban-blocker",
|
|
27
|
+
file: "ban-blocker.js",
|
|
28
|
+
name: "Ban Blocker",
|
|
29
|
+
version: "3.4.0",
|
|
30
|
+
description: "During bans, blocking a teammate instantly claims the champion they hover to ban.",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: "hide-unowned-skins",
|
|
34
|
+
file: "hide-unowned-skins.js",
|
|
35
|
+
name: "Hide Unowned Skins",
|
|
36
|
+
version: "2.1.0",
|
|
37
|
+
description: "In champion select, only shows the skins you own for the picked champion.",
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const STYLE_CSS = `
|
|
42
|
+
#${PANEL_ID} { padding: 12px 24px 24px 8px; box-sizing: border-box; color: #f0e6d2; font-family: "LoL Body", Arial, "Helvetica Neue", Helvetica, sans-serif; }
|
|
43
|
+
#${PANEL_ID} .perf-card { display: flex; align-items: flex-start; justify-content: space-between; gap: 24px;
|
|
44
|
+
padding: 16px 18px; margin-bottom: 12px; box-sizing: border-box;
|
|
45
|
+
background: #010a13; border: 1px solid #785a28; }
|
|
46
|
+
#${PANEL_ID} .perf-card:hover { border-color: #c89b3c; }
|
|
47
|
+
#${PANEL_ID} .perf-card--missing { opacity: 0.75; }
|
|
48
|
+
#${PANEL_ID} .perf-info { min-width: 0; }
|
|
49
|
+
#${PANEL_ID} .perf-name { font-family: "LoL Display", "Times New Roman", Times, Baskerville, Georgia, serif;
|
|
50
|
+
font-size: 15px; font-weight: 700; letter-spacing: 1.05px; text-transform: uppercase; color: #f0e6d2; }
|
|
51
|
+
#${PANEL_ID} .perf-version { font-family: "LoL Body", Arial, sans-serif; font-size: 10px; font-weight: 700;
|
|
52
|
+
letter-spacing: 0.25px; text-transform: none; color: #c89b3c; margin-left: 8px; }
|
|
53
|
+
#${PANEL_ID} .perf-desc { font-size: 12px; letter-spacing: 0.3px; color: #a09b8c; margin-top: 7px; line-height: 1.5; max-width: 560px; }
|
|
54
|
+
#${PANEL_ID} .perf-actions { display: flex; align-items: center; gap: 12px; flex: 0 0 auto; }
|
|
55
|
+
#${PANEL_ID} .perf-toggle { position: relative; display: flex; align-items: center; flex: 0 0 auto; cursor: pointer; }
|
|
56
|
+
#${PANEL_ID} .perf-toggle input { position: absolute; opacity: 0; width: 0; height: 0; margin: 0; }
|
|
57
|
+
#${PANEL_ID} .perf-box { position: relative; width: 18px; height: 18px; box-sizing: border-box;
|
|
58
|
+
border: 1px solid #785a28; background: #0a1428; }
|
|
59
|
+
#${PANEL_ID} .perf-box::after { content: ""; position: absolute; inset: 1px; display: none;
|
|
60
|
+
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; }
|
|
61
|
+
#${PANEL_ID} .perf-toggle input:checked + .perf-box::after { display: block; }
|
|
62
|
+
#${PANEL_ID} .perf-toggle:hover .perf-box { border-color: #c89b3c; }
|
|
63
|
+
#${PANEL_ID} .perf-toggle input:focus-visible + .perf-box { outline: 1px solid #c89b3c; outline-offset: 2px; }
|
|
64
|
+
.perf-btn { font-family: "LoL Body", Arial, sans-serif; font-size: 11px; font-weight: 700; line-height: 12px;
|
|
65
|
+
letter-spacing: 1px; text-transform: uppercase; color: #c89b3c; background: #0a1428;
|
|
66
|
+
border: 1px solid #785a28; padding: 8px 16px; cursor: pointer; }
|
|
67
|
+
.perf-btn:hover { color: #f0e6d2; border-color: #c89b3c; background: #1e2328; }
|
|
68
|
+
.perf-btn:disabled { opacity: 0.5; cursor: default; }
|
|
69
|
+
#${PANEL_ID} .perf-uninstall { width: 30px; height: 30px; padding: 0; display: flex; align-items: center; justify-content: center; }
|
|
70
|
+
#${PANEL_ID} .perf-uninstall::after { content: ""; width: 11px; height: 11px;
|
|
71
|
+
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; }
|
|
72
|
+
#${PANEL_ID} .perf-empty { font-size: 12px; color: #a09b8c; }
|
|
73
|
+
|
|
74
|
+
.perf-update { position: fixed; inset: 0; z-index: 100000; display: flex; align-items: center;
|
|
75
|
+
justify-content: center; background: rgba(1, 10, 19, 0.72); }
|
|
76
|
+
.perf-update-box { width: 430px; box-sizing: border-box; padding: 22px 26px;
|
|
77
|
+
background: #010a13; border: 1px solid #785a28; box-shadow: 0 0 32px rgba(0, 0, 0, 0.8); }
|
|
78
|
+
.perf-update-title { font-family: "LoL Display", "Times New Roman", Times, Baskerville, Georgia, serif;
|
|
79
|
+
font-size: 15px; font-weight: 700; letter-spacing: 1.05px; text-transform: uppercase; color: #f0e6d2; }
|
|
80
|
+
.perf-update-status { font-family: "LoL Body", Arial, sans-serif; font-size: 12px; letter-spacing: 0.3px;
|
|
81
|
+
color: #a09b8c; margin-top: 12px; line-height: 1.5; }
|
|
82
|
+
.perf-update-track { height: 6px; margin-top: 16px; background: #0a1428; border: 1px solid #785a28; }
|
|
83
|
+
.perf-update-fill { height: 100%; width: 0; background: #c89b3c; transition: width 0.25s; }
|
|
84
|
+
.perf-update-close { margin-top: 18px; }
|
|
85
|
+
`;
|
|
86
|
+
|
|
87
|
+
const registry = new Map();
|
|
88
|
+
const watchers = new Map();
|
|
89
|
+
const requestCache = new Map();
|
|
90
|
+
let socket = null;
|
|
91
|
+
let observer = null;
|
|
92
|
+
let scanScheduled = false;
|
|
93
|
+
|
|
94
|
+
let coords = null;
|
|
95
|
+
const resolved = new Map();
|
|
96
|
+
let availableModules = BUNDLED_MODULES;
|
|
97
|
+
let pendingReload = false;
|
|
98
|
+
let runningVersion = version;
|
|
99
|
+
|
|
100
|
+
let state = { installed: {}, enabled: {} };
|
|
101
|
+
|
|
102
|
+
async function loadState() {
|
|
103
|
+
let saved = null;
|
|
104
|
+
try {
|
|
105
|
+
saved =
|
|
106
|
+
typeof DataStore !== "undefined"
|
|
107
|
+
? await DataStore.get(STATE_KEY)
|
|
108
|
+
: JSON.parse(localStorage.getItem(STATE_KEY) || "null");
|
|
109
|
+
} catch {
|
|
110
|
+
saved = null;
|
|
111
|
+
}
|
|
112
|
+
if (saved?.installed && typeof saved.installed === "object") {
|
|
113
|
+
state = { installed: saved.installed, enabled: saved.enabled ?? {} };
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
for (const mod of BUNDLED_MODULES) {
|
|
117
|
+
if (mod.core) continue;
|
|
118
|
+
state.installed[mod.id] = true;
|
|
119
|
+
state.enabled[mod.id] = mod.defaultEnabled ?? true;
|
|
120
|
+
}
|
|
121
|
+
saveState();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function saveState() {
|
|
125
|
+
const record = {
|
|
126
|
+
version: /^\d/.test(runningVersion) ? runningVersion : undefined,
|
|
127
|
+
installed: state.installed,
|
|
128
|
+
enabled: state.enabled,
|
|
129
|
+
};
|
|
130
|
+
try {
|
|
131
|
+
if (typeof DataStore !== "undefined") DataStore.set(STATE_KEY, record);
|
|
132
|
+
localStorage.setItem(STATE_KEY, JSON.stringify(record));
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.error("[Perfectionism] Could not save plugin state.", error);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function logError(scope, error) {
|
|
139
|
+
console.error(`[Perfectionism] ${scope}`, error);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function isPluginEnabled(id) {
|
|
143
|
+
const entry = registry.get(id);
|
|
144
|
+
if (!entry) return false;
|
|
145
|
+
return typeof state.enabled[id] === "boolean" ? state.enabled[id] : entry.defaultEnabled;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function applyLifecycle(entry, enabled) {
|
|
149
|
+
if (enabled && !entry.active) {
|
|
150
|
+
entry.active = true;
|
|
151
|
+
try {
|
|
152
|
+
entry.onEnable?.(utils);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
entry.active = false;
|
|
155
|
+
logError(`${entry.name} failed to enable.`, error);
|
|
156
|
+
}
|
|
157
|
+
} else if (!enabled && entry.active) {
|
|
158
|
+
entry.active = false;
|
|
159
|
+
try {
|
|
160
|
+
entry.onDisable?.(utils);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
logError(`${entry.name} failed to disable cleanly.`, error);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function registerPlugin(definition) {
|
|
168
|
+
const { id, name, description = "", version = "1.0.0", defaultEnabled = true, onEnable, onDisable } = definition ?? {};
|
|
169
|
+
if (!id || !name) {
|
|
170
|
+
throw new Error("[Perfectionism] registerPlugin needs an id and a name.");
|
|
171
|
+
}
|
|
172
|
+
if (registry.has(id)) {
|
|
173
|
+
console.warn(`[Perfectionism] ${name} is already registered.`);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const entry = { id, name, description, version, defaultEnabled, onEnable, onDisable, active: false };
|
|
177
|
+
registry.set(id, entry);
|
|
178
|
+
applyLifecycle(entry, isPluginEnabled(id));
|
|
179
|
+
refreshPanel();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function unregisterPlugin(id) {
|
|
183
|
+
const entry = registry.get(id);
|
|
184
|
+
if (!entry) return;
|
|
185
|
+
applyLifecycle(entry, false);
|
|
186
|
+
registry.delete(id);
|
|
187
|
+
refreshPanel();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function setPluginEnabled(id, enabled) {
|
|
191
|
+
const entry = registry.get(id);
|
|
192
|
+
state.enabled[id] = Boolean(enabled);
|
|
193
|
+
saveState();
|
|
194
|
+
if (!entry) return false;
|
|
195
|
+
applyLifecycle(entry, Boolean(enabled));
|
|
196
|
+
refreshPanel();
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function getPlugins() {
|
|
201
|
+
return [...registry.values()]
|
|
202
|
+
.map(({ id, name, description, version }) => ({ id, name, description, version, enabled: isPluginEnabled(id) }))
|
|
203
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function getAvailableModules() {
|
|
207
|
+
return availableModules.filter((m) => !m.core);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function isInstalled(mod) {
|
|
211
|
+
const installed = state.installed[mod.id];
|
|
212
|
+
return installed === undefined ? registry.has(mod.id) : Boolean(installed);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function newerVersion(a, b) {
|
|
216
|
+
const pa = String(a ?? "0")
|
|
217
|
+
.split(".")
|
|
218
|
+
.map(Number);
|
|
219
|
+
const pb = String(b ?? "0")
|
|
220
|
+
.split(".")
|
|
221
|
+
.map(Number);
|
|
222
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
223
|
+
const diff = (pa[i] || 0) - (pb[i] || 0);
|
|
224
|
+
if (diff !== 0) return diff > 0;
|
|
225
|
+
}
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function moduleUrl(file) {
|
|
230
|
+
return `${CDN}/${PKG}@${runningVersion}/${file}`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function importModule(url) {
|
|
234
|
+
return coords?.importModule ? coords.importModule(url) : import(url);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function installModule(mod) {
|
|
238
|
+
state.installed[mod.id] = true;
|
|
239
|
+
delete state.enabled[mod.id];
|
|
240
|
+
saveState();
|
|
241
|
+
pendingReload = true;
|
|
242
|
+
refreshPanel();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function uninstallModule(mod) {
|
|
246
|
+
unregisterPlugin(mod.id);
|
|
247
|
+
state.enabled[mod.id] = false;
|
|
248
|
+
state.installed[mod.id] = false;
|
|
249
|
+
saveState();
|
|
250
|
+
refreshPanel();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const SAFE_PHASES = new Set(["None", "Lobby", "EndOfGame"]);
|
|
254
|
+
|
|
255
|
+
function reloadClientNow() {
|
|
256
|
+
if (typeof window.reloadClient === "function") window.reloadClient();
|
|
257
|
+
else window.location.reload();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function reloadWhenSafe() {
|
|
261
|
+
for (;;) {
|
|
262
|
+
let phase = null;
|
|
263
|
+
try {
|
|
264
|
+
phase = await apiGet("/lol-gameflow/v1/gameflow-phase");
|
|
265
|
+
} catch {
|
|
266
|
+
phase = null;
|
|
267
|
+
}
|
|
268
|
+
if (phase == null || SAFE_PHASES.has(phase)) break;
|
|
269
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
270
|
+
}
|
|
271
|
+
reloadClientNow();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function apiGet(path) {
|
|
275
|
+
return fetch(path, { headers: { Accept: "application/json" } }).then((response) => {
|
|
276
|
+
if (!response.ok) throw new Error(`${path} responded ${response.status}`);
|
|
277
|
+
return response.status === 204 ? null : response.json();
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function buildUpdateWindow() {
|
|
282
|
+
const root = el("div", "perf-update");
|
|
283
|
+
const box = el("div", "perf-update-box");
|
|
284
|
+
box.append(el("div", "perf-update-title", "Installing Update"));
|
|
285
|
+
const status = el("div", "perf-update-status", "Downloading...");
|
|
286
|
+
const track = el("div", "perf-update-track");
|
|
287
|
+
const fill = el("div", "perf-update-fill");
|
|
288
|
+
track.append(fill);
|
|
289
|
+
box.append(status, track);
|
|
290
|
+
root.append(box);
|
|
291
|
+
return {
|
|
292
|
+
root,
|
|
293
|
+
setStatus: (text) => {
|
|
294
|
+
status.textContent = text;
|
|
295
|
+
},
|
|
296
|
+
setProgress: (fraction) => {
|
|
297
|
+
fill.style.width = `${Math.round(Math.max(0, Math.min(1, fraction)) * 100)}%`;
|
|
298
|
+
},
|
|
299
|
+
fail: (text) => {
|
|
300
|
+
status.textContent = text;
|
|
301
|
+
const close = el("button", "perf-btn perf-update-close", "Close");
|
|
302
|
+
close.addEventListener("click", () => root.remove());
|
|
303
|
+
box.append(close);
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function runUpdate(target) {
|
|
309
|
+
const win = buildUpdateWindow();
|
|
310
|
+
(document.body ?? document.documentElement).append(win.root);
|
|
311
|
+
const files = [{ name: "Perfectionism Core", file: CORE_FILE }, ...BUNDLED_MODULES.filter((m) => !m.core)];
|
|
312
|
+
const sources = {};
|
|
313
|
+
for (let i = 0; i < files.length; i++) {
|
|
314
|
+
win.setStatus(`Downloading ${files[i].name}...`);
|
|
315
|
+
win.setProgress(i / files.length);
|
|
316
|
+
try {
|
|
317
|
+
const response = await fetch(`${CDN}/${PKG}@${target}/${files[i].file}`);
|
|
318
|
+
if (!response.ok) throw new Error(`download failed (${response.status})`);
|
|
319
|
+
const text = await response.text();
|
|
320
|
+
if (!text || !/export/.test(text)) throw new Error("the downloaded file looks invalid");
|
|
321
|
+
if (files[i].id) sources[files[i].id] = text;
|
|
322
|
+
} catch (error) {
|
|
323
|
+
logError(`could not update to ${target}.`, error);
|
|
324
|
+
win.fail(`Could not update to ${target}. It will be retried on the next client start.`);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
localStorage.setItem(CACHE_KEY, JSON.stringify({ version: target, files: sources }));
|
|
330
|
+
} catch {}
|
|
331
|
+
win.setProgress(1);
|
|
332
|
+
runningVersion = target;
|
|
333
|
+
saveState();
|
|
334
|
+
win.setStatus("Update installed. Restarting the client...");
|
|
335
|
+
reloadWhenSafe();
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function checkForUpdates() {
|
|
339
|
+
if (!/^\d+\.\d+\.\d+$/.test(runningVersion)) return;
|
|
340
|
+
let target = null;
|
|
341
|
+
try {
|
|
342
|
+
const response = await fetch(REGISTRY_URL, { headers: { Accept: "application/json" }, cache: "no-store" });
|
|
343
|
+
if (response.ok) target = (await response.json())?.version ?? null;
|
|
344
|
+
} catch {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
if (target && newerVersion(target, runningVersion)) runUpdate(target);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function loadModules() {
|
|
351
|
+
for (const mod of BUNDLED_MODULES) {
|
|
352
|
+
if (mod.core || resolved.has(mod.id) || registry.has(mod.id) || !isInstalled(mod)) continue;
|
|
353
|
+
try {
|
|
354
|
+
const plugin = await importModule(moduleUrl(mod.file));
|
|
355
|
+
resolved.set(mod.id, plugin);
|
|
356
|
+
plugin.init?.();
|
|
357
|
+
} catch (error) {
|
|
358
|
+
logError(`could not load ${mod.name}.`, error);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function scheduleScan() {
|
|
364
|
+
if (scanScheduled) return;
|
|
365
|
+
scanScheduled = true;
|
|
366
|
+
requestAnimationFrame(() => {
|
|
367
|
+
scanScheduled = false;
|
|
368
|
+
runScan();
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function runScan() {
|
|
373
|
+
for (const [selector, entries] of watchers) {
|
|
374
|
+
let nodes;
|
|
375
|
+
try {
|
|
376
|
+
nodes = document.querySelectorAll(selector);
|
|
377
|
+
} catch (error) {
|
|
378
|
+
logError(`Invalid selector "${selector}".`, error);
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
for (const node of nodes) {
|
|
382
|
+
for (const entry of entries) {
|
|
383
|
+
if (entry.seen.has(node)) continue;
|
|
384
|
+
entry.seen.add(node);
|
|
385
|
+
try {
|
|
386
|
+
entry.callback(node);
|
|
387
|
+
} catch (error) {
|
|
388
|
+
logError(`Watcher for "${selector}" failed.`, error);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function ensureObserver() {
|
|
396
|
+
if (observer) return;
|
|
397
|
+
observer = new MutationObserver(scheduleScan);
|
|
398
|
+
observer.observe(document.documentElement, { childList: true, subtree: true });
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function watch(selector, callback) {
|
|
402
|
+
const entry = { callback, seen: new WeakSet() };
|
|
403
|
+
let entries = watchers.get(selector);
|
|
404
|
+
if (!entries) {
|
|
405
|
+
entries = new Set();
|
|
406
|
+
watchers.set(selector, entries);
|
|
407
|
+
}
|
|
408
|
+
entries.add(entry);
|
|
409
|
+
ensureObserver();
|
|
410
|
+
scheduleScan();
|
|
411
|
+
return () => {
|
|
412
|
+
entries.delete(entry);
|
|
413
|
+
if (entries.size === 0 && watchers.get(selector) === entries) watchers.delete(selector);
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function waitFor(selector, timeout = 15000) {
|
|
418
|
+
return new Promise((resolve, reject) => {
|
|
419
|
+
let unwatch = null;
|
|
420
|
+
const timer = setTimeout(() => {
|
|
421
|
+
unwatch?.();
|
|
422
|
+
reject(new Error(`Timed out waiting for ${selector}`));
|
|
423
|
+
}, timeout);
|
|
424
|
+
unwatch = watch(selector, (element) => {
|
|
425
|
+
clearTimeout(timer);
|
|
426
|
+
unwatch();
|
|
427
|
+
resolve(element);
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function addStyle(name, css) {
|
|
433
|
+
let element = document.getElementById(name);
|
|
434
|
+
if (!element) {
|
|
435
|
+
element = document.createElement("style");
|
|
436
|
+
element.id = name;
|
|
437
|
+
(document.head ?? document.documentElement).appendChild(element);
|
|
438
|
+
}
|
|
439
|
+
element.textContent = css;
|
|
440
|
+
return () => element.remove();
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function lcuGet(path, { ttl = 0 } = {}) {
|
|
444
|
+
const now = Date.now();
|
|
445
|
+
const cached = requestCache.get(path);
|
|
446
|
+
if (cached && (cached.pending || cached.expires > now)) return cached.promise;
|
|
447
|
+
|
|
448
|
+
const entry = { pending: true, expires: 0, promise: null };
|
|
449
|
+
entry.promise = fetch(path, { headers: { Accept: "application/json" } }).then(async (response) => {
|
|
450
|
+
if (!response.ok) throw new Error(`LCU ${path} responded ${response.status}`);
|
|
451
|
+
return response.status === 204 ? null : response.json();
|
|
452
|
+
});
|
|
453
|
+
requestCache.set(path, entry);
|
|
454
|
+
entry.promise.then(
|
|
455
|
+
() => {
|
|
456
|
+
entry.pending = false;
|
|
457
|
+
entry.expires = Date.now() + ttl;
|
|
458
|
+
},
|
|
459
|
+
() => {
|
|
460
|
+
requestCache.delete(path);
|
|
461
|
+
},
|
|
462
|
+
);
|
|
463
|
+
return entry.promise;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async function lcuSend(method, path, body) {
|
|
467
|
+
const response = await fetch(path, {
|
|
468
|
+
method,
|
|
469
|
+
headers: { Accept: "application/json", "Content-Type": "application/json" },
|
|
470
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
471
|
+
});
|
|
472
|
+
if (!response.ok) throw new Error(`LCU ${method} ${path} responded ${response.status}`);
|
|
473
|
+
return response.status === 204 ? null : response.json();
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function subscribe(uri, listener) {
|
|
477
|
+
if (!socket?.observe) return null;
|
|
478
|
+
const handle = socket.observe(uri, listener);
|
|
479
|
+
return () => handle?.disconnect?.();
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function whenRcpReady(name, timeout = 15000) {
|
|
483
|
+
return new Promise((resolve, reject) => {
|
|
484
|
+
const timer = setTimeout(() => reject(new Error(`Timed out waiting for ${name}`)), timeout);
|
|
485
|
+
const attempt = () => {
|
|
486
|
+
if (window.rcp?.whenReady) {
|
|
487
|
+
window.rcp.whenReady(name).then((api) => {
|
|
488
|
+
clearTimeout(timer);
|
|
489
|
+
resolve(api);
|
|
490
|
+
});
|
|
491
|
+
} else {
|
|
492
|
+
setTimeout(attempt, 250);
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
attempt();
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function el(tag, className, text) {
|
|
500
|
+
const node = document.createElement(tag);
|
|
501
|
+
if (className) node.className = className;
|
|
502
|
+
if (text !== undefined) node.textContent = text;
|
|
503
|
+
return node;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function buildToggle(mod) {
|
|
507
|
+
const label = el("label", "perf-toggle");
|
|
508
|
+
label.title = `Enable ${mod.name}`;
|
|
509
|
+
const input = el("input");
|
|
510
|
+
input.type = "checkbox";
|
|
511
|
+
input.checked = isPluginEnabled(mod.id);
|
|
512
|
+
input.setAttribute("aria-label", `Enable ${mod.name}`);
|
|
513
|
+
input.addEventListener("change", () => setPluginEnabled(mod.id, input.checked));
|
|
514
|
+
label.append(input, el("span", "perf-box"));
|
|
515
|
+
return label;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function buildInstallButton(mod) {
|
|
519
|
+
const button = el("button", "perf-btn perf-install", "Install");
|
|
520
|
+
button.title = `Install ${mod.name}`;
|
|
521
|
+
button.addEventListener("click", async () => {
|
|
522
|
+
button.disabled = true;
|
|
523
|
+
button.textContent = "Installing";
|
|
524
|
+
try {
|
|
525
|
+
await installModule(mod);
|
|
526
|
+
} catch (error) {
|
|
527
|
+
logError(`could not install ${mod.name}.`, error);
|
|
528
|
+
button.disabled = false;
|
|
529
|
+
button.textContent = "Install";
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
return button;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function buildUninstallButton(mod) {
|
|
536
|
+
const button = el("button", "perf-btn perf-uninstall");
|
|
537
|
+
button.title = `Uninstall ${mod.name}`;
|
|
538
|
+
button.setAttribute("aria-label", `Uninstall ${mod.name}`);
|
|
539
|
+
button.addEventListener("click", () => uninstallModule(mod).catch((error) => logError(`could not uninstall ${mod.name}.`, error)));
|
|
540
|
+
return button;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function buildCard(mod) {
|
|
544
|
+
const installed = isInstalled(mod);
|
|
545
|
+
const card = el("div", installed ? "perf-card" : "perf-card perf-card--missing");
|
|
546
|
+
|
|
547
|
+
const info = el("div", "perf-info");
|
|
548
|
+
const name = el("div", "perf-name", mod.name);
|
|
549
|
+
name.append(el("span", "perf-version", `v${registry.get(mod.id)?.version ?? mod.version}`));
|
|
550
|
+
info.append(name);
|
|
551
|
+
if (mod.description) info.append(el("div", "perf-desc", mod.description));
|
|
552
|
+
|
|
553
|
+
const actions = el("div", "perf-actions");
|
|
554
|
+
if (installed) actions.append(buildToggle(mod));
|
|
555
|
+
actions.append(installed ? buildUninstallButton(mod) : buildInstallButton(mod));
|
|
556
|
+
|
|
557
|
+
card.append(info, actions);
|
|
558
|
+
return card;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function buildPanel() {
|
|
562
|
+
const panel = el("div");
|
|
563
|
+
panel.id = PANEL_ID;
|
|
564
|
+
|
|
565
|
+
const modules = getAvailableModules();
|
|
566
|
+
if (modules.length === 0) {
|
|
567
|
+
panel.append(el("div", "perf-empty", "No plugins are available."));
|
|
568
|
+
return panel;
|
|
569
|
+
}
|
|
570
|
+
for (const mod of modules) panel.append(buildCard(mod));
|
|
571
|
+
return panel;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function refreshPanel() {
|
|
575
|
+
const current = document.getElementById(PANEL_ID);
|
|
576
|
+
if (current) current.replaceWith(buildPanel());
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function setNavActive(activeNav) {
|
|
580
|
+
for (const nav of document.querySelectorAll(".lol-settings-nav")) {
|
|
581
|
+
if (nav === activeNav) {
|
|
582
|
+
nav.setAttribute("active", "true");
|
|
583
|
+
} else if (nav.hasAttribute("active")) {
|
|
584
|
+
nav.removeAttribute("active");
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
let panelRequested = false;
|
|
590
|
+
|
|
591
|
+
function openPanel() {
|
|
592
|
+
panelRequested = true;
|
|
593
|
+
const content = document.querySelector(".lol-settings-content");
|
|
594
|
+
if (!content) return;
|
|
595
|
+
|
|
596
|
+
const options = content.querySelector(".lol-settings-options");
|
|
597
|
+
if (options) options.style.display = "none";
|
|
598
|
+
|
|
599
|
+
const existing = document.getElementById(PANEL_ID);
|
|
600
|
+
if (existing) existing.remove();
|
|
601
|
+
content.append(buildPanel());
|
|
602
|
+
|
|
603
|
+
setNavActive(document.getElementById(NAV_ID));
|
|
604
|
+
const currentTitle = document.querySelector(".lol-settings-title-current");
|
|
605
|
+
if (currentTitle) currentTitle.textContent = TAB_TITLE.toUpperCase();
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function closePanel() {
|
|
609
|
+
panelRequested = false;
|
|
610
|
+
const panel = document.getElementById(PANEL_ID);
|
|
611
|
+
if (!panel) return;
|
|
612
|
+
panel.remove();
|
|
613
|
+
const options = document.querySelector(".lol-settings-content .lol-settings-options");
|
|
614
|
+
if (options) options.style.display = "";
|
|
615
|
+
setNavActive(null);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function keepPanelInSync() {
|
|
619
|
+
const content = document.querySelector(".lol-settings-content");
|
|
620
|
+
if (!content) {
|
|
621
|
+
panelRequested = false;
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
if (!panelRequested) {
|
|
625
|
+
const staleNav = document.getElementById(NAV_ID);
|
|
626
|
+
if (staleNav && staleNav.hasAttribute("active")) staleNav.removeAttribute("active");
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const panel = document.getElementById(PANEL_ID);
|
|
631
|
+
if (!panel || panel.parentElement !== content) {
|
|
632
|
+
openPanel();
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const options = content.querySelector(".lol-settings-options");
|
|
637
|
+
if (options && options.style.display !== "none") options.style.display = "none";
|
|
638
|
+
|
|
639
|
+
const nav = document.getElementById(NAV_ID);
|
|
640
|
+
if (nav) {
|
|
641
|
+
for (const other of document.querySelectorAll(".lol-settings-nav")) {
|
|
642
|
+
if (other !== nav && other.hasAttribute("active")) other.removeAttribute("active");
|
|
643
|
+
}
|
|
644
|
+
if (nav.getAttribute("active") !== "true") nav.setAttribute("active", "true");
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const title = document.querySelector(".lol-settings-title-current");
|
|
648
|
+
if (title && title.textContent !== TAB_TITLE.toUpperCase()) title.textContent = TAB_TITLE.toUpperCase();
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function ensureSettingsTab(titleNode) {
|
|
652
|
+
if (document.getElementById(NAV_ID)) return;
|
|
653
|
+
|
|
654
|
+
const scope = titleNode.closest("lol-uikit-scrollable") ?? titleNode.parentElement;
|
|
655
|
+
const firstTitle = scope?.querySelector(".lol-settings-nav-title");
|
|
656
|
+
if (!firstTitle) return;
|
|
657
|
+
|
|
658
|
+
const groupTitle = el("div", "lol-settings-nav-title", "Plugins");
|
|
659
|
+
groupTitle.dataset.perfectionist = "title";
|
|
660
|
+
|
|
661
|
+
const item = document.createElement("lol-uikit-navigation-item");
|
|
662
|
+
item.id = NAV_ID;
|
|
663
|
+
item.className = "lol-settings-nav";
|
|
664
|
+
item.setAttribute("name", "perfectionist-plugins");
|
|
665
|
+
item.append(el("div", null, TAB_TITLE.toUpperCase()));
|
|
666
|
+
|
|
667
|
+
const bar = document.createElement("lol-uikit-navigation-bar");
|
|
668
|
+
bar.setAttribute("direction", "down");
|
|
669
|
+
bar.setAttribute("type", "tabbed");
|
|
670
|
+
bar.dataset.perfectionist = "bar";
|
|
671
|
+
bar.append(item);
|
|
672
|
+
|
|
673
|
+
firstTitle.before(groupTitle, bar);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function isDoneButton(target) {
|
|
677
|
+
const button = target.closest("button, lol-uikit-flat-button, [role=button]");
|
|
678
|
+
return !!button && /^\s*done\s*$/i.test(button.textContent ?? "");
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function installSettingsTab() {
|
|
682
|
+
addStyle(STYLE_ID, STYLE_CSS);
|
|
683
|
+
|
|
684
|
+
document.addEventListener(
|
|
685
|
+
"click",
|
|
686
|
+
(event) => {
|
|
687
|
+
const target = event.target;
|
|
688
|
+
if (!(target instanceof Element)) return;
|
|
689
|
+
if (pendingReload && isDoneButton(target)) {
|
|
690
|
+
pendingReload = false;
|
|
691
|
+
reloadClientNow();
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
if (target.closest(`#${NAV_ID}`)) {
|
|
695
|
+
event.stopPropagation();
|
|
696
|
+
openPanel();
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
const nativeNav = target.closest(".lol-settings-nav");
|
|
700
|
+
if (nativeNav && document.getElementById(PANEL_ID)) {
|
|
701
|
+
closePanel();
|
|
702
|
+
const label = nativeNav.textContent.trim();
|
|
703
|
+
setTimeout(() => {
|
|
704
|
+
const title = document.querySelector(".lol-settings-title-current");
|
|
705
|
+
if (title && label && !document.getElementById(PANEL_ID)) title.textContent = label;
|
|
706
|
+
}, 0);
|
|
707
|
+
}
|
|
708
|
+
},
|
|
709
|
+
true,
|
|
710
|
+
);
|
|
711
|
+
|
|
712
|
+
watch(".lol-settings-nav-title", ensureSettingsTab);
|
|
713
|
+
setInterval(keepPanelInSync, 300);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const utils = {
|
|
717
|
+
version,
|
|
718
|
+
registerPlugin,
|
|
719
|
+
unregisterPlugin,
|
|
720
|
+
setPluginEnabled,
|
|
721
|
+
isPluginEnabled,
|
|
722
|
+
getPlugins,
|
|
723
|
+
getAvailableModules,
|
|
724
|
+
installModule,
|
|
725
|
+
uninstallModule,
|
|
726
|
+
checkForUpdates,
|
|
727
|
+
watch,
|
|
728
|
+
waitFor,
|
|
729
|
+
addStyle,
|
|
730
|
+
lcu: { get: lcuGet, send: lcuSend },
|
|
731
|
+
subscribe,
|
|
732
|
+
whenRcpReady,
|
|
733
|
+
};
|
|
734
|
+
|
|
735
|
+
export async function init(context, coordsProvided) {
|
|
736
|
+
if (window.PerfectionistUtils) return;
|
|
737
|
+
coords = coordsProvided ?? null;
|
|
738
|
+
runningVersion = coords?.version || version;
|
|
739
|
+
|
|
740
|
+
await loadState();
|
|
741
|
+
saveState();
|
|
742
|
+
|
|
743
|
+
socket = context?.socket ?? null;
|
|
744
|
+
window.PerfectionistUtils = utils;
|
|
745
|
+
installSettingsTab();
|
|
746
|
+
window.dispatchEvent(new CustomEvent(READY_EVENT, { detail: utils }));
|
|
747
|
+
|
|
748
|
+
try {
|
|
749
|
+
await loadModules();
|
|
750
|
+
checkForUpdates();
|
|
751
|
+
} catch (error) {
|
|
752
|
+
logError("startup failed.", error);
|
|
753
|
+
}
|
|
754
|
+
}
|