perfectionist-plugins 2.4.8 → 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 -732
- package/better-client.js +362 -793
- package/dodge-helper.js +377 -713
- package/hide-unowned-skins.js +63 -687
- package/package.json +40 -40
- package/perfectionist-utils.js +539 -301
- package/unlock-profile-skins.js +173 -652
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
|
}
|
|
@@ -426,10 +430,9 @@ async function loadModule(mod) {
|
|
|
426
430
|
}
|
|
427
431
|
|
|
428
432
|
async function loadEnabledModules() {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
}
|
|
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)));
|
|
433
436
|
}
|
|
434
437
|
|
|
435
438
|
async function loadModules() {
|
|
@@ -438,6 +441,11 @@ async function loadModules() {
|
|
|
438
441
|
await loadEnabledModules();
|
|
439
442
|
}
|
|
440
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
|
+
|
|
441
449
|
function scheduleScan() {
|
|
442
450
|
if (scanScheduled) return;
|
|
443
451
|
scanScheduled = true;
|
|
@@ -447,37 +455,76 @@ function scheduleScan() {
|
|
|
447
455
|
});
|
|
448
456
|
}
|
|
449
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
|
+
|
|
450
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
|
+
|
|
451
486
|
for (const [selector, entries] of watchers) {
|
|
452
|
-
|
|
487
|
+
if (entries.size === 0) continue;
|
|
453
488
|
try {
|
|
454
|
-
|
|
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
|
+
}
|
|
455
498
|
} catch (error) {
|
|
456
499
|
logError(`Invalid selector "${selector}".`, error);
|
|
457
|
-
continue;
|
|
458
500
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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]);
|
|
468
510
|
}
|
|
469
511
|
}
|
|
512
|
+
if (pendingNodes.length > FULL_SCAN_THRESHOLD) {
|
|
513
|
+
fullScan = true;
|
|
514
|
+
pendingNodes = [];
|
|
515
|
+
}
|
|
470
516
|
}
|
|
517
|
+
scheduleScan();
|
|
471
518
|
}
|
|
472
519
|
|
|
473
520
|
function ensureObserver() {
|
|
474
521
|
if (observer) return;
|
|
475
|
-
observer = new MutationObserver(
|
|
522
|
+
observer = new MutationObserver(onMutations);
|
|
476
523
|
observer.observe(document.documentElement, { childList: true, subtree: true });
|
|
477
524
|
}
|
|
478
525
|
|
|
479
526
|
function watch(selector, callback) {
|
|
480
|
-
const entry = { callback, seen: new WeakSet() };
|
|
527
|
+
const entry = { selector, callback, seen: new WeakSet() };
|
|
481
528
|
let entries = watchers.get(selector);
|
|
482
529
|
if (!entries) {
|
|
483
530
|
entries = new Set();
|
|
@@ -485,23 +532,37 @@ function watch(selector, callback) {
|
|
|
485
532
|
}
|
|
486
533
|
entries.add(entry);
|
|
487
534
|
ensureObserver();
|
|
488
|
-
|
|
535
|
+
try {
|
|
536
|
+
visitMatches([entry], document.querySelectorAll(selector));
|
|
537
|
+
} catch (error) {
|
|
538
|
+
logError(`Invalid selector "${selector}".`, error);
|
|
539
|
+
}
|
|
489
540
|
return () => {
|
|
490
541
|
entries.delete(entry);
|
|
491
542
|
if (entries.size === 0 && watchers.get(selector) === entries) watchers.delete(selector);
|
|
492
543
|
};
|
|
493
544
|
}
|
|
494
545
|
|
|
546
|
+
function onFrame(hook) {
|
|
547
|
+
frameHooks.add(hook);
|
|
548
|
+
ensureObserver();
|
|
549
|
+
return () => frameHooks.delete(hook);
|
|
550
|
+
}
|
|
551
|
+
|
|
495
552
|
function waitFor(selector, timeout = 15000) {
|
|
496
553
|
return new Promise((resolve, reject) => {
|
|
497
554
|
let unwatch = null;
|
|
555
|
+
let done = false;
|
|
498
556
|
const timer = setTimeout(() => {
|
|
557
|
+
done = true;
|
|
499
558
|
unwatch?.();
|
|
500
559
|
reject(new Error(`Timed out waiting for ${selector}`));
|
|
501
560
|
}, timeout);
|
|
502
561
|
unwatch = watch(selector, (element) => {
|
|
562
|
+
if (done) return;
|
|
563
|
+
done = true;
|
|
503
564
|
clearTimeout(timer);
|
|
504
|
-
unwatch();
|
|
565
|
+
queueMicrotask(() => unwatch?.());
|
|
505
566
|
resolve(element);
|
|
506
567
|
});
|
|
507
568
|
});
|
|
@@ -518,6 +579,10 @@ function addStyle(name, css) {
|
|
|
518
579
|
return () => element.remove();
|
|
519
580
|
}
|
|
520
581
|
|
|
582
|
+
/* ------------------------------------------------------------------------------------------------
|
|
583
|
+
* LCU helpers
|
|
584
|
+
* ---------------------------------------------------------------------------------------------- */
|
|
585
|
+
|
|
521
586
|
const LCU_TIMEOUT_MS = 10000;
|
|
522
587
|
|
|
523
588
|
function lcuGet(path, { ttl = 0 } = {}) {
|
|
@@ -540,9 +605,10 @@ function lcuGet(path, { ttl = 0 } = {}) {
|
|
|
540
605
|
() => {
|
|
541
606
|
entry.pending = false;
|
|
542
607
|
entry.expires = Date.now() + ttl;
|
|
608
|
+
if (ttl <= 0 && requestCache.get(path) === entry) requestCache.delete(path);
|
|
543
609
|
},
|
|
544
610
|
() => {
|
|
545
|
-
requestCache.delete(path);
|
|
611
|
+
if (requestCache.get(path) === entry) requestCache.delete(path);
|
|
546
612
|
},
|
|
547
613
|
);
|
|
548
614
|
return entry.promise;
|
|
@@ -555,7 +621,14 @@ async function lcuSend(method, path, body) {
|
|
|
555
621
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
556
622
|
});
|
|
557
623
|
if (!response.ok) throw new Error(`LCU ${method} ${path} responded ${response.status}`);
|
|
558
|
-
|
|
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
|
+
}
|
|
559
632
|
}
|
|
560
633
|
|
|
561
634
|
function subscribe(uri, listener) {
|
|
@@ -566,13 +639,28 @@ function subscribe(uri, listener) {
|
|
|
566
639
|
|
|
567
640
|
function whenRcpReady(name, timeout = 15000) {
|
|
568
641
|
return new Promise((resolve, reject) => {
|
|
569
|
-
|
|
642
|
+
let settled = false;
|
|
643
|
+
const timer = setTimeout(() => {
|
|
644
|
+
settled = true;
|
|
645
|
+
reject(new Error(`Timed out waiting for ${name}`));
|
|
646
|
+
}, timeout);
|
|
570
647
|
const attempt = () => {
|
|
648
|
+
if (settled) return;
|
|
571
649
|
if (window.rcp?.whenReady) {
|
|
572
|
-
window.rcp.whenReady(name).then(
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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
|
+
);
|
|
576
664
|
} else {
|
|
577
665
|
setTimeout(attempt, 250);
|
|
578
666
|
}
|
|
@@ -581,6 +669,172 @@ function whenRcpReady(name, timeout = 15000) {
|
|
|
581
669
|
});
|
|
582
670
|
}
|
|
583
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
|
+
|
|
584
838
|
function el(tag, className, text) {
|
|
585
839
|
const node = document.createElement(tag);
|
|
586
840
|
if (className) node.className = className;
|
|
@@ -588,22 +842,60 @@ function el(tag, className, text) {
|
|
|
588
842
|
return node;
|
|
589
843
|
}
|
|
590
844
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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;
|
|
596
851
|
const input = el("input");
|
|
852
|
+
input.slot = "input";
|
|
597
853
|
input.type = "checkbox";
|
|
598
|
-
input.checked =
|
|
854
|
+
input.checked = Boolean(checked);
|
|
599
855
|
input.disabled = Boolean(disabled);
|
|
600
|
-
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
|
+
}
|
|
601
863
|
input.addEventListener("change", () => {
|
|
602
|
-
|
|
603
|
-
|
|
864
|
+
box.classList.toggle("checked", input.checked);
|
|
865
|
+
onChange(input.checked);
|
|
604
866
|
});
|
|
605
|
-
|
|
606
|
-
|
|
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
|
+
);
|
|
607
899
|
}
|
|
608
900
|
|
|
609
901
|
function buildChips(mod, option, locked) {
|
|
@@ -638,7 +930,7 @@ function buildOptions(mod, locked) {
|
|
|
638
930
|
continue;
|
|
639
931
|
}
|
|
640
932
|
if (!option.children?.length) {
|
|
641
|
-
wrap.append(buildOptionRow(mod, option,
|
|
933
|
+
wrap.append(buildOptionRow(mod, option, locked));
|
|
642
934
|
continue;
|
|
643
935
|
}
|
|
644
936
|
const group = el("div", "perf-opt-group");
|
|
@@ -647,14 +939,10 @@ function buildOptions(mod, locked) {
|
|
|
647
939
|
caret.setAttribute("role", "button");
|
|
648
940
|
caret.setAttribute("aria-label", `Show ${mod.name} options`);
|
|
649
941
|
const kids = el("div", "perf-opt-children");
|
|
650
|
-
const
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
kids.append(childRow);
|
|
655
|
-
}
|
|
656
|
-
const master = buildOptionRow(mod, option, Boolean(locked), (checked) => {
|
|
657
|
-
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);
|
|
658
946
|
setOption(mod.id, option.key, checked, false);
|
|
659
947
|
});
|
|
660
948
|
const groupKey = `${mod.id}:${option.key}`;
|
|
@@ -676,44 +964,6 @@ function buildOptions(mod, locked) {
|
|
|
676
964
|
return wrap;
|
|
677
965
|
}
|
|
678
966
|
|
|
679
|
-
function buildToggle(mod, locked) {
|
|
680
|
-
const label = el("label", "perf-toggle");
|
|
681
|
-
label.title = `Enable ${mod.name}`;
|
|
682
|
-
const input = el("input");
|
|
683
|
-
input.type = "checkbox";
|
|
684
|
-
input.checked = isPluginEnabled(mod.id);
|
|
685
|
-
input.disabled = Boolean(locked);
|
|
686
|
-
input.setAttribute("aria-label", `Enable ${mod.name}`);
|
|
687
|
-
input.addEventListener("change", () => setPluginEnabled(mod.id, input.checked));
|
|
688
|
-
label.append(input, el("span", "perf-box"));
|
|
689
|
-
return label;
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
function buildInstallButton(mod) {
|
|
693
|
-
const button = el("button", "perf-btn perf-install", "Install");
|
|
694
|
-
button.title = `Install ${mod.name}`;
|
|
695
|
-
button.addEventListener("click", async () => {
|
|
696
|
-
button.disabled = true;
|
|
697
|
-
button.textContent = "Installing";
|
|
698
|
-
try {
|
|
699
|
-
await installModule(mod);
|
|
700
|
-
} catch (error) {
|
|
701
|
-
logError(`could not install ${mod.name}.`, error);
|
|
702
|
-
button.disabled = false;
|
|
703
|
-
button.textContent = "Install";
|
|
704
|
-
}
|
|
705
|
-
});
|
|
706
|
-
return button;
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
function buildUninstallButton(mod) {
|
|
710
|
-
const button = el("button", "perf-btn perf-uninstall");
|
|
711
|
-
button.title = `Uninstall ${mod.name}`;
|
|
712
|
-
button.setAttribute("aria-label", `Uninstall ${mod.name}`);
|
|
713
|
-
button.addEventListener("click", () => uninstallModule(mod).catch((error) => logError(`could not uninstall ${mod.name}.`, error)));
|
|
714
|
-
return button;
|
|
715
|
-
}
|
|
716
|
-
|
|
717
967
|
function installedModules() {
|
|
718
968
|
return getAvailableModules().filter(isInstalled);
|
|
719
969
|
}
|
|
@@ -766,7 +1016,9 @@ function applySearchFilter(panel = document.getElementById(PANEL_ID)) {
|
|
|
766
1016
|
function buildTools() {
|
|
767
1017
|
const tools = el("div", "perf-tools");
|
|
768
1018
|
|
|
769
|
-
const
|
|
1019
|
+
const field = el("lol-uikit-flat-input");
|
|
1020
|
+
field.setAttribute("type", "search");
|
|
1021
|
+
const search = el("input");
|
|
770
1022
|
search.type = "search";
|
|
771
1023
|
search.placeholder = "Search plugins...";
|
|
772
1024
|
search.value = searchQuery;
|
|
@@ -775,16 +1027,15 @@ function buildTools() {
|
|
|
775
1027
|
searchQuery = search.value.trim().toLowerCase();
|
|
776
1028
|
applySearchFilter();
|
|
777
1029
|
});
|
|
1030
|
+
field.append(search);
|
|
778
1031
|
|
|
779
|
-
const master =
|
|
780
|
-
master.type = "button";
|
|
781
|
-
master.disabled = installedModules().length === 0;
|
|
782
|
-
master.addEventListener("click", () => {
|
|
1032
|
+
const master = buildButton("lol-uikit-flat-button-secondary", masterLocked() ? "Enable All" : "Disable All", () => {
|
|
783
1033
|
if (masterLocked()) enableAllPlugins();
|
|
784
1034
|
else disableAllPlugins();
|
|
785
1035
|
});
|
|
1036
|
+
if (installedModules().length === 0) master.setAttribute("disabled", "true");
|
|
786
1037
|
|
|
787
|
-
tools.append(
|
|
1038
|
+
tools.append(field, master);
|
|
788
1039
|
return tools;
|
|
789
1040
|
}
|
|
790
1041
|
|
|
@@ -803,8 +1054,32 @@ function buildCard(mod, locked) {
|
|
|
803
1054
|
if (optionList) info.append(optionList);
|
|
804
1055
|
|
|
805
1056
|
const actions = el("div", "perf-actions");
|
|
806
|
-
if (installed)
|
|
807
|
-
|
|
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
|
+
}
|
|
808
1083
|
|
|
809
1084
|
card.append(info, actions);
|
|
810
1085
|
return card;
|
|
@@ -814,8 +1089,12 @@ function buildPanel() {
|
|
|
814
1089
|
const panel = el("div");
|
|
815
1090
|
panel.id = PANEL_ID;
|
|
816
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
|
+
}
|
|
817
1095
|
|
|
818
1096
|
const scroller = el("lol-uikit-scrollable");
|
|
1097
|
+
scroller.setAttribute("overflow-masks", "enabled");
|
|
819
1098
|
panel.append(scroller);
|
|
820
1099
|
|
|
821
1100
|
const modules = getAvailableModules();
|
|
@@ -843,142 +1122,102 @@ function refreshPanel() {
|
|
|
843
1122
|
const current = document.getElementById(PANEL_ID);
|
|
844
1123
|
if (!current) return;
|
|
845
1124
|
const top = panelScrollTop(current);
|
|
1125
|
+
const focused = document.activeElement?.closest?.(`#${PANEL_ID} .perf-toolbar`) ? document.activeElement : null;
|
|
846
1126
|
const next = buildPanel();
|
|
847
1127
|
current.replaceWith(next);
|
|
1128
|
+
if (panel) panel.element = next;
|
|
1129
|
+
if (focused) next.querySelector(".perf-toolbar input")?.focus();
|
|
848
1130
|
if (!top) return;
|
|
849
1131
|
applyScrollTop(next, top);
|
|
850
1132
|
requestAnimationFrame(() => applyScrollTop(next, top));
|
|
851
1133
|
}
|
|
852
1134
|
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
}
|
|
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;
|
|
859
1140
|
|
|
860
|
-
function
|
|
861
|
-
|
|
862
|
-
if (!scope) return [];
|
|
863
|
-
return [...scope.querySelectorAll('lol-uikit-navigation-item, [class*="settings-nav"]')];
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
function setNavActive(activeNav) {
|
|
867
|
-
for (const nav of navItems()) {
|
|
868
|
-
if (nav === activeNav) {
|
|
869
|
-
if (nav.getAttribute("active") !== "true") nav.setAttribute("active", "true");
|
|
870
|
-
} else if (nav.hasAttribute("active")) {
|
|
871
|
-
nav.removeAttribute("active");
|
|
872
|
-
}
|
|
873
|
-
}
|
|
1141
|
+
function ownNav() {
|
|
1142
|
+
return document.getElementById(NAV_ID);
|
|
874
1143
|
}
|
|
875
1144
|
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
if (!own) return;
|
|
884
|
-
if (!panelRequested) {
|
|
885
|
-
if (own.hasAttribute("active")) own.removeAttribute("active");
|
|
886
|
-
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);
|
|
887
1152
|
}
|
|
888
|
-
|
|
889
|
-
|
|
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);
|
|
890
1157
|
}
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
function watchNavActive() {
|
|
894
|
-
if (navObserver || !document.body) return;
|
|
895
|
-
navObserver = new MutationObserver(suppressNativeNavActive);
|
|
896
|
-
navObserver.observe(document.body, { subtree: true, attributes: true, attributeFilter: ["active"] });
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
function startSync() {
|
|
900
|
-
if (syncTimer === null) syncTimer = setInterval(keepPanelInSync, 300);
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
function stopSync() {
|
|
904
|
-
if (syncTimer === null) return;
|
|
905
|
-
clearInterval(syncTimer);
|
|
906
|
-
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);
|
|
907
1160
|
}
|
|
908
1161
|
|
|
909
1162
|
function openPanel() {
|
|
910
|
-
panelRequested = true;
|
|
911
|
-
startSync();
|
|
912
1163
|
const content = document.querySelector(".lol-settings-content");
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
if (
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
if (existing) existing.remove();
|
|
920
|
-
content.append(buildPanel());
|
|
921
|
-
|
|
922
|
-
const ownNav = document.getElementById(NAV_ID);
|
|
923
|
-
if (!nativeActiveBeforeOpen) {
|
|
924
|
-
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() };
|
|
925
1170
|
}
|
|
926
|
-
|
|
927
|
-
const currentTitle = document.querySelector(".lol-settings-title-current");
|
|
928
|
-
if (currentTitle) currentTitle.textContent = TAB_TITLE.toUpperCase();
|
|
1171
|
+
syncPanel();
|
|
929
1172
|
}
|
|
930
1173
|
|
|
931
|
-
function
|
|
932
|
-
panelRequested = false;
|
|
933
|
-
const panel = document.getElementById(PANEL_ID);
|
|
1174
|
+
function syncPanel() {
|
|
934
1175
|
if (!panel) return;
|
|
935
|
-
panel
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
document.getElementById(NAV_ID)?.removeAttribute("active");
|
|
939
|
-
if (nativeActiveBeforeOpen?.isConnected) nativeActiveBeforeOpen.setAttribute("active", "true");
|
|
940
|
-
nativeActiveBeforeOpen = null;
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
function keepPanelInSync() {
|
|
944
|
-
const content = document.querySelector(".lol-settings-content");
|
|
945
|
-
if (!content) {
|
|
946
|
-
panelRequested = false;
|
|
947
|
-
stopSync();
|
|
948
|
-
document.getElementById(NAV_ID)?.removeAttribute("active");
|
|
949
|
-
return;
|
|
950
|
-
}
|
|
951
|
-
if (!panelRequested) {
|
|
952
|
-
const staleNav = document.getElementById(NAV_ID);
|
|
953
|
-
if (staleNav && staleNav.hasAttribute("active")) staleNav.removeAttribute("active");
|
|
1176
|
+
const { content, container } = panel;
|
|
1177
|
+
if (!content.isConnected) {
|
|
1178
|
+
closePanel();
|
|
954
1179
|
return;
|
|
955
1180
|
}
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
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);
|
|
961
1186
|
}
|
|
1187
|
+
const nav = ownNav();
|
|
1188
|
+
panel.nav = nav;
|
|
1189
|
+
if (nav && nav.getAttribute("active") !== "true") nav.setAttribute("active", "true");
|
|
1190
|
+
}
|
|
962
1191
|
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
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
|
+
}
|
|
968
1205
|
|
|
969
|
-
|
|
970
|
-
|
|
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"] });
|
|
971
1212
|
}
|
|
972
1213
|
|
|
973
1214
|
function ensureSettingsTab(titleNode) {
|
|
974
|
-
|
|
975
|
-
if (document.getElementById(NAV_ID)) return;
|
|
976
|
-
|
|
1215
|
+
if (ownNav()) return;
|
|
977
1216
|
const scope = titleNode.closest("lol-uikit-scrollable") ?? titleNode.parentElement;
|
|
978
|
-
const firstTitle = scope?.querySelector(".lol-settings-nav-title");
|
|
1217
|
+
const firstTitle = scope?.querySelector(".lol-settings-nav-title:not([data-perfectionist])");
|
|
979
1218
|
if (!firstTitle) return;
|
|
980
1219
|
|
|
981
|
-
const groupTitle = el("div", "lol-settings-nav-title",
|
|
1220
|
+
const groupTitle = el("div", "lol-settings-nav-title", GROUP_TITLE);
|
|
982
1221
|
groupTitle.dataset.perfectionist = "title";
|
|
983
1222
|
|
|
984
1223
|
const item = document.createElement("lol-uikit-navigation-item");
|
|
@@ -990,50 +1229,47 @@ function ensureSettingsTab(titleNode) {
|
|
|
990
1229
|
const bar = document.createElement("lol-uikit-navigation-bar");
|
|
991
1230
|
bar.setAttribute("direction", "down");
|
|
992
1231
|
bar.setAttribute("type", "tabbed");
|
|
1232
|
+
bar.setAttribute("selectedindex", "-1");
|
|
993
1233
|
bar.dataset.perfectionist = "bar";
|
|
994
1234
|
bar.append(item);
|
|
995
1235
|
|
|
996
1236
|
firstTitle.before(groupTitle, bar);
|
|
1237
|
+
keepOwnNavQuiet(item);
|
|
1238
|
+
if (!panel) item.removeAttribute("active");
|
|
997
1239
|
}
|
|
998
1240
|
|
|
999
1241
|
function isDoneButton(target) {
|
|
1242
|
+
if (target.closest(".lol-settings-close-button")) return true;
|
|
1000
1243
|
const button = target.closest("button, lol-uikit-flat-button, [role=button]");
|
|
1001
|
-
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();
|
|
1002
1265
|
}
|
|
1003
1266
|
|
|
1004
1267
|
function installSettingsTab() {
|
|
1005
1268
|
addStyle(STYLE_ID, STYLE_CSS);
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
(event) => {
|
|
1011
|
-
const target = event.target;
|
|
1012
|
-
if (!(target instanceof Element)) return;
|
|
1013
|
-
if (needsReload() && isDoneButton(target)) {
|
|
1014
|
-
pendingReload = false;
|
|
1015
|
-
unloadPending.clear();
|
|
1016
|
-
reloadClientNow();
|
|
1017
|
-
return;
|
|
1018
|
-
}
|
|
1019
|
-
if (target.closest(`#${NAV_ID}`)) {
|
|
1020
|
-
event.stopPropagation();
|
|
1021
|
-
openPanel();
|
|
1022
|
-
return;
|
|
1023
|
-
}
|
|
1024
|
-
const nativeNav = target.closest(".lol-settings-nav");
|
|
1025
|
-
if (nativeNav && document.getElementById(PANEL_ID)) {
|
|
1026
|
-
closePanel();
|
|
1027
|
-
const label = nativeNav.textContent.trim();
|
|
1028
|
-
setTimeout(() => {
|
|
1029
|
-
const title = document.querySelector(".lol-settings-title-current");
|
|
1030
|
-
if (title && label && !document.getElementById(PANEL_ID)) title.textContent = label;
|
|
1031
|
-
}, 0);
|
|
1032
|
-
}
|
|
1033
|
-
},
|
|
1034
|
-
true,
|
|
1035
|
-
);
|
|
1036
|
-
|
|
1269
|
+
document.addEventListener("click", onSettingsClick, true);
|
|
1270
|
+
onFrame(() => {
|
|
1271
|
+
if (panel) syncPanel();
|
|
1272
|
+
});
|
|
1037
1273
|
watch(".lol-settings-nav-title", ensureSettingsTab);
|
|
1038
1274
|
}
|
|
1039
1275
|
|
|
@@ -1051,10 +1287,12 @@ const utils = {
|
|
|
1051
1287
|
uninstallModule,
|
|
1052
1288
|
watch,
|
|
1053
1289
|
waitFor,
|
|
1290
|
+
onFrame,
|
|
1054
1291
|
addStyle,
|
|
1055
1292
|
lcu: { get: lcuGet, send: lcuSend },
|
|
1056
1293
|
subscribe,
|
|
1057
1294
|
whenRcpReady,
|
|
1295
|
+
transformBinding,
|
|
1058
1296
|
};
|
|
1059
1297
|
|
|
1060
1298
|
export async function init(context, coordsProvided) {
|
|
@@ -1076,4 +1314,4 @@ export async function init(context, coordsProvided) {
|
|
|
1076
1314
|
} catch (error) {
|
|
1077
1315
|
logError("startup failed.", error);
|
|
1078
1316
|
}
|
|
1079
|
-
}
|
|
1317
|
+
}
|