@spicetify/kit 0.3.0 → 0.4.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 (37) hide show
  1. package/README.md +82 -0
  2. package/dist/args.js +15 -0
  3. package/dist/cli.js +4 -1
  4. package/dist/create.js +9 -5
  5. package/dist/dev.js +86 -22
  6. package/dist/install.js +4 -6
  7. package/dist/launch.js +24 -24
  8. package/dist/push.js +187 -44
  9. package/dist/remove.js +37 -0
  10. package/dist/stdlib-boundary.js +2 -2
  11. package/dist/version.js +1 -1
  12. package/package.json +6 -4
  13. package/vendor/classmaps/1030000/classmap-19f856aefd5.json +109 -0
  14. package/vendor/shims/platform.d.ts +1640 -3235
  15. package/vendor/stdlib/metadata.json +1 -1
  16. package/vendor/stdlib/query.ts +31 -0
  17. package/vendor/stdlib/src/client.ts +90 -1
  18. package/vendor/stdlib/src/events.mix.ts +9 -0
  19. package/vendor/stdlib/src/events.ts +1 -1
  20. package/vendor/stdlib/src/registers/menu-order.ts +17 -0
  21. package/vendor/stdlib/src/registers/menu.ts +8 -4
  22. package/vendor/stdlib/src/registers/mount.ts +35 -9
  23. package/vendor/stdlib/src/registers/navlink.tsx +3 -50
  24. package/vendor/stdlib/src/registers/playbarButton.tsx +0 -19
  25. package/vendor/stdlib/src/registers/playbarWidget.tsx +0 -19
  26. package/vendor/stdlib/src/registers/root.ts +1 -1
  27. package/vendor/stdlib/src/registers/route.ts +1 -23
  28. package/vendor/stdlib/src/registers/topbarLeftButton.tsx +0 -24
  29. package/vendor/stdlib/src/registers/topbarRightButton.tsx +0 -24
  30. package/vendor/stdlib/src/storage.ts +1 -1
  31. package/vendor/stdlib/src/webpack/ReactComponents.xpui.ts +3 -9
  32. package/vendor/stdlib/src/webpack/URI.xpui.ts +1 -1
  33. package/vendor/stdlib/src/webpack/capture-readiness.ts +5 -3
  34. package/vendor/stdlib/src/webpack/index.ts +10 -4
  35. package/vendor/stdlib/src/webpack/playlist-menu.ts +54 -0
  36. package/vendor/stdlib/src/wpunpk.mix.ts +1 -1
  37. package/vendor/classmaps/1020096/classmap-19f856aefd5.json +0 -109
@@ -3,7 +3,7 @@
3
3
  "kind": "lib",
4
4
  "hidden": true,
5
5
  "preview": "./assets/PREVIEW.png",
6
- "version": "1.10.1",
6
+ "version": "1.13.0",
7
7
  "compat": ["0.3.0"],
8
8
  "authors": ["spicetify", "Delusoire"],
9
9
  "description": "The standard library",
@@ -0,0 +1,31 @@
1
+ /*
2
+ * Copyright (C) 2026 spicetify
3
+ * SPDX-License-Identifier: GPL-3.0-or-later
4
+ */
5
+
6
+ import { QueryClient } from "@tanstack/query-core";
7
+ import type { ModuleRuntimeContext } from "./mod.ts";
8
+
9
+ export { QueryClient, QueryObserver, isCancelledError } from "@tanstack/query-core";
10
+ export type { QueryKey, QueryFunctionContext } from "@tanstack/query-core";
11
+
12
+ /** A cache owned by one module load; unloading cancels queries and discards data. */
13
+ export function createModuleQueryClient(ctx: Pick<ModuleRuntimeContext, "defer">): QueryClient {
14
+ const client = new QueryClient({
15
+ defaultOptions: {
16
+ queries: {
17
+ staleTime: 5 * 60 * 1000,
18
+ gcTime: 10 * 60 * 1000,
19
+ retry: false,
20
+ refetchOnWindowFocus: false,
21
+ refetchOnReconnect: false,
22
+ },
23
+ mutations: { retry: false },
24
+ },
25
+ });
26
+ ctx.defer(async () => {
27
+ await client.cancelQueries();
28
+ client.clear();
29
+ });
30
+ return client;
31
+ }
@@ -25,22 +25,111 @@ export interface PopupModalCompatibility {
25
25
  }
