@spicetify/kit 0.1.0 → 0.2.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/dist/check.js +9 -5
- package/dist/create.js +10 -11
- package/dist/from-theme.js +2 -1
- package/dist/stdlib-boundary.js +259 -0
- package/dist/version.js +8 -0
- package/package.json +52 -48
- package/vendor/shims/spicetify.d.ts +14 -2
- package/vendor/stdlib/lib/dialog-lifecycle.ts +53 -0
- package/vendor/stdlib/lib/floating-position.ts +57 -0
- package/vendor/stdlib/lib/floating.tsx +298 -0
- package/vendor/stdlib/lib/modal.tsx +20 -113
- package/vendor/stdlib/lib/popover.ts +26 -19
- package/vendor/stdlib/lib/primitives-classes.ts +12 -0
- package/vendor/stdlib/lib/primitives-vanilla.ts +27 -3
- package/vendor/stdlib/lib/primitives.tsx +176 -26
- package/vendor/stdlib/metadata.json +1 -1
- package/vendor/stdlib/mod.ts +8 -5
- package/vendor/stdlib/src/client.ts +65 -2
- package/vendor/stdlib/src/registers/index.ts +40 -3
- package/vendor/stdlib/src/registers/panel-logic.ts +371 -0
- package/vendor/stdlib/src/registers/panel.ts +63 -151
- package/vendor/stdlib/src/registers/playbarButton.tsx +1 -1
- package/vendor/stdlib/src/registers/route.ts +2 -1
- package/vendor/stdlib/src/registers/settingsRow.tsx +4 -4
- package/vendor/stdlib/src/registers/settingsSection.ts +61 -41
- package/vendor/stdlib/src/registers/topbarLeftButton.tsx +1 -1
- package/vendor/stdlib/src/registers/topbarRightButton.tsx +3 -5
- package/vendor/stdlib/src/settings/corsProxy.tsx +102 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Afonso Jorge Ramos
|
|
3
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface PanelWidth {
|
|
7
|
+
default?: number;
|
|
8
|
+
min?: number;
|
|
9
|
+
max?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface PanelRegistration<Content = unknown> {
|
|
13
|
+
id: string;
|
|
14
|
+
label: string;
|
|
15
|
+
render: () => Content;
|
|
16
|
+
width?: PanelWidth;
|
|
17
|
+
onOpen?: () => void;
|
|
18
|
+
onClose?: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface PanelController {
|
|
22
|
+
open(): void;
|
|
23
|
+
close(): void;
|
|
24
|
+
toggle(): void;
|
|
25
|
+
isOpen(): boolean;
|
|
26
|
+
subscribe(listener: (open: boolean) => void): () => void;
|
|
27
|
+
dispose(): void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface PanelCoordinatorOptions<Content = unknown> {
|
|
31
|
+
document: Document;
|
|
32
|
+
window: Window;
|
|
33
|
+
mount(host: HTMLElement, panel: PanelRegistration<Content>, close: () => void): () => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface PanelCoordinator<Content = unknown> {
|
|
37
|
+
register(panel: PanelRegistration<Content>): PanelController;
|
|
38
|
+
dispose(): void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type RegistrationState<Content> = {
|
|
42
|
+
panel: PanelRegistration<Content>;
|
|
43
|
+
disposed: boolean;
|
|
44
|
+
listeners: Set<(open: boolean) => void>;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
type NativeSnapshot = { hidden: string | null; inert: string | null };
|
|
48
|
+
type StyleSnapshot = { value: string; priority: string };
|
|
49
|
+
|
|
50
|
+
const WIDTH_DEFAULT = 360;
|
|
51
|
+
const WIDTH_MIN = 280;
|
|
52
|
+
const WIDTH_MAX = 520;
|
|
53
|
+
|
|
54
|
+
const panelWidth = ({ width }: PanelRegistration): number => {
|
|
55
|
+
const min = width?.min ?? WIDTH_MIN;
|
|
56
|
+
const max = Math.max(min, width?.max ?? WIDTH_MAX);
|
|
57
|
+
return Math.min(max, Math.max(min, width?.default ?? WIDTH_DEFAULT));
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const validatePanelWidth = (width: PanelWidth | undefined) => {
|
|
61
|
+
if (!width) return;
|
|
62
|
+
for (const key of ["default", "min", "max"] as const) {
|
|
63
|
+
const value = width[key];
|
|
64
|
+
if (value !== undefined && (!Number.isFinite(value) || value < 0)) {
|
|
65
|
+
throw new Error(`[stdlib] panel width ${key} must be a finite non-negative number`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const gridTracks = (template: string): string[] => {
|
|
71
|
+
const tracks: string[] = [];
|
|
72
|
+
let token = "";
|
|
73
|
+
let parentheses = 0;
|
|
74
|
+
let brackets = 0;
|
|
75
|
+
for (const character of template.trim()) {
|
|
76
|
+
if (/\s/.test(character) && parentheses === 0 && brackets === 0) {
|
|
77
|
+
if (token) tracks.push(token);
|
|
78
|
+
token = "";
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
token += character;
|
|
82
|
+
if (character === "(") parentheses += 1;
|
|
83
|
+
else if (character === ")") parentheses -= 1;
|
|
84
|
+
else if (character === "[") brackets += 1;
|
|
85
|
+
else if (character === "]") brackets -= 1;
|
|
86
|
+
}
|
|
87
|
+
if (token) tracks.push(token);
|
|
88
|
+
return tracks.filter((track) => !(track.startsWith("[") && track.endsWith("]")));
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export function createPanelCoordinator<Content = unknown>(options: PanelCoordinatorOptions<Content>) {
|
|
92
|
+
const registrations = new Map<string, RegistrationState<Content>>();
|
|
93
|
+
const suspended = new Map<HTMLElement, NativeSnapshot>();
|
|
94
|
+
const ownedStyles = new Map<HTMLElement, Map<string, StyleSnapshot>>();
|
|
95
|
+
let active: RegistrationState<Content> | undefined;
|
|
96
|
+
let activeHost: HTMLElement | undefined;
|
|
97
|
+
let activeSidebar: HTMLElement | undefined;
|
|
98
|
+
let activeTopContainer: HTMLElement | undefined;
|
|
99
|
+
let unmount: (() => void) | undefined;
|
|
100
|
+
let shellObserver: MutationObserver | undefined;
|
|
101
|
+
let sidebarObserver: MutationObserver | undefined;
|
|
102
|
+
let returnFocus: HTMLElement | undefined;
|
|
103
|
+
let disposed = false;
|
|
104
|
+
let transitioning = false;
|
|
105
|
+
const transitions: Array<() => void> = [];
|
|
106
|
+
const Observer = (options.window as unknown as { MutationObserver: typeof MutationObserver }).MutationObserver;
|
|
107
|
+
|
|
108
|
+
const safely = (label: string, callback: (() => void) | undefined) => {
|
|
109
|
+
if (!callback) return;
|
|
110
|
+
try {
|
|
111
|
+
callback();
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error(`[stdlib] panel ${label} failed:`, error);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const notify = (state: RegistrationState<Content>, open: boolean) => {
|
|
118
|
+
for (const listener of state.listeners) safely("subscriber", () => listener(open));
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const transition = (action: () => void) => {
|
|
122
|
+
transitions.push(action);
|
|
123
|
+
if (transitioning) return;
|
|
124
|
+
transitioning = true;
|
|
125
|
+
let firstError: unknown;
|
|
126
|
+
try {
|
|
127
|
+
let next: (() => void) | undefined;
|
|
128
|
+
while ((next = transitions.shift())) {
|
|
129
|
+
try {
|
|
130
|
+
next();
|
|
131
|
+
} catch (error) {
|
|
132
|
+
firstError ??= error;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} finally {
|
|
136
|
+
transitioning = false;
|
|
137
|
+
}
|
|
138
|
+
if (firstError) throw firstError;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const restoreNative = () => {
|
|
142
|
+
for (const [element, snapshot] of suspended) {
|
|
143
|
+
for (const attribute of ["hidden", "inert"] as const) {
|
|
144
|
+
const value = snapshot[attribute];
|
|
145
|
+
if (value === null) element.removeAttribute(attribute);
|
|
146
|
+
else element.setAttribute(attribute, value);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
suspended.clear();
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const setOwnedStyle = (element: HTMLElement, property: string, value: string) => {
|
|
153
|
+
let snapshots = ownedStyles.get(element);
|
|
154
|
+
if (!snapshots) {
|
|
155
|
+
snapshots = new Map();
|
|
156
|
+
ownedStyles.set(element, snapshots);
|
|
157
|
+
}
|
|
158
|
+
if (!snapshots.has(property)) {
|
|
159
|
+
snapshots.set(property, {
|
|
160
|
+
value: element.style.getPropertyValue(property),
|
|
161
|
+
priority: element.style.getPropertyPriority(property),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
element.style.setProperty(property, value, "important");
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const restoreOwnedStyles = (element: HTMLElement | undefined) => {
|
|
168
|
+
if (!element) return;
|
|
169
|
+
const snapshots = ownedStyles.get(element);
|
|
170
|
+
if (!snapshots) return;
|
|
171
|
+
for (const [property, snapshot] of snapshots) {
|
|
172
|
+
if (snapshot.value) element.style.setProperty(property, snapshot.value, snapshot.priority);
|
|
173
|
+
else element.style.removeProperty(property);
|
|
174
|
+
}
|
|
175
|
+
ownedStyles.delete(element);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const suspendNative = (sidebar: HTMLElement) => {
|
|
179
|
+
for (const child of sidebar.children) {
|
|
180
|
+
if (!(child instanceof HTMLElement) || child === activeHost) continue;
|
|
181
|
+
if (!suspended.has(child)) {
|
|
182
|
+
suspended.set(child, { hidden: child.getAttribute("hidden"), inert: child.getAttribute("inert") });
|
|
183
|
+
}
|
|
184
|
+
child.setAttribute("hidden", "");
|
|
185
|
+
child.setAttribute("inert", "");
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const observeSidebar = (sidebar: HTMLElement) => {
|
|
190
|
+
sidebarObserver?.disconnect();
|
|
191
|
+
sidebarObserver = new Observer(() => suspendNative(sidebar));
|
|
192
|
+
sidebarObserver.observe(sidebar, { childList: true });
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const clearLayout = () => {
|
|
196
|
+
activeTopContainer?.removeAttribute("data-spicetify-panel-active");
|
|
197
|
+
restoreOwnedStyles(activeTopContainer);
|
|
198
|
+
restoreOwnedStyles(activeSidebar);
|
|
199
|
+
activeTopContainer = undefined;
|
|
200
|
+
activeSidebar = undefined;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const placeActiveHost = (): boolean => {
|
|
204
|
+
if (!active || !activeHost) return false;
|
|
205
|
+
const sidebar = options.document.querySelector<HTMLElement>(".Root__right-sidebar");
|
|
206
|
+
const topContainer = options.document.querySelector<HTMLElement>(".Root__top-container");
|
|
207
|
+
if (!sidebar || !topContainer) return false;
|
|
208
|
+
|
|
209
|
+
if (activeSidebar !== sidebar) {
|
|
210
|
+
restoreNative();
|
|
211
|
+
restoreOwnedStyles(activeSidebar);
|
|
212
|
+
activeSidebar = sidebar;
|
|
213
|
+
observeSidebar(sidebar);
|
|
214
|
+
}
|
|
215
|
+
if (activeHost.parentElement !== sidebar) sidebar.prepend(activeHost);
|
|
216
|
+
if (activeTopContainer !== topContainer) {
|
|
217
|
+
activeTopContainer?.removeAttribute("data-spicetify-panel-active");
|
|
218
|
+
restoreOwnedStyles(activeTopContainer);
|
|
219
|
+
activeTopContainer = topContainer;
|
|
220
|
+
}
|
|
221
|
+
topContainer.dataset.spicetifyPanelActive = "";
|
|
222
|
+
const trailingTracks = gridTracks(options.window.getComputedStyle(topContainer).gridTemplateColumns).slice(3);
|
|
223
|
+
const trailingWidth = trailingTracks.reduce(
|
|
224
|
+
(total, track) => total + (track.endsWith("px") ? Number.parseFloat(track) || 0 : 0),
|
|
225
|
+
0,
|
|
226
|
+
);
|
|
227
|
+
setOwnedStyle(topContainer, "--spicetify-panel-requested-width", `${panelWidth(active.panel)}px`);
|
|
228
|
+
setOwnedStyle(topContainer, "--spicetify-panel-trailing-width", `${trailingWidth}px`);
|
|
229
|
+
setOwnedStyle(
|
|
230
|
+
topContainer,
|
|
231
|
+
"grid-template-columns",
|
|
232
|
+
["auto", "minmax(0, 1fr)", "var(--spicetify-panel-width)", ...trailingTracks].join(" "),
|
|
233
|
+
);
|
|
234
|
+
setOwnedStyle(sidebar, "width", "var(--spicetify-panel-width)");
|
|
235
|
+
setOwnedStyle(sidebar, "min-width", "var(--spicetify-panel-width)");
|
|
236
|
+
suspendNative(sidebar);
|
|
237
|
+
return true;
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const closeActive = (restoreFocus: boolean) => {
|
|
241
|
+
const closing = active;
|
|
242
|
+
if (!closing) return;
|
|
243
|
+
active = undefined;
|
|
244
|
+
shellObserver?.disconnect();
|
|
245
|
+
shellObserver = undefined;
|
|
246
|
+
sidebarObserver?.disconnect();
|
|
247
|
+
sidebarObserver = undefined;
|
|
248
|
+
safely("unmount", unmount);
|
|
249
|
+
unmount = undefined;
|
|
250
|
+
activeHost?.remove();
|
|
251
|
+
activeHost = undefined;
|
|
252
|
+
restoreNative();
|
|
253
|
+
clearLayout();
|
|
254
|
+
safely("onClose", closing.panel.onClose);
|
|
255
|
+
notify(closing, false);
|
|
256
|
+
if (restoreFocus) {
|
|
257
|
+
if (returnFocus?.isConnected) returnFocus.focus();
|
|
258
|
+
returnFocus = undefined;
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const open = (state: RegistrationState<Content>) => {
|
|
263
|
+
if (disposed || state.disposed) return;
|
|
264
|
+
if (active === state) return;
|
|
265
|
+
const replacing = !!active;
|
|
266
|
+
if (replacing) closeActive(false);
|
|
267
|
+
const focused = options.document.activeElement;
|
|
268
|
+
if (!replacing) returnFocus = focused instanceof HTMLElement ? focused : undefined;
|
|
269
|
+
active = state;
|
|
270
|
+
activeHost = options.document.createElement("section");
|
|
271
|
+
activeHost.className = "spicetify-panel-host";
|
|
272
|
+
activeHost.dataset.panelId = state.panel.id;
|
|
273
|
+
activeHost.setAttribute("role", "complementary");
|
|
274
|
+
activeHost.setAttribute("aria-label", state.panel.label);
|
|
275
|
+
activeHost.tabIndex = -1;
|
|
276
|
+
try {
|
|
277
|
+
unmount = options.mount(activeHost, state.panel, () => transition(() => closeActive(true)));
|
|
278
|
+
} catch (error) {
|
|
279
|
+
active = undefined;
|
|
280
|
+
activeHost.remove();
|
|
281
|
+
activeHost = undefined;
|
|
282
|
+
if (returnFocus?.isConnected) returnFocus.focus();
|
|
283
|
+
returnFocus = undefined;
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
placeActiveHost();
|
|
287
|
+
shellObserver = new Observer(() => {
|
|
288
|
+
if (activeHost?.isConnected && activeSidebar?.isConnected && activeTopContainer?.isConnected) return;
|
|
289
|
+
placeActiveHost();
|
|
290
|
+
});
|
|
291
|
+
if (options.document.body) shellObserver.observe(options.document.body, { childList: true, subtree: true });
|
|
292
|
+
safely("onOpen", state.panel.onOpen);
|
|
293
|
+
notify(state, true);
|
|
294
|
+
activeHost.focus();
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
298
|
+
if (event.key !== "Escape" || event.defaultPrevented || !active) return;
|
|
299
|
+
if (
|
|
300
|
+
options.document.querySelector(
|
|
301
|
+
".spicetify-popover, .spicetify-scrim, .spicetify-root-children [role='dialog']",
|
|
302
|
+
)
|
|
303
|
+
)
|
|
304
|
+
return;
|
|
305
|
+
event.preventDefault();
|
|
306
|
+
transition(() => closeActive(true));
|
|
307
|
+
};
|
|
308
|
+
options.document.addEventListener("keydown", onKeyDown);
|
|
309
|
+
|
|
310
|
+
const register = (panel: PanelRegistration<Content>): PanelController => {
|
|
311
|
+
if (disposed) throw new Error("[stdlib] panel coordinator is disposed");
|
|
312
|
+
if (!panel.id.trim()) throw new Error("[stdlib] panel id must not be empty");
|
|
313
|
+
if (!panel.label.trim()) throw new Error("[stdlib] panel label must not be empty");
|
|
314
|
+
validatePanelWidth(panel.width);
|
|
315
|
+
if (registrations.has(panel.id)) throw new Error(`[stdlib] duplicate panel id: ${panel.id}`);
|
|
316
|
+
const state: RegistrationState<Content> = { panel, disposed: false, listeners: new Set() };
|
|
317
|
+
registrations.set(panel.id, state);
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
open: () => {
|
|
321
|
+
if (disposed || state.disposed) throw new Error(`[stdlib] panel "${state.panel.id}" is disposed`);
|
|
322
|
+
transition(() => open(state));
|
|
323
|
+
},
|
|
324
|
+
close: () => {
|
|
325
|
+
transition(() => {
|
|
326
|
+
if (active === state) closeActive(true);
|
|
327
|
+
});
|
|
328
|
+
},
|
|
329
|
+
toggle: () => {
|
|
330
|
+
if (disposed || state.disposed) throw new Error(`[stdlib] panel "${state.panel.id}" is disposed`);
|
|
331
|
+
transition(() => {
|
|
332
|
+
if (active === state) closeActive(true);
|
|
333
|
+
else open(state);
|
|
334
|
+
});
|
|
335
|
+
},
|
|
336
|
+
isOpen: () => active === state,
|
|
337
|
+
subscribe(listener) {
|
|
338
|
+
if (state.disposed) throw new Error(`[stdlib] panel "${state.panel.id}" is disposed`);
|
|
339
|
+
state.listeners.add(listener);
|
|
340
|
+
safely("subscriber", () => listener(active === state));
|
|
341
|
+
return () => state.listeners.delete(listener);
|
|
342
|
+
},
|
|
343
|
+
dispose() {
|
|
344
|
+
if (state.disposed) return;
|
|
345
|
+
state.disposed = true;
|
|
346
|
+
transition(() => {
|
|
347
|
+
if (active === state) closeActive(true);
|
|
348
|
+
state.listeners.clear();
|
|
349
|
+
registrations.delete(panel.id);
|
|
350
|
+
});
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const dispose = () => {
|
|
356
|
+
if (disposed) return;
|
|
357
|
+
disposed = true;
|
|
358
|
+
transition(() => {
|
|
359
|
+
closeActive(false);
|
|
360
|
+
returnFocus = undefined;
|
|
361
|
+
for (const state of registrations.values()) {
|
|
362
|
+
state.disposed = true;
|
|
363
|
+
state.listeners.clear();
|
|
364
|
+
}
|
|
365
|
+
registrations.clear();
|
|
366
|
+
options.document.removeEventListener("keydown", onKeyDown);
|
|
367
|
+
});
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
return { register, dispose } satisfies PanelCoordinator<Content>;
|
|
371
|
+
}
|
|
@@ -1,168 +1,80 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* Copyright (C) 2024 Delusoire
|
|
3
|
+
* Copyright (C) 2026 Afonso Jorge Ramos
|
|
3
4
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
import type { createMachine as createMachineT } from "npm:xstate";
|
|
7
|
+
import { React, ReactDOM } from "../expose/React.ts";
|
|
8
|
+
import { createItemBoundary } from "./mount.ts";
|
|
9
9
|
import { Registry } from "./registry.ts";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
STATES[state] = {
|
|
42
|
-
on: Object.setPrototypeOf(
|
|
43
|
-
{
|
|
44
|
-
[event]: {
|
|
45
|
-
target: "disabled",
|
|
10
|
+
import {
|
|
11
|
+
createPanelCoordinator,
|
|
12
|
+
type PanelController,
|
|
13
|
+
type PanelCoordinator,
|
|
14
|
+
type PanelRegistration,
|
|
15
|
+
} from "./panel-logic.ts";
|
|
16
|
+
|
|
17
|
+
export type { PanelController, PanelRegistration, PanelWidth } from "./panel-logic.ts";
|
|
18
|
+
|
|
19
|
+
const mountReactPanel = (host: HTMLElement, panel: PanelRegistration<React.ReactNode>, close: () => void) => {
|
|
20
|
+
const root = ReactDOM.createRoot(host);
|
|
21
|
+
const PanelBoundary = createItemBoundary(React, "panel") as unknown as React.ComponentType<{
|
|
22
|
+
children?: React.ReactNode;
|
|
23
|
+
}>;
|
|
24
|
+
const PanelContent = () => React.createElement("div", { className: "spicetify-panel-content" }, panel.render());
|
|
25
|
+
const PanelSurface = () =>
|
|
26
|
+
React.createElement(
|
|
27
|
+
React.Fragment,
|
|
28
|
+
null,
|
|
29
|
+
React.createElement(
|
|
30
|
+
"header",
|
|
31
|
+
{ className: "spicetify-panel-header" },
|
|
32
|
+
React.createElement("h2", { className: "spicetify-panel-title" }, panel.label),
|
|
33
|
+
React.createElement(
|
|
34
|
+
"button",
|
|
35
|
+
{
|
|
36
|
+
className: "spicetify-panel-close spicetify-button-circle",
|
|
37
|
+
"aria-label": `Close ${panel.label}`,
|
|
38
|
+
onClick: close,
|
|
46
39
|
},
|
|
47
|
-
|
|
48
|
-
|
|
40
|
+
"×",
|
|
41
|
+
),
|
|
49
42
|
),
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
ACTIONS[entry] = onEntry;
|
|
56
|
-
}
|
|
57
|
-
if (onExit) {
|
|
58
|
-
const exit = `bespoke_${hash}_exit`;
|
|
59
|
-
Machine.config.states![state].exit = [exit];
|
|
60
|
-
ACTIONS[exit] = onExit;
|
|
61
|
-
}
|
|
43
|
+
React.createElement(PanelBoundary, null, React.createElement(PanelContent)),
|
|
44
|
+
);
|
|
45
|
+
root.render(React.createElement(PanelSurface));
|
|
46
|
+
return () => root.unmount();
|
|
47
|
+
};
|
|
62
48
|
|
|
63
|
-
|
|
64
|
-
|
|
49
|
+
const panelCoordinatorKey = Symbol.for("spicetify.stdlib.panel-coordinator");
|
|
50
|
+
const shared = globalThis as typeof globalThis & Record<symbol, unknown>;
|
|
51
|
+
const previousCoordinator = shared[panelCoordinatorKey] as PanelCoordinator<React.ReactNode> | undefined;
|
|
52
|
+
previousCoordinator?.dispose();
|
|
65
53
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
54
|
+
export const panelCoordinator = createPanelCoordinator<React.ReactNode>({
|
|
55
|
+
document,
|
|
56
|
+
window,
|
|
57
|
+
mount: mountReactPanel,
|
|
58
|
+
});
|
|
59
|
+
shared[panelCoordinatorKey] = panelCoordinator;
|
|
71
60
|
|
|
72
|
-
|
|
61
|
+
class PanelRegistry extends Registry<PanelRegistration<React.ReactNode>> {
|
|
62
|
+
private controllers = new Map<PanelRegistration<React.ReactNode>, PanelController>();
|
|
73
63
|
|
|
74
|
-
|
|
75
|
-
|
|
64
|
+
override add(panel: PanelRegistration<React.ReactNode>): this {
|
|
65
|
+
this.controllers.set(panel, panelCoordinator.register(panel));
|
|
66
|
+
return super.add(panel);
|
|
76
67
|
}
|
|
77
|
-
})();
|
|
78
|
-
export default registry;
|
|
79
|
-
|
|
80
|
-
const stateToNode = new Map<string, React.ReactNode>();
|
|
81
|
-
|
|
82
|
-
declare global {
|
|
83
|
-
var __renderPanel: any;
|
|
84
|
-
}
|
|
85
68
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
69
|
+
override delete(panel: PanelRegistration<React.ReactNode>): boolean {
|
|
70
|
+
this.controllers.get(panel)?.dispose();
|
|
71
|
+
this.controllers.delete(panel);
|
|
72
|
+
return super.delete(panel);
|
|
89
73
|
}
|
|
90
74
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
let STATES: Record<string, any> = {};
|
|
96
|
-
let ACTIONS: Record<string, any> = {};
|
|
97
|
-
|
|
98
|
-
transformer(
|
|
99
|
-
(emit) => (str) => {
|
|
100
|
-
emit();
|
|
101
|
-
|
|
102
|
-
str = str.replace(/(=\(0,[a-zA-Z_\$][\w\$]*\.[a-zA-Z_\$][\w\$]*\)\(\{id:"RightPanelState)/, "=__Machine$1");
|
|
103
|
-
Object.defineProperty(globalThis, "__Machine", {
|
|
104
|
-
set: ($: StateMachine) => {
|
|
105
|
-
Machine = $;
|
|
106
|
-
|
|
107
|
-
queueMicrotask(() => {
|
|
108
|
-
ON = {
|
|
109
|
-
...ON,
|
|
110
|
-
...Machine.config.states!.disabled.on,
|
|
111
|
-
panel_close_click: [
|
|
112
|
-
{
|
|
113
|
-
target: "disabled",
|
|
114
|
-
},
|
|
115
|
-
],
|
|
116
|
-
};
|
|
117
|
-
delete ON.playback_autoplay_context_changed;
|
|
118
|
-
|
|
119
|
-
for (const [k, v] of Object.entries(Machine.config.states! as Record<string, any>)) {
|
|
120
|
-
if (k === "puffin_activation") {
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
v.on = new Proxy(v.on!, {
|
|
124
|
-
get(target, p, _receiver) {
|
|
125
|
-
// @ts-ignore
|
|
126
|
-
if (p.startsWith("bespoke_")) {
|
|
127
|
-
// @ts-ignore
|
|
128
|
-
return ON[p];
|
|
129
|
-
}
|
|
130
|
-
// @ts-ignore
|
|
131
|
-
return target[p];
|
|
132
|
-
},
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
Object.setPrototypeOf(Machine.config.states!, STATES);
|
|
137
|
-
|
|
138
|
-
Machine._options.actions = ACTIONS;
|
|
139
|
-
});
|
|
140
|
-
},
|
|
141
|
-
get: () => Machine,
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
// ! HACKY ALERT
|
|
145
|
-
str = str.replace(
|
|
146
|
-
/(case [a-zA-Z_\$][\w\$]*\.[a-zA-Z_\$][\w\$]*\.Disabled:return!0;default:)/,
|
|
147
|
-
"$1return true;",
|
|
148
|
-
);
|
|
149
|
-
|
|
150
|
-
return str;
|
|
151
|
-
},
|
|
152
|
-
{
|
|
153
|
-
glob: /^\/xpui\.js/,
|
|
154
|
-
},
|
|
155
|
-
);
|
|
156
|
-
|
|
157
|
-
transformer(
|
|
158
|
-
(emit) => (str) => {
|
|
159
|
-
emit();
|
|
160
|
-
|
|
161
|
-
str = str.replace(/(\(([a-zA-Z_\$][\w\$]*),"PanelSection".+?children:\[?)/, "$1__renderPanel($2),");
|
|
75
|
+
controller(panel: PanelRegistration<React.ReactNode>): PanelController | undefined {
|
|
76
|
+
return this.controllers.get(panel);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
162
79
|
|
|
163
|
-
|
|
164
|
-
},
|
|
165
|
-
{
|
|
166
|
-
glob: /^\/dwp\-panel\-section\.js/,
|
|
167
|
-
},
|
|
168
|
-
);
|
|
80
|
+
export default new PanelRegistry();
|
|
@@ -63,7 +63,7 @@ export type PlaybarButtonProps = {
|
|
|
63
63
|
isActiveNoIndicator?: boolean;
|
|
64
64
|
disabled?: boolean;
|
|
65
65
|
icon?: string;
|
|
66
|
-
onClick: () => void;
|
|
66
|
+
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
|
67
67
|
/** Position within the module-button group; lower renders earlier. Read by the mount sorter. */
|
|
68
68
|
order?: number;
|
|
69
69
|
};
|
|
@@ -97,11 +97,12 @@ mountRegistryAnchor({
|
|
|
97
97
|
const route = items.find((item: any) => {
|
|
98
98
|
const path = item?.props?.path;
|
|
99
99
|
return typeof path === "string" && matchRoute(pathname, path);
|
|
100
|
-
}) as { props: { element?: unknown; children?: unknown } } | undefined;
|
|
100
|
+
}) as { props: { path: string; element?: unknown; children?: unknown } } | undefined;
|
|
101
101
|
if (!route) return null;
|
|
102
102
|
return (React as any).createElement(
|
|
103
103
|
"div",
|
|
104
104
|
{
|
|
105
|
+
key: route.props.path,
|
|
105
106
|
className: "spicetify-route-page",
|
|
106
107
|
style: {
|
|
107
108
|
position: "absolute",
|
|
@@ -8,9 +8,9 @@ import { SettingsSection } from "../../lib/primitives.tsx";
|
|
|
8
8
|
import { Registry } from "./registry.ts";
|
|
9
9
|
import settingsSection from "./settingsSection.ts";
|
|
10
10
|
|
|
11
|
-
// Rows from every module, rendered together under one "
|
|
12
|
-
//
|
|
13
|
-
// row here instead of claiming a whole section (or a
|
|
11
|
+
// Rows from every module, rendered together under one "General" section on
|
|
12
|
+
// the standalone Spicetify Settings page. A module with a single setting
|
|
13
|
+
// registers a row here instead of claiming a whole section (or a menu slot) of
|
|
14
14
|
// its own; SettingsToggleRow (lib/primitives.tsx) is the one-boolean case.
|
|
15
15
|
const listeners = new Set<() => void>();
|
|
16
16
|
|
|
@@ -38,7 +38,7 @@ const SpicetifyGroup = () => {
|
|
|
38
38
|
const rows = registry.all();
|
|
39
39
|
if (rows.length === 0) return null;
|
|
40
40
|
return (
|
|
41
|
-
<SettingsSection title="
|
|
41
|
+
<SettingsSection title="General">
|
|
42
42
|
{rows.map((row, i) => (
|
|
43
43
|
<React.Fragment key={i}>{row}</React.Fragment>
|
|
44
44
|
))}
|