Skip to content

Commit 30ff1ce

Browse files
committed
feat(plugin): settings-ui
1 parent d7bf973 commit 30ff1ce

File tree

18 files changed

+1646
-14
lines changed

18 files changed

+1646
-14
lines changed

src/config/defaults.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export interface DefaultConfig {
3535
usePodcastParticipantAsArtist: boolean;
3636
themes: string[];
3737
};
38-
'plugins': Record<string, unknown>;
38+
'plugins': Record<string, Record<string, unknown> & { enabled: boolean }>;
3939
}
4040

4141
const defaultConfig: DefaultConfig = {

src/config/index.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { deepmergeCustom } from 'deepmerge-ts';
22

3-
import defaultConfig from './defaults';
3+
import defaultConfig, { DefaultConfig } from './defaults';
44

55
import store, { IStore } from './store';
66
import plugins from './plugins';
@@ -26,6 +26,8 @@ function setMenuOption(key: string, value: unknown) {
2626
}
2727
}
2828

29+
const getStore = () => store.store;
30+
2931
// MAGIC OF TYPESCRIPT
3032

3133
type Prev = [
@@ -58,7 +60,8 @@ type Join<K, P> = K extends string | number
5860
? `${K}${'' extends P ? '' : '.'}${P}`
5961
: never
6062
: never;
61-
type Paths<T, D extends number = 10> = [D] extends [never]
63+
64+
export type Paths<T, D extends number = 10> = [D] extends [never]
6265
? never
6366
: T extends object
6467
? {
@@ -69,20 +72,21 @@ type Paths<T, D extends number = 10> = [D] extends [never]
6972
: '';
7073

7174
type SplitKey<K> = K extends `${infer A}.${infer B}` ? [A, B] : [K, string];
72-
type PathValue<T, K extends string> =
75+
export type PathValue<T, K extends string> =
7376
SplitKey<K> extends [infer A extends keyof T, infer B extends string]
7477
? PathValue<T[A], B>
7578
: T;
7679

77-
const get = <Key extends Paths<typeof defaultConfig>>(key: Key) =>
78-
store.get(key) as PathValue<typeof defaultConfig, typeof key>;
80+
const get = <Key extends Paths<DefaultConfig>>(key: Key) =>
81+
store.get(key) as PathValue<DefaultConfig, typeof key>;
7982

8083
export default {
8184
defaultConfig,
8285
get,
8386
set,
8487
setPartial,
8588
setMenuOption,
89+
getStore,
8690
edit: () => store.openInEditor(),
8791
watch(cb: Parameters<IStore['onDidAnyChange']>[0]) {
8892
store.onDidAnyChange(cb);

src/config/plugins.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export function getPlugins() {
1313

1414
export async function isEnabled(plugin: string) {
1515
const pluginConfig = deepmerge(
16-
(await allPlugins())[plugin].config ?? { enabled: false },
16+
(await allPlugins())[plugin]?.config ?? { enabled: false },
1717
(store.get('plugins') as Record<string, PluginConfig>)[plugin] ?? {},
1818
);
1919
return pluginConfig !== undefined && pluginConfig.enabled;

src/i18n/resources/en.json

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -349,11 +349,11 @@
349349
"prompt": {
350350
"hostname": {
351351
"title": "Proxy Hostname",
352-
"label": "Enter hostname for local proxy server (requires restart):"
352+
"label": "Enter hostname for local proxy server (requires restart):"
353353
},
354354
"port": {
355355
"title": "Proxy Port",
356-
"label": "Enter port for local proxy server (requires restart):"
356+
"label": "Enter port for local proxy server (requires restart):"
357357
}
358358
}
359359
},
@@ -838,6 +838,25 @@
838838
"instrumental": "⚠️ - This is an instrumental song"
839839
}
840840
},
841+
"settings-ui": {
842+
"name": "Settings UI",
843+
"description": "todo!()",
844+
"button": "Settings",
845+
"title": {
846+
"general": "General",
847+
"appearance": "Appearance",
848+
"plugins": "Plugins",
849+
"advanced": "Advanced",
850+
"about": "About"
851+
},
852+
"label": {
853+
"general": "General",
854+
"appearance": "Appearance",
855+
"plugins": "Plugins",
856+
"advanced": "Advanced",
857+
"about": "About"
858+
}
859+
},
841860
"taskbar-mediacontrol": {
842861
"description": "Control playback from your Windows taskbar",
843862
"name": "Taskbar Media Control"

src/plugins/adblocker/injectors/inject.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export const inject = (contextBridge) => {
3737

3838
//
3939
return o;
40-
}
40+
};
4141

4242
contextBridge.exposeInMainWorld('_pruner', pruner);
4343
}
@@ -58,7 +58,7 @@ export const inject = (contextBridge) => {
5858
{
5959
chain: 'ytInitialPlayerResponse.adSlots',
6060
cValue: 'undefined',
61-
}
61+
},
6262
];
6363

6464
chains.forEach(function ({ chain, cValue }) {

src/plugins/settings-ui/backend.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import config from '@/config';
2+
import Plugins from '@/config/plugins';
3+
import { createBackend } from '@/utils';
4+
import is from 'electron-is';
5+
import { app } from 'electron/main';
6+
7+
const getVersion = () => app.getVersion();
8+
9+
const platform = () => {
10+
const { arch } = process;
11+
12+
if (is.windows()) return `Windows (${arch})`;
13+
if (is.macOS()) return `macOS (${arch})`;
14+
if (is.linux()) {
15+
const desktop =
16+
process.env.XDG_CURRENT_DESKTOP || process.env.XDG_SESSION_DESKTOP;
17+
const type = process.env.XDG_SESSION_TYPE;
18+
19+
return `Linux (desktop=${desktop}; type=${type}; arch=${arch})`;
20+
}
21+
return process.platform + ` (${arch})`;
22+
};
23+
24+
const versions = () => process.versions;
25+
26+
const plugins = {
27+
enable: (id: string) => {
28+
Plugins.enable(id);
29+
},
30+
disable: (id: string) => {
31+
Plugins.disable(id);
32+
},
33+
};
34+
35+
export type ConfigKey = Parameters<typeof config.get>[0];
36+
37+
const configHandlers = {
38+
get: <T extends ConfigKey>(key: T) => config.get(key),
39+
set: (key: ConfigKey, value: unknown) => config.set(key, value),
40+
};
41+
42+
const loadSettings = () => config.getStore();
43+
44+
export const backend = createBackend({
45+
start(ctx) {
46+
ctx.ipc.handle('ytmd-sui:app-version', getVersion);
47+
ctx.ipc.handle('ytmd-sui:platform', platform);
48+
ctx.ipc.handle('ytmd-sui:versions', versions);
49+
ctx.ipc.handle('ytmd-sui:load-settings', loadSettings);
50+
51+
ctx.ipc.handle('ytmd-sui:config-get', configHandlers.get);
52+
ctx.ipc.handle('ytmd-sui:config-set', configHandlers.set);
53+
54+
ctx.ipc.handle('ytmd-sui:plugins-enable', plugins.enable);
55+
ctx.ipc.handle('ytmd-sui:plugins-disable', plugins.disable);
56+
},
57+
58+
stop(ctx) {
59+
ctx.ipc.removeHandler('ytmd-sui:app-version');
60+
ctx.ipc.removeHandler('ytmd-sui:platform');
61+
ctx.ipc.removeHandler('ytmd-sui:versions');
62+
ctx.ipc.removeHandler('ytmd-sui:load-settings');
63+
64+
ctx.ipc.removeHandler('ytmd-sui:config-get');
65+
ctx.ipc.removeHandler('ytmd-sui:config-set');
66+
67+
ctx.ipc.removeHandler('ytmd-sui:plugins-enable');
68+
ctx.ipc.removeHandler('ytmd-sui:plugins-disable');
69+
ctx.ipc.removeHandler('ytmd-sui:plugins-isEnabled');
70+
},
71+
});
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { For } from 'solid-js';
2+
3+
interface SelectProps {
4+
label: string;
5+
description: string;
6+
7+
value: string;
8+
options: { label: string; value: string }[];
9+
onSelect: (value: string) => void;
10+
}
11+
12+
export const Select = (props: SelectProps) => {
13+
return (
14+
<div class="ytmd-sui-settingItem">
15+
<div class="ytmd-sui-settingText">
16+
<div class="ytmd-sui-settingLabel">
17+
<span class="ytmd-sui-settingTitle">{props.label}</span>
18+
<span class="ytmd-sui-settingDescription">{props.description}</span>
19+
</div>
20+
<select
21+
class="ytmd-sui-select"
22+
tabIndex={1}
23+
value={props.value}
24+
onChange={(e) => props.onSelect(e.currentTarget.value)}
25+
>
26+
<For each={props.options}>
27+
{({ label, value }) => <option value={value}>{label}</option>}
28+
</For>
29+
</select>
30+
</div>
31+
</div>
32+
);
33+
};
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import {
2+
createSignal,
3+
Switch,
4+
Match,
5+
For,
6+
createEffect,
7+
onCleanup,
8+
Suspense,
9+
} from 'solid-js';
10+
11+
import General from './settings/General';
12+
import Appearance from './settings/Appearance';
13+
import Plugins from './settings/Plugins';
14+
import Advanced from './settings/Advanced';
15+
import About from './settings/About';
16+
import { Icons } from '@/types/icons';
17+
import { t } from '@/i18n';
18+
19+
interface SidebarItem {
20+
icon: Icons;
21+
id: string;
22+
}
23+
24+
const sidebar: SidebarItem[] = [
25+
{ icon: 'icons:settings', id: 'general' },
26+
{ icon: 'yt-icons:color_lens', id: 'appearance' },
27+
{ icon: 'icons:extension', id: 'plugins' },
28+
{ icon: 'icons:dashboard', id: 'advanced' },
29+
{ icon: 'icons:info', id: 'about' },
30+
];
31+
32+
interface SettingsModalProps {
33+
close: () => void;
34+
}
35+
36+
export default (props: SettingsModalProps) => {
37+
const [currentCategory, setCurrentCategory] = createSignal('general');
38+
const [isSidebarExpanded, setIsSidebarExpanded] = createSignal(true);
39+
40+
createEffect(() => {
41+
const handleResize = () => {
42+
const isMobile = window.innerWidth <= 908;
43+
if (isMobile) {
44+
setIsSidebarExpanded(false);
45+
}
46+
};
47+
48+
// initial call
49+
handleResize();
50+
51+
window.addEventListener('resize', handleResize);
52+
onCleanup(() => window.removeEventListener('resize', handleResize));
53+
});
54+
55+
return (
56+
<>
57+
<div
58+
class="ytmd-sui-modalOverlay"
59+
onClick={(e) => {
60+
if (e.target === e.currentTarget) {
61+
props.close();
62+
}
63+
}}
64+
>
65+
<div class="ytmd-sui-modal">
66+
<div
67+
class={`ytmd-sui-sidebar ${
68+
!isSidebarExpanded() ? 'collapsed' : ''
69+
}`}
70+
>
71+
<div class="ytmd-sui-sidebarContent">
72+
<For each={sidebar}>
73+
{(item) => (
74+
<button
75+
class={`ytmd-sui-menuItem ${
76+
currentCategory() === item.id ? 'active' : ''
77+
}`}
78+
onClick={() => setCurrentCategory(item.id)}
79+
title={
80+
!isSidebarExpanded()
81+
? t(`plugins.settings-ui.label.${item.id}`)
82+
: undefined
83+
}
84+
>
85+
<span class="ytmd-sui-icon">
86+
<yt-icon icon={item.icon} tabindex="0" />
87+
</span>
88+
<yt-formatted-string
89+
class="ytmd-sui-menuLabel title style-scope ytmusic-guide-entry-renderer"
90+
text={{
91+
runs: [
92+
{ text: t(`plugins.settings-ui.label.${item.id}`) },
93+
],
94+
}}
95+
/>
96+
</button>
97+
)}
98+
</For>
99+
</div>
100+
<button
101+
class="ytmd-sui-sidebarToggle"
102+
onClick={() => setIsSidebarExpanded((old) => !old)}
103+
title={isSidebarExpanded() ? 'Collapse menu' : 'Expand menu'}
104+
>
105+
106+
</button>
107+
</div>
108+
109+
<div class="ytmd-sui-content">
110+
<div class="ytmd-sui-header">
111+
<h2>
112+
{t(
113+
`plugins.settings-ui.title.${
114+
sidebar.find((item) => item.id === currentCategory())!.id
115+
}`,
116+
)}
117+
</h2>
118+
<button class="ytmd-sui-closeButton" onClick={props.close}>
119+
120+
</button>
121+
</div>
122+
123+
<Suspense fallback={<div class="ytmd-sui-loading">Loading...</div>}>
124+
<Switch fallback={<General />}>
125+
<Match when={currentCategory() === 'general'}>
126+
<General />
127+
</Match>
128+
<Match when={currentCategory() === 'appearance'}>
129+
<Appearance />
130+
</Match>
131+
<Match when={currentCategory() === 'plugins'}>
132+
<Plugins />
133+
</Match>
134+
<Match when={currentCategory() === 'advanced'}>
135+
<Advanced />
136+
</Match>
137+
<Match when={currentCategory() === 'about'}>
138+
<About />
139+
</Match>
140+
</Switch>
141+
</Suspense>
142+
</div>
143+
</div>
144+
</div>
145+
</>
146+
);
147+
};

0 commit comments

Comments
 (0)