@spicetify/kit 0.1.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/bin/spicetify-kit.js +4 -0
- package/dist/build.js +335 -0
- package/dist/check.js +284 -0
- package/dist/classmap.js +187 -0
- package/dist/cli.js +53 -0
- package/dist/create.js +429 -0
- package/dist/dev.js +100 -0
- package/dist/from-theme.js +96 -0
- package/dist/install.js +60 -0
- package/dist/launch.js +122 -0
- package/dist/pack.js +31 -0
- package/dist/push.js +211 -0
- package/dist/vault.js +108 -0
- package/package.json +49 -0
- package/vendor/classmaps/1020096/classmap-19f856aefd5.json +109 -0
- package/vendor/shims/chunks.d.ts +10 -0
- package/vendor/shims/remote-modules.d.ts +6 -0
- package/vendor/shims/spicetify.d.ts +2437 -0
- package/vendor/stdlib/deps.ts +24 -0
- package/vendor/stdlib/index.ts +24 -0
- package/vendor/stdlib/lib/modal.tsx +128 -0
- package/vendor/stdlib/lib/popover.ts +84 -0
- package/vendor/stdlib/lib/primitives-classes.ts +66 -0
- package/vendor/stdlib/lib/primitives-vanilla.ts +277 -0
- package/vendor/stdlib/lib/primitives.tsx +314 -0
- package/vendor/stdlib/load.ts +17 -0
- package/vendor/stdlib/metadata.json +18 -0
- package/vendor/stdlib/mixin.ts +32 -0
- package/vendor/stdlib/mod.ts +26 -0
- package/vendor/stdlib/src/chunks.d.ts +10 -0
- package/vendor/stdlib/src/client.ts +114 -0
- package/vendor/stdlib/src/createIconComponent.tsx +29 -0
- package/vendor/stdlib/src/events.mix.ts +55 -0
- package/vendor/stdlib/src/events.ts +58 -0
- package/vendor/stdlib/src/expose/GraphQL.ts +40 -0
- package/vendor/stdlib/src/expose/Platform.ts +48 -0
- package/vendor/stdlib/src/expose/React.ts +95 -0
- package/vendor/stdlib/src/expose/ReactFlipToolkitSpring.ts +27 -0
- package/vendor/stdlib/src/expose/ReduxStore.ts +39 -0
- package/vendor/stdlib/src/expose/SettingsSection.ts +60 -0
- package/vendor/stdlib/src/expose/Snackbar.ts +34 -0
- package/vendor/stdlib/src/expose/Tippy.ts +26 -0
- package/vendor/stdlib/src/expose/enqueueImageSnackbar.ts +25 -0
- package/vendor/stdlib/src/expose/index.ts +14 -0
- package/vendor/stdlib/src/expose/jsx-runtime.ts +46 -0
- package/vendor/stdlib/src/expose/react-dom-shim.ts +50 -0
- package/vendor/stdlib/src/expose/react-shim.ts +116 -0
- package/vendor/stdlib/src/logger.ts +35 -0
- package/vendor/stdlib/src/playbar-compat.tsx +136 -0
- package/vendor/stdlib/src/registers/index.ts +176 -0
- package/vendor/stdlib/src/registers/menu.ts +160 -0
- package/vendor/stdlib/src/registers/mount.ts +307 -0
- package/vendor/stdlib/src/registers/nativeAnchors.ts +49 -0
- package/vendor/stdlib/src/registers/navlink.tsx +165 -0
- package/vendor/stdlib/src/registers/order.ts +22 -0
- package/vendor/stdlib/src/registers/panel.ts +168 -0
- package/vendor/stdlib/src/registers/playbarButton.tsx +93 -0
- package/vendor/stdlib/src/registers/playbarWidget.tsx +71 -0
- package/vendor/stdlib/src/registers/registry.ts +12 -0
- package/vendor/stdlib/src/registers/root.ts +101 -0
- package/vendor/stdlib/src/registers/route.ts +123 -0
- package/vendor/stdlib/src/registers/settingsRow.tsx +56 -0
- package/vendor/stdlib/src/registers/settingsSection.ts +59 -0
- package/vendor/stdlib/src/registers/topbarLeftButton.tsx +86 -0
- package/vendor/stdlib/src/registers/topbarRightButton.tsx +87 -0
- package/vendor/stdlib/src/storage.ts +163 -0
- package/vendor/stdlib/src/util.ts +62 -0
- package/vendor/stdlib/src/utils/index.ts +16 -0
- package/vendor/stdlib/src/webpack/ClassNames.gen.ts +10 -0
- package/vendor/stdlib/src/webpack/ClassNames.ts +6 -0
- package/vendor/stdlib/src/webpack/ClassNames.xpui.ts +16 -0
- package/vendor/stdlib/src/webpack/ComponentLibrary.gen.ts +10 -0
- package/vendor/stdlib/src/webpack/ComponentLibrary.ts +6 -0
- package/vendor/stdlib/src/webpack/ComponentLibrary.xpui.ts +18 -0
- package/vendor/stdlib/src/webpack/FilterContext.gen.ts +10 -0
- package/vendor/stdlib/src/webpack/FilterContext.ts +6 -0
- package/vendor/stdlib/src/webpack/FilterContext.xpui.ts +10 -0
- package/vendor/stdlib/src/webpack/Mousetrap.gen.ts +10 -0
- package/vendor/stdlib/src/webpack/Mousetrap.ts +6 -0
- package/vendor/stdlib/src/webpack/Mousetrap.xpui.ts +12 -0
- package/vendor/stdlib/src/webpack/React.gen.ts +14 -0
- package/vendor/stdlib/src/webpack/React.ts +6 -0
- package/vendor/stdlib/src/webpack/React.xpui.ts +15 -0
- package/vendor/stdlib/src/webpack/ReactComponents.desktop.ts +17 -0
- package/vendor/stdlib/src/webpack/ReactComponents.gen.ts +74 -0
- package/vendor/stdlib/src/webpack/ReactComponents.panel.ts +19 -0
- package/vendor/stdlib/src/webpack/ReactComponents.ts +50 -0
- package/vendor/stdlib/src/webpack/ReactComponents.xpui.ts +164 -0
- package/vendor/stdlib/src/webpack/ReactFlipToolkit.gen.ts +12 -0
- package/vendor/stdlib/src/webpack/ReactFlipToolkit.ts +6 -0
- package/vendor/stdlib/src/webpack/ReactFlipToolkit.xpui.ts +14 -0
- package/vendor/stdlib/src/webpack/ReactHooks.fullscreen.ts +15 -0
- package/vendor/stdlib/src/webpack/ReactHooks.gen.ts +32 -0
- package/vendor/stdlib/src/webpack/ReactHooks.ts +6 -0
- package/vendor/stdlib/src/webpack/ReactHooks.xpui.ts +38 -0
- package/vendor/stdlib/src/webpack/ReactQuery.gen.ts +26 -0
- package/vendor/stdlib/src/webpack/ReactQuery.ts +6 -0
- package/vendor/stdlib/src/webpack/ReactQuery.xpui.ts +46 -0
- package/vendor/stdlib/src/webpack/ReactRouter.gen.ts +12 -0
- package/vendor/stdlib/src/webpack/ReactRouter.ts +6 -0
- package/vendor/stdlib/src/webpack/ReactRouter.xpui.ts +22 -0
- package/vendor/stdlib/src/webpack/Snackbar.gen.ts +12 -0
- package/vendor/stdlib/src/webpack/Snackbar.ts +6 -0
- package/vendor/stdlib/src/webpack/Snackbar.xpui.ts +27 -0
- package/vendor/stdlib/src/webpack/URI.gen.ts +26 -0
- package/vendor/stdlib/src/webpack/URI.ts +82 -0
- package/vendor/stdlib/src/webpack/URI.xpui.ts +111 -0
- package/vendor/stdlib/src/webpack/capture-readiness.ts +49 -0
- package/vendor/stdlib/src/webpack/index.ts +186 -0
- package/vendor/stdlib/src/webpack/misc.gen.ts +16 -0
- package/vendor/stdlib/src/webpack/misc.ts +6 -0
- package/vendor/stdlib/src/webpack/misc.xpui.ts +27 -0
- package/vendor/stdlib/src/wpunpk.mix.ts +104 -0
- package/vendor/stdlib/src/wpunpk.ts +36 -0
- package/vendor/stdlib/vendor/rxjs.d.ts +5 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Afonso Jorge Ramos
|
|
3
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The React tier of the component kit: the reusable UI primitives, the
|
|
8
|
+
* same Spotify-look chrome as the vanilla kit (lib/primitives-vanilla.ts) but as React
|
|
9
|
+
* components for module UI that already renders inside a React tree
|
|
10
|
+
* (routes, registers, modals). Both tiers apply the shared class contract
|
|
11
|
+
* (primitives-classes.ts) so they can never drift.
|
|
12
|
+
*
|
|
13
|
+
* Deep-import at runtime: `import { Button } from
|
|
14
|
+
* "/modules/stdlib/lib/primitives.js"`. This is not in the mod.ts barrel
|
|
15
|
+
* to avoid colliding with the vanilla kit's identical export names; a
|
|
16
|
+
* module uses one tier. Requires the client React instance, so only
|
|
17
|
+
* modules that declare stdlib as a dependency should use it (the vanilla
|
|
18
|
+
* kit is the React-free choice for standalone surfaces).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { React, ReactDOM } from "../src/expose/React.ts";
|
|
22
|
+
import {
|
|
23
|
+
activateToggleOnKeyDown,
|
|
24
|
+
TOGGLE_CLASSES,
|
|
25
|
+
SETTINGS_ROW_CLASS,
|
|
26
|
+
SETTINGS_ROW_CONTROL_CLASS,
|
|
27
|
+
SETTINGS_ROW_LABEL_CLASS,
|
|
28
|
+
SETTINGS_SECTION_CLASS,
|
|
29
|
+
badgeClass,
|
|
30
|
+
buttonClass,
|
|
31
|
+
type BadgeTone,
|
|
32
|
+
type ButtonVariant,
|
|
33
|
+
CARD_CLASS,
|
|
34
|
+
chipClass,
|
|
35
|
+
DIALOG_BODY_CLASS,
|
|
36
|
+
DIALOG_CLASS,
|
|
37
|
+
DIALOG_HEADER_CLASS,
|
|
38
|
+
ICON_BUTTON_CLASS,
|
|
39
|
+
MENU_ITEM_CLASS,
|
|
40
|
+
SCRIM_CLASS,
|
|
41
|
+
SEARCHBAR_CLASS,
|
|
42
|
+
SELECT_CLASS,
|
|
43
|
+
} from "./primitives-classes.ts";
|
|
44
|
+
|
|
45
|
+
export type { BadgeTone, ButtonVariant } from "./primitives-classes.ts";
|
|
46
|
+
|
|
47
|
+
// ---------- buttons ----------
|
|
48
|
+
|
|
49
|
+
export const Button: React.FC<{
|
|
50
|
+
variant?: ButtonVariant;
|
|
51
|
+
disabled?: boolean;
|
|
52
|
+
onClick?: () => void;
|
|
53
|
+
children: React.ReactNode;
|
|
54
|
+
}> = (props) => (
|
|
55
|
+
<button type="button" className={buttonClass(props.variant)} disabled={props.disabled} onClick={props.onClick}>
|
|
56
|
+
{props.children}
|
|
57
|
+
</button>
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// The circular icon button Spotify uses for modal close (Credits, etc).
|
|
61
|
+
export const IconButton: React.FC<{
|
|
62
|
+
ariaLabel: string;
|
|
63
|
+
disabled?: boolean;
|
|
64
|
+
onClick?: () => void;
|
|
65
|
+
children: React.ReactNode;
|
|
66
|
+
}> = (props) => (
|
|
67
|
+
<button
|
|
68
|
+
type="button"
|
|
69
|
+
className={ICON_BUTTON_CLASS}
|
|
70
|
+
aria-label={props.ariaLabel}
|
|
71
|
+
disabled={props.disabled}
|
|
72
|
+
onClick={props.onClick}
|
|
73
|
+
>
|
|
74
|
+
{props.children}
|
|
75
|
+
</button>
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// ---------- inputs ----------
|
|
79
|
+
|
|
80
|
+
export function Select<T extends string>(props: {
|
|
81
|
+
options: ReadonlyArray<{ value: T; label: string }>;
|
|
82
|
+
value: T;
|
|
83
|
+
onChange: (value: T) => void;
|
|
84
|
+
}): React.ReactElement {
|
|
85
|
+
return (
|
|
86
|
+
<select className={SELECT_CLASS} value={props.value} onChange={(e) => props.onChange(e.target.value as T)}>
|
|
87
|
+
{props.options.map((option) => (
|
|
88
|
+
<option key={option.value} value={option.value}>
|
|
89
|
+
{option.label}
|
|
90
|
+
</option>
|
|
91
|
+
))}
|
|
92
|
+
</select>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export const TextInput: React.FC<{
|
|
97
|
+
placeholder?: string;
|
|
98
|
+
value?: string;
|
|
99
|
+
disabled?: boolean;
|
|
100
|
+
onInput?: (value: string) => void;
|
|
101
|
+
}> = (props) => (
|
|
102
|
+
<input
|
|
103
|
+
type="text"
|
|
104
|
+
className={SEARCHBAR_CLASS}
|
|
105
|
+
placeholder={props.placeholder}
|
|
106
|
+
value={props.value}
|
|
107
|
+
disabled={props.disabled}
|
|
108
|
+
onChange={(e) => props.onInput?.(e.target.value)}
|
|
109
|
+
/>
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
export const Textarea: React.FC<{
|
|
113
|
+
placeholder?: string;
|
|
114
|
+
value?: string;
|
|
115
|
+
onInput?: (value: string) => void;
|
|
116
|
+
}> = (props) => (
|
|
117
|
+
<textarea placeholder={props.placeholder} value={props.value} onChange={(e) => props.onInput?.(e.target.value)} />
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
// ---------- badges, chips, cards ----------
|
|
121
|
+
|
|
122
|
+
export const Badge: React.FC<{ tone?: BadgeTone; children: React.ReactNode }> = (props) => (
|
|
123
|
+
<span className={badgeClass(props.tone)}>{props.children}</span>
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
export const Chip: React.FC<{ active: boolean; onClick?: () => void; children: React.ReactNode }> = (props) => (
|
|
127
|
+
<button type="button" className={chipClass(props.active)} onClick={props.onClick}>
|
|
128
|
+
{props.children}
|
|
129
|
+
</button>
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
// A plain elevated container; slot layout is the consumer's own concern.
|
|
133
|
+
export const Card: React.FC<{ children: React.ReactNode }> = (props) => (
|
|
134
|
+
<article className={CARD_CLASS}>{props.children}</article>
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// ---------- settings ----------
|
|
138
|
+
|
|
139
|
+
// Native-looking settings chrome for the `settingsSection` register. These
|
|
140
|
+
// render the client's own section/row structure rather than capturing its
|
|
141
|
+
// components: the settings route is a lazily loaded chunk, and the loader
|
|
142
|
+
// does not source-transform those, so a capture there never resolves.
|
|
143
|
+
export const SettingsSection: React.FC<{ title?: React.ReactNode; children: React.ReactNode }> = (props) => (
|
|
144
|
+
<div className={SETTINGS_SECTION_CLASS}>
|
|
145
|
+
{props.title === undefined ? null : <h2>{props.title}</h2>}
|
|
146
|
+
{props.children}
|
|
147
|
+
</div>
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
// One labelled row. `label` fills the client's first column and `children`
|
|
151
|
+
// the control column, so a toggle or select lines up with the native rows
|
|
152
|
+
// above and below it.
|
|
153
|
+
export const SettingsRow: React.FC<{ label: React.ReactNode; htmlFor?: string; children: React.ReactNode }> = (
|
|
154
|
+
props,
|
|
155
|
+
) => (
|
|
156
|
+
<div className={SETTINGS_ROW_CLASS}>
|
|
157
|
+
<div className={SETTINGS_ROW_LABEL_CLASS}>
|
|
158
|
+
{props.htmlFor === undefined ? props.label : <label htmlFor={props.htmlFor}>{props.label}</label>}
|
|
159
|
+
</div>
|
|
160
|
+
<div className={SETTINGS_ROW_CONTROL_CLASS}>{props.children}</div>
|
|
161
|
+
</div>
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
// The client's own switch structure around a native checkbox. Its semantic
|
|
165
|
+
// classes are recreated by the CLI css-map, so Spotify and themes style this
|
|
166
|
+
// exactly like the adjacent settings controls.
|
|
167
|
+
export const Toggle: React.FC<{
|
|
168
|
+
id?: string;
|
|
169
|
+
ariaLabel?: string;
|
|
170
|
+
value: boolean;
|
|
171
|
+
onChange: (value: boolean) => void;
|
|
172
|
+
}> = (props) => (
|
|
173
|
+
<label className={TOGGLE_CLASSES.wrapper}>
|
|
174
|
+
<input
|
|
175
|
+
type="checkbox"
|
|
176
|
+
className={TOGGLE_CLASSES.input}
|
|
177
|
+
id={props.id}
|
|
178
|
+
aria-label={props.ariaLabel}
|
|
179
|
+
checked={props.value}
|
|
180
|
+
onKeyDown={activateToggleOnKeyDown}
|
|
181
|
+
onChange={(e) => props.onChange((e.target as HTMLInputElement).checked)}
|
|
182
|
+
/>
|
|
183
|
+
<span className={TOGGLE_CLASSES.indicatorWrapper} aria-hidden="true">
|
|
184
|
+
<span className={TOGGLE_CLASSES.indicator} />
|
|
185
|
+
</span>
|
|
186
|
+
</label>
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
// The "module with one boolean setting" pattern as a single row for the
|
|
190
|
+
// `settingsRow` register. `getValue` is read lazily on every mount so
|
|
191
|
+
// reopening settings always reflects the current state; the element itself
|
|
192
|
+
// is created once at module load, so a plain value prop would go stale.
|
|
193
|
+
export const SettingsToggleRow: React.FC<{
|
|
194
|
+
label: React.ReactNode;
|
|
195
|
+
getValue: () => boolean;
|
|
196
|
+
onChange: (value: boolean) => void;
|
|
197
|
+
}> = (props) => {
|
|
198
|
+
const id = React.useId();
|
|
199
|
+
const [value, setValue] = React.useState(props.getValue);
|
|
200
|
+
return (
|
|
201
|
+
<SettingsRow label={props.label} htmlFor={id}>
|
|
202
|
+
<Toggle
|
|
203
|
+
id={id}
|
|
204
|
+
value={value}
|
|
205
|
+
onChange={(v) => {
|
|
206
|
+
setValue(v);
|
|
207
|
+
props.onChange(v);
|
|
208
|
+
}}
|
|
209
|
+
/>
|
|
210
|
+
</SettingsRow>
|
|
211
|
+
);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// ---------- context-menu item ----------
|
|
215
|
+
|
|
216
|
+
// A row for one of Spotify's context menus — register it through the
|
|
217
|
+
// "menu" register. It owns the native context-menu class so module code
|
|
218
|
+
// gets Spotify styling without ever naming a client class; pass `className`
|
|
219
|
+
// only for a module-specific hook on top of the native look.
|
|
220
|
+
export const MenuItem: React.FC<{
|
|
221
|
+
onClick?: () => void;
|
|
222
|
+
className?: string;
|
|
223
|
+
children: React.ReactNode;
|
|
224
|
+
}> = (props) => (
|
|
225
|
+
<button
|
|
226
|
+
type="button"
|
|
227
|
+
role="menuitem"
|
|
228
|
+
className={props.className ? `${MENU_ITEM_CLASS} ${props.className}` : MENU_ITEM_CLASS}
|
|
229
|
+
onClick={props.onClick}
|
|
230
|
+
>
|
|
231
|
+
<span>{props.children}</span>
|
|
232
|
+
</button>
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
// ---------- two-step confirm ----------
|
|
236
|
+
|
|
237
|
+
// The arm-then-confirm pattern: the first click swaps the label and arms
|
|
238
|
+
// a revert timer; a second click within the window confirms.
|
|
239
|
+
export const ConfirmButton: React.FC<{
|
|
240
|
+
label: string;
|
|
241
|
+
confirmLabel: string;
|
|
242
|
+
onConfirm: () => void;
|
|
243
|
+
variant?: ButtonVariant;
|
|
244
|
+
windowMs?: number;
|
|
245
|
+
}> = (props) => {
|
|
246
|
+
const [armed, setArmed] = React.useState(false);
|
|
247
|
+
const timer = React.useRef<ReturnType<typeof setTimeout>>();
|
|
248
|
+
React.useEffect(() => () => clearTimeout(timer.current), []);
|
|
249
|
+
return (
|
|
250
|
+
<button
|
|
251
|
+
type="button"
|
|
252
|
+
className={buttonClass(props.variant)}
|
|
253
|
+
onClick={() => {
|
|
254
|
+
if (armed) {
|
|
255
|
+
clearTimeout(timer.current);
|
|
256
|
+
setArmed(false);
|
|
257
|
+
props.onConfirm();
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
setArmed(true);
|
|
261
|
+
timer.current = setTimeout(() => setArmed(false), props.windowMs ?? 4000);
|
|
262
|
+
}}
|
|
263
|
+
>
|
|
264
|
+
{armed ? props.confirmLabel : props.label}
|
|
265
|
+
</button>
|
|
266
|
+
);
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
// ---------- dialog ----------
|
|
270
|
+
|
|
271
|
+
// Modal chrome: the consumer controls mounting with a conditional render
|
|
272
|
+
// (`{open && <Dialog ... />}`), the idiomatic React equivalent of the
|
|
273
|
+
// vanilla kit's openDialog(). It portals to document.body so the
|
|
274
|
+
// position:fixed scrim anchors to the viewport — a fixed element inside a
|
|
275
|
+
// transformed ancestor (the norm for Spotify's scroll containers) would
|
|
276
|
+
// otherwise be clipped/offset to that ancestor. The backdrop and the ×
|
|
277
|
+
// button both call onClose.
|
|
278
|
+
export const Dialog: React.FC<{ title: string; onClose: () => void; children: React.ReactNode }> = (props) => {
|
|
279
|
+
const { onClose } = props;
|
|
280
|
+
// Escape closes the dialog, matching every native modal. A capturing
|
|
281
|
+
// document listener catches the key wherever focus sits (the client's own
|
|
282
|
+
// components steal focus), and the client's shortcut handlers do not see it.
|
|
283
|
+
React.useEffect(() => {
|
|
284
|
+
const onKey = (e: KeyboardEvent) => {
|
|
285
|
+
if (e.key === "Escape") {
|
|
286
|
+
e.preventDefault();
|
|
287
|
+
e.stopPropagation();
|
|
288
|
+
onClose();
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
document.addEventListener("keydown", onKey, true);
|
|
292
|
+
return () => document.removeEventListener("keydown", onKey, true);
|
|
293
|
+
}, [onClose]);
|
|
294
|
+
|
|
295
|
+
return ReactDOM.createPortal(
|
|
296
|
+
<div
|
|
297
|
+
className={SCRIM_CLASS}
|
|
298
|
+
onClick={(e) => {
|
|
299
|
+
if (e.target === e.currentTarget) onClose();
|
|
300
|
+
}}
|
|
301
|
+
>
|
|
302
|
+
<div className={DIALOG_CLASS} role="dialog" aria-modal="true" aria-label={props.title}>
|
|
303
|
+
<div className={DIALOG_HEADER_CLASS}>
|
|
304
|
+
<h2>{props.title}</h2>
|
|
305
|
+
<IconButton ariaLabel="Close" onClick={onClose}>
|
|
306
|
+
×
|
|
307
|
+
</IconButton>
|
|
308
|
+
</div>
|
|
309
|
+
<div className={DIALOG_BODY_CLASS}>{props.children}</div>
|
|
310
|
+
</div>
|
|
311
|
+
</div>,
|
|
312
|
+
document.body,
|
|
313
|
+
);
|
|
314
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2024 Delusoire
|
|
3
|
+
* Copyright (C) 2026 Afonso Jorge Ramos
|
|
4
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { startEventHandlers } from "./src/events.js";
|
|
8
|
+
import { installPlaybarCompat } from "./src/playbar-compat.js";
|
|
9
|
+
|
|
10
|
+
export default async function (_ctx: { spotifyVersion: string }) {
|
|
11
|
+
const cancelEventHandlers = startEventHandlers();
|
|
12
|
+
const uninstallPlaybarCompat = installPlaybarCompat();
|
|
13
|
+
return () => {
|
|
14
|
+
uninstallPlaybarCompat();
|
|
15
|
+
cancelEventHandlers();
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "stdlib",
|
|
3
|
+
"kind": "lib",
|
|
4
|
+
"hidden": true,
|
|
5
|
+
"preview": "./assets/PREVIEW.png",
|
|
6
|
+
"version": "1.5.0",
|
|
7
|
+
"compat": ["0.3.0"],
|
|
8
|
+
"authors": ["spicetify", "Delusoire"],
|
|
9
|
+
"description": "The standard library",
|
|
10
|
+
"readme": "./assets/README.md",
|
|
11
|
+
"entries": {
|
|
12
|
+
"js": "index.js",
|
|
13
|
+
"css": "index.css"
|
|
14
|
+
},
|
|
15
|
+
"hasMixins": true,
|
|
16
|
+
"dependencies": [],
|
|
17
|
+
"tree": true
|
|
18
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2024 Delusoire
|
|
3
|
+
* Copyright (C) 2026 Afonso Jorge Ramos
|
|
4
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// allows us to patch webpack module exports directly
|
|
8
|
+
const nativeObjectDefineProperty = Object.defineProperty;
|
|
9
|
+
Object.defineProperty = function (obj, prop, descriptor) {
|
|
10
|
+
if (prop !== "prototype" && descriptor) descriptor.configurable ??= true;
|
|
11
|
+
return nativeObjectDefineProperty(obj, prop, descriptor);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// Shape of the loader-provided source transformer: registers a rewrite
|
|
15
|
+
// over client bundles matched by glob. emit() reports the needle matched
|
|
16
|
+
// (optionally with a captured value the returned promise resolves with).
|
|
17
|
+
export type Transformer = <T = unknown>(
|
|
18
|
+
fn: (emit: (value?: T) => void) => (str: string) => string,
|
|
19
|
+
opts?: { glob?: RegExp; wait?: boolean; noAwait?: boolean },
|
|
20
|
+
) => Promise<T>;
|
|
21
|
+
|
|
22
|
+
export let transformer: Transformer;
|
|
23
|
+
|
|
24
|
+
export default async function (t: Transformer, _ctx: { spotifyVersion: string }) {
|
|
25
|
+
transformer = t;
|
|
26
|
+
await Promise.all([
|
|
27
|
+
import("./src/expose/index.js"),
|
|
28
|
+
import("./src/registers/index.js"),
|
|
29
|
+
import("./src/events.mix.js"),
|
|
30
|
+
import("./src/wpunpk.mix.js"),
|
|
31
|
+
]);
|
|
32
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2024 Delusoire
|
|
3
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Runtime context the v3 loader passes into preload/load.
|
|
7
|
+
export interface ModuleRuntimeContext {
|
|
8
|
+
spotifyVersion: string;
|
|
9
|
+
identifier: string;
|
|
10
|
+
defer: (fn: () => void | Promise<void>) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export * from "./src/registers/index.ts";
|
|
14
|
+
export * from "./src/client.ts";
|
|
15
|
+
export * from "./src/events.ts";
|
|
16
|
+
// Vanilla component kit for module-owned DOM (React-free).
|
|
17
|
+
export * from "./lib/primitives-vanilla.ts";
|
|
18
|
+
export * from "./src/storage.ts";
|
|
19
|
+
export * from "./src/wpunpk.ts";
|
|
20
|
+
export * from "./src/logger.ts";
|
|
21
|
+
|
|
22
|
+
// Public barrel: everything dependent modules are allowed to deep-import.
|
|
23
|
+
// Import from /modules/stdlib/mod.js at runtime; nothing else is public.
|
|
24
|
+
export * from "./src/webpack/misc.xpui.ts";
|
|
25
|
+
export * from "./src/webpack/ComponentLibrary.xpui.ts";
|
|
26
|
+
export * from "./src/webpack/ReactComponents.xpui.ts";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Ambient declaration for the chunk-promise table the modular loader
|
|
2
|
+
// maintains on globalThis; keyed by chunk path plus aliases like "xpui".
|
|
3
|
+
declare global {
|
|
4
|
+
var CHUNKS: Record<
|
|
5
|
+
string,
|
|
6
|
+
{ promise: Promise<unknown>; resolve?: (value?: unknown) => void; reject?: (reason?: unknown) => void }
|
|
7
|
+
>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 spicetify
|
|
3
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The stable module-facing boundary around the wrapper runtime.
|
|
8
|
+
*
|
|
9
|
+
* Keep ambient `Spicetify` access in this adapter. Modules import `client`
|
|
10
|
+
* instead, which gives stdlib one place to replace or harden a capability as
|
|
11
|
+
* Spotify changes. Getters are intentionally lazy: some surfaces, notably the
|
|
12
|
+
* modular loader API, are attached after the client has started loading
|
|
13
|
+
* modules.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
type SpicetifyRuntime = typeof Spicetify;
|
|
17
|
+
|
|
18
|
+
export type PlayerState = Spicetify.PlayerState;
|
|
19
|
+
|
|
20
|
+
export interface ClientCapabilities {
|
|
21
|
+
readonly player: typeof Spicetify.Player;
|
|
22
|
+
readonly platform: typeof Spicetify.Platform;
|
|
23
|
+
readonly storage: typeof Spicetify.LocalStorage;
|
|
24
|
+
readonly uri: typeof Spicetify.URI;
|
|
25
|
+
readonly cosmos: typeof Spicetify.CosmosAsync;
|
|
26
|
+
readonly corsProxy: typeof Spicetify.CORSProxy;
|
|
27
|
+
readonly graphQL: typeof Spicetify.GraphQL;
|
|
28
|
+
readonly locale: typeof Spicetify.Locale;
|
|
29
|
+
readonly icons: typeof Spicetify.SVGIcons;
|
|
30
|
+
readonly keyboard: typeof Spicetify.Keyboard;
|
|
31
|
+
readonly mousetrap: typeof Spicetify.Mousetrap;
|
|
32
|
+
readonly contextMenu: typeof Spicetify.ContextMenu;
|
|
33
|
+
readonly popupModal: typeof Spicetify.PopupModal;
|
|
34
|
+
readonly config: typeof Spicetify.Config;
|
|
35
|
+
readonly modules: typeof Spicetify.Modules;
|
|
36
|
+
readonly react: typeof Spicetify.React;
|
|
37
|
+
readonly reactDOM: typeof Spicetify.ReactDOM;
|
|
38
|
+
readonly tippy: typeof Spicetify.Tippy;
|
|
39
|
+
readonly tippyProps: typeof Spicetify.TippyProps;
|
|
40
|
+
readonly playbar: typeof Spicetify.Playbar;
|
|
41
|
+
readonly notify: typeof Spicetify.showNotification;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const runtime = (): SpicetifyRuntime => {
|
|
45
|
+
const value = (globalThis as unknown as { Spicetify?: SpicetifyRuntime }).Spicetify;
|
|
46
|
+
if (!value) throw new Error("Spicetify client runtime is unavailable");
|
|
47
|
+
return value;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const client: ClientCapabilities = {
|
|
51
|
+
get player() {
|
|
52
|
+
return runtime().Player;
|
|
53
|
+
},
|
|
54
|
+
get platform() {
|
|
55
|
+
return runtime().Platform;
|
|
56
|
+
},
|
|
57
|
+
get storage() {
|
|
58
|
+
return runtime().LocalStorage;
|
|
59
|
+
},
|
|
60
|
+
get uri() {
|
|
61
|
+
return runtime().URI;
|
|
62
|
+
},
|
|
63
|
+
get cosmos() {
|
|
64
|
+
return runtime().CosmosAsync;
|
|
65
|
+
},
|
|
66
|
+
get corsProxy() {
|
|
67
|
+
return runtime().CORSProxy;
|
|
68
|
+
},
|
|
69
|
+
get graphQL() {
|
|
70
|
+
return runtime().GraphQL;
|
|
71
|
+
},
|
|
72
|
+
get locale() {
|
|
73
|
+
return runtime().Locale;
|
|
74
|
+
},
|
|
75
|
+
get icons() {
|
|
76
|
+
return runtime().SVGIcons;
|
|
77
|
+
},
|
|
78
|
+
get keyboard() {
|
|
79
|
+
return runtime().Keyboard;
|
|
80
|
+
},
|
|
81
|
+
get mousetrap() {
|
|
82
|
+
return runtime().Mousetrap;
|
|
83
|
+
},
|
|
84
|
+
get contextMenu() {
|
|
85
|
+
return runtime().ContextMenu;
|
|
86
|
+
},
|
|
87
|
+
get popupModal() {
|
|
88
|
+
return runtime().PopupModal;
|
|
89
|
+
},
|
|
90
|
+
get config() {
|
|
91
|
+
return runtime().Config;
|
|
92
|
+
},
|
|
93
|
+
get modules() {
|
|
94
|
+
return runtime().Modules;
|
|
95
|
+
},
|
|
96
|
+
get react() {
|
|
97
|
+
return runtime().React;
|
|
98
|
+
},
|
|
99
|
+
get reactDOM() {
|
|
100
|
+
return runtime().ReactDOM;
|
|
101
|
+
},
|
|
102
|
+
get tippy() {
|
|
103
|
+
return runtime().Tippy;
|
|
104
|
+
},
|
|
105
|
+
get tippyProps() {
|
|
106
|
+
return runtime().TippyProps;
|
|
107
|
+
},
|
|
108
|
+
get playbar() {
|
|
109
|
+
return runtime().Playbar;
|
|
110
|
+
},
|
|
111
|
+
get notify() {
|
|
112
|
+
return runtime().showNotification;
|
|
113
|
+
},
|
|
114
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2024 Delusoire
|
|
3
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { React } from "./expose/React.ts";
|
|
7
|
+
import { UI } from "./webpack/ComponentLibrary.ts";
|
|
8
|
+
|
|
9
|
+
export const createIconComponent = ({
|
|
10
|
+
icon,
|
|
11
|
+
iconSize = 16,
|
|
12
|
+
realIconSize = iconSize,
|
|
13
|
+
...props
|
|
14
|
+
}: {
|
|
15
|
+
[k: string]: any;
|
|
16
|
+
icon: string;
|
|
17
|
+
realIconSize?: number;
|
|
18
|
+
iconSize?: number;
|
|
19
|
+
}) => {
|
|
20
|
+
return (
|
|
21
|
+
<UI.Icon
|
|
22
|
+
autoMirror={false}
|
|
23
|
+
iconSize={realIconSize}
|
|
24
|
+
viewBox={`0 0 ${iconSize} ${iconSize}`}
|
|
25
|
+
dangerouslySetInnerHTML={{ __html: icon }}
|
|
26
|
+
{...props}
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2024 Delusoire
|
|
3
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { postWebpackRequireHooks } from "./wpunpk.mix.ts";
|
|
7
|
+
|
|
8
|
+
import { BehaviorSubject, Subject } from "../deps.ts";
|
|
9
|
+
|
|
10
|
+
function createGlobalThisShadow() {
|
|
11
|
+
const globalThisShadow = {} as typeof globalThis;
|
|
12
|
+
|
|
13
|
+
Object.setPrototypeOf(globalThisShadow, globalThis);
|
|
14
|
+
|
|
15
|
+
Object.defineProperty(globalThisShadow, "self", { value: globalThisShadow });
|
|
16
|
+
Object.defineProperty(globalThisShadow, "globalThis", { value: globalThisShadow });
|
|
17
|
+
Object.defineProperty(globalThisShadow, "window", { value: globalThisShadow });
|
|
18
|
+
|
|
19
|
+
return globalThisShadow;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type CosmosRequest = { uri: string; method: string; body: string };
|
|
23
|
+
|
|
24
|
+
const CosmosRequestSubject = new Subject<CosmosRequest>();
|
|
25
|
+
export const UpdateTitlebarSubject = new BehaviorSubject<number>(-1);
|
|
26
|
+
|
|
27
|
+
postWebpackRequireHooks.push(($) => {
|
|
28
|
+
const globalThisShadow = ($.g = createGlobalThisShadow());
|
|
29
|
+
|
|
30
|
+
const $sendCosmosRequest = globalThis.sendCosmosRequest;
|
|
31
|
+
Object.defineProperty(globalThisShadow, "sendCosmosRequest", {
|
|
32
|
+
value: function ($: any) {
|
|
33
|
+
const request = JSON.parse($.request);
|
|
34
|
+
|
|
35
|
+
CosmosRequestSubject.next(request);
|
|
36
|
+
|
|
37
|
+
return $sendCosmosRequest($);
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const controlMessageListener = (request: CosmosRequest) => {
|
|
43
|
+
const { uri, method } = request;
|
|
44
|
+
|
|
45
|
+
if (uri === "sp://messages/v1/container/control" && method === "POST") {
|
|
46
|
+
const body = JSON.parse(request.body);
|
|
47
|
+
const { type, height } = body;
|
|
48
|
+
|
|
49
|
+
if (type === "update_titlebar") {
|
|
50
|
+
UpdateTitlebarSubject.next(height);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
CosmosRequestSubject.subscribe(controlMessageListener);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2024 Delusoire
|
|
3
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Platform } from "./expose/Platform.ts";
|
|
7
|
+
|
|
8
|
+
import { BehaviorSubject } from "../deps.ts";
|
|
9
|
+
import { UpdateTitlebarSubject } from "./events.mix.ts";
|
|
10
|
+
|
|
11
|
+
const newEventBus = () => {
|
|
12
|
+
const PlayerAPI = Platform.getPlayerAPI();
|
|
13
|
+
const History = Platform.getHistory();
|
|
14
|
+
|
|
15
|
+
const playerState = PlayerAPI.getState();
|
|
16
|
+
return {
|
|
17
|
+
Player: {
|
|
18
|
+
state_updated: new BehaviorSubject(playerState),
|
|
19
|
+
status_changed: new BehaviorSubject(playerState),
|
|
20
|
+
song_changed: new BehaviorSubject(playerState),
|
|
21
|
+
},
|
|
22
|
+
History: {
|
|
23
|
+
updated: new BehaviorSubject(History.location),
|
|
24
|
+
},
|
|
25
|
+
ControlMessage: {
|
|
26
|
+
titlebar_updated: new BehaviorSubject<number>(-1),
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const EventBus = newEventBus();
|
|
32
|
+
export type EventBus = typeof EventBus;
|
|
33
|
+
|
|
34
|
+
let cachedState: any = {};
|
|
35
|
+
const playerListener = ({ data: state }: any) => {
|
|
36
|
+
EventBus.Player.state_updated.next(state);
|
|
37
|
+
if (state?.item?.uri !== cachedState?.item?.uri) EventBus.Player.song_changed.next(state);
|
|
38
|
+
if (state?.isPaused !== cachedState?.isPaused || state?.isBuffering !== cachedState?.isBuffering) {
|
|
39
|
+
EventBus.Player.status_changed.next(state);
|
|
40
|
+
}
|
|
41
|
+
cachedState = state;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const historyListener = (location) => EventBus.History.updated.next(location);
|
|
45
|
+
|
|
46
|
+
const updateTitlebarListener = (height: number) => EventBus.ControlMessage.titlebar_updated.next(height);
|
|
47
|
+
|
|
48
|
+
export function startEventHandlers() {
|
|
49
|
+
const cancelPlayerListener = Platform.getPlayerAPI().getEvents().addListener("update", playerListener);
|
|
50
|
+
const cancelHistoryListener = Platform.getHistory().listen(historyListener);
|
|
51
|
+
const updateTitlebarListenerSubscription = UpdateTitlebarSubject.subscribe(updateTitlebarListener);
|
|
52
|
+
|
|
53
|
+
return () => {
|
|
54
|
+
cancelPlayerListener();
|
|
55
|
+
cancelHistoryListener();
|
|
56
|
+
updateTitlebarListenerSubscription.unsubscribe();
|
|
57
|
+
};
|
|
58
|
+
}
|