@spicetify/kit 0.1.0 → 0.2.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.
@@ -3,57 +3,77 @@
3
3
  * SPDX-License-Identifier: GPL-3.0-or-later
4
4
  */
5
5
 
6
- import { transformer } from "../../mixin.ts";
7
- import { mountRegistryAnchor } from "./mount.ts";
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 registry = new (class extends Registry<React.ReactNode> {
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
- refresh?.();
13
- return super.add(value);
21
+ super.add(value);
22
+ notify();
23
+ return this;
14
24
  }
15
25
 
16
26
  override delete(value: React.ReactNode): boolean {
17
- refresh?.();
18
- return super.delete(value);
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
- let refresh: (() => void) | undefined;
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
- declare global {
26
- var __renderSettingSections: any;
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
- globalThis.__renderSettingSections = () => registry.all();
30
- transformer(
31
- (emit) => (str) => {
32
- emit();
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
- 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()]",
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
- 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
- });
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
- condensedAll
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
+ };