@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
|
@@ -25,7 +25,12 @@ import {
|
|
|
25
25
|
SETTINGS_ROW_CLASS,
|
|
26
26
|
SETTINGS_ROW_CONTROL_CLASS,
|
|
27
27
|
SETTINGS_ROW_LABEL_CLASS,
|
|
28
|
+
SETTINGS_ROW_TEXT_CLASS,
|
|
29
|
+
SETTINGS_ACTION_GROUP_CLASS,
|
|
30
|
+
SETTINGS_HELP_TEXT_CLASS,
|
|
31
|
+
SETTINGS_LABEL_COPY_CLASS,
|
|
28
32
|
SETTINGS_SECTION_CLASS,
|
|
33
|
+
SETTINGS_SECTION_HEADING_CLASS,
|
|
29
34
|
badgeClass,
|
|
30
35
|
buttonClass,
|
|
31
36
|
type BadgeTone,
|
|
@@ -34,6 +39,7 @@ import {
|
|
|
34
39
|
chipClass,
|
|
35
40
|
DIALOG_BODY_CLASS,
|
|
36
41
|
DIALOG_CLASS,
|
|
42
|
+
DIALOG_LARGE_CLASS,
|
|
37
43
|
DIALOG_HEADER_CLASS,
|
|
38
44
|
ICON_BUTTON_CLASS,
|
|
39
45
|
MENU_ITEM_CLASS,
|
|
@@ -41,8 +47,11 @@ import {
|
|
|
41
47
|
SEARCHBAR_CLASS,
|
|
42
48
|
SELECT_CLASS,
|
|
43
49
|
} from "./primitives-classes.ts";
|
|
50
|
+
import { activateDialog } from "./dialog-lifecycle.ts";
|
|
51
|
+
import { Tooltip } from "./floating.tsx";
|
|
44
52
|
|
|
45
53
|
export type { BadgeTone, ButtonVariant } from "./primitives-classes.ts";
|
|
54
|
+
export { Popover, PopoverMenu, PopoverMenuItem, Tooltip } from "./floating.tsx";
|
|
46
55
|
|
|
47
56
|
// ---------- buttons ----------
|
|
48
57
|
|
|
@@ -75,15 +84,40 @@ export const IconButton: React.FC<{
|
|
|
75
84
|
</button>
|
|
76
85
|
);
|
|
77
86
|
|
|
87
|
+
// One shared ordering affordance for provider lists and other short ordered
|
|
88
|
+
// settings. Keeping the arrows here prevents each module from inventing a
|
|
89
|
+
// different size, icon, disabled state, or accessible label.
|
|
90
|
+
export const ReorderButtons: React.FC<{
|
|
91
|
+
label: string;
|
|
92
|
+
disableUp: boolean;
|
|
93
|
+
disableDown: boolean;
|
|
94
|
+
onMove: (direction: -1 | 1) => void;
|
|
95
|
+
}> = (props) => (
|
|
96
|
+
<span className={SETTINGS_ACTION_GROUP_CLASS}>
|
|
97
|
+
<IconButton ariaLabel={`Move ${props.label} up`} disabled={props.disableUp} onClick={() => props.onMove(-1)}>
|
|
98
|
+
↑
|
|
99
|
+
</IconButton>
|
|
100
|
+
<IconButton ariaLabel={`Move ${props.label} down`} disabled={props.disableDown} onClick={() => props.onMove(1)}>
|
|
101
|
+
↓
|
|
102
|
+
</IconButton>
|
|
103
|
+
</span>
|
|
104
|
+
);
|
|
105
|
+
|
|
78
106
|
// ---------- inputs ----------
|
|
79
107
|
|
|
80
108
|
export function Select<T extends string>(props: {
|
|
81
109
|
options: ReadonlyArray<{ value: T; label: string }>;
|
|
82
110
|
value: T;
|
|
83
111
|
onChange: (value: T) => void;
|
|
112
|
+
ariaLabel?: string;
|
|
84
113
|
}): React.ReactElement {
|
|
85
114
|
return (
|
|
86
|
-
<select
|
|
115
|
+
<select
|
|
116
|
+
className={SELECT_CLASS}
|
|
117
|
+
value={props.value}
|
|
118
|
+
aria-label={props.ariaLabel}
|
|
119
|
+
onChange={(e) => props.onChange(e.target.value as T)}
|
|
120
|
+
>
|
|
87
121
|
{props.options.map((option) => (
|
|
88
122
|
<option key={option.value} value={option.value}>
|
|
89
123
|
{option.label}
|
|
@@ -97,7 +131,11 @@ export const TextInput: React.FC<{
|
|
|
97
131
|
placeholder?: string;
|
|
98
132
|
value?: string;
|
|
99
133
|
disabled?: boolean;
|
|
134
|
+
readOnly?: boolean;
|
|
100
135
|
onInput?: (value: string) => void;
|
|
136
|
+
onFocus?: React.FocusEventHandler<HTMLInputElement>;
|
|
137
|
+
onBlur?: React.FocusEventHandler<HTMLInputElement>;
|
|
138
|
+
ariaLabel?: string;
|
|
101
139
|
}> = (props) => (
|
|
102
140
|
<input
|
|
103
141
|
type="text"
|
|
@@ -105,6 +143,10 @@ export const TextInput: React.FC<{
|
|
|
105
143
|
placeholder={props.placeholder}
|
|
106
144
|
value={props.value}
|
|
107
145
|
disabled={props.disabled}
|
|
146
|
+
readOnly={props.readOnly}
|
|
147
|
+
aria-label={props.ariaLabel}
|
|
148
|
+
onFocus={props.onFocus}
|
|
149
|
+
onBlur={props.onBlur}
|
|
108
150
|
onChange={(e) => props.onInput?.(e.target.value)}
|
|
109
151
|
/>
|
|
110
152
|
);
|
|
@@ -136,13 +178,12 @@ export const Card: React.FC<{ children: React.ReactNode }> = (props) => (
|
|
|
136
178
|
|
|
137
179
|
// ---------- settings ----------
|
|
138
180
|
|
|
139
|
-
// Native-looking settings chrome for the `settingsSection` register
|
|
140
|
-
// render the client's own section
|
|
141
|
-
//
|
|
142
|
-
// does not source-transform those, so a capture there never resolves.
|
|
181
|
+
// Native-looking settings chrome for the `settingsSection` register on the
|
|
182
|
+
// standalone Spicetify Settings page. These render the client's own section
|
|
183
|
+
// and row structure rather than capturing its route-local components.
|
|
143
184
|
export const SettingsSection: React.FC<{ title?: React.ReactNode; children: React.ReactNode }> = (props) => (
|
|
144
185
|
<div className={SETTINGS_SECTION_CLASS}>
|
|
145
|
-
{props.title === undefined ? null : <h2>{props.title}</h2>}
|
|
186
|
+
{props.title === undefined ? null : <h2 className={SETTINGS_SECTION_HEADING_CLASS}>{props.title}</h2>}
|
|
146
187
|
{props.children}
|
|
147
188
|
</div>
|
|
148
189
|
);
|
|
@@ -155,18 +196,79 @@ export const SettingsRow: React.FC<{ label: React.ReactNode; htmlFor?: string; c
|
|
|
155
196
|
) => (
|
|
156
197
|
<div className={SETTINGS_ROW_CLASS}>
|
|
157
198
|
<div className={SETTINGS_ROW_LABEL_CLASS}>
|
|
158
|
-
{props.htmlFor === undefined ?
|
|
199
|
+
{props.htmlFor === undefined ? (
|
|
200
|
+
<span className={SETTINGS_ROW_TEXT_CLASS}>{props.label}</span>
|
|
201
|
+
) : (
|
|
202
|
+
<label className={SETTINGS_ROW_TEXT_CLASS} htmlFor={props.htmlFor}>
|
|
203
|
+
{props.label}
|
|
204
|
+
</label>
|
|
205
|
+
)}
|
|
159
206
|
</div>
|
|
160
207
|
<div className={SETTINGS_ROW_CONTROL_CLASS}>{props.children}</div>
|
|
161
208
|
</div>
|
|
162
209
|
);
|
|
163
210
|
|
|
211
|
+
export const SettingsLabel: React.FC<{ label: React.ReactNode; description?: React.ReactNode }> = (props) => (
|
|
212
|
+
<span className={SETTINGS_LABEL_COPY_CLASS}>
|
|
213
|
+
<span>{props.label}</span>
|
|
214
|
+
{props.description === undefined ? null : <span className={SETTINGS_HELP_TEXT_CLASS}>{props.description}</span>}
|
|
215
|
+
</span>
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
export const SettingsActions: React.FC<{ children: React.ReactNode }> = (props) => (
|
|
219
|
+
<span className={SETTINGS_ACTION_GROUP_CLASS}>{props.children}</span>
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
export const SettingsButtonRow: React.FC<{
|
|
223
|
+
label: React.ReactNode;
|
|
224
|
+
description?: React.ReactNode;
|
|
225
|
+
buttonLabel: React.ReactNode;
|
|
226
|
+
disabled?: boolean;
|
|
227
|
+
variant?: ButtonVariant;
|
|
228
|
+
onClick: () => void;
|
|
229
|
+
}> = (props) => (
|
|
230
|
+
<SettingsRow label={<SettingsLabel label={props.label} description={props.description} />}>
|
|
231
|
+
<Button variant={props.variant ?? "secondary"} disabled={props.disabled} onClick={props.onClick}>
|
|
232
|
+
{props.buttonLabel}
|
|
233
|
+
</Button>
|
|
234
|
+
</SettingsRow>
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
export const SettingsTextInputRow: React.FC<{
|
|
238
|
+
label: React.ReactNode;
|
|
239
|
+
description?: React.ReactNode;
|
|
240
|
+
value: string;
|
|
241
|
+
placeholder?: string;
|
|
242
|
+
ariaLabel?: string;
|
|
243
|
+
onInput: (value: string) => void;
|
|
244
|
+
actionLabel?: React.ReactNode;
|
|
245
|
+
actionDisabled?: boolean;
|
|
246
|
+
onAction?: () => void;
|
|
247
|
+
}> = (props) => (
|
|
248
|
+
<SettingsRow label={<SettingsLabel label={props.label} description={props.description} />}>
|
|
249
|
+
<SettingsActions>
|
|
250
|
+
<TextInput
|
|
251
|
+
value={props.value}
|
|
252
|
+
placeholder={props.placeholder}
|
|
253
|
+
ariaLabel={props.ariaLabel}
|
|
254
|
+
onInput={props.onInput}
|
|
255
|
+
/>
|
|
256
|
+
{props.actionLabel === undefined || props.onAction === undefined ? null : (
|
|
257
|
+
<Button variant="secondary" disabled={props.actionDisabled} onClick={props.onAction}>
|
|
258
|
+
{props.actionLabel}
|
|
259
|
+
</Button>
|
|
260
|
+
)}
|
|
261
|
+
</SettingsActions>
|
|
262
|
+
</SettingsRow>
|
|
263
|
+
);
|
|
264
|
+
|
|
164
265
|
// The client's own switch structure around a native checkbox. Its semantic
|
|
165
266
|
// classes are recreated by the CLI css-map, so Spotify and themes style this
|
|
166
267
|
// exactly like the adjacent settings controls.
|
|
167
268
|
export const Toggle: React.FC<{
|
|
168
269
|
id?: string;
|
|
169
270
|
ariaLabel?: string;
|
|
271
|
+
disabled?: boolean;
|
|
170
272
|
value: boolean;
|
|
171
273
|
onChange: (value: boolean) => void;
|
|
172
274
|
}> = (props) => (
|
|
@@ -177,6 +279,7 @@ export const Toggle: React.FC<{
|
|
|
177
279
|
id={props.id}
|
|
178
280
|
aria-label={props.ariaLabel}
|
|
179
281
|
checked={props.value}
|
|
282
|
+
disabled={props.disabled}
|
|
180
283
|
onKeyDown={activateToggleOnKeyDown}
|
|
181
284
|
onChange={(e) => props.onChange((e.target as HTMLInputElement).checked)}
|
|
182
285
|
/>
|
|
@@ -186,6 +289,46 @@ export const Toggle: React.FC<{
|
|
|
186
289
|
</label>
|
|
187
290
|
);
|
|
188
291
|
|
|
292
|
+
export const SettingsProviderRow: React.FC<{
|
|
293
|
+
label: string;
|
|
294
|
+
description?: React.ReactNode;
|
|
295
|
+
value: boolean;
|
|
296
|
+
disabled?: boolean;
|
|
297
|
+
disabledReason?: string;
|
|
298
|
+
index: number;
|
|
299
|
+
total: number;
|
|
300
|
+
onChange: (value: boolean) => void;
|
|
301
|
+
onMove: (direction: -1 | 1) => void;
|
|
302
|
+
}> = (props) => {
|
|
303
|
+
const id = React.useId();
|
|
304
|
+
const toggle = (
|
|
305
|
+
<Toggle
|
|
306
|
+
id={id}
|
|
307
|
+
ariaLabel={`${props.label} provider`}
|
|
308
|
+
value={props.value}
|
|
309
|
+
disabled={props.disabled}
|
|
310
|
+
onChange={props.onChange}
|
|
311
|
+
/>
|
|
312
|
+
);
|
|
313
|
+
return (
|
|
314
|
+
<SettingsRow label={<SettingsLabel label={props.label} description={props.description} />} htmlFor={id}>
|
|
315
|
+
<SettingsActions>
|
|
316
|
+
<ReorderButtons
|
|
317
|
+
label={props.label}
|
|
318
|
+
disableUp={props.index === 0}
|
|
319
|
+
disableDown={props.index === props.total - 1}
|
|
320
|
+
onMove={props.onMove}
|
|
321
|
+
/>
|
|
322
|
+
{props.disabled && props.disabledReason ? (
|
|
323
|
+
<Tooltip label={props.disabledReason}>{toggle}</Tooltip>
|
|
324
|
+
) : (
|
|
325
|
+
toggle
|
|
326
|
+
)}
|
|
327
|
+
</SettingsActions>
|
|
328
|
+
</SettingsRow>
|
|
329
|
+
);
|
|
330
|
+
};
|
|
331
|
+
|
|
189
332
|
// The "module with one boolean setting" pattern as a single row for the
|
|
190
333
|
// `settingsRow` register. `getValue` is read lazily on every mount so
|
|
191
334
|
// reopening settings always reflects the current state; the element itself
|
|
@@ -275,34 +418,41 @@ export const ConfirmButton: React.FC<{
|
|
|
275
418
|
// transformed ancestor (the norm for Spotify's scroll containers) would
|
|
276
419
|
// otherwise be clipped/offset to that ancestor. The backdrop and the ×
|
|
277
420
|
// button both call onClose.
|
|
278
|
-
export const Dialog: React.FC<{
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
421
|
+
export const Dialog: React.FC<{
|
|
422
|
+
title: string;
|
|
423
|
+
onClose: () => void;
|
|
424
|
+
size?: "normal" | "large";
|
|
425
|
+
children: React.ReactNode;
|
|
426
|
+
}> = (props) => {
|
|
427
|
+
const dialogRef = React.useRef<HTMLDivElement>(null);
|
|
428
|
+
const onCloseRef = React.useRef(props.onClose);
|
|
429
|
+
const titleId = React.useId();
|
|
430
|
+
onCloseRef.current = props.onClose;
|
|
283
431
|
React.useEffect(() => {
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}
|
|
290
|
-
};
|
|
291
|
-
document.addEventListener("keydown", onKey, true);
|
|
292
|
-
return () => document.removeEventListener("keydown", onKey, true);
|
|
293
|
-
}, [onClose]);
|
|
432
|
+
const dialog = dialogRef.current;
|
|
433
|
+
if (!dialog) return;
|
|
434
|
+
return activateDialog(dialog, () => onCloseRef.current());
|
|
435
|
+
}, []);
|
|
436
|
+
const dialogClass = props.size === "large" ? `${DIALOG_CLASS} ${DIALOG_LARGE_CLASS}` : DIALOG_CLASS;
|
|
294
437
|
|
|
295
438
|
return ReactDOM.createPortal(
|
|
296
439
|
<div
|
|
297
440
|
className={SCRIM_CLASS}
|
|
298
441
|
onClick={(e) => {
|
|
299
|
-
if (e.target === e.currentTarget) onClose();
|
|
442
|
+
if (e.target === e.currentTarget) props.onClose();
|
|
300
443
|
}}
|
|
301
444
|
>
|
|
302
|
-
<div
|
|
445
|
+
<div
|
|
446
|
+
ref={dialogRef}
|
|
447
|
+
className={dialogClass}
|
|
448
|
+
role="dialog"
|
|
449
|
+
aria-modal="true"
|
|
450
|
+
aria-labelledby={titleId}
|
|
451
|
+
tabIndex={-1}
|
|
452
|
+
>
|
|
303
453
|
<div className={DIALOG_HEADER_CLASS}>
|
|
304
|
-
<h2>{props.title}</h2>
|
|
305
|
-
<IconButton ariaLabel="Close" onClick={onClose}>
|
|
454
|
+
<h2 id={titleId}>{props.title}</h2>
|
|
455
|
+
<IconButton ariaLabel="Close" onClick={props.onClose}>
|
|
306
456
|
×
|
|
307
457
|
</IconButton>
|
|
308
458
|
</div>
|
package/vendor/stdlib/mod.ts
CHANGED
|
@@ -18,9 +18,12 @@ export * from "./lib/primitives-vanilla.ts";
|
|
|
18
18
|
export * from "./src/storage.ts";
|
|
19
19
|
export * from "./src/wpunpk.ts";
|
|
20
20
|
export * from "./src/logger.ts";
|
|
21
|
+
export { display as displayModal, hide as hideModal, type ModalOptions } from "./lib/modal.tsx";
|
|
22
|
+
export { React, ReactDOM, ReactDOMServer } from "./src/expose/React.ts";
|
|
23
|
+
export { Platform } from "./src/expose/Platform.ts";
|
|
24
|
+
export { createIconComponent } from "./src/createIconComponent.tsx";
|
|
25
|
+
export { startCase } from "./deps.ts";
|
|
21
26
|
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
export
|
|
25
|
-
export * from "./src/webpack/ComponentLibrary.xpui.ts";
|
|
26
|
-
export * from "./src/webpack/ReactComponents.xpui.ts";
|
|
27
|
+
// Palette Manager needs the client's Color constructor and its companion
|
|
28
|
+
// type. Export that semantic capability without exposing the webpack module.
|
|
29
|
+
export { Color } from "./src/webpack/misc.xpui.ts";
|
|
@@ -13,10 +13,28 @@
|
|
|
13
13
|
* modules.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
import type { ModalOptions } from "../lib/modal.tsx";
|
|
17
|
+
|
|
16
18
|
type SpicetifyRuntime = typeof Spicetify;
|
|
17
19
|
|
|
18
20
|
export type PlayerState = Spicetify.PlayerState;
|
|
19
21
|
|
|
22
|
+
export interface PopupModalCompatibility {
|
|
23
|
+
display(options: ModalOptions): void;
|
|
24
|
+
hide(): void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface DaemonCapabilities {
|
|
28
|
+
available(): Promise<boolean>;
|
|
29
|
+
apply(): Promise<unknown>;
|
|
30
|
+
blockUpdates(): Promise<unknown>;
|
|
31
|
+
unblockUpdates(): Promise<unknown>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface SnackbarCapabilities {
|
|
35
|
+
enqueueSnackbar(message: string, options?: { variant?: string }): unknown;
|
|
36
|
+
}
|
|
37
|
+
|
|
20
38
|
export interface ClientCapabilities {
|
|
21
39
|
readonly player: typeof Spicetify.Player;
|
|
22
40
|
readonly platform: typeof Spicetify.Platform;
|
|
@@ -30,9 +48,14 @@ export interface ClientCapabilities {
|
|
|
30
48
|
readonly keyboard: typeof Spicetify.Keyboard;
|
|
31
49
|
readonly mousetrap: typeof Spicetify.Mousetrap;
|
|
32
50
|
readonly contextMenu: typeof Spicetify.ContextMenu;
|
|
33
|
-
|
|
51
|
+
/** @deprecated Import displayModal and hideModal from /modules/stdlib/mod.ts. */
|
|
52
|
+
readonly popupModal: PopupModalCompatibility;
|
|
34
53
|
readonly config: typeof Spicetify.Config;
|
|
35
54
|
readonly modules: typeof Spicetify.Modules;
|
|
55
|
+
/** V3 manifest version, with the legacy wrapper config as a fallback. */
|
|
56
|
+
readonly spicetifyVersion: string | undefined;
|
|
57
|
+
readonly daemon: DaemonCapabilities | undefined;
|
|
58
|
+
readonly snackbar: SnackbarCapabilities | undefined;
|
|
36
59
|
readonly react: typeof Spicetify.React;
|
|
37
60
|
readonly reactDOM: typeof Spicetify.ReactDOM;
|
|
38
61
|
readonly tippy: typeof Spicetify.Tippy;
|
|
@@ -47,6 +70,26 @@ const runtime = (): SpicetifyRuntime => {
|
|
|
47
70
|
return value;
|
|
48
71
|
};
|
|
49
72
|
|
|
73
|
+
const nonEmptyVersion = (value: unknown): string | undefined =>
|
|
74
|
+
typeof value === "string" && value.trim() ? value : undefined;
|
|
75
|
+
|
|
76
|
+
let modalRequestGeneration = 0;
|
|
77
|
+
const reportModalFailure = (error: unknown) => console.error("[stdlib] failed to load the owned modal:", error);
|
|
78
|
+
const popupModalCompatibility: PopupModalCompatibility = {
|
|
79
|
+
display(options) {
|
|
80
|
+
const generation = ++modalRequestGeneration;
|
|
81
|
+
void import("../lib/modal.tsx")
|
|
82
|
+
.then(({ display }) => {
|
|
83
|
+
if (generation === modalRequestGeneration) display(options);
|
|
84
|
+
})
|
|
85
|
+
.catch(reportModalFailure);
|
|
86
|
+
},
|
|
87
|
+
hide() {
|
|
88
|
+
++modalRequestGeneration;
|
|
89
|
+
void import("../lib/modal.tsx").then(({ hide }) => hide()).catch(reportModalFailure);
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
50
93
|
export const client: ClientCapabilities = {
|
|
51
94
|
get player() {
|
|
52
95
|
return runtime().Player;
|
|
@@ -85,7 +128,7 @@ export const client: ClientCapabilities = {
|
|
|
85
128
|
return runtime().ContextMenu;
|
|
86
129
|
},
|
|
87
130
|
get popupModal() {
|
|
88
|
-
return
|
|
131
|
+
return popupModalCompatibility;
|
|
89
132
|
},
|
|
90
133
|
get config() {
|
|
91
134
|
return runtime().Config;
|
|
@@ -93,6 +136,26 @@ export const client: ClientCapabilities = {
|
|
|
93
136
|
get modules() {
|
|
94
137
|
return runtime().Modules;
|
|
95
138
|
},
|
|
139
|
+
get spicetifyVersion() {
|
|
140
|
+
const current = runtime() as unknown as {
|
|
141
|
+
Modules?: {
|
|
142
|
+
manifest?: { cliVersion?: unknown };
|
|
143
|
+
registry?: { manifest?: { cliVersion?: unknown } };
|
|
144
|
+
};
|
|
145
|
+
Config?: { version?: unknown };
|
|
146
|
+
};
|
|
147
|
+
return (
|
|
148
|
+
nonEmptyVersion(current.Modules?.manifest?.cliVersion) ??
|
|
149
|
+
nonEmptyVersion(current.Modules?.registry?.manifest?.cliVersion) ??
|
|
150
|
+
nonEmptyVersion(current.Config?.version)
|
|
151
|
+
);
|
|
152
|
+
},
|
|
153
|
+
get daemon() {
|
|
154
|
+
return (runtime() as unknown as { Daemon?: DaemonCapabilities }).Daemon;
|
|
155
|
+
},
|
|
156
|
+
get snackbar() {
|
|
157
|
+
return (runtime() as unknown as { Snackbar?: SnackbarCapabilities }).Snackbar;
|
|
158
|
+
},
|
|
96
159
|
get react() {
|
|
97
160
|
return runtime().React;
|
|
98
161
|
},
|
|
@@ -11,16 +11,22 @@ import menu from "./menu.ts";
|
|
|
11
11
|
import { mountAdjacent } from "./mount.ts";
|
|
12
12
|
import { isNativeAnchor, type NativeAnchor, resolveNativeAnchor } from "./nativeAnchors.ts";
|
|
13
13
|
import navlink from "./navlink.tsx";
|
|
14
|
-
import panel from "./panel.ts";
|
|
14
|
+
import panel, { type PanelController, type PanelRegistration } from "./panel.ts";
|
|
15
15
|
import playbarButton, { PlaybarButton } from "./playbarButton.tsx";
|
|
16
16
|
import playbarWidget from "./playbarWidget.tsx";
|
|
17
17
|
import { Registry } from "./registry.ts";
|
|
18
18
|
import root from "./root.ts";
|
|
19
19
|
import route from "./route.ts";
|
|
20
20
|
import settingsRow from "./settingsRow.tsx";
|
|
21
|
-
import settingsSection from "./settingsSection.ts";
|
|
21
|
+
import settingsSection, { settingsAction } from "./settingsSection.ts";
|
|
22
22
|
import topbarLeftButton, { TopbarLeftButton } from "./topbarLeftButton.tsx";
|
|
23
23
|
import topbarRightButton, { TopbarRightButton } from "./topbarRightButton.tsx";
|
|
24
|
+
export type { PanelController, PanelRegistration, PanelWidth } from "./panel.ts";
|
|
25
|
+
export { closeMenu, openedFromProfileMenu, useMenuItem } from "./menu.ts";
|
|
26
|
+
export { NavLink, type NavLinkProps, useHistoryRefresh } from "./navlink.tsx";
|
|
27
|
+
export { PlaybarButton, type PlaybarButtonProps } from "./playbarButton.tsx";
|
|
28
|
+
export { TopbarLeftButton } from "./topbarLeftButton.tsx";
|
|
29
|
+
export { TopbarRightButton } from "./topbarRightButton.tsx";
|
|
24
30
|
const [rootChild, rootProvider] = root;
|
|
25
31
|
const registers = {
|
|
26
32
|
menu,
|
|
@@ -32,12 +38,15 @@ const registers = {
|
|
|
32
38
|
rootProvider,
|
|
33
39
|
route,
|
|
34
40
|
settingsRow,
|
|
41
|
+
settingsAction,
|
|
35
42
|
settingsSection,
|
|
36
43
|
topbarLeftButton,
|
|
37
44
|
topbarRightButton,
|
|
38
45
|
} satisfies Record<string, Registry<any>>;
|
|
39
46
|
type Registers = typeof registers;
|
|
40
47
|
|
|
48
|
+
export { SPICETIFY_SETTINGS_ROUTE } from "./settingsSection.ts";
|
|
49
|
+
|
|
41
50
|
// ---- placeButton: one ergonomic, position-controllable way to add a button ----
|
|
42
51
|
// Instead of picking the right register key and hand-building the matching
|
|
43
52
|
// component (easy to mismatch, no ordering), a module names a location and
|
|
@@ -52,7 +61,7 @@ export interface PlaceButtonOptions {
|
|
|
52
61
|
label: string;
|
|
53
62
|
/** Inner SVG markup drawn on the stdlib 16-grid. */
|
|
54
63
|
icon?: string;
|
|
55
|
-
onClick: () => void;
|
|
64
|
+
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
|
56
65
|
disabled?: boolean;
|
|
57
66
|
/** Position within this slot's module buttons; lower renders earlier. Default 0. */
|
|
58
67
|
order?: number;
|
|
@@ -73,6 +82,11 @@ export interface ButtonHandle {
|
|
|
73
82
|
remove(): void;
|
|
74
83
|
}
|
|
75
84
|
|
|
85
|
+
export interface RegisterPanelOptions extends Omit<PanelRegistration<React.ReactNode>, "id"> {
|
|
86
|
+
/** Stable within this module. Defaults to "default". */
|
|
87
|
+
id?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
76
90
|
const BUTTON_SLOTS = {
|
|
77
91
|
"topbar-left": ["topbarLeftButton", TopbarLeftButton],
|
|
78
92
|
"topbar-right": ["topbarRightButton", TopbarRightButton],
|
|
@@ -139,6 +153,29 @@ export class Registrar {
|
|
|
139
153
|
};
|
|
140
154
|
}
|
|
141
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Register a Spicetify-owned right-sidebar panel. The returned controller
|
|
158
|
+
* opens, closes and observes the panel; module unload disposes it.
|
|
159
|
+
*/
|
|
160
|
+
registerPanel(options: RegisterPanelOptions): PanelController {
|
|
161
|
+
const registration: PanelRegistration<React.ReactNode> = {
|
|
162
|
+
...options,
|
|
163
|
+
id: `${this.id}:${options.id ?? "default"}`,
|
|
164
|
+
};
|
|
165
|
+
panel.add(registration);
|
|
166
|
+
const controller = panel.controller(registration);
|
|
167
|
+
if (!controller) throw new Error(`[stdlib] failed to register panel: ${registration.id}`);
|
|
168
|
+
const dispose = () => panel.delete(registration);
|
|
169
|
+
this.disposers.add(dispose);
|
|
170
|
+
return {
|
|
171
|
+
...controller,
|
|
172
|
+
dispose: () => {
|
|
173
|
+
this.disposers.delete(dispose);
|
|
174
|
+
dispose();
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
142
179
|
register<R extends keyof Registers>(type: R, ...args: Parameters<Registers[R]["add"]>) {
|
|
143
180
|
this.ledger.set(args[0], type);
|
|
144
181
|
// @ts-ignore
|