@spicetify/kit 0.1.0 → 0.3.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/dist/check.js +9 -5
- package/dist/create.js +10 -11
- package/dist/from-theme.js +2 -1
- package/dist/stdlib-boundary.js +259 -0
- package/dist/version.js +8 -0
- package/package.json +52 -48
- package/vendor/shims/platform.d.ts +21201 -0
- package/vendor/shims/spicetify.d.ts +23 -23
- package/vendor/stdlib/lib/dialog-lifecycle.ts +53 -0
- package/vendor/stdlib/lib/floating-position.ts +57 -0
- package/vendor/stdlib/lib/floating.tsx +298 -0
- package/vendor/stdlib/lib/modal.tsx +20 -113
- package/vendor/stdlib/lib/popover.ts +26 -19
- package/vendor/stdlib/lib/primitives-classes.ts +12 -0
- package/vendor/stdlib/lib/primitives-vanilla.ts +27 -3
- package/vendor/stdlib/lib/primitives.tsx +176 -26
- package/vendor/stdlib/metadata.json +1 -1
- package/vendor/stdlib/mod.ts +8 -5
- package/vendor/stdlib/src/client.ts +65 -2
- package/vendor/stdlib/src/expose/Platform.ts +5 -2
- package/vendor/stdlib/src/registers/index.ts +40 -3
- package/vendor/stdlib/src/registers/panel-logic.ts +371 -0
- package/vendor/stdlib/src/registers/panel.ts +63 -151
- package/vendor/stdlib/src/registers/playbarButton.tsx +1 -1
- package/vendor/stdlib/src/registers/route.ts +2 -1
- package/vendor/stdlib/src/registers/settingsRow.tsx +4 -4
- package/vendor/stdlib/src/registers/settingsSection.ts +61 -41
- package/vendor/stdlib/src/registers/topbarLeftButton.tsx +1 -1
- package/vendor/stdlib/src/registers/topbarRightButton.tsx +3 -5
- package/vendor/stdlib/src/settings/corsProxy.tsx +102 -0
|
@@ -63,7 +63,7 @@ export type PlaybarButtonProps = {
|
|
|
63
63
|
isActiveNoIndicator?: boolean;
|
|
64
64
|
disabled?: boolean;
|
|
65
65
|
icon?: string;
|
|
66
|
-
onClick: () => void;
|
|
66
|
+
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
|
67
67
|
/** Position within the module-button group; lower renders earlier. Read by the mount sorter. */
|
|
68
68
|
order?: number;
|
|
69
69
|
};
|
|
@@ -97,11 +97,12 @@ mountRegistryAnchor({
|
|
|
97
97
|
const route = items.find((item: any) => {
|
|
98
98
|
const path = item?.props?.path;
|
|
99
99
|
return typeof path === "string" && matchRoute(pathname, path);
|
|
100
|
-
}) as { props: { element?: unknown; children?: unknown } } | undefined;
|
|
100
|
+
}) as { props: { path: string; element?: unknown; children?: unknown } } | undefined;
|
|
101
101
|
if (!route) return null;
|
|
102
102
|
return (React as any).createElement(
|
|
103
103
|
"div",
|
|
104
104
|
{
|
|
105
|
+
key: route.props.path,
|
|
105
106
|
className: "spicetify-route-page",
|
|
106
107
|
style: {
|
|
107
108
|
position: "absolute",
|
|
@@ -8,9 +8,9 @@ import { SettingsSection } from "../../lib/primitives.tsx";
|
|
|
8
8
|
import { Registry } from "./registry.ts";
|
|
9
9
|
import settingsSection from "./settingsSection.ts";
|
|
10
10
|
|
|
11
|
-
// Rows from every module, rendered together under one "
|
|
12
|
-
//
|
|
13
|
-
// row here instead of claiming a whole section (or a
|
|
11
|
+
// Rows from every module, rendered together under one "General" section on
|
|
12
|
+
// the standalone Spicetify Settings page. A module with a single setting
|
|
13
|
+
// registers a row here instead of claiming a whole section (or a menu slot) of
|
|
14
14
|
// its own; SettingsToggleRow (lib/primitives.tsx) is the one-boolean case.
|
|
15
15
|
const listeners = new Set<() => void>();
|
|
16
16
|
|
|
@@ -38,7 +38,7 @@ const SpicetifyGroup = () => {
|
|
|
38
38
|
const rows = registry.all();
|
|
39
39
|
if (rows.length === 0) return null;
|
|
40
40
|
return (
|
|
41
|
-
<SettingsSection title="
|
|
41
|
+
<SettingsSection title="General">
|
|
42
42
|
{rows.map((row, i) => (
|
|
43
43
|
<React.Fragment key={i}>{row}</React.Fragment>
|
|
44
44
|
))}
|
|
@@ -3,57 +3,77 @@
|
|
|
3
3
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { React } from "../expose/React.ts";
|
|
7
|
+
import { SETTINGS_HEADER_CLASS, SETTINGS_HEADER_CONTAINER_CLASS } from "../../lib/primitives-classes.ts";
|
|
8
8
|
import { Registry } from "./registry.ts";
|
|
9
|
+
import route from "./route.ts";
|
|
10
|
+
import { CorsProxySettings } from "../settings/corsProxy.tsx";
|
|
9
11
|
|
|
10
|
-
const
|
|
12
|
+
export const SPICETIFY_SETTINGS_ROUTE = "/bespoke/settings";
|
|
13
|
+
|
|
14
|
+
const listeners = new Set<() => void>();
|
|
15
|
+
const notify = () => {
|
|
16
|
+
for (const listener of listeners) listener();
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
class SettingsRegistry extends Registry<React.ReactNode> {
|
|
11
20
|
override add(value: React.ReactNode): this {
|
|
12
|
-
|
|
13
|
-
|
|
21
|
+
super.add(value);
|
|
22
|
+
notify();
|
|
23
|
+
return this;
|
|
14
24
|
}
|
|
15
25
|
|
|
16
26
|
override delete(value: React.ReactNode): boolean {
|
|
17
|
-
|
|
18
|
-
|
|
27
|
+
const deleted = super.delete(value);
|
|
28
|
+
if (deleted) notify();
|
|
29
|
+
return deleted;
|
|
19
30
|
}
|
|
20
|
-
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const registry = new SettingsRegistry();
|
|
21
34
|
export default registry;
|
|
22
35
|
|
|
23
|
-
|
|
36
|
+
// Navigation and management actions belong after every module's controls.
|
|
37
|
+
// Keeping them in a separate register makes the ordering structural instead
|
|
38
|
+
// of dependent on module load order.
|
|
39
|
+
export const settingsAction = new SettingsRegistry();
|
|
24
40
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
41
|
+
const SpicetifySettingsPage = () => {
|
|
42
|
+
const [, forceRender] = React.useReducer((value: number) => value + 1, 0);
|
|
43
|
+
React.useEffect(() => {
|
|
44
|
+
listeners.add(forceRender);
|
|
45
|
+
return () => void listeners.delete(forceRender);
|
|
46
|
+
}, []);
|
|
28
47
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
48
|
+
const sections = registry.all();
|
|
49
|
+
const actions = settingsAction.all();
|
|
50
|
+
return React.createElement(
|
|
51
|
+
"main",
|
|
52
|
+
{ className: "spicetify-settings-page x-settings-container" },
|
|
53
|
+
React.createElement(
|
|
54
|
+
"div",
|
|
55
|
+
{ className: SETTINGS_HEADER_CONTAINER_CLASS },
|
|
56
|
+
React.createElement(
|
|
57
|
+
"h1",
|
|
58
|
+
{ className: SETTINGS_HEADER_CLASS, style: { paddingBlockEnd: "16px" } },
|
|
59
|
+
"Spicetify Settings",
|
|
60
|
+
),
|
|
61
|
+
),
|
|
62
|
+
...sections.map((section, index) => React.createElement(React.Fragment, { key: `section-${index}` }, section)),
|
|
63
|
+
React.createElement(CorsProxySettings, { key: "cors-proxy" }),
|
|
64
|
+
...actions.map((action, index) => React.createElement(React.Fragment, { key: `action-${index}` }, action)),
|
|
65
|
+
);
|
|
66
|
+
};
|
|
33
67
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
68
|
+
// The page belongs to stdlib rather than Manager so settings remain available
|
|
69
|
+
// when the optional management UI fails or is disabled.
|
|
70
|
+
if (typeof document !== "undefined") {
|
|
71
|
+
void CHUNKS.xpui.promise.then(() => {
|
|
72
|
+
route.add(
|
|
73
|
+
React.createElement("route", {
|
|
74
|
+
path: SPICETIFY_SETTINGS_ROUTE,
|
|
75
|
+
element: React.createElement(SpicetifySettingsPage),
|
|
76
|
+
}),
|
|
37
77
|
);
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
});
|
|
78
|
+
});
|
|
79
|
+
}
|
|
@@ -64,7 +64,7 @@ mountRegistryAnchor({
|
|
|
64
64
|
type TopbarLeftButtonProps = {
|
|
65
65
|
label: string;
|
|
66
66
|
disabled?: boolean;
|
|
67
|
-
onClick: () => void;
|
|
67
|
+
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
|
68
68
|
icon?: string;
|
|
69
69
|
/** Position within the module-button group; lower renders earlier. Read by the mount sorter. */
|
|
70
70
|
order?: number;
|
|
@@ -64,7 +64,7 @@ mountRegistryAnchor({
|
|
|
64
64
|
type TopbarRightButtonProps = {
|
|
65
65
|
label: string;
|
|
66
66
|
disabled?: boolean;
|
|
67
|
-
onClick: () => void;
|
|
67
|
+
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
|
68
68
|
icon?: string;
|
|
69
69
|
/** Position within the module-button group; lower renders earlier. Read by the mount sorter. */
|
|
70
70
|
order?: number;
|
|
@@ -78,10 +78,8 @@ export const TopbarRightButton: TopbarRightButtonFactory = (props) => (
|
|
|
78
78
|
aria-label={props.label}
|
|
79
79
|
onClick={props.onClick}
|
|
80
80
|
size="small"
|
|
81
|
-
|
|
81
|
+
iconOnly={() => props.icon && createIconComponent({ icon: props.icon, iconSize: 16 })}
|
|
82
82
|
className={MAP.main.topbar.right.button_t.wrapper}
|
|
83
|
-
|
|
84
|
-
{props.icon && createIconComponent({ icon: props.icon, iconSize: 16 })}
|
|
85
|
-
</UI.ButtonTertiary>
|
|
83
|
+
/>
|
|
86
84
|
</Tooltip>
|
|
87
85
|
);
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Afonso Jorge Ramos
|
|
3
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { React } from "../expose/React.ts";
|
|
7
|
+
import { Button, Select, SettingsRow, SettingsSection, TextInput } from "../../lib/primitives.tsx";
|
|
8
|
+
import { SETTINGS_HELP_TEXT_CLASS } from "../../lib/primitives-classes.ts";
|
|
9
|
+
|
|
10
|
+
const MODE_OPTIONS = [
|
|
11
|
+
{ value: "automatic", label: "Automatic" },
|
|
12
|
+
{ value: "custom", label: "Custom" },
|
|
13
|
+
] as const;
|
|
14
|
+
|
|
15
|
+
const SettingsLabel = (props: { title: string; description: string }) => (
|
|
16
|
+
<span className="spicetify-settings-label-copy">
|
|
17
|
+
<span>{props.title}</span>
|
|
18
|
+
<span className={SETTINGS_HELP_TEXT_CLASS}>{props.description}</span>
|
|
19
|
+
</span>
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const CorsProxySettingsAvailable = (props: { api: typeof Spicetify.CORSProxy }) => {
|
|
23
|
+
const { api } = props;
|
|
24
|
+
const [initial] = React.useState(() => api.configuration());
|
|
25
|
+
const [mode, setMode] = React.useState<Spicetify.CORSProxy.Mode>(initial.mode);
|
|
26
|
+
const [template, setTemplate] = React.useState(initial.template ?? "");
|
|
27
|
+
const [message, setMessage] = React.useState("");
|
|
28
|
+
const valid = api.isValidTemplate(template);
|
|
29
|
+
const [daemonTemplate, hostedTemplate] = initial.automaticTemplates;
|
|
30
|
+
|
|
31
|
+
const changeMode = (next: Spicetify.CORSProxy.Mode) => {
|
|
32
|
+
setMode(next);
|
|
33
|
+
setMessage("");
|
|
34
|
+
if (next === "automatic") {
|
|
35
|
+
api.configure({ mode: "automatic" });
|
|
36
|
+
setMessage("Automatic routing is active.");
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const applyCustom = () => {
|
|
41
|
+
try {
|
|
42
|
+
api.configure({ mode: "custom", template });
|
|
43
|
+
setMessage("Custom proxy is active.");
|
|
44
|
+
} catch (error) {
|
|
45
|
+
setMessage(error instanceof Error ? error.message : "The custom proxy template is invalid.");
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<SettingsSection title="CORS Proxy">
|
|
51
|
+
<SettingsRow
|
|
52
|
+
label={
|
|
53
|
+
<SettingsLabel
|
|
54
|
+
title="Routing"
|
|
55
|
+
description="Automatic tries the authenticated local daemon first, then Spicetify's hosted proxy."
|
|
56
|
+
/>
|
|
57
|
+
}
|
|
58
|
+
>
|
|
59
|
+
<Select ariaLabel="CORS proxy routing" options={MODE_OPTIONS} value={mode} onChange={changeMode} />
|
|
60
|
+
</SettingsRow>
|
|
61
|
+
{mode === "custom" ? (
|
|
62
|
+
<SettingsRow
|
|
63
|
+
label={
|
|
64
|
+
<SettingsLabel
|
|
65
|
+
title="Custom template"
|
|
66
|
+
description={
|
|
67
|
+
'Use an absolute URL containing "{url}". A custom template replaces automatic fallback.'
|
|
68
|
+
}
|
|
69
|
+
/>
|
|
70
|
+
}
|
|
71
|
+
>
|
|
72
|
+
<div className="spicetify-cors-proxy-controls">
|
|
73
|
+
<TextInput
|
|
74
|
+
ariaLabel="Custom CORS proxy template"
|
|
75
|
+
placeholder="https://proxy.example/{url}"
|
|
76
|
+
value={template}
|
|
77
|
+
onInput={setTemplate}
|
|
78
|
+
/>
|
|
79
|
+
<Button variant="secondary" disabled={!valid} onClick={applyCustom}>
|
|
80
|
+
Apply
|
|
81
|
+
</Button>
|
|
82
|
+
</div>
|
|
83
|
+
</SettingsRow>
|
|
84
|
+
) : null}
|
|
85
|
+
<span className={SETTINGS_HELP_TEXT_CLASS}>
|
|
86
|
+
{message || `Automatic chain: ${daemonTemplate} → ${hostedTemplate}`}
|
|
87
|
+
</span>
|
|
88
|
+
</SettingsSection>
|
|
89
|
+
);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const CorsProxySettings = () => {
|
|
93
|
+
const api = Spicetify.CORSProxy;
|
|
94
|
+
if (
|
|
95
|
+
typeof api?.configuration !== "function" ||
|
|
96
|
+
typeof api?.configure !== "function" ||
|
|
97
|
+
typeof api?.isValidTemplate !== "function"
|
|
98
|
+
) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
return <CorsProxySettingsAvailable api={api} />;
|
|
102
|
+
};
|