26
26
 
27
27
  export interface DaemonCapabilities {
28
+ acquireWindowControls?(onDisconnect: (error: Error) => void): Promise<{ release(): Promise<void> }>;
28
29
  available(): Promise<boolean>;
29
30
  apply(): Promise<unknown>;
30
31
  blockUpdates(): Promise<unknown>;
31
32
  unblockUpdates(): Promise<unknown>;
33
+ updateAndApplySupported?(): Promise<boolean | null>;
34
+ updateAndApply?: UpdateAndApplyCapability;
35
+ managedSpotify?: ManagedSpotifyCapability;
36
+ }
37
+
38
+ export type ManagedSpotifyJob =
39
+ | { kind: "idle" }
40
+ | { kind: "running"; jobId: string; phase: "checking" | "downloading" | "preparing" | "activating" }
41
+ | { kind: "complete"; jobId: string }
42
+ | { kind: "failed"; jobId: string; message: string };
43
+
44
+ export type ManagedSpotifyInstallation =
45
+ | { kind: "external" }
46
+ | { kind: "unavailable"; message: string }
47
+ | { kind: "managed"; version: string; channel: "stable" | "testing"; nativeBlocked: boolean | null };
48
+
49
+ export type ManagedSpotifyAvailability =
50
+ | { kind: "current"; version: string }
51
+ | { kind: "ready"; version: string }
52
+ | { kind: "unavailable"; version: string; message: string };
53
+
54
+ export interface ManagedSpotifySnapshot {
55
+ installation: ManagedSpotifyInstallation;
56
+ job: ManagedSpotifyJob;
57
+ }
58
+
59
+ export interface ManagedSpotifyCapability {
60
+ status(): Promise<ManagedSpotifySnapshot | null>;
61
+ check(): Promise<ManagedSpotifyAvailability>;
62
+ update(): Promise<UpdateAdmission>;
63
+ }
64
+
65
+ export type UpdateFailureCode =
66
+ | "unsupported-platform"
67
+ | "unsupported-target"
68
+ | "update-unavailable"
69
+ | "renderer-timeout"
70
+ | "spotify-update-failed"
71
+ | "apply-failed";
72
+
73
+ export type UpdateAndApplyStatus =
74
+ | { kind: "idle" }
75
+ | { kind: "accepted"; jobId: string; fromVersion: string }
76
+ | { kind: "waiting-for-update"; jobId: string; fromVersion: string }
77
+ | { kind: "downloading"; jobId: string; targetVersion: string }
78
+ | { kind: "installing-spotify"; jobId: string; targetVersion: string }
79
+ | { kind: "applying-spicetify"; jobId: string; targetVersion: string }
80
+ | { kind: "securing"; jobId: string; targetVersion?: string; message?: string; manualRecovery?: boolean }
81
+ | { kind: "complete"; jobId: string; fromVersion: string; toVersion: string }
82
+ | { kind: "failed-safe"; jobId: string; code: UpdateFailureCode; message: string };
83
+
84
+ export interface UpdateAdmission {
85
+ jobId: string;
86
+ disposition: "accepted" | "joined";
87
+ }
88
+
89
+ export interface UpdateAndApplyCapability {
90
+ (): Promise<UpdateAdmission>;
91
+ observe(listener: (status: UpdateAndApplyStatus) => void): () => void;
32
92
  }
33
93
 
34
94
  export interface SnackbarCapabilities {
35
95
  enqueueSnackbar(message: string, options?: { variant?: string }): unknown;
36
96
  }
37
97
 
98
+ type CosmosBody = Record<string, unknown>;
99
+ type CosmosHeaders = Spicetify.CosmosAsync.Headers;
100
+ type CosmosMethod = Spicetify.CosmosAsync.Method;
101
+ type CosmosResponse = Omit<Spicetify.CosmosAsync.Response, "body"> & { body: unknown };
102
+
103
+ export interface CosmosCapabilities {
104
+ head(url: string, headers?: CosmosHeaders): Promise<CosmosHeaders>;
105
+ get(url: string, body?: CosmosBody, headers?: CosmosHeaders): Promise<unknown>;
106
+ post(url: string, body?: CosmosBody, headers?: CosmosHeaders): Promise<unknown>;
107
+ put(url: string, body?: CosmosBody, headers?: CosmosHeaders): Promise<unknown>;
108
+ del(url: string, body?: CosmosBody, headers?: CosmosHeaders): Promise<unknown>;
109
+ patch(url: string, body?: CosmosBody, headers?: CosmosHeaders): Promise<unknown>;
110
+ sub(
111
+ url: string,
112
+ callback: (body: unknown) => void,
113
+ onError?: (error: unknown) => void,
114
+ body?: CosmosBody,
115
+ headers?: CosmosHeaders,
116
+ ): Promise<unknown>;
117
+ postSub(
118
+ url: string,
119
+ body: CosmosBody | null,
120
+ callback: (body: unknown) => void,
121
+ onError?: (error: unknown) => void,
122
+ ): Promise<unknown>;
123
+ request(method: CosmosMethod, url: string, body?: CosmosBody, headers?: CosmosHeaders): Promise<CosmosResponse>;
124
+ resolve(method: CosmosMethod, url: string, body?: CosmosBody, headers?: CosmosHeaders): Promise<CosmosResponse>;
125
+ }
126
+
38
127
  export interface ClientCapabilities {
39
128
  readonly player: typeof Spicetify.Player;
40
129
  readonly platform: typeof Spicetify.Platform;
41
130
  readonly storage: typeof Spicetify.LocalStorage;
42
131
  readonly uri: typeof Spicetify.URI;
43
- readonly cosmos: typeof Spicetify.CosmosAsync;
132
+ readonly cosmos: CosmosCapabilities;
44
133
  readonly corsProxy: typeof Spicetify.CORSProxy;
45
134
  readonly graphQL: typeof Spicetify.GraphQL;
46
135
  readonly locale: typeof Spicetify.Locale;
@@ -21,6 +21,15 @@ function createGlobalThisShadow() {
21
21
 
22
22
  type CosmosRequest = { uri: string; method: string; body: string };
23
23
 
24
+ declare global {
25
+ var sendCosmosRequest: (request: {
26
+ request: string;
27
+ persistent?: boolean;
28
+ onSuccess?: (response: string) => void;
29
+ onFailure?: (error: unknown) => void;
30
+ }) => unknown;
31
+ }
32
+
24
33
  const CosmosRequestSubject = new Subject<CosmosRequest>();
25
34
  export const UpdateTitlebarSubject = new BehaviorSubject<number>(-1);
26
35
 
@@ -41,7 +41,7 @@ const playerListener = ({ data: state }: any) => {
41
41
  cachedState = state;
42
42
  };
43
43
 
44
- const historyListener = (location) => EventBus.History.updated.next(location);
44
+ const historyListener = (location: unknown) => EventBus.History.updated.next(location);
45
45
 
46
46
  const updateTitlebarListener = (height: number) => EventBus.ControlMessage.titlebar_updated.next(height);
47
47
 
@@ -0,0 +1,17 @@
1
+ /*
2
+ * Copyright (C) 2026 Afonso Jorge Ramos
3
+ * SPDX-License-Identifier: GPL-3.0-or-later
4
+ */
5
+
6
+ export const placeMenuItemsHost = (menu: Element, host: HTMLElement): void => {
7
+ const settingsRow = Array.from(menu.children).find(
8
+ (child) => child.matches('a[href="/preferences"]') || child.querySelector('a[href="/preferences"]'),
9
+ );
10
+
11
+ if (settingsRow) {
12
+ if (settingsRow.nextElementSibling !== host) settingsRow.after(host);
13
+ return;
14
+ }
15
+
16
+ if (host.parentElement !== menu) menu.append(host);
17
+ };
@@ -9,6 +9,7 @@ import { matchLast } from "../util.ts";
9
9
 
10
10
  import { warn } from "../logger.ts";
11
11
  import { transformer } from "../../mixin.ts";
12
+ import { placeMenuItemsHost } from "./menu-order.ts";
12
13
  import { Registry } from "./registry.ts";
13
14
 
14
15
  type __MenuContext = React.Context<MenuContext>;
@@ -125,14 +126,17 @@ if (
125
126
  );
126
127
 
127
128
  const ItemBoundary = createItemBoundary(R, "menu");
128
- const live: Array<{ menu: Element; root: { unmount: () => void } }> = [];
129
+ const live: Array<{ menu: Element; host: HTMLElement; root: { unmount: () => void } }> = [];
129
130
  const processed = new WeakSet<Element>();
130
131
 
131
- const sweep = () => {
132
+ const sweep = (records: MutationRecord[] = []) => {
133
+ const changedMenus = new Set(records.map((record) => record.target));
132
134
  for (let i = live.length - 1; i >= 0; i--) {
133
135
  if (!live[i].menu.isConnected) {
134
136
  live[i].root.unmount();
135
137
  live.splice(i, 1);
138
+ } else if (changedMenus.has(live[i].menu)) {
139
+ placeMenuItemsHost(live[i].menu, live[i].host);
136
140
  }
137
141
  }
138
142
  if (items.size === 0) return;
@@ -142,7 +146,7 @@ if (
142
146
  const host = document.createElement("li");
143
147
  host.setAttribute("role", "presentation");
144
148
  host.className = "spicetify-menu-items";
145
- menu.appendChild(host);
149
+ placeMenuItemsHost(menu, host);
146
150
  const root = createRoot(host);
147
151
  globalThis.__MenuContext ??= R.createContext(null);
148
152
  root.render(
@@ -152,7 +156,7 @@ if (
152
156
  ...items.all().map((item, i) => R.createElement(ItemBoundary, { key: i }, item)),
153
157
  ),
154
158
  );
155
- live.push({ menu, root });
159
+ live.push({ menu, host, root });
156
160
  }
157
161
  };
158
162
  new MutationObserver(sweep).observe(document.body, { childList: true, subtree: true });
@@ -143,10 +143,10 @@ const ensureAnchorStyle = () => {
143
143
  // constantly, and per-anchor subtree observers multiply that cost. Each
144
144
  // tick is one isConnected check per pending or placed anchor.
145
145
  type WatchedAnchor = { host: HTMLElement; place: () => boolean; started: boolean; start: () => void };
146
- const watched: WatchedAnchor[] = [];
146
+ const watched = new Set<WatchedAnchor>();
147
147
  let bodyObserver: MutationObserver | undefined;
148
148
  const watchAnchor = (anchor: WatchedAnchor) => {
149
- watched.push(anchor);
149
+ watched.add(anchor);
150
150
  bodyObserver ??= (() => {
151
151
  const observer = new MutationObserver(() => {
152
152
  for (const a of watched) {
@@ -160,6 +160,13 @@ const watchAnchor = (anchor: WatchedAnchor) => {
160
160
  observer.observe(document.body, { childList: true, subtree: true });
161
161
  return observer;
162
162
  })();
163
+ return () => {
164
+ watched.delete(anchor);
165
+ if (watched.size === 0) {
166
+ bodyObserver?.disconnect();
167
+ bodyObserver = undefined;
168
+ }
169
+ };
163
170
  };
164
171
 
165
172
  export function mountRegistryAnchor(spec: AnchorSpec): void {
@@ -192,6 +199,8 @@ export function mountRegistryAnchor(spec: AnchorSpec): void {
192
199
  const start = () => {
193
200
  const root = createRoot(host);
194
201
  const ItemBoundary = createItemBoundary(R, spec.className);
202
+ const itemKeys = new Map<unknown, number>();
203
+ let nextItemKey = 0;
195
204
  const Wrapper = () => {
196
205
  const [, refresh] = R.useReducer((n: number) => n + 1, 0);
197
206
  R.useEffect(() => {
@@ -201,12 +210,20 @@ export function mountRegistryAnchor(spec: AnchorSpec): void {
201
210
  if (spec.renderItems) {
202
211
  return R.createElement(ItemBoundary, null, spec.renderItems(spec.registry.all()));
203
212
  }
213
+ for (const item of itemKeys.keys()) {
214
+ if (!spec.registry.has(item)) itemKeys.delete(item);
215
+ }
204
216
  return R.createElement(
205
217
  R.Fragment,
206
218
  null,
207
- ...byOrder(spec.registry.all()).map((item: unknown, i: number) =>
208
- R.createElement(ItemBoundary, { key: i }, item),
209
- ),
219
+ ...byOrder(spec.registry.all()).map((item: unknown) => {
220
+ let key = itemKeys.get(item);
221
+ if (key === undefined) {
222
+ key = nextItemKey++;
223
+ itemKeys.set(item, key);
224
+ }
225
+ return R.createElement(ItemBoundary, { key }, item);
226
+ }),
210
227
  );
211
228
  };
212
229
  root.render(R.createElement(Wrapper));
@@ -240,6 +257,8 @@ export interface AdjacentSpec {
240
257
 
241
258
  export function mountAdjacent(spec: AdjacentSpec): { remove: () => void } {
242
259
  let removed = false;
260
+ let unwatch: (() => void) | undefined;
261
+ let retry: ReturnType<typeof setTimeout> | undefined;
243
262
  let root: { render?: (node: unknown) => void; unmount?: () => void } | undefined;
244
263
  const host = document.createElement("span");
245
264
  host.className = spec.className;
@@ -247,6 +266,7 @@ export function mountAdjacent(spec: AdjacentSpec): { remove: () => void } {
247
266
  host.dataset.spicetifyAnchor = "";
248
267
 
249
268
  const place = (): boolean => {
269
+ if (removed) return false;
250
270
  const target = spec.findTarget();
251
271
  if (!target?.parentElement) return false;
252
272
  const before = spec.side === "before" ? target : target.nextSibling;
@@ -255,9 +275,15 @@ export function mountAdjacent(spec: AdjacentSpec): { remove: () => void } {
255
275
  };
256
276
 
257
277
  const remove = () => {
278
+ if (removed) return;
258
279
  removed = true;
259
- root?.unmount?.();
260
- host.remove();
280
+ clearTimeout(retry);
281
+ unwatch?.();
282
+ try {
283
+ root?.unmount?.();
284
+ } finally {
285
+ host.remove();
286
+ }
261
287
  };
262
288
 
263
289
  // Transform experiments render through injected __renderX() calls, and there
@@ -291,14 +317,14 @@ export function mountAdjacent(spec: AdjacentSpec): { remove: () => void } {
291
317
  if (place()) {
292
318
  start();
293
319
  // Reuse the shared observer for re-placement only (already started).
294
- watchAnchor({ host, place, started: true, start: () => {} });
320
+ unwatch = watchAnchor({ host, place, started: true, start: () => {} });
295
321
  return;
296
322
  }
297
323
  if (Date.now() > deadline) {
298
324
  spec.onGiveUp();
299
325
  return;
300
326
  }
301
- setTimeout(attempt, 150);
327
+ retry = setTimeout(attempt, 150);
302
328
  };
303
329
  attempt();
304
330
  });
@@ -5,11 +5,10 @@
5
5
 
6
6
  import { React } from "../expose/React.ts";
7
7
  import { createIconComponent } from "../createIconComponent.tsx";
8
- import { transformer } from "../../mixin.ts";
9
8
  import { Platform } from "../expose/Platform.ts";
10
9
  import { warn } from "../logger.ts";
11
10
  import { classnames } from "../webpack/ClassNames.ts";
12
- import { ScrollableContainer, Tooltip } from "../webpack/ReactComponents.ts";
11
+ import { Tooltip } from "../webpack/ReactComponents.ts";
13
12
  import { UI } from "../webpack/ComponentLibrary.ts";
14
13
  import { mountRegistryAnchor } from "./mount.ts";
15
14
  import { Registry } from "./registry.ts";
@@ -29,52 +28,6 @@ export default registry;
29
28
 
30
29
  let refresh: React.DispatchWithoutAction | undefined;
31
30
 
32
- declare global {
33
- var __renderNavLinks: () => React.ReactNode;
34
- }
35
-
36
- globalThis.__renderNavLinks = () =>
37
- React.createElement(() => {
38
- [, refresh] = React.useReducer((n) => n + 1, 0);
39
-
40
- if (!ScrollableContainer) {
41
- return;
42
- }
43
-
44
- return (
45
- <ScrollableContainer className="custom-navlinks-scrollable_container" onlyHorizontalWheel>
46
- {registry.all()}
47
- </ScrollableContainer>
48
- );
49
- });
50
- transformer(
51
- (emit) => (str) => {
52
- emit();
53
-
54
- str = str.replace(
55
- /{(?=[^{}]*(?:{[^{}]*(?:{[^{}]*(?:{[^{}]*}[^{}]*)*}[^{}]*)*}[^{}]*)*(?<=[,{])"data-testid":"global-nav-bar")([^{}]*(?:{[^{}]*(?:{[^{}]*(?:{[^{}]*}[^{}]*)*}[^{}]*)*}[^{}]*)*(?<=[,{]))children:([^{}]*(?:{[^{}]*(?:{[^{}]*(?:{[^{}]*}[^{}]*)*}[^{}]*)*}[^{}]*)*)(,[^{}]*(?:{[^{}]*(?:{[^{}]*(?:{[^{}]*}[^{}]*)*}[^{}]*)*}[^{}]*)*|)}/,
56
- "{$1children:(function(children){const p=children[0].props;p.children=[p.children,__renderNavLinks()].flat();return children})($2)$3}",
57
- );
58
-
59
- return str;
60
- },
61
- {
62
- glob: /^\/xpui\.js/,
63
- },
64
- );
65
- transformer(
66
- (emit) => (str) => {
67
- emit();
68
-
69
- str = str.replace('["","/","/home/",', '["","/","/home/","/bespoke/*",');
70
-
71
- return str;
72
- },
73
- {
74
- glob: /^\/dwp\-top\-bar\.js/,
75
- },
76
- );
77
-
78
31
  mountRegistryAnchor({
79
32
  className: "spicetify-navlinks-anchor",
80
33
  registry,
@@ -88,8 +41,8 @@ mountRegistryAnchor({
88
41
  // A real flex box, not the default display:contents. The history-buttons
89
42
  // wrapper spaces its children with a wide gap meant to separate button
90
43
  // groups; as one box the anchor takes that gap once (separating our links
91
- // from the back/forward group), while the nav links inside space
92
- // themselves with the client's own 8px navlink margins.
44
+ // from the back/forward group), while the nav links inside are spaced by
45
+ // the anchor's own gap (stdlib index.scss).
93
46
  hostDisplay: "flex",
94
47
  });
95
48
 
@@ -5,7 +5,6 @@
5
5
 
6
6
  import { React } from "../expose/React.ts";
7
7
  import { createIconComponent } from "../createIconComponent.tsx";
8
- import { transformer } from "../../mixin.ts";
9
8
  import { Tooltip } from "../webpack/ReactComponents.ts";
10
9
  import { UI } from "../webpack/ComponentLibrary.ts";
11
10
  import { classnames } from "../webpack/ClassNames.ts";
@@ -27,24 +26,6 @@ export default registry;
27
26
 
28
27
  let refresh: React.DispatchWithoutAction | undefined;
29
28
 
30
- declare global {
31
- var __renderPlaybarBarControls: any;
32
- }
33
-
34
- globalThis.__renderPlaybarBarControls = () => registry.all().reverse();
35
- transformer(
36
- (emit) => (str) => {
37
- emit();
38
-
39
- str = str.replace(/(children:\[)([^\[]*djJumpButtonFactory)/, "$1...__renderPlaybarBarControls(),$2");
40
-
41
- return str;
42
- },
43
- {
44
- glob: /^\/xpui\.js/,
45
- },
46
- );
47
-
48
29
  mountRegistryAnchor({
49
30
  className: "spicetify-playbar-buttons",
50
31
  registry,
@@ -5,7 +5,6 @@
5
5
 
6
6
  import { React } from "../expose/React.ts";
7
7
  import { createIconComponent } from "../createIconComponent.tsx";
8
- import { transformer } from "../../mixin.ts";
9
8
  import { Tooltip } from "../webpack/ReactComponents.ts";
10
9
  import { UI } from "../webpack/ComponentLibrary.ts";
11
10
  import { mountRegistryAnchor } from "./mount.ts";
@@ -26,24 +25,6 @@ export default registry;
26
25
 
27
26
  let refresh: React.DispatchWithoutAction | undefined;
28
27
 
29
- declare global {
30
- var __renderNowPlayingWidgets: any;
31
- }
32
-
33
- globalThis.__renderNowPlayingWidgets = () => registry.all();
34
- transformer(
35
- (emit) => (str) => {
36
- emit();
37
-
38
- str = str.replace(/(hideButtonFactory[^\]]*)/, "$1,...__renderNowPlayingWidgets()");
39
-
40
- return str;
41
- },
42
- {
43
- glob: /^\/xpui\.js/,
44
- },
45
- );
46
-
47
28
  mountRegistryAnchor({
48
29
  className: "spicetify-playbar-widgets",
49
30
  registry,
@@ -56,7 +56,7 @@ globalThis.__renderRootChildren = () =>
56
56
  return childrenRegistry.all();
57
57
  });
58
58
  globalThis.__renderRootProviders = (providers: React.ReactElement[]) => {
59
- const MultiProvider = ({ children }) => {
59
+ const MultiProvider = ({ children }: React.PropsWithChildren) => {
60
60
  const [, refresh] = React.useReducer((n) => n + 1, 0);
61
61
  if (!providersRegistry.refresh.value) {
62
62
  providersRegistry.refresh.resolve(refresh);
@@ -3,7 +3,6 @@
3
3
  * SPDX-License-Identifier: GPL-3.0-or-later
4
4
  */
5
5
 
6
- import { transformer } from "../../mixin.ts";
7
6
  import { Platform } from "../expose/Platform.ts";
8
7
  import { React } from "../expose/React.ts";
9
8
  import { mountRegistryAnchor } from "./mount.ts";
@@ -25,28 +24,7 @@ export default registry;
25
24
  let refresh: (() => void) | undefined;
26
25
  let historyHooked = false;
27
26
 
28
- declare global {
29
- var __renderRoutes: any;
30
- }
31
-
32
- globalThis.__renderRoutes = () => registry.all();
33
- transformer(
34
- (emit) => (str) => {
35
- emit();
36
-
37
- str = str.replace(
38
- /(\(0,[a-zA-Z_$][\w$]*\.jsx\)\([a-zA-Z_$][\w$]*\.[a-zA-Z_$][\w$]*,\{[^{]*path:"\/search\/\*")/,
39
- "...__renderRoutes(),$1",
40
- );
41
-
42
- return str;
43
- },
44
- {
45
- glob: /^\/xpui\.js/,
46
- },
47
- );
48
-
49
- // Transform-free path: the client router never learns module routes, so a
27
+ // The client router never learns module routes, so a
50
28
  // history-driven overlay covers the main view whenever the current path
51
29
  // matches a registered <Route>. The client renders its own not-found page
52
30
  // underneath; back/forward keep working because only real history is used.
@@ -5,7 +5,6 @@
5
5
 
6
6
  import { React } from "../expose/React.ts";
7
7
  import { createIconComponent } from "../createIconComponent.tsx";
8
- import { transformer } from "../../mixin.ts";
9
8
  import { Tooltip } from "../webpack/ReactComponents.ts";
10
9
  import { UI } from "../webpack/ComponentLibrary.ts";
11
10
  import { mountRegistryAnchor } from "./mount.ts";
@@ -26,29 +25,6 @@ export default registry;
26
25
 
27
26
  let refresh: React.DispatchWithoutAction | undefined;
28
27
 
29
- declare global {
30
- var __renderTopbarLeftButtons: () => React.ReactNode;
31
- }
32
-
33
- globalThis.__renderTopbarLeftButtons = () =>
34
- React.createElement(() => {
35
- [, refresh] = React.useReducer((n) => n + 1, 0);
36
-
37
- return <>{registry.all()}</>;
38
- });
39
- transformer(
40
- (emit) => (str) => {
41
- emit();
42
-
43
- str = str.replace(/("top-bar-forward-button"[^\]]*)/g, "$1,__renderTopbarLeftButtons()");
44
-
45
- return str;
46
- },
47
- {
48
- glob: /^\/xpui\.js/,
49
- },
50
- );
51
-
52
28
  mountRegistryAnchor({
53
29
  className: "spicetify-topbar-left-buttons",
54
30
  registry,
@@ -5,7 +5,6 @@
5
5
 
6
6
  import { React } from "../expose/React.ts";
7
7
  import { createIconComponent } from "../createIconComponent.tsx";
8
- import { transformer } from "../../mixin.ts";
9
8
  import { Tooltip } from "../webpack/ReactComponents.ts";
10
9
  import { UI } from "../webpack/ComponentLibrary.ts";
11
10
  import { mountRegistryAnchor } from "./mount.ts";
@@ -26,29 +25,6 @@ export default registry;
26
25
 
27
26
  let refresh: React.DispatchWithoutAction | undefined;
28
27
 
29
- declare global {
30
- var __renderTopbarRightButtons: () => React.ReactNode;
31
- }
32
-
33
- globalThis.__renderTopbarRightButtons = () =>
34
- React.createElement(() => {
35
- [, refresh] = React.useReducer((n) => n + 1, 0);
36
-
37
- return <>{registry.all().reverse()}</>;
38
- });
39
- transformer(
40
- (emit) => (str) => {
41
- emit();
42
-
43
- str = str.replace(/("login-button"[^\}]*\}[^\}]*\}[^\}]*\}\))/, "$1,__renderTopbarRightButtons()");
44
-
45
- return str;
46
- },
47
- {
48
- glob: /^\/xpui\.js/,
49
- },
50
- );
51
-
52
28
  mountRegistryAnchor({
53
29
  className: "spicetify-topbar-right-buttons",
54
30
  registry,
@@ -55,7 +55,7 @@ export function createSyncedStorage(playlistUri: string) {
55
55
  async function getUris(key: string) {
56
56
  assertSmallerSize(encodeURIComponent(key), CHUNK_SIZE, 1, "Can't fit key in a single chunk");
57
57
 
58
- const { items } = await PlaylistAPI.getContents(playlistUri, {
58
+ const { items }: { items: Array<{ uri: string }> } = await PlaylistAPI.getContents(playlistUri, {
59
59
  filter: key,
60
60
  limit: 1e9,
61
61
  });
@@ -11,6 +11,7 @@ import { Platform } from "../expose/Platform.ts";
11
11
  import { exportedFunctions, exportedMemos, modules, src } from "./index.ts";
12
12
  import { webpackRequire } from "../wpunpk.mix.ts";
13
13
  import { React } from "../expose/React.ts";
14
+ import { findPlaylistMenu } from "./playlist-menu.ts";
14
15
 
15
16
  type SnackbarProviderT = any;
16
17
 
@@ -43,16 +44,9 @@ const requireExports = (id: ReturnType<typeof findModuleId>): Record<string, any
43
44
  id === undefined ? {} : (webpackRequire(id) as Record<string, any>);
44
45
 
45
46
  const ContextMenuModuleID = findModuleId("toggleContextMenu", (s) => s.includes("toggleContextMenu"));
46
- // Dead since 1.2.9x: no module carries value:"playlist" + canView +
47
- // permissions anymore; Menus.Playlist stays undefined until re-needled.
48
- const playlistMenuModuleID = findModuleId(
49
- 'value:"playlist" + canView + permissions',
50
- (s) => s.includes('value:"playlist"') && s.includes("canView") && s.includes("permissions"),
51
- );
52
47
 
53
- Menus.Playlist = Object.values(requireExports(playlistMenuModuleID)).find(
54
- (m) => typeof m === "function" || typeof m === "object",
55
- );
48
+ Menus.Playlist = findPlaylistMenu(modules, requireExports);
49
+ if (Menus.Playlist === undefined) warn("[stdlib] webpack needle miss: playlist menu component");
56
50
 
57
51
  export const Cards: any = Object.assign(
58
52
  {
@@ -105,7 +105,7 @@ export const from: (uri: ParsableAsURI) => URIClass<any> = findAndExcludeBy("all
105
105
  export const fromString: (str: string) => URIClass<any> = findAndExcludeBy("Argument `uri` must be a string.") as any;
106
106
 
107
107
  is.PlaylistV1OrV2 = (
108
- is.Playlist && is.PlaylistV2
108
+ typeof is.Playlist === "function" && typeof is.PlaylistV2 === "function"
109
109
  ? findAndExcludeBy(`${(is.Playlist as Function).name}(e)||${(is.PlaylistV2 as Function).name}(e)`)
110
110
  : undefined
111
111
  ) as any;