@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,168 @@
1
+ /*
2
+ * Copyright (C) 2024 Delusoire
3
+ * SPDX-License-Identifier: GPL-3.0-or-later
4
+ */
5
+
6
+ import { transformer } from "../../mixin.ts";
7
+
8
+ import type { createMachine as createMachineT } from "npm:xstate";
9
+ import { Registry } from "./registry.ts";
10
+
11
+ export type StateMachine = ReturnType<typeof createMachineT>;
12
+ export let Machine: StateMachine;
13
+
14
+ const registry = new (class extends Registry<React.ReactNode> {
15
+ private static NodeNash = Symbol.for("NodeHash");
16
+
17
+ getHash(value: React.ReactNode) {
18
+ if (!this.has(value)) {
19
+ return null;
20
+ }
21
+ // @ts-ignore
22
+ const hash = value[this.constructor.NodeNash];
23
+ const state = `bespoke_${hash}`;
24
+ const event = `bespoke_${hash}_button_click`;
25
+ return { state, event };
26
+ }
27
+
28
+ override add(value: React.ReactNode, onEntry?: any, onExit?: any): this {
29
+ const hash = crypto.randomUUID();
30
+ // @ts-ignore
31
+ value[this.constructor.NodeNash] = hash;
32
+ const state = `bespoke_${hash}`;
33
+ const event = `bespoke_${hash}_button_click`;
34
+
35
+ stateToNode.set(state, value);
36
+
37
+ ON[event] = {
38
+ target: state,
39
+ };
40
+
41
+ STATES[state] = {
42
+ on: Object.setPrototypeOf(
43
+ {
44
+ [event]: {
45
+ target: "disabled",
46
+ },
47
+ },
48
+ ON,
49
+ ),
50
+ };
51
+
52
+ if (onEntry) {
53
+ const entry = `bespoke_${hash}_entry`;
54
+ STATES[state].entry = [entry];
55
+ ACTIONS[entry] = onEntry;
56
+ }
57
+ if (onExit) {
58
+ const exit = `bespoke_${hash}_exit`;
59
+ Machine.config.states![state].exit = [exit];
60
+ ACTIONS[exit] = onExit;
61
+ }
62
+
63
+ return super.add(value);
64
+ }
65
+
66
+ override delete(item: React.ReactNode): boolean {
67
+ // @ts-ignore
68
+ const hash = item[this.constructor.NodeNash];
69
+ const state = `bespoke_${hash}`;
70
+ const event = `bespoke_${hash}_button_click`;
71
+
72
+ stateToNode.delete(state);
73
+
74
+ delete ON[event];
75
+ return super.delete(item);
76
+ }
77
+ })();
78
+ export default registry;
79
+
80
+ const stateToNode = new Map<string, React.ReactNode>();
81
+
82
+ declare global {
83
+ var __renderPanel: any;
84
+ }
85
+
86
+ globalThis.__renderPanel = (state: string) => {
87
+ if (!state.startsWith("bespoke_")) {
88
+ return null;
89
+ }
90
+
91
+ return stateToNode.get(state);
92
+ };
93
+
94
+ let ON: Record<string, any> = {};
95
+ let STATES: Record<string, any> = {};
96
+ let ACTIONS: Record<string, any> = {};
97
+
98
+ transformer(
99
+ (emit) => (str) => {
100
+ emit();
101
+
102
+ str = str.replace(/(=\(0,[a-zA-Z_\$][\w\$]*\.[a-zA-Z_\$][\w\$]*\)\(\{id:"RightPanelState)/, "=__Machine$1");
103
+ Object.defineProperty(globalThis, "__Machine", {
104
+ set: ($: StateMachine) => {
105
+ Machine = $;
106
+
107
+ queueMicrotask(() => {
108
+ ON = {
109
+ ...ON,
110
+ ...Machine.config.states!.disabled.on,
111
+ panel_close_click: [
112
+ {
113
+ target: "disabled",
114
+ },
115
+ ],
116
+ };
117
+ delete ON.playback_autoplay_context_changed;
118
+
119
+ for (const [k, v] of Object.entries(Machine.config.states! as Record<string, any>)) {
120
+ if (k === "puffin_activation") {
121
+ continue;
122
+ }
123
+ v.on = new Proxy(v.on!, {
124
+ get(target, p, _receiver) {
125
+ // @ts-ignore
126
+ if (p.startsWith("bespoke_")) {
127
+ // @ts-ignore
128
+ return ON[p];
129
+ }
130
+ // @ts-ignore
131
+ return target[p];
132
+ },
133
+ });
134
+ }
135
+
136
+ Object.setPrototypeOf(Machine.config.states!, STATES);
137
+
138
+ Machine._options.actions = ACTIONS;
139
+ });
140
+ },
141
+ get: () => Machine,
142
+ });
143
+
144
+ // ! HACKY ALERT
145
+ str = str.replace(
146
+ /(case [a-zA-Z_\$][\w\$]*\.[a-zA-Z_\$][\w\$]*\.Disabled:return!0;default:)/,
147
+ "$1return true;",
148
+ );
149
+
150
+ return str;
151
+ },
152
+ {
153
+ glob: /^\/xpui\.js/,
154
+ },
155
+ );
156
+
157
+ transformer(
158
+ (emit) => (str) => {
159
+ emit();
160
+
161
+ str = str.replace(/(\(([a-zA-Z_\$][\w\$]*),"PanelSection".+?children:\[?)/, "$1__renderPanel($2),");
162
+
163
+ return str;
164
+ },
165
+ {
166
+ glob: /^\/dwp\-panel\-section\.js/,
167
+ },
168
+ );
@@ -0,0 +1,93 @@
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 { createIconComponent } from "../createIconComponent.tsx";
8
+ import { transformer } from "../../mixin.ts";
9
+ import { Tooltip } from "../webpack/ReactComponents.ts";
10
+ import { UI } from "../webpack/ComponentLibrary.ts";
11
+ import { classnames } from "../webpack/ClassNames.ts";
12
+ import { mountRegistryAnchor } from "./mount.ts";
13
+ import { Registry } from "./registry.ts";
14
+
15
+ const registry = new (class extends Registry<React.ReactNode> {
16
+ override add(value: React.ReactNode): this {
17
+ refresh?.();
18
+ return super.add(value);
19
+ }
20
+
21
+ override delete(value: React.ReactNode): boolean {
22
+ refresh?.();
23
+ return super.delete(value);
24
+ }
25
+ })();
26
+ export default registry;
27
+
28
+ let refresh: React.DispatchWithoutAction | undefined;
29
+
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
+ mountRegistryAnchor({
49
+ className: "spicetify-playbar-buttons",
50
+ registry,
51
+ setRefresh: (cb) => {
52
+ refresh = cb;
53
+ },
54
+ findSlot: () => {
55
+ const controls = document.querySelector(".main-nowPlayingBar-extraControls");
56
+ return controls ? { parent: controls, before: controls.firstChild } : null;
57
+ },
58
+ });
59
+
60
+ export type PlaybarButtonProps = {
61
+ label: string;
62
+ isActive?: boolean;
63
+ isActiveNoIndicator?: boolean;
64
+ disabled?: boolean;
65
+ icon?: string;
66
+ onClick: () => void;
67
+ /** Position within the module-button group; lower renders earlier. Read by the mount sorter. */
68
+ order?: number;
69
+ };
70
+ export const PlaybarButton = ({
71
+ label,
72
+ isActive = false,
73
+ isActiveNoIndicator = false,
74
+ disabled = false,
75
+ icon,
76
+ onClick,
77
+ }: PlaybarButtonProps) => (
78
+ <Tooltip label={label}>
79
+ <UI.ButtonTertiary
80
+ aria-label={label}
81
+ size="small"
82
+ className={classnames(MAP.main.playbar.buttons.button.wrapper, {
83
+ [MAP.main.playbar.buttons.button.wrapper__indicator]: isActive,
84
+ [MAP.main.playbar.buttons.button.wrapper__active]: isActive || isActiveNoIndicator,
85
+ })}
86
+ disabled={disabled}
87
+ iconOnly={icon && (() => createIconComponent({ icon }))}
88
+ onClick={onClick}
89
+ data-active={isActive.toString()}
90
+ aria-pressed={isActive}
91
+ />
92
+ </Tooltip>
93
+ );
@@ -0,0 +1,71 @@
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 { createIconComponent } from "../createIconComponent.tsx";
8
+ import { transformer } from "../../mixin.ts";
9
+ import { Tooltip } from "../webpack/ReactComponents.ts";
10
+ import { UI } from "../webpack/ComponentLibrary.ts";
11
+ import { mountRegistryAnchor } from "./mount.ts";
12
+ import { Registry } from "./registry.ts";
13
+
14
+ const registry = new (class extends Registry<React.ReactNode> {
15
+ override add(value: React.ReactNode): this {
16
+ refresh?.();
17
+ return super.add(value);
18
+ }
19
+
20
+ override delete(value: React.ReactNode): boolean {
21
+ refresh?.();
22
+ return super.delete(value);
23
+ }
24
+ })();
25
+ export default registry;
26
+
27
+ let refresh: React.DispatchWithoutAction | undefined;
28
+
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
+ mountRegistryAnchor({
48
+ className: "spicetify-playbar-widgets",
49
+ registry,
50
+ setRefresh: (cb) => {
51
+ refresh = cb;
52
+ },
53
+ findSlot: () => {
54
+ const left = document.querySelector(".main-nowPlayingBar-left");
55
+ return left ? { parent: left } : null;
56
+ },
57
+ });
58
+
59
+ export type PlaybarWidgetProps = { label: string; icon?: string; onClick: () => void };
60
+ export const PlaybarWidget = ({ label, icon, onClick }: PlaybarWidgetProps) => (
61
+ <Tooltip label={label}>
62
+ <UI.ButtonTertiary
63
+ size="small"
64
+ className={undefined}
65
+ aria-label={label}
66
+ condensed={false}
67
+ iconOnly={icon && (() => createIconComponent({ icon }))}
68
+ onClick={onClick}
69
+ />
70
+ </Tooltip>
71
+ );
@@ -0,0 +1,12 @@
1
+ /*
2
+ * Copyright (C) 2024 Delusoire
3
+ * SPDX-License-Identifier: GPL-3.0-or-later
4
+ */
5
+
6
+ export class Registry<E> extends Set<E> {
7
+ _E!: E;
8
+
9
+ public all(): Array<E> {
10
+ return Array.from(this);
11
+ }
12
+ }
@@ -0,0 +1,101 @@
1
+ /*
2
+ * Copyright (C) 2024 Delusoire
3
+ * SPDX-License-Identifier: GPL-3.0-or-later
4
+ */
5
+
6
+ import { transformer } from "../../mixin.ts";
7
+ import { mountRegistryAnchor } from "./mount.ts";
8
+ import { Registry } from "./registry.ts";
9
+ import { React } from "../expose/React.ts";
10
+
11
+ type Refresh = PromiseWithResolvers<() => void> & {
12
+ value: (() => void) | undefined;
13
+ };
14
+
15
+ class RootRegistry extends Registry<React.ReactNode> {
16
+ refresh = Object.assign(Promise.withResolvers<() => void>(), {
17
+ value: undefined as (() => void) | undefined,
18
+ });
19
+
20
+ static queueRefresh(refresh: Refresh) {
21
+ if (refresh.value) {
22
+ refresh.value();
23
+ } else {
24
+ refresh.promise.then((value) => value());
25
+ }
26
+ }
27
+
28
+ override add(value: React.ReactNode): this {
29
+ const ret = super.add(value);
30
+ RootRegistry.queueRefresh(this.refresh);
31
+ return ret;
32
+ }
33
+
34
+ override delete(value: React.ReactNode): boolean {
35
+ const ret = super.delete(value);
36
+ RootRegistry.queueRefresh(this.refresh);
37
+ return ret;
38
+ }
39
+ }
40
+
41
+ const childrenRegistry = new RootRegistry();
42
+ const providersRegistry = new RootRegistry();
43
+ export default [childrenRegistry, providersRegistry];
44
+
45
+ declare global {
46
+ var __renderRootChildren: () => React.ReactNode;
47
+ var __renderRootProviders: (providers: React.ReactElement[]) => React.ReactNode;
48
+ }
49
+ globalThis.__renderRootChildren = () =>
50
+ React.createElement(() => {
51
+ const [, refresh] = React.useReducer((n) => n + 1, 0);
52
+ if (!childrenRegistry.refresh.value) {
53
+ childrenRegistry.refresh.resolve(refresh);
54
+ }
55
+ childrenRegistry.refresh.value = refresh;
56
+ return childrenRegistry.all();
57
+ });
58
+ globalThis.__renderRootProviders = (providers: React.ReactElement[]) => {
59
+ const MultiProvider = ({ children }) => {
60
+ const [, refresh] = React.useReducer((n) => n + 1, 0);
61
+ if (!providersRegistry.refresh.value) {
62
+ providersRegistry.refresh.resolve(refresh);
63
+ }
64
+ providersRegistry.refresh.value = refresh;
65
+ const providers = providersRegistry.all() as React.ReactElement[];
66
+ return providers.reduceRight(
67
+ (acc: React.ReactElement, e: React.ReactElement) => React.cloneElement(e, undefined, acc),
68
+ React.createElement(React.Fragment, undefined, children),
69
+ );
70
+ };
71
+
72
+ return React.useMemo(() => [providers, React.createElement(MultiProvider)].flat(), [providers]);
73
+ };
74
+ transformer(
75
+ (emit) => (str) => {
76
+ emit();
77
+
78
+ str = str.replace(
79
+ /\bproviders:([a-zA-Z_\$][\w\$]*),children:\[([^\]]+"data-testid.:.root")/,
80
+ "providers:__renderRootProviders($1),children:[__renderRootChildren(),$2",
81
+ );
82
+
83
+ return str;
84
+ },
85
+ {
86
+ glob: /^\/xpui\.js/,
87
+ },
88
+ );
89
+
90
+ // Transform-free path: root children (modals, portals) render into a
91
+ // body-level anchor. Providers cannot wrap the client tree without a
92
+ // transform, so they stay transform-only.
93
+ mountRegistryAnchor({
94
+ className: "spicetify-root-children",
95
+ registry: childrenRegistry,
96
+ setRefresh: (cb) => {
97
+ if (cb && !childrenRegistry.refresh.value) childrenRegistry.refresh.resolve(cb);
98
+ childrenRegistry.refresh.value = cb;
99
+ },
100
+ findSlot: () => (document.body ? { parent: document.body } : null),
101
+ });
@@ -0,0 +1,123 @@
1
+ /*
2
+ * Copyright (C) 2024 Delusoire
3
+ * SPDX-License-Identifier: GPL-3.0-or-later
4
+ */
5
+
6
+ import { transformer } from "../../mixin.ts";
7
+ import { Platform } from "../expose/Platform.ts";
8
+ import { React } from "../expose/React.ts";
9
+ import { mountRegistryAnchor } from "./mount.ts";
10
+ import { Registry } from "./registry.ts";
11
+
12
+ const registry = new (class extends Registry<React.ReactNode> {
13
+ override add(value: React.ReactNode): this {
14
+ refresh?.();
15
+ return super.add(value);
16
+ }
17
+
18
+ override delete(value: React.ReactNode): boolean {
19
+ refresh?.();
20
+ return super.delete(value);
21
+ }
22
+ })();
23
+ export default registry;
24
+
25
+ let refresh: (() => void) | undefined;
26
+ let historyHooked = false;
27
+
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
50
+ // history-driven overlay covers the main view whenever the current path
51
+ // matches a registered <Route>. The client renders its own not-found page
52
+ // underneath; back/forward keep working because only real history is used.
53
+ const matchRoute = (pathname: string, pattern: string): boolean => {
54
+ const path = pathname.split("/").filter(Boolean);
55
+ const parts = pattern.split("/").filter(Boolean);
56
+ for (let i = 0; i < parts.length; i++) {
57
+ if (parts[i] === "*") return true;
58
+ if (parts[i].startsWith(":")) {
59
+ if (path[i] === undefined) return false;
60
+ continue;
61
+ }
62
+ if (parts[i] !== path[i]) return false;
63
+ }
64
+ return path.length === parts.length;
65
+ };
66
+
67
+ mountRegistryAnchor({
68
+ className: "spicetify-route-overlay",
69
+ registry,
70
+ setRefresh: (cb) => {
71
+ refresh = cb;
72
+ if (cb && !historyHooked) {
73
+ historyHooked = true;
74
+ try {
75
+ Platform.getHistory().listen(() => refresh?.());
76
+ } catch (e) {
77
+ console.warn("[stdlib] route overlay cannot follow history:", e);
78
+ }
79
+ }
80
+ },
81
+ findSlot: () => {
82
+ const main = document.querySelector(".main-view-container");
83
+ return main ? { parent: main } : null;
84
+ },
85
+ renderItems: (items) => {
86
+ const pathname = (() => {
87
+ try {
88
+ return Platform.getHistory().location.pathname as string;
89
+ } catch {
90
+ return null;
91
+ }
92
+ })();
93
+ if (!pathname) return null;
94
+ // Items are react-router <Route path element> elements; a Route
95
+ // cannot render outside the client's <Routes>, so the overlay
96
+ // renders the element's content directly.
97
+ const route = items.find((item: any) => {
98
+ const path = item?.props?.path;
99
+ return typeof path === "string" && matchRoute(pathname, path);
100
+ }) as { props: { element?: unknown; children?: unknown } } | undefined;
101
+ if (!route) return null;
102
+ return (React as any).createElement(
103
+ "div",
104
+ {
105
+ className: "spicetify-route-page",
106
+ style: {
107
+ position: "absolute",
108
+ inset: 0,
109
+ // Just high enough to cover the client's own view (its
110
+ // positioned children top out at 2) and no higher: this page
111
+ // is opaque, so anything above it in the stack disappears.
112
+ // Theme chrome drawn over the view — panel borders, labels —
113
+ // sits above that and must keep showing.
114
+ zIndex: 2,
115
+ overflow: "auto",
116
+ // The opaque background lives in index.scss so a theme can
117
+ // restyle it without fighting an inline declaration.
118
+ },
119
+ },
120
+ (route.props.element ?? route.props.children) as never,
121
+ );
122
+ },
123
+ });
@@ -0,0 +1,56 @@
1
+ /*
2
+ * Copyright (C) 2026 spicetify
3
+ * SPDX-License-Identifier: GPL-3.0-or-later
4
+ */
5
+
6
+ import { React } from "../expose/React.ts";
7
+ import { SettingsSection } from "../../lib/primitives.tsx";
8
+ import { Registry } from "./registry.ts";
9
+ import settingsSection from "./settingsSection.ts";
10
+
11
+ // Rows from every module, rendered together under one "Spicetify" section
12
+ // in the client's settings page. A module with a single setting registers a
13
+ // row here instead of claiming a whole section (or a profile-menu slot) of
14
+ // its own; SettingsToggleRow (lib/primitives.tsx) is the one-boolean case.
15
+ const listeners = new Set<() => void>();
16
+
17
+ const registry = new (class extends Registry<React.ReactNode> {
18
+ override add(value: React.ReactNode): this {
19
+ super.add(value);
20
+ for (const l of listeners) l();
21
+ return this;
22
+ }
23
+
24
+ override delete(value: React.ReactNode): boolean {
25
+ const deleted = super.delete(value);
26
+ for (const l of listeners) l();
27
+ return deleted;
28
+ }
29
+ })();
30
+ export default registry;
31
+
32
+ const SpicetifyGroup = () => {
33
+ const [, force] = React.useReducer((n: number) => n + 1, 0);
34
+ React.useEffect(() => {
35
+ listeners.add(force);
36
+ return () => void listeners.delete(force);
37
+ }, []);
38
+ const rows = registry.all();
39
+ if (rows.length === 0) return null;
40
+ return (
41
+ <SettingsSection title="Spicetify">
42
+ {rows.map((row, i) => (
43
+ <React.Fragment key={i}>{row}</React.Fragment>
44
+ ))}
45
+ </SettingsSection>
46
+ );
47
+ };
48
+
49
+ // The group element must not be created at import time: this module is also
50
+ // evaluated during the mixin phase, before the client React instance exists,
51
+ // and the jsx runtime would call into a React that is not there yet.
52
+ if (typeof document !== "undefined") {
53
+ void CHUNKS.xpui.promise.then(() => {
54
+ settingsSection.add(React.createElement(SpicetifyGroup));
55
+ });
56
+ }
@@ -0,0 +1,59 @@
1
+ /*
2
+ * Copyright (C) 2024 Delusoire
3
+ * SPDX-License-Identifier: GPL-3.0-or-later
4
+ */
5
+
6
+ import { transformer } from "../../mixin.ts";
7
+ import { mountRegistryAnchor } from "./mount.ts";
8
+ import { Registry } from "./registry.ts";
9
+
10
+ const registry = new (class extends Registry<React.ReactNode> {
11
+ override add(value: React.ReactNode): this {
12
+ refresh?.();
13
+ return super.add(value);
14
+ }
15
+
16
+ override delete(value: React.ReactNode): boolean {
17
+ refresh?.();
18
+ return super.delete(value);
19
+ }
20
+ })();
21
+ export default registry;
22
+
23
+ let refresh: (() => void) | undefined;
24
+
25
+ declare global {
26
+ var __renderSettingSections: any;
27
+ }
28
+
29
+ globalThis.__renderSettingSections = () => registry.all();
30
+ transformer(
31
+ (emit) => (str) => {
32
+ emit();
33
+
34
+ str = str.replace(
35
+ /(\(0,[a-zA-Z_\$][\w\$]*\.jsx\)\([a-zA-Z_\$][\w\$]*,{settings:[a-zA-Z_\$][\w\$]*,setValue:[a-zA-Z_\$][\w\$]*}\))]/,
36
+ "$1,...__renderSettingSections()]",
37
+ );
38
+
39
+ return str;
40
+ },
41
+ {
42
+ wait: false,
43
+ glob: /^\/xpui-routes-desktop-settings\.js/,
44
+ },
45
+ );
46
+
47
+ // The settings container only exists while the settings route is open; the
48
+ // anchor keeper re-places the host every time it reappears.
49
+ mountRegistryAnchor({
50
+ className: "spicetify-settings-sections",
51
+ registry,
52
+ setRefresh: (cb) => {
53
+ refresh = cb;
54
+ },
55
+ findSlot: () => {
56
+ const container = document.querySelector(".x-settings-container");
57
+ return container ? { parent: container } : null;
58
+ },
59
+ });