@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.
Files changed (115) hide show
  1. package/bin/spicetify-kit.js +4 -0
  2. package/dist/build.js +335 -0
  3. package/dist/check.js +284 -0
  4. package/dist/classmap.js +187 -0
  5. package/dist/cli.js +53 -0
  6. package/dist/create.js +429 -0
  7. package/dist/dev.js +100 -0
  8. package/dist/from-theme.js +96 -0
  9. package/dist/install.js +60 -0
  10. package/dist/launch.js +122 -0
  11. package/dist/pack.js +31 -0
  12. package/dist/push.js +211 -0
  13. package/dist/vault.js +108 -0
  14. package/package.json +49 -0
  15. package/vendor/classmaps/1020096/classmap-19f856aefd5.json +109 -0
  16. package/vendor/shims/chunks.d.ts +10 -0
  17. package/vendor/shims/remote-modules.d.ts +6 -0
  18. package/vendor/shims/spicetify.d.ts +2437 -0
  19. package/vendor/stdlib/deps.ts +24 -0
  20. package/vendor/stdlib/index.ts +24 -0
  21. package/vendor/stdlib/lib/modal.tsx +128 -0
  22. package/vendor/stdlib/lib/popover.ts +84 -0
  23. package/vendor/stdlib/lib/primitives-classes.ts +66 -0
  24. package/vendor/stdlib/lib/primitives-vanilla.ts +277 -0
  25. package/vendor/stdlib/lib/primitives.tsx +314 -0
  26. package/vendor/stdlib/load.ts +17 -0
  27. package/vendor/stdlib/metadata.json +18 -0
  28. package/vendor/stdlib/mixin.ts +32 -0
  29. package/vendor/stdlib/mod.ts +26 -0
  30. package/vendor/stdlib/src/chunks.d.ts +10 -0
  31. package/vendor/stdlib/src/client.ts +114 -0
  32. package/vendor/stdlib/src/createIconComponent.tsx +29 -0
  33. package/vendor/stdlib/src/events.mix.ts +55 -0
  34. package/vendor/stdlib/src/events.ts +58 -0
  35. package/vendor/stdlib/src/expose/GraphQL.ts +40 -0
  36. package/vendor/stdlib/src/expose/Platform.ts +48 -0
  37. package/vendor/stdlib/src/expose/React.ts +95 -0
  38. package/vendor/stdlib/src/expose/ReactFlipToolkitSpring.ts +27 -0
  39. package/vendor/stdlib/src/expose/ReduxStore.ts +39 -0
  40. package/vendor/stdlib/src/expose/SettingsSection.ts +60 -0
  41. package/vendor/stdlib/src/expose/Snackbar.ts +34 -0
  42. package/vendor/stdlib/src/expose/Tippy.ts +26 -0
  43. package/vendor/stdlib/src/expose/enqueueImageSnackbar.ts +25 -0
  44. package/vendor/stdlib/src/expose/index.ts +14 -0
  45. package/vendor/stdlib/src/expose/jsx-runtime.ts +46 -0
  46. package/vendor/stdlib/src/expose/react-dom-shim.ts +50 -0
  47. package/vendor/stdlib/src/expose/react-shim.ts +116 -0
  48. package/vendor/stdlib/src/logger.ts +35 -0
  49. package/vendor/stdlib/src/playbar-compat.tsx +136 -0
  50. package/vendor/stdlib/src/registers/index.ts +176 -0
  51. package/vendor/stdlib/src/registers/menu.ts +160 -0
  52. package/vendor/stdlib/src/registers/mount.ts +307 -0
  53. package/vendor/stdlib/src/registers/nativeAnchors.ts +49 -0
  54. package/vendor/stdlib/src/registers/navlink.tsx +165 -0
  55. package/vendor/stdlib/src/registers/order.ts +22 -0
  56. package/vendor/stdlib/src/registers/panel.ts +168 -0
  57. package/vendor/stdlib/src/registers/playbarButton.tsx +93 -0
  58. package/vendor/stdlib/src/registers/playbarWidget.tsx +71 -0
  59. package/vendor/stdlib/src/registers/registry.ts +12 -0
  60. package/vendor/stdlib/src/registers/root.ts +101 -0
  61. package/vendor/stdlib/src/registers/route.ts +123 -0
  62. package/vendor/stdlib/src/registers/settingsRow.tsx +56 -0
  63. package/vendor/stdlib/src/registers/settingsSection.ts +59 -0
  64. package/vendor/stdlib/src/registers/topbarLeftButton.tsx +86 -0
  65. package/vendor/stdlib/src/registers/topbarRightButton.tsx +87 -0
  66. package/vendor/stdlib/src/storage.ts +163 -0
  67. package/vendor/stdlib/src/util.ts +62 -0
  68. package/vendor/stdlib/src/utils/index.ts +16 -0
  69. package/vendor/stdlib/src/webpack/ClassNames.gen.ts +10 -0
  70. package/vendor/stdlib/src/webpack/ClassNames.ts +6 -0
  71. package/vendor/stdlib/src/webpack/ClassNames.xpui.ts +16 -0
  72. package/vendor/stdlib/src/webpack/ComponentLibrary.gen.ts +10 -0
  73. package/vendor/stdlib/src/webpack/ComponentLibrary.ts +6 -0
  74. package/vendor/stdlib/src/webpack/ComponentLibrary.xpui.ts +18 -0
  75. package/vendor/stdlib/src/webpack/FilterContext.gen.ts +10 -0
  76. package/vendor/stdlib/src/webpack/FilterContext.ts +6 -0
  77. package/vendor/stdlib/src/webpack/FilterContext.xpui.ts +10 -0
  78. package/vendor/stdlib/src/webpack/Mousetrap.gen.ts +10 -0
  79. package/vendor/stdlib/src/webpack/Mousetrap.ts +6 -0
  80. package/vendor/stdlib/src/webpack/Mousetrap.xpui.ts +12 -0
  81. package/vendor/stdlib/src/webpack/React.gen.ts +14 -0
  82. package/vendor/stdlib/src/webpack/React.ts +6 -0
  83. package/vendor/stdlib/src/webpack/React.xpui.ts +15 -0
  84. package/vendor/stdlib/src/webpack/ReactComponents.desktop.ts +17 -0
  85. package/vendor/stdlib/src/webpack/ReactComponents.gen.ts +74 -0
  86. package/vendor/stdlib/src/webpack/ReactComponents.panel.ts +19 -0
  87. package/vendor/stdlib/src/webpack/ReactComponents.ts +50 -0
  88. package/vendor/stdlib/src/webpack/ReactComponents.xpui.ts +164 -0
  89. package/vendor/stdlib/src/webpack/ReactFlipToolkit.gen.ts +12 -0
  90. package/vendor/stdlib/src/webpack/ReactFlipToolkit.ts +6 -0
  91. package/vendor/stdlib/src/webpack/ReactFlipToolkit.xpui.ts +14 -0
  92. package/vendor/stdlib/src/webpack/ReactHooks.fullscreen.ts +15 -0
  93. package/vendor/stdlib/src/webpack/ReactHooks.gen.ts +32 -0
  94. package/vendor/stdlib/src/webpack/ReactHooks.ts +6 -0
  95. package/vendor/stdlib/src/webpack/ReactHooks.xpui.ts +38 -0
  96. package/vendor/stdlib/src/webpack/ReactQuery.gen.ts +26 -0
  97. package/vendor/stdlib/src/webpack/ReactQuery.ts +6 -0
  98. package/vendor/stdlib/src/webpack/ReactQuery.xpui.ts +46 -0
  99. package/vendor/stdlib/src/webpack/ReactRouter.gen.ts +12 -0
  100. package/vendor/stdlib/src/webpack/ReactRouter.ts +6 -0
  101. package/vendor/stdlib/src/webpack/ReactRouter.xpui.ts +22 -0
  102. package/vendor/stdlib/src/webpack/Snackbar.gen.ts +12 -0
  103. package/vendor/stdlib/src/webpack/Snackbar.ts +6 -0
  104. package/vendor/stdlib/src/webpack/Snackbar.xpui.ts +27 -0
  105. package/vendor/stdlib/src/webpack/URI.gen.ts +26 -0
  106. package/vendor/stdlib/src/webpack/URI.ts +82 -0
  107. package/vendor/stdlib/src/webpack/URI.xpui.ts +111 -0
  108. package/vendor/stdlib/src/webpack/capture-readiness.ts +49 -0
  109. package/vendor/stdlib/src/webpack/index.ts +186 -0
  110. package/vendor/stdlib/src/webpack/misc.gen.ts +16 -0
  111. package/vendor/stdlib/src/webpack/misc.ts +6 -0
  112. package/vendor/stdlib/src/webpack/misc.xpui.ts +27 -0
  113. package/vendor/stdlib/src/wpunpk.mix.ts +104 -0
  114. package/vendor/stdlib/src/wpunpk.ts +36 -0
  115. 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
+ }