@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,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,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
|
+
});
|