perfectionist-plugins 2.4.7 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -52
- package/auto-accept.js +296 -294
- package/auto-claimer.js +1 -37
- package/auto-read.js +107 -42
- package/ban-blocker.js +483 -677
- package/better-client.js +362 -793
- package/dodge-helper.js +375 -673
- package/hide-unowned-skins.js +63 -686
- package/package.json +40 -40
- package/perfectionist-utils.js +540 -301
- package/unlock-profile-skins.js +174 -620
package/perfectionist-utils.js
CHANGED
|
@@ -4,8 +4,10 @@ const NAV_ID = "perfectionist-plugins-nav";
|
|
|
4
4
|
const PANEL_ID = "perfectionist-plugins-panel";
|
|
5
5
|
const STYLE_ID = "perfectionist-plugins-style";
|
|
6
6
|
const TAB_TITLE = "Perfectionism";
|
|
7
|
+
const GROUP_TITLE = "Plugins";
|
|
8
|
+
const OPEN_CLASS = "perf-plugins-open";
|
|
7
9
|
|
|
8
|
-
export const version = "2.
|
|
10
|
+
export const version = "2.5.0";
|
|
9
11
|
|
|
10
12
|
const PKG = "perfectionist-plugins";
|
|
11
13
|
const CDN = "https://cdn.jsdelivr.net/npm";
|
|
@@ -23,143 +25,143 @@ const BUNDLED_MODULES = [
|
|
|
23
25
|
id: "better-client",
|
|
24
26
|
file: "better-client.js",
|
|
25
27
|
name: "BetterClient",
|
|
26
|
-
version: "
|
|
27
|
-
description:
|
|
28
|
+
version: "2.0.0",
|
|
29
|
+
description:
|
|
30
|
+
"Makes the client lighter without changing how it looks: stops videos and animations nobody can see, blocks tracking, and pauses the client while you play. Performance mode helps weaker PCs.",
|
|
28
31
|
},
|
|
29
32
|
{
|
|
30
33
|
id: "ban-blocker",
|
|
31
34
|
file: "ban-blocker.js",
|
|
32
35
|
name: "Ban Blocker",
|
|
33
|
-
version: "
|
|
34
|
-
description:
|
|
36
|
+
version: "4.0.0",
|
|
37
|
+
description:
|
|
38
|
+
"Adds a BLOCK button to your teammates in champ select. While a teammate is blocked, the champion they hover to ban is instantly declared as your pick, so their ban can never land on it.",
|
|
35
39
|
},
|
|
36
40
|
{
|
|
37
41
|
id: "unlock-profile-skins",
|
|
38
42
|
file: "unlock-profile-skins.js",
|
|
39
43
|
name: "Unlock Profile Skins",
|
|
40
|
-
version: "
|
|
41
|
-
description:
|
|
44
|
+
version: "2.0.0",
|
|
45
|
+
description:
|
|
46
|
+
"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
47
|
},
|
|
43
48
|
{
|
|
44
49
|
id: "hide-unowned-skins",
|
|
45
50
|
file: "hide-unowned-skins.js",
|
|
46
51
|
name: "Hide Unowned Skins",
|
|
47
|
-
version: "
|
|
52
|
+
version: "3.0.0",
|
|
48
53
|
description: "In champ select, the skin carousel only shows the skins you own for the champion you picked.",
|
|
49
54
|
},
|
|
50
55
|
{
|
|
51
56
|
id: "auto-accept",
|
|
52
57
|
file: "auto-accept.js",
|
|
53
58
|
name: "Auto Accept",
|
|
54
|
-
version: "1.
|
|
59
|
+
version: "1.2.0",
|
|
55
60
|
description: "Accepts the match for you when the ready check pops up.",
|
|
56
61
|
},
|
|
57
62
|
{
|
|
58
63
|
id: "dodge-helper",
|
|
59
64
|
file: "dodge-helper.js",
|
|
60
65
|
name: "Dodge Helper",
|
|
61
|
-
version: "
|
|
62
|
-
description:
|
|
66
|
+
version: "2.0.0",
|
|
67
|
+
description:
|
|
68
|
+
"Adds a DODGE button to champ select (QUIT outside ranked) that leaves cleanly, showing the LP and queue lockout your next dodge will cost.",
|
|
63
69
|
},
|
|
64
70
|
{
|
|
65
71
|
id: "auto-claimer",
|
|
66
72
|
file: "auto-claimer.js",
|
|
67
73
|
name: "Auto Claimer",
|
|
68
|
-
version: "1.
|
|
69
|
-
description:
|
|
74
|
+
version: "1.3.0",
|
|
75
|
+
description:
|
|
76
|
+
"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
77
|
},
|
|
71
78
|
{
|
|
72
79
|
id: "auto-read",
|
|
73
80
|
file: "auto-read.js",
|
|
74
81
|
name: "Auto Read",
|
|
75
|
-
version: "1.
|
|
82
|
+
version: "1.3.0",
|
|
76
83
|
description: "Marks quests, loot, Clash, TFT and client notices as read for you, so no badge or dot stays lit.",
|
|
77
84
|
},
|
|
78
85
|
];
|
|
79
86
|
|
|
87
|
+
// One box per plugin with its Enabled box and Install/Uninstall button, option groups with a caret, and chips.
|
|
88
|
+
// Colours come from the client's own palette and the inputs are the client's own elements.
|
|
80
89
|
const STYLE_CSS = `
|
|
81
|
-
#${PANEL_ID} { display: flex; flex-direction: column; overflow-x: hidden; padding: 15px 20px; position: relative;
|
|
90
|
+
#${PANEL_ID} { display: flex; flex-direction: column; overflow-x: hidden; padding: 15px 20px; position: relative;
|
|
91
|
+
width: calc(100% - 40px); box-sizing: content-box; color: #a09b8c; font-family: var(--font-body, "LoL Body", Arial, sans-serif); }
|
|
82
92
|
#${PANEL_ID} > lol-uikit-scrollable { display: block; flex: 1 1 auto; min-height: 0; }
|
|
93
|
+
#${PANEL_ID} .perf-tools { display: flex; align-items: center; gap: 10px; flex: 0 0 auto; margin-bottom: 14px; }
|
|
94
|
+
#${PANEL_ID} .perf-tools lol-uikit-flat-input { flex: 1 1 auto; min-width: 0; }
|
|
95
|
+
#${PANEL_ID} .perf-tools lol-uikit-flat-button-secondary { flex: 0 0 auto; min-width: 112px; }
|
|
96
|
+
#${PANEL_ID} .perf-notice { flex: 0 0 auto; margin: -6px 0 12px; }
|
|
83
97
|
#${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
|
-
|
|
86
|
-
#${PANEL_ID} .perf-card
|
|
87
|
-
#${PANEL_ID} .perf-card--missing { opacity: 0.75; }
|
|
98
|
+
padding: 16px 18px; margin-bottom: 12px; box-sizing: border-box; background: #010a13; border: 1px solid #463714; }
|
|
99
|
+
#${PANEL_ID} .perf-card:hover { border-color: #785a28; }
|
|
100
|
+
#${PANEL_ID} .perf-card--missing .perf-info { opacity: 0.7; }
|
|
88
101
|
#${PANEL_ID} .perf-info { min-width: 0; }
|
|
89
|
-
#${PANEL_ID} .perf-name { font-family: "LoL Display",
|
|
90
|
-
|
|
91
|
-
#${PANEL_ID} .perf-version { font-family: "LoL Body", Arial, sans-serif; font-size: 10px; font-weight: 700;
|
|
102
|
+
#${PANEL_ID} .perf-name { font-family: var(--font-display, "LoL Display", serif); font-size: 14px; font-weight: 700;
|
|
103
|
+
line-height: 18px; letter-spacing: 1.05px; text-transform: uppercase; color: #f0e6d2; }
|
|
104
|
+
#${PANEL_ID} .perf-version { font-family: var(--font-body, "LoL Body", Arial, sans-serif); font-size: 10px; font-weight: 700;
|
|
92
105
|
letter-spacing: 0.25px; text-transform: none; color: #c89b3c; margin-left: 8px; }
|
|
93
106
|
#${PANEL_ID} .perf-desc { font-size: 12px; letter-spacing: 0.3px; color: #a09b8c; margin-top: 7px; line-height: 1.5; max-width: 560px; }
|
|
94
107
|
#${PANEL_ID} .perf-actions { display: flex; align-items: center; gap: 12px; flex: 0 0 auto; }
|
|
95
|
-
#${PANEL_ID} .perf-
|
|
96
|
-
#${PANEL_ID} .perf-
|
|
97
|
-
#${PANEL_ID} .perf-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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; }
|
|
108
|
+
#${PANEL_ID} .perf-actions lol-uikit-flat-checkbox { margin: 0; }
|
|
109
|
+
#${PANEL_ID} .perf-actions lol-uikit-flat-button-secondary,
|
|
110
|
+
#${PANEL_ID} .perf-actions lol-uikit-flat-button { min-width: 96px; }
|
|
111
|
+
#${PANEL_ID} .perf-uninstall { width: 30px; height: 30px; box-sizing: border-box; padding: 0; display: flex; align-items: center;
|
|
112
|
+
justify-content: center; cursor: pointer; background: #1e2328; border: 1px solid #785a28; }
|
|
113
|
+
#${PANEL_ID} .perf-uninstall:hover { border-color: #c8aa6e; background: #1e2328; }
|
|
110
114
|
#${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='%
|
|
115
|
+
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='%23cdbe91' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E") center / 11px 11px no-repeat; }
|
|
116
|
+
#${PANEL_ID} .perf-uninstall:hover::after { filter: brightness(1.3); }
|
|
112
117
|
#${PANEL_ID} .perf-empty { font-size: 12px; color: #a09b8c; }
|
|
113
118
|
#${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
119
|
#${PANEL_ID} .perf-opt-head { display: flex; align-items: center; gap: 9px; }
|
|
119
120
|
#${PANEL_ID} .perf-caret { flex: 0 0 auto; width: 28px; height: 28px; cursor: pointer; display: flex; align-items: center; justify-content: center;
|
|
120
121
|
border: 1px solid transparent; background: transparent; }
|
|
121
|
-
#${PANEL_ID} .perf-caret:hover { border-color: #
|
|
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='%
|
|
124
|
-
|
|
125
|
-
#${PANEL_ID} .perf-caret:hover::after { filter: brightness(1.4); }
|
|
122
|
+
#${PANEL_ID} .perf-caret:hover { border-color: #463714; background: #1e2328; }
|
|
123
|
+
#${PANEL_ID} .perf-caret::after { content: ""; width: 12px; height: 12px; transform-origin: center; transition: transform 0.15s;
|
|
124
|
+
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='%23c8aa6e'/%3E%3C/svg%3E") center / 12px 12px no-repeat; }
|
|
125
|
+
#${PANEL_ID} .perf-caret:hover::after { filter: brightness(1.3); }
|
|
126
126
|
#${PANEL_ID} .perf-caret--open::after { transform: rotate(90deg); }
|
|
127
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 #
|
|
129
|
-
#${PANEL_ID} .perf-
|
|
130
|
-
#${PANEL_ID} .perf-
|
|
131
|
-
#${PANEL_ID} .perf-toggle input:disabled ~ .perf-opt-label { cursor: default; }
|
|
128
|
+
border-left: 1px solid #463714; }
|
|
129
|
+
#${PANEL_ID} lol-uikit-flat-checkbox.perf-disabled { opacity: 0.45; pointer-events: none; }
|
|
130
|
+
#${PANEL_ID} .perf-card--locked .perf-caret { opacity: 0.45; }
|
|
132
131
|
#${PANEL_ID} .perf-chip-block { margin-top: 11px; }
|
|
133
132
|
#${PANEL_ID} .perf-chips-label { font-size: 10px; font-weight: 700; letter-spacing: 0.6px; text-transform: uppercase; color: #c89b3c; margin-bottom: 7px; }
|
|
134
133
|
#${PANEL_ID} .perf-chips { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
135
|
-
#${PANEL_ID} .perf-chip { font-family: "LoL Body", Arial,
|
|
136
|
-
letter-spacing: 0.5px; text-transform: uppercase; color: #
|
|
134
|
+
#${PANEL_ID} .perf-chip { font-family: var(--font-body, "LoL Body", Arial, sans-serif); font-size: 10px; font-weight: 700;
|
|
135
|
+
letter-spacing: 0.5px; text-transform: uppercase; color: #5b5a56; background: #010a13; border: 1px solid #1e2328;
|
|
137
136
|
padding: 6px 11px; cursor: pointer; }
|
|
138
|
-
#${PANEL_ID} .perf-chip:hover { color: #
|
|
139
|
-
#${PANEL_ID} .perf-chip--on { color: #
|
|
140
|
-
#${PANEL_ID} .perf-chip--on:hover {
|
|
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; }
|
|
137
|
+
#${PANEL_ID} .perf-chip:hover { color: #a09b8c; border-color: #463714; }
|
|
138
|
+
#${PANEL_ID} .perf-chip--on { color: #c8aa6e; background: #0f1418; border-color: #463714; }
|
|
139
|
+
#${PANEL_ID} .perf-chip--on:hover { color: #c8aa6e; border-color: #785a28; }
|
|
152
140
|
#${PANEL_ID} .perf-chip:disabled { opacity: 0.45; cursor: default; }
|
|
153
|
-
#${PANEL_ID} .perf-chip:disabled:hover { color: #
|
|
154
|
-
#${PANEL_ID} .perf-chip--on:disabled:hover { color: #
|
|
141
|
+
#${PANEL_ID} .perf-chip:disabled:hover { color: #5b5a56; border-color: #1e2328; }
|
|
142
|
+
#${PANEL_ID} .perf-chip--on:disabled:hover { color: #c8aa6e; border-color: #463714; }
|
|
143
|
+
|
|
144
|
+
/* While the page is open: hide the client's own options pane and tab highlight, and show our breadcrumb.
|
|
145
|
+
Pure CSS, so nothing the client renders (attributes, text nodes, inline styles) is ever modified. */
|
|
146
|
+
.lol-settings-container.${OPEN_CLASS} .lol-settings-options { display: none !important; }
|
|
147
|
+
.lol-settings-container.${OPEN_CLASS} lol-uikit-navigation-item.lol-settings-nav:not(#${NAV_ID})[active] { color: #c8aa6e !important; cursor: pointer !important; padding-left: var(--perf-nav-pad, 14px) !important; }
|
|
148
|
+
.lol-settings-container.${OPEN_CLASS} lol-uikit-navigation-item.lol-settings-nav:not(#${NAV_ID})[active]:hover { color: #f0e6d2 !important; }
|
|
149
|
+
.lol-settings-container.${OPEN_CLASS} lol-uikit-navigation-item.lol-settings-nav:not(#${NAV_ID})[active]::before { opacity: 0 !important; }
|
|
150
|
+
.lol-settings-container.${OPEN_CLASS} .lol-settings-header-group-name,
|
|
151
|
+
.lol-settings-container.${OPEN_CLASS} .lol-settings-title-current { font-size: 0 !important; letter-spacing: 0 !important; }
|
|
152
|
+
.lol-settings-container.${OPEN_CLASS} .lol-settings-header-group-name::after { content: "${GROUP_TITLE.toUpperCase()}"; font-size: var(--perf-crumb-group-size, 20px); letter-spacing: var(--perf-crumb-group-spacing, normal); }
|
|
153
|
+
.lol-settings-container.${OPEN_CLASS} .lol-settings-title-current::after { content: "${TAB_TITLE.toUpperCase()}"; font-size: var(--perf-crumb-size, 20px); letter-spacing: var(--perf-crumb-spacing, normal); }
|
|
155
154
|
`;
|
|
156
155
|
|
|
157
156
|
const registry = new Map();
|
|
158
157
|
const watchers = new Map();
|
|
159
158
|
const requestCache = new Map();
|
|
159
|
+
const frameHooks = new Set();
|
|
160
160
|
let socket = null;
|
|
161
161
|
let observer = null;
|
|
162
162
|
let scanScheduled = false;
|
|
163
|
+
let pendingNodes = [];
|
|
164
|
+
let fullScan = true;
|
|
163
165
|
|
|
164
166
|
let coords = null;
|
|
165
167
|
const resolved = new Map();
|
|
@@ -176,6 +178,7 @@ let saveBlocked = false;
|
|
|
176
178
|
|
|
177
179
|
const SAVED_STATE_TRIES = 3;
|
|
178
180
|
const SAVED_STATE_RETRY_MS = 250;
|
|
181
|
+
const FULL_SCAN_THRESHOLD = 300;
|
|
179
182
|
|
|
180
183
|
function isObject(value) {
|
|
181
184
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
@@ -215,6 +218,7 @@ function normalizeState(saved) {
|
|
|
215
218
|
options: isObject(saved?.options) ? { ...saved.options } : {},
|
|
216
219
|
masterLocked: saved?.masterLocked === true,
|
|
217
220
|
};
|
|
221
|
+
delete next.enabled.undefined;
|
|
218
222
|
for (const mod of BUNDLED_MODULES) {
|
|
219
223
|
if (mod.core) continue;
|
|
220
224
|
if (next.installed[mod.id] === undefined && (next.enabled[mod.id] === true || isObject(next.options[mod.id]))) {
|
|
@@ -361,7 +365,7 @@ function setOption(id, key, value, refresh = true) {
|
|
|
361
365
|
saveState();
|
|
362
366
|
const entry = registry.get(id);
|
|
363
367
|
try {
|
|
364
|
-
entry?.onOptionChange?.(key, Boolean(value));
|
|
368
|
+
if (entry?.active) entry.onOptionChange?.(key, Boolean(value));
|
|
365
369
|
} catch (error) {
|
|
366
370
|
logError(`${entry?.name ?? id} could not apply the option change.`, error);
|
|
367
371
|
}
|
|
@@ -384,6 +388,7 @@ function isInstalled(mod) {
|
|
|
384
388
|
}
|
|
385
389
|
|
|
386
390
|
function moduleUrl(file) {
|
|
391
|
+
if (coords?.moduleBase) return new URL(file, coords.moduleBase).href;
|
|
387
392
|
return `${CDN}/${PKG}@${runningVersion}/${file}`;
|
|
388
393
|
}
|
|
389
394
|
|
|
@@ -425,10 +430,9 @@ async function loadModule(mod) {
|
|
|
425
430
|
}
|
|
426
431
|
|
|
427
432
|
async function loadEnabledModules() {
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
}
|
|
433
|
+
const wanted = BUNDLED_MODULES.filter((mod) => !mod.core && isInstalled(mod) && isPluginEnabled(mod.id));
|
|
434
|
+
// Fetch every enabled module in parallel; each one registers itself as soon as it arrives.
|
|
435
|
+
await Promise.all(wanted.map((mod) => loadModule(mod)));
|
|
432
436
|
}
|
|
433
437
|
|
|
434
438
|
async function loadModules() {
|
|
@@ -437,6 +441,11 @@ async function loadModules() {
|
|
|
437
441
|
await loadEnabledModules();
|
|
438
442
|
}
|
|
439
443
|
|
|
444
|
+
/* ------------------------------------------------------------------------------------------------
|
|
445
|
+
* DOM watching. One shared MutationObserver for every plugin. A frame only inspects the nodes that
|
|
446
|
+
* were added since the last frame, and falls back to a full document query for large re-renders.
|
|
447
|
+
* ---------------------------------------------------------------------------------------------- */
|
|
448
|
+
|
|
440
449
|
function scheduleScan() {
|
|
441
450
|
if (scanScheduled) return;
|
|
442
451
|
scanScheduled = true;
|
|
@@ -446,37 +455,76 @@ function scheduleScan() {
|
|
|
446
455
|
});
|
|
447
456
|
}
|
|
448
457
|
|
|
458
|
+
function visitMatches(entries, nodes) {
|
|
459
|
+
for (const node of nodes) {
|
|
460
|
+
for (const entry of entries) {
|
|
461
|
+
if (entry.seen.has(node)) continue;
|
|
462
|
+
entry.seen.add(node);
|
|
463
|
+
try {
|
|
464
|
+
entry.callback(node);
|
|
465
|
+
} catch (error) {
|
|
466
|
+
logError(`Watcher for "${entry.selector}" failed.`, error);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
449
472
|
function runScan() {
|
|
473
|
+
const nodes = pendingNodes;
|
|
474
|
+
const full = fullScan;
|
|
475
|
+
pendingNodes = [];
|
|
476
|
+
fullScan = false;
|
|
477
|
+
|
|
478
|
+
for (const hook of frameHooks) {
|
|
479
|
+
try {
|
|
480
|
+
hook();
|
|
481
|
+
} catch (error) {
|
|
482
|
+
logError("Frame hook failed.", error);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
450
486
|
for (const [selector, entries] of watchers) {
|
|
451
|
-
|
|
487
|
+
if (entries.size === 0) continue;
|
|
452
488
|
try {
|
|
453
|
-
|
|
489
|
+
if (full) {
|
|
490
|
+
visitMatches(entries, document.querySelectorAll(selector));
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
for (const node of nodes) {
|
|
494
|
+
if (!node.isConnected) continue;
|
|
495
|
+
if (node.matches(selector)) visitMatches(entries, [node]);
|
|
496
|
+
if (node.firstElementChild) visitMatches(entries, node.querySelectorAll(selector));
|
|
497
|
+
}
|
|
454
498
|
} catch (error) {
|
|
455
499
|
logError(`Invalid selector "${selector}".`, error);
|
|
456
|
-
continue;
|
|
457
500
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function onMutations(records) {
|
|
505
|
+
if (!fullScan) {
|
|
506
|
+
for (const record of records) {
|
|
507
|
+
const added = record.addedNodes;
|
|
508
|
+
for (let index = 0; index < added.length; index += 1) {
|
|
509
|
+
if (added[index].nodeType === 1) pendingNodes.push(added[index]);
|
|
467
510
|
}
|
|
468
511
|
}
|
|
512
|
+
if (pendingNodes.length > FULL_SCAN_THRESHOLD) {
|
|
513
|
+
fullScan = true;
|
|
514
|
+
pendingNodes = [];
|
|
515
|
+
}
|
|
469
516
|
}
|
|
517
|
+
scheduleScan();
|
|
470
518
|
}
|
|
471
519
|
|
|
472
520
|
function ensureObserver() {
|
|
473
521
|
if (observer) return;
|
|
474
|
-
observer = new MutationObserver(
|
|
522
|
+
observer = new MutationObserver(onMutations);
|
|
475
523
|
observer.observe(document.documentElement, { childList: true, subtree: true });
|
|
476
524
|
}
|
|
477
525
|
|
|
478
526
|
function watch(selector, callback) {
|
|
479
|
-
const entry = { callback, seen: new WeakSet() };
|
|
527
|
+
const entry = { selector, callback, seen: new WeakSet() };
|
|
480
528
|
let entries = watchers.get(selector);
|
|
481
529
|
if (!entries) {
|
|
482
530
|
entries = new Set();
|
|
@@ -484,23 +532,37 @@ function watch(selector, callback) {
|
|
|
484
532
|
}
|
|
485
533
|
entries.add(entry);
|
|
486
534
|
ensureObserver();
|
|
487
|
-
|
|
535
|
+
try {
|
|
536
|
+
visitMatches([entry], document.querySelectorAll(selector));
|
|
537
|
+
} catch (error) {
|
|
538
|
+
logError(`Invalid selector "${selector}".`, error);
|
|
539
|
+
}
|
|
488
540
|
return () => {
|
|
489
541
|
entries.delete(entry);
|
|
490
542
|
if (entries.size === 0 && watchers.get(selector) === entries) watchers.delete(selector);
|
|
491
543
|
};
|
|
492
544
|
}
|
|
493
545
|
|
|
546
|
+
function onFrame(hook) {
|
|
547
|
+
frameHooks.add(hook);
|
|
548
|
+
ensureObserver();
|
|
549
|
+
return () => frameHooks.delete(hook);
|
|
550
|
+
}
|
|
551
|
+
|
|
494
552
|
function waitFor(selector, timeout = 15000) {
|
|
495
553
|
return new Promise((resolve, reject) => {
|
|
496
554
|
let unwatch = null;
|
|
555
|
+
let done = false;
|
|
497
556
|
const timer = setTimeout(() => {
|
|
557
|
+
done = true;
|
|
498
558
|
unwatch?.();
|
|
499
559
|
reject(new Error(`Timed out waiting for ${selector}`));
|
|
500
560
|
}, timeout);
|
|
501
561
|
unwatch = watch(selector, (element) => {
|
|
562
|
+
if (done) return;
|
|
563
|
+
done = true;
|
|
502
564
|
clearTimeout(timer);
|
|
503
|
-
unwatch();
|
|
565
|
+
queueMicrotask(() => unwatch?.());
|
|
504
566
|
resolve(element);
|
|
505
567
|
});
|
|
506
568
|
});
|
|
@@ -517,6 +579,10 @@ function addStyle(name, css) {
|
|
|
517
579
|
return () => element.remove();
|
|
518
580
|
}
|
|
519
581
|
|
|
582
|
+
/* ------------------------------------------------------------------------------------------------
|
|
583
|
+
* LCU helpers
|
|
584
|
+
* ---------------------------------------------------------------------------------------------- */
|
|
585
|
+
|
|
520
586
|
const LCU_TIMEOUT_MS = 10000;
|
|
521
587
|
|
|
522
588
|
function lcuGet(path, { ttl = 0 } = {}) {
|
|
@@ -539,9 +605,10 @@ function lcuGet(path, { ttl = 0 } = {}) {
|
|
|
539
605
|
() => {
|
|
540
606
|
entry.pending = false;
|
|
541
607
|
entry.expires = Date.now() + ttl;
|
|
608
|
+
if (ttl <= 0 && requestCache.get(path) === entry) requestCache.delete(path);
|
|
542
609
|
},
|
|
543
610
|
() => {
|
|
544
|
-
requestCache.delete(path);
|
|
611
|
+
if (requestCache.get(path) === entry) requestCache.delete(path);
|
|
545
612
|
},
|
|
546
613
|
);
|
|
547
614
|
return entry.promise;
|
|
@@ -554,7 +621,14 @@ async function lcuSend(method, path, body) {
|
|
|
554
621
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
555
622
|
});
|
|
556
623
|
if (!response.ok) throw new Error(`LCU ${method} ${path} responded ${response.status}`);
|
|
557
|
-
|
|
624
|
+
if (response.status === 204) return null;
|
|
625
|
+
const text = await response.text();
|
|
626
|
+
if (!text) return null;
|
|
627
|
+
try {
|
|
628
|
+
return JSON.parse(text);
|
|
629
|
+
} catch {
|
|
630
|
+
return text;
|
|
631
|
+
}
|
|
558
632
|
}
|
|
559
633
|
|
|
560
634
|
function subscribe(uri, listener) {
|
|
@@ -565,13 +639,28 @@ function subscribe(uri, listener) {
|
|
|
565
639
|
|
|
566
640
|
function whenRcpReady(name, timeout = 15000) {
|
|
567
641
|
return new Promise((resolve, reject) => {
|
|
568
|
-
|
|
642
|
+
let settled = false;
|
|
643
|
+
const timer = setTimeout(() => {
|
|
644
|
+
settled = true;
|
|
645
|
+
reject(new Error(`Timed out waiting for ${name}`));
|
|
646
|
+
}, timeout);
|
|
569
647
|
const attempt = () => {
|
|
648
|
+
if (settled) return;
|
|
570
649
|
if (window.rcp?.whenReady) {
|
|
571
|
-
window.rcp.whenReady(name).then(
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
650
|
+
window.rcp.whenReady(name).then(
|
|
651
|
+
(api) => {
|
|
652
|
+
if (settled) return;
|
|
653
|
+
settled = true;
|
|
654
|
+
clearTimeout(timer);
|
|
655
|
+
resolve(api);
|
|
656
|
+
},
|
|
657
|
+
(error) => {
|
|
658
|
+
if (settled) return;
|
|
659
|
+
settled = true;
|
|
660
|
+
clearTimeout(timer);
|
|
661
|
+
reject(error);
|
|
662
|
+
},
|
|
663
|
+
);
|
|
575
664
|
} else {
|
|
576
665
|
setTimeout(attempt, 250);
|
|
577
666
|
}
|
|
@@ -580,6 +669,172 @@ function whenRcpReady(name, timeout = 15000) {
|
|
|
580
669
|
});
|
|
581
670
|
}
|
|
582
671
|
|
|
672
|
+
/* ------------------------------------------------------------------------------------------------
|
|
673
|
+
* Data binding transforms.
|
|
674
|
+
*
|
|
675
|
+
* Every client plugin (champ select, skins picker, profiles, ...) reads LCU data through its own
|
|
676
|
+
* "identified" DataBinding from rcp-fe-common-libs, and each binding keeps a private ResourceCache.
|
|
677
|
+
* Plugins register a transform for one identifier + path. The transform runs on every value that
|
|
678
|
+
* enters that binding's cache (HTTP reads and WebSocket updates alike), so only that one client
|
|
679
|
+
* plugin sees the modified data. No global fetch/XHR/WebSocket wrapping and no timing windows.
|
|
680
|
+
* ---------------------------------------------------------------------------------------------- */
|
|
681
|
+
|
|
682
|
+
const bindingRules = new Set();
|
|
683
|
+
const bindingCaches = new Set();
|
|
684
|
+
let bindingSetup = null;
|
|
685
|
+
|
|
686
|
+
function ruleAppliesTo(rule, owner) {
|
|
687
|
+
return Boolean(owner) && rule.identifier === owner.identifier && (!rule.base || rule.base === owner.base);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function rulesFor(owner) {
|
|
691
|
+
if (!owner) return null;
|
|
692
|
+
const list = [];
|
|
693
|
+
for (const rule of bindingRules) if (ruleAppliesTo(rule, owner)) list.push(rule);
|
|
694
|
+
return list.length ? list : null;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// Transforms must not mutate their input and must be idempotent: re-applying one to data it already
|
|
698
|
+
// produced gives the same result. That lets cached data be re-transformed in place without keeping a copy.
|
|
699
|
+
function applyRules(rules, path, data) {
|
|
700
|
+
let out = data;
|
|
701
|
+
for (const rule of rules) {
|
|
702
|
+
let hit = false;
|
|
703
|
+
try {
|
|
704
|
+
hit = rule.match(path);
|
|
705
|
+
} catch {}
|
|
706
|
+
if (!hit) continue;
|
|
707
|
+
try {
|
|
708
|
+
const next = rule.transform(out, path);
|
|
709
|
+
if (next !== undefined) out = next;
|
|
710
|
+
} catch (error) {
|
|
711
|
+
logError(`Data transform for ${path} failed.`, error);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
return out;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// reload: ask the client to fetch the real data again (used when a transform is removed).
|
|
718
|
+
function rewriteCachedPaths(cache, predicate, reload = false) {
|
|
719
|
+
const data = cache._data;
|
|
720
|
+
if (!(data instanceof Map)) return;
|
|
721
|
+
for (const [path, entry] of [...data]) {
|
|
722
|
+
if (!entry || entry.data == null || !predicate(path)) continue;
|
|
723
|
+
try {
|
|
724
|
+
if (reload) cache._resourceRead(path);
|
|
725
|
+
else cache._cacheWrite(path, entry.data, entry.timestamp);
|
|
726
|
+
} catch (error) {
|
|
727
|
+
logError(`Could not refresh ${path}.`, error);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function adoptCache(cache, binding) {
|
|
733
|
+
const owner = { identifier: binding?.options?.identifier ?? null, base: binding?.basePath ?? null };
|
|
734
|
+
if (!owner.identifier) {
|
|
735
|
+
cache.__perfOwner = null;
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
cache.__perfOwner = owner;
|
|
739
|
+
cache.__perfRules = rulesFor(owner);
|
|
740
|
+
bindingCaches.add(cache);
|
|
741
|
+
if (cache.__perfRules) {
|
|
742
|
+
const rules = cache.__perfRules;
|
|
743
|
+
rewriteCachedPaths(cache, (path) => rules.some((rule) => rule.match(path)));
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function setupBindings() {
|
|
748
|
+
if (bindingSetup) return bindingSetup;
|
|
749
|
+
bindingSetup = (async () => {
|
|
750
|
+
const libs = await whenRcpReady("rcp-fe-common-libs", 60000);
|
|
751
|
+
const factory = await libs.getDataBinding("rcp-fe-perfectionism");
|
|
752
|
+
const probe = factory("/perfectionism-probe");
|
|
753
|
+
const bindingProto = Object.getPrototypeOf(probe);
|
|
754
|
+
const probeCache = probe._getCache("/perfectionism-probe/state");
|
|
755
|
+
const cacheProto = probeCache ? Object.getPrototypeOf(probeCache) : null;
|
|
756
|
+
if (
|
|
757
|
+
typeof bindingProto?._getCache !== "function" ||
|
|
758
|
+
typeof cacheProto?._cacheWrite !== "function" ||
|
|
759
|
+
!(probeCache._data instanceof Map)
|
|
760
|
+
) {
|
|
761
|
+
throw new Error("The client's data binding layout changed.");
|
|
762
|
+
}
|
|
763
|
+
if (!bindingProto._getCache.__perfPatched) {
|
|
764
|
+
const getCache = bindingProto._getCache;
|
|
765
|
+
const patchedGetCache = function (path) {
|
|
766
|
+
const cache = getCache.call(this, path);
|
|
767
|
+
if (cache && typeof cache === "object") {
|
|
768
|
+
const owner = cache.__perfOwner;
|
|
769
|
+
if (owner === undefined) adoptCache(cache, this);
|
|
770
|
+
else if (owner && owner.identifier !== this.options?.identifier) {
|
|
771
|
+
// Shared cache (static assets): never transform it.
|
|
772
|
+
cache.__perfOwner = null;
|
|
773
|
+
cache.__perfRules = null;
|
|
774
|
+
bindingCaches.delete(cache);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
return cache;
|
|
778
|
+
};
|
|
779
|
+
patchedGetCache.__perfPatched = true;
|
|
780
|
+
bindingProto._getCache = patchedGetCache;
|
|
781
|
+
|
|
782
|
+
const write = cacheProto._cacheWrite;
|
|
783
|
+
const patchedWrite = function (path, data, stamp, error) {
|
|
784
|
+
const rules = this.__perfRules;
|
|
785
|
+
if (rules && !error && data != null) data = applyRules(rules, path, data);
|
|
786
|
+
return write.call(this, path, data, stamp, error);
|
|
787
|
+
};
|
|
788
|
+
patchedWrite.__perfPatched = true;
|
|
789
|
+
cacheProto._cacheWrite = patchedWrite;
|
|
790
|
+
}
|
|
791
|
+
})();
|
|
792
|
+
bindingSetup.catch((error) => logError("Data binding hooks are unavailable.", error));
|
|
793
|
+
return bindingSetup;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function refreshRule(rule, removed = false) {
|
|
797
|
+
for (const cache of bindingCaches) {
|
|
798
|
+
const owner = cache.__perfOwner;
|
|
799
|
+
if (!ruleAppliesTo(rule, owner)) continue;
|
|
800
|
+
cache.__perfRules = rulesFor(owner);
|
|
801
|
+
const matches = (path) => {
|
|
802
|
+
try {
|
|
803
|
+
return rule.match(path);
|
|
804
|
+
} catch {
|
|
805
|
+
return false;
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
rewriteCachedPaths(cache, matches, removed);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function transformBinding({ identifier, base = null, match, transform }) {
|
|
813
|
+
if (!identifier || typeof match !== "function" || typeof transform !== "function") {
|
|
814
|
+
throw new Error("[Perfectionism] transformBinding needs an identifier, match() and transform().");
|
|
815
|
+
}
|
|
816
|
+
const rule = { identifier, base, match, transform };
|
|
817
|
+
bindingRules.add(rule);
|
|
818
|
+
const ready = setupBindings().then(() => {
|
|
819
|
+
if (bindingRules.has(rule)) refreshRule(rule);
|
|
820
|
+
});
|
|
821
|
+
ready.catch(() => {});
|
|
822
|
+
return {
|
|
823
|
+
ready,
|
|
824
|
+
reapply() {
|
|
825
|
+
if (bindingRules.has(rule)) refreshRule(rule);
|
|
826
|
+
},
|
|
827
|
+
dispose() {
|
|
828
|
+
if (!bindingRules.delete(rule)) return;
|
|
829
|
+
refreshRule(rule, true);
|
|
830
|
+
},
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/* ------------------------------------------------------------------------------------------------
|
|
835
|
+
* Settings page
|
|
836
|
+
* ---------------------------------------------------------------------------------------------- */
|
|
837
|
+
|
|
583
838
|
function el(tag, className, text) {
|
|
584
839
|
const node = document.createElement(tag);
|
|
585
840
|
if (className) node.className = className;
|
|
@@ -587,22 +842,60 @@ function el(tag, className, text) {
|
|
|
587
842
|
return node;
|
|
588
843
|
}
|
|
589
844
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
845
|
+
// The client's own checkbox element. `label` may be empty (the plugin's Enabled box).
|
|
846
|
+
function buildCheckbox(label, checked, disabled, onChange, title) {
|
|
847
|
+
const box = el("lol-uikit-flat-checkbox");
|
|
848
|
+
if (checked) box.classList.add("checked");
|
|
849
|
+
if (disabled) box.classList.add("perf-disabled");
|
|
850
|
+
if (title) box.title = title;
|
|
595
851
|
const input = el("input");
|
|
852
|
+
input.slot = "input";
|
|
596
853
|
input.type = "checkbox";
|
|
597
|
-
input.checked =
|
|
854
|
+
input.checked = Boolean(checked);
|
|
598
855
|
input.disabled = Boolean(disabled);
|
|
599
|
-
input.setAttribute("aria-label",
|
|
856
|
+
if (title) input.setAttribute("aria-label", title);
|
|
857
|
+
box.append(input);
|
|
858
|
+
if (label) {
|
|
859
|
+
const text = el("label", null, label);
|
|
860
|
+
text.slot = "label";
|
|
861
|
+
box.append(text);
|
|
862
|
+
}
|
|
600
863
|
input.addEventListener("change", () => {
|
|
601
|
-
|
|
602
|
-
|
|
864
|
+
box.classList.toggle("checked", input.checked);
|
|
865
|
+
onChange(input.checked);
|
|
603
866
|
});
|
|
604
|
-
|
|
605
|
-
|
|
867
|
+
return box;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function setCheckboxDisabled(box, disabled) {
|
|
871
|
+
const input = box?.querySelector("input");
|
|
872
|
+
if (!input) return;
|
|
873
|
+
input.disabled = Boolean(disabled);
|
|
874
|
+
box.classList.toggle("perf-disabled", Boolean(disabled));
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function buildButton(tag, text, onClick, title) {
|
|
878
|
+
const button = el(tag, null, text);
|
|
879
|
+
if (title) button.title = title;
|
|
880
|
+
button.addEventListener("click", (event) => {
|
|
881
|
+
event.preventDefault();
|
|
882
|
+
event.stopPropagation();
|
|
883
|
+
if (button.hasAttribute("disabled")) return;
|
|
884
|
+
onClick(button);
|
|
885
|
+
});
|
|
886
|
+
return button;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const optionGroupState = new Map();
|
|
890
|
+
|
|
891
|
+
function buildOptionRow(mod, option, disabled, onChange) {
|
|
892
|
+
return buildCheckbox(
|
|
893
|
+
option.label,
|
|
894
|
+
getOption(mod.id, option.key),
|
|
895
|
+
disabled,
|
|
896
|
+
(checked) => (onChange ? onChange(checked) : setOption(mod.id, option.key, checked, false)),
|
|
897
|
+
option.description,
|
|
898
|
+
);
|
|
606
899
|
}
|
|
607
900
|
|
|
608
901
|
function buildChips(mod, option, locked) {
|
|
@@ -637,7 +930,7 @@ function buildOptions(mod, locked) {
|
|
|
637
930
|
continue;
|
|
638
931
|
}
|
|
639
932
|
if (!option.children?.length) {
|
|
640
|
-
wrap.append(buildOptionRow(mod, option,
|
|
933
|
+
wrap.append(buildOptionRow(mod, option, locked));
|
|
641
934
|
continue;
|
|
642
935
|
}
|
|
643
936
|
const group = el("div", "perf-opt-group");
|
|
@@ -646,14 +939,10 @@ function buildOptions(mod, locked) {
|
|
|
646
939
|
caret.setAttribute("role", "button");
|
|
647
940
|
caret.setAttribute("aria-label", `Show ${mod.name} options`);
|
|
648
941
|
const kids = el("div", "perf-opt-children");
|
|
649
|
-
const
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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);
|
|
942
|
+
const childBoxes = option.children.map((child) => buildOptionRow(mod, child, locked || !getOption(mod.id, option.key)));
|
|
943
|
+
kids.append(...childBoxes);
|
|
944
|
+
const master = buildOptionRow(mod, option, locked, (checked) => {
|
|
945
|
+
for (const box of childBoxes) setCheckboxDisabled(box, !checked || locked);
|
|
657
946
|
setOption(mod.id, option.key, checked, false);
|
|
658
947
|
});
|
|
659
948
|
const groupKey = `${mod.id}:${option.key}`;
|
|
@@ -675,44 +964,6 @@ function buildOptions(mod, locked) {
|
|
|
675
964
|
return wrap;
|
|
676
965
|
}
|
|
677
966
|
|
|
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
967
|
function installedModules() {
|
|
717
968
|
return getAvailableModules().filter(isInstalled);
|
|
718
969
|
}
|
|
@@ -765,7 +1016,9 @@ function applySearchFilter(panel = document.getElementById(PANEL_ID)) {
|
|
|
765
1016
|
function buildTools() {
|
|
766
1017
|
const tools = el("div", "perf-tools");
|
|
767
1018
|
|
|
768
|
-
const
|
|
1019
|
+
const field = el("lol-uikit-flat-input");
|
|
1020
|
+
field.setAttribute("type", "search");
|
|
1021
|
+
const search = el("input");
|
|
769
1022
|
search.type = "search";
|
|
770
1023
|
search.placeholder = "Search plugins...";
|
|
771
1024
|
search.value = searchQuery;
|
|
@@ -774,16 +1027,15 @@ function buildTools() {
|
|
|
774
1027
|
searchQuery = search.value.trim().toLowerCase();
|
|
775
1028
|
applySearchFilter();
|
|
776
1029
|
});
|
|
1030
|
+
field.append(search);
|
|
777
1031
|
|
|
778
|
-
const master =
|
|
779
|
-
master.type = "button";
|
|
780
|
-
master.disabled = installedModules().length === 0;
|
|
781
|
-
master.addEventListener("click", () => {
|
|
1032
|
+
const master = buildButton("lol-uikit-flat-button-secondary", masterLocked() ? "Enable All" : "Disable All", () => {
|
|
782
1033
|
if (masterLocked()) enableAllPlugins();
|
|
783
1034
|
else disableAllPlugins();
|
|
784
1035
|
});
|
|
1036
|
+
if (installedModules().length === 0) master.setAttribute("disabled", "true");
|
|
785
1037
|
|
|
786
|
-
tools.append(
|
|
1038
|
+
tools.append(field, master);
|
|
787
1039
|
return tools;
|
|
788
1040
|
}
|
|
789
1041
|
|
|
@@ -802,8 +1054,32 @@ function buildCard(mod, locked) {
|
|
|
802
1054
|
if (optionList) info.append(optionList);
|
|
803
1055
|
|
|
804
1056
|
const actions = el("div", "perf-actions");
|
|
805
|
-
if (installed)
|
|
806
|
-
|
|
1057
|
+
if (installed) {
|
|
1058
|
+
actions.append(buildCheckbox("", isPluginEnabled(mod.id), locked, (checked) => setPluginEnabled(mod.id, checked), `Enable ${mod.name}`));
|
|
1059
|
+
const remove = el("button", "perf-uninstall");
|
|
1060
|
+
remove.type = "button";
|
|
1061
|
+
remove.title = `Uninstall ${mod.name}`;
|
|
1062
|
+
remove.setAttribute("aria-label", `Uninstall ${mod.name}`);
|
|
1063
|
+
remove.addEventListener("click", () => uninstallModule(mod).catch((error) => logError(`could not uninstall ${mod.name}.`, error)));
|
|
1064
|
+
actions.append(remove);
|
|
1065
|
+
} else {
|
|
1066
|
+
actions.append(
|
|
1067
|
+
buildButton(
|
|
1068
|
+
"lol-uikit-flat-button-secondary",
|
|
1069
|
+
"Install",
|
|
1070
|
+
(button) => {
|
|
1071
|
+
button.setAttribute("disabled", "true");
|
|
1072
|
+
button.textContent = "Installing";
|
|
1073
|
+
installModule(mod).catch((error) => {
|
|
1074
|
+
logError(`could not install ${mod.name}.`, error);
|
|
1075
|
+
button.removeAttribute("disabled");
|
|
1076
|
+
button.textContent = "Install";
|
|
1077
|
+
});
|
|
1078
|
+
},
|
|
1079
|
+
`Install ${mod.name}`,
|
|
1080
|
+
),
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
807
1083
|
|
|
808
1084
|
card.append(info, actions);
|
|
809
1085
|
return card;
|
|
@@ -813,8 +1089,12 @@ function buildPanel() {
|
|
|
813
1089
|
const panel = el("div");
|
|
814
1090
|
panel.id = PANEL_ID;
|
|
815
1091
|
panel.append(buildTools());
|
|
1092
|
+
if (needsReload()) {
|
|
1093
|
+
panel.append(el("div", "lol-settings-general-subtitle perf-notice", "Press Done to reload the client and apply your changes."));
|
|
1094
|
+
}
|
|
816
1095
|
|
|
817
1096
|
const scroller = el("lol-uikit-scrollable");
|
|
1097
|
+
scroller.setAttribute("overflow-masks", "enabled");
|
|
818
1098
|
panel.append(scroller);
|
|
819
1099
|
|
|
820
1100
|
const modules = getAvailableModules();
|
|
@@ -842,142 +1122,102 @@ function refreshPanel() {
|
|
|
842
1122
|
const current = document.getElementById(PANEL_ID);
|
|
843
1123
|
if (!current) return;
|
|
844
1124
|
const top = panelScrollTop(current);
|
|
1125
|
+
const focused = document.activeElement?.closest?.(`#${PANEL_ID} .perf-toolbar`) ? document.activeElement : null;
|
|
845
1126
|
const next = buildPanel();
|
|
846
1127
|
current.replaceWith(next);
|
|
1128
|
+
if (panel) panel.element = next;
|
|
1129
|
+
if (focused) next.querySelector(".perf-toolbar input")?.focus();
|
|
847
1130
|
if (!top) return;
|
|
848
1131
|
applyScrollTop(next, top);
|
|
849
1132
|
requestAnimationFrame(() => applyScrollTop(next, top));
|
|
850
1133
|
}
|
|
851
1134
|
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
}
|
|
1135
|
+
// The native settings modal is not destroyed when it closes: the client detaches it and re-attaches the
|
|
1136
|
+
// same DOM on the next open. While our page is open, a class on the settings container hides the client's
|
|
1137
|
+
// options pane and tab highlight and swaps the breadcrumb, all in CSS, so nothing the client renders is
|
|
1138
|
+
// ever modified. `panel` holds direct references, so it can be torn down even after the modal is detached.
|
|
1139
|
+
let panel = null;
|
|
858
1140
|
|
|
859
|
-
function
|
|
860
|
-
|
|
861
|
-
if (!scope) return [];
|
|
862
|
-
return [...scope.querySelectorAll('lol-uikit-navigation-item, [class*="settings-nav"]')];
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
function setNavActive(activeNav) {
|
|
866
|
-
for (const nav of navItems()) {
|
|
867
|
-
if (nav === activeNav) {
|
|
868
|
-
if (nav.getAttribute("active") !== "true") nav.setAttribute("active", "true");
|
|
869
|
-
} else if (nav.hasAttribute("active")) {
|
|
870
|
-
nav.removeAttribute("active");
|
|
871
|
-
}
|
|
872
|
-
}
|
|
1141
|
+
function ownNav() {
|
|
1142
|
+
return document.getElementById(NAV_ID);
|
|
873
1143
|
}
|
|
874
1144
|
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
if (!own) return;
|
|
883
|
-
if (!panelRequested) {
|
|
884
|
-
if (own.hasAttribute("active")) own.removeAttribute("active");
|
|
885
|
-
return;
|
|
1145
|
+
function measureCrumb(container) {
|
|
1146
|
+
const group = container.querySelector(".lol-settings-header-group-name");
|
|
1147
|
+
const current = container.querySelector(".lol-settings-title-current");
|
|
1148
|
+
if (group) {
|
|
1149
|
+
const style = getComputedStyle(group);
|
|
1150
|
+
container.style.setProperty("--perf-crumb-group-size", style.fontSize);
|
|
1151
|
+
container.style.setProperty("--perf-crumb-group-spacing", style.letterSpacing);
|
|
886
1152
|
}
|
|
887
|
-
|
|
888
|
-
|
|
1153
|
+
if (current) {
|
|
1154
|
+
const style = getComputedStyle(current);
|
|
1155
|
+
container.style.setProperty("--perf-crumb-size", style.fontSize);
|
|
1156
|
+
container.style.setProperty("--perf-crumb-spacing", style.letterSpacing);
|
|
889
1157
|
}
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
function watchNavActive() {
|
|
893
|
-
if (navObserver || !document.body) return;
|
|
894
|
-
navObserver = new MutationObserver(suppressNativeNavActive);
|
|
895
|
-
navObserver.observe(document.body, { subtree: true, attributes: true, attributeFilter: ["active"] });
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
function startSync() {
|
|
899
|
-
if (syncTimer === null) syncTimer = setInterval(keepPanelInSync, 300);
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
function stopSync() {
|
|
903
|
-
if (syncTimer === null) return;
|
|
904
|
-
clearInterval(syncTimer);
|
|
905
|
-
syncTimer = null;
|
|
1158
|
+
const idle = container.querySelector(`lol-uikit-navigation-item.lol-settings-nav:not(#${NAV_ID}):not([active])`);
|
|
1159
|
+
if (idle) container.style.setProperty("--perf-nav-pad", getComputedStyle(idle).paddingLeft);
|
|
906
1160
|
}
|
|
907
1161
|
|
|
908
1162
|
function openPanel() {
|
|
909
|
-
panelRequested = true;
|
|
910
|
-
startSync();
|
|
911
1163
|
const content = document.querySelector(".lol-settings-content");
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
if (
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
if (existing) existing.remove();
|
|
919
|
-
content.append(buildPanel());
|
|
920
|
-
|
|
921
|
-
const ownNav = document.getElementById(NAV_ID);
|
|
922
|
-
if (!nativeActiveBeforeOpen) {
|
|
923
|
-
nativeActiveBeforeOpen = navItems().find((nav) => nav !== ownNav && nav.hasAttribute("active")) ?? null;
|
|
1164
|
+
const container = content?.closest(".lol-settings-container");
|
|
1165
|
+
if (!content || !container) return;
|
|
1166
|
+
if (panel && panel.content !== content) closePanel();
|
|
1167
|
+
if (!panel) {
|
|
1168
|
+
measureCrumb(container);
|
|
1169
|
+
panel = { content, container, element: null, nav: ownNav() };
|
|
924
1170
|
}
|
|
925
|
-
|
|
926
|
-
const currentTitle = document.querySelector(".lol-settings-title-current");
|
|
927
|
-
if (currentTitle) currentTitle.textContent = TAB_TITLE.toUpperCase();
|
|
1171
|
+
syncPanel();
|
|
928
1172
|
}
|
|
929
1173
|
|
|
930
|
-
function
|
|
931
|
-
panelRequested = false;
|
|
932
|
-
const panel = document.getElementById(PANEL_ID);
|
|
1174
|
+
function syncPanel() {
|
|
933
1175
|
if (!panel) return;
|
|
934
|
-
panel
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
document.getElementById(NAV_ID)?.removeAttribute("active");
|
|
938
|
-
if (nativeActiveBeforeOpen?.isConnected) nativeActiveBeforeOpen.setAttribute("active", "true");
|
|
939
|
-
nativeActiveBeforeOpen = null;
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
function keepPanelInSync() {
|
|
943
|
-
const content = document.querySelector(".lol-settings-content");
|
|
944
|
-
if (!content) {
|
|
945
|
-
panelRequested = false;
|
|
946
|
-
stopSync();
|
|
947
|
-
document.getElementById(NAV_ID)?.removeAttribute("active");
|
|
948
|
-
return;
|
|
949
|
-
}
|
|
950
|
-
if (!panelRequested) {
|
|
951
|
-
const staleNav = document.getElementById(NAV_ID);
|
|
952
|
-
if (staleNav && staleNav.hasAttribute("active")) staleNav.removeAttribute("active");
|
|
1176
|
+
const { content, container } = panel;
|
|
1177
|
+
if (!content.isConnected) {
|
|
1178
|
+
closePanel();
|
|
953
1179
|
return;
|
|
954
1180
|
}
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
1181
|
+
if (!container.classList.contains(OPEN_CLASS)) container.classList.add(OPEN_CLASS);
|
|
1182
|
+
if (!panel.element || !panel.element.isConnected || panel.element.parentElement !== content) {
|
|
1183
|
+
panel.element?.remove();
|
|
1184
|
+
panel.element = buildPanel();
|
|
1185
|
+
content.append(panel.element);
|
|
960
1186
|
}
|
|
1187
|
+
const nav = ownNav();
|
|
1188
|
+
panel.nav = nav;
|
|
1189
|
+
if (nav && nav.getAttribute("active") !== "true") nav.setAttribute("active", "true");
|
|
1190
|
+
}
|
|
961
1191
|
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
1192
|
+
// Runs in the capture phase of a native tab click (before the client handles it), and when the settings
|
|
1193
|
+
// modal is detached, so the next open starts clean on the client's own tab.
|
|
1194
|
+
function closePanel() {
|
|
1195
|
+
if (!panel) return;
|
|
1196
|
+
const closing = panel;
|
|
1197
|
+
panel = null;
|
|
1198
|
+
const current = document.getElementById(PANEL_ID);
|
|
1199
|
+
if (current && current !== closing.element) current.remove();
|
|
1200
|
+
closing.element?.remove();
|
|
1201
|
+
closing.container.classList.remove(OPEN_CLASS);
|
|
1202
|
+
closing.nav?.removeAttribute("active");
|
|
1203
|
+
ownNav()?.removeAttribute("active");
|
|
1204
|
+
}
|
|
967
1205
|
|
|
968
|
-
|
|
969
|
-
|
|
1206
|
+
function keepOwnNavQuiet(nav) {
|
|
1207
|
+
// The uikit navigation bar auto-selects its first item; ours must only light up while our page is open.
|
|
1208
|
+
const quiet = new MutationObserver(() => {
|
|
1209
|
+
if (!panel && nav.hasAttribute("active")) nav.removeAttribute("active");
|
|
1210
|
+
});
|
|
1211
|
+
quiet.observe(nav, { attributes: true, attributeFilter: ["active"] });
|
|
970
1212
|
}
|
|
971
1213
|
|
|
972
1214
|
function ensureSettingsTab(titleNode) {
|
|
973
|
-
|
|
974
|
-
if (document.getElementById(NAV_ID)) return;
|
|
975
|
-
|
|
1215
|
+
if (ownNav()) return;
|
|
976
1216
|
const scope = titleNode.closest("lol-uikit-scrollable") ?? titleNode.parentElement;
|
|
977
|
-
const firstTitle = scope?.querySelector(".lol-settings-nav-title");
|
|
1217
|
+
const firstTitle = scope?.querySelector(".lol-settings-nav-title:not([data-perfectionist])");
|
|
978
1218
|
if (!firstTitle) return;
|
|
979
1219
|
|
|
980
|
-
const groupTitle = el("div", "lol-settings-nav-title",
|
|
1220
|
+
const groupTitle = el("div", "lol-settings-nav-title", GROUP_TITLE);
|
|
981
1221
|
groupTitle.dataset.perfectionist = "title";
|
|
982
1222
|
|
|
983
1223
|
const item = document.createElement("lol-uikit-navigation-item");
|
|
@@ -989,50 +1229,47 @@ function ensureSettingsTab(titleNode) {
|
|
|
989
1229
|
const bar = document.createElement("lol-uikit-navigation-bar");
|
|
990
1230
|
bar.setAttribute("direction", "down");
|
|
991
1231
|
bar.setAttribute("type", "tabbed");
|
|
1232
|
+
bar.setAttribute("selectedindex", "-1");
|
|
992
1233
|
bar.dataset.perfectionist = "bar";
|
|
993
1234
|
bar.append(item);
|
|
994
1235
|
|
|
995
1236
|
firstTitle.before(groupTitle, bar);
|
|
1237
|
+
keepOwnNavQuiet(item);
|
|
1238
|
+
if (!panel) item.removeAttribute("active");
|
|
996
1239
|
}
|
|
997
1240
|
|
|
998
1241
|
function isDoneButton(target) {
|
|
1242
|
+
if (target.closest(".lol-settings-close-button")) return true;
|
|
999
1243
|
const button = target.closest("button, lol-uikit-flat-button, [role=button]");
|
|
1000
|
-
return !!button && /^\s*done\s*$/i.test(button.textContent ?? "");
|
|
1244
|
+
return !!button && !!button.closest(".lol-settings-container") && /^\s*done\s*$/i.test(button.textContent ?? "");
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
function onSettingsClick(event) {
|
|
1248
|
+
const target = event.target;
|
|
1249
|
+
if (!(target instanceof Element)) return;
|
|
1250
|
+
if (needsReload() && isDoneButton(target)) {
|
|
1251
|
+
pendingReload = false;
|
|
1252
|
+
unloadPending.clear();
|
|
1253
|
+
reloadClientNow();
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
if (target.closest(`#${NAV_ID}`)) {
|
|
1257
|
+
event.stopPropagation();
|
|
1258
|
+
event.preventDefault();
|
|
1259
|
+
openPanel();
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
if (!panel) return;
|
|
1263
|
+
const nativeNav = target.closest(".lol-settings-nav");
|
|
1264
|
+
if (nativeNav && nativeNav.id !== NAV_ID && nativeNav.closest(".lol-settings-container")) closePanel();
|
|
1001
1265
|
}
|
|
1002
1266
|
|
|
1003
1267
|
function installSettingsTab() {
|
|
1004
1268
|
addStyle(STYLE_ID, STYLE_CSS);
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
(event) => {
|
|
1010
|
-
const target = event.target;
|
|
1011
|
-
if (!(target instanceof Element)) return;
|
|
1012
|
-
if (needsReload() && isDoneButton(target)) {
|
|
1013
|
-
pendingReload = false;
|
|
1014
|
-
unloadPending.clear();
|
|
1015
|
-
reloadClientNow();
|
|
1016
|
-
return;
|
|
1017
|
-
}
|
|
1018
|
-
if (target.closest(`#${NAV_ID}`)) {
|
|
1019
|
-
event.stopPropagation();
|
|
1020
|
-
openPanel();
|
|
1021
|
-
return;
|
|
1022
|
-
}
|
|
1023
|
-
const nativeNav = target.closest(".lol-settings-nav");
|
|
1024
|
-
if (nativeNav && document.getElementById(PANEL_ID)) {
|
|
1025
|
-
closePanel();
|
|
1026
|
-
const label = nativeNav.textContent.trim();
|
|
1027
|
-
setTimeout(() => {
|
|
1028
|
-
const title = document.querySelector(".lol-settings-title-current");
|
|
1029
|
-
if (title && label && !document.getElementById(PANEL_ID)) title.textContent = label;
|
|
1030
|
-
}, 0);
|
|
1031
|
-
}
|
|
1032
|
-
},
|
|
1033
|
-
true,
|
|
1034
|
-
);
|
|
1035
|
-
|
|
1269
|
+
document.addEventListener("click", onSettingsClick, true);
|
|
1270
|
+
onFrame(() => {
|
|
1271
|
+
if (panel) syncPanel();
|
|
1272
|
+
});
|
|
1036
1273
|
watch(".lol-settings-nav-title", ensureSettingsTab);
|
|
1037
1274
|
}
|
|
1038
1275
|
|
|
@@ -1050,10 +1287,12 @@ const utils = {
|
|
|
1050
1287
|
uninstallModule,
|
|
1051
1288
|
watch,
|
|
1052
1289
|
waitFor,
|
|
1290
|
+
onFrame,
|
|
1053
1291
|
addStyle,
|
|
1054
1292
|
lcu: { get: lcuGet, send: lcuSend },
|
|
1055
1293
|
subscribe,
|
|
1056
1294
|
whenRcpReady,
|
|
1295
|
+
transformBinding,
|
|
1057
1296
|
};
|
|
1058
1297
|
|
|
1059
1298
|
export async function init(context, coordsProvided) {
|
|
@@ -1075,4 +1314,4 @@ export async function init(context, coordsProvided) {
|
|
|
1075
1314
|
} catch (error) {
|
|
1076
1315
|
logError("startup failed.", error);
|
|
1077
1316
|
}
|
|
1078
|
-
}
|
|
1317
|
+
}
|