diff options
author | Kristóf Marussy <kristof@marussy.com> | 2021-12-23 21:29:26 +0100 |
---|---|---|
committer | Kristóf Marussy <kristof@marussy.com> | 2021-12-23 21:29:26 +0100 |
commit | d303f2e3415237e1a519db21ad4e089c2ba7e9f9 (patch) | |
tree | 5d562dcaf7bb9c83c1930c7d7cf1b2c9de75e15b /packages | |
parent | build: Enable asar (diff) | |
download | sophie-d303f2e3415237e1a519db21ad4e089c2ba7e9f9.tar.gz sophie-d303f2e3415237e1a519db21ad4e089c2ba7e9f9.tar.zst sophie-d303f2e3415237e1a519db21ad4e089c2ba7e9f9.zip |
feat: Add BrowserView and synchronize its position
Diffstat (limited to 'packages')
21 files changed, 427 insertions, 63 deletions
diff --git a/packages/main/package.json b/packages/main/package.json index 509acdc..014c511 100644 --- a/packages/main/package.json +++ b/packages/main/package.json | |||
@@ -11,7 +11,7 @@ | |||
11 | }, | 11 | }, |
12 | "dependencies": { | 12 | "dependencies": { |
13 | "@sophie/shared": "workspace:*", | 13 | "@sophie/shared": "workspace:*", |
14 | "electron": "^16.0.5", | 14 | "electron": "16.0.5", |
15 | "mobx": "^6.3.10", | 15 | "mobx": "^6.3.10", |
16 | "mobx-state-tree": "^5.1.0" | 16 | "mobx-state-tree": "^5.1.0" |
17 | }, | 17 | }, |
diff --git a/packages/main/src/index.ts b/packages/main/src/index.ts index 90102a7..59b9ecb 100644 --- a/packages/main/src/index.ts +++ b/packages/main/src/index.ts | |||
@@ -18,11 +18,14 @@ | |||
18 | * SPDX-License-Identifier: AGPL-3.0-only | 18 | * SPDX-License-Identifier: AGPL-3.0-only |
19 | */ | 19 | */ |
20 | 20 | ||
21 | import { app, BrowserWindow } from 'electron'; | 21 | import { app, BrowserView, BrowserWindow } from 'electron'; |
22 | import { autorun } from 'mobx'; | ||
22 | import { getSnapshot, onPatch } from 'mobx-state-tree'; | 23 | import { getSnapshot, onPatch } from 'mobx-state-tree'; |
23 | import { join } from 'path'; | 24 | import { join } from 'path'; |
24 | import { | 25 | import { |
26 | browserViewBounds, | ||
25 | MainToRendererIpcMessage, | 27 | MainToRendererIpcMessage, |
28 | paletteMode, | ||
26 | RendererToMainIpcMessage, | 29 | RendererToMainIpcMessage, |
27 | } from '@sophie/shared'; | 30 | } from '@sophie/shared'; |
28 | import { URL } from 'url'; | 31 | import { URL } from 'url'; |
@@ -31,7 +34,7 @@ import { | |||
31 | installDevToolsExtensions, | 34 | installDevToolsExtensions, |
32 | openDevToolsWhenReady, | 35 | openDevToolsWhenReady, |
33 | } from './devTools'; | 36 | } from './devTools'; |
34 | import { rootStore } from './stores/RootStore'; | 37 | import { createRootStore } from './stores/RootStore'; |
35 | 38 | ||
36 | const isSingleInstance = app.requestSingleInstanceLock(); | 39 | const isSingleInstance = app.requestSingleInstanceLock(); |
37 | const isDevelopment = import.meta.env.MODE === 'development'; | 40 | const isDevelopment = import.meta.env.MODE === 'development'; |
@@ -49,19 +52,13 @@ if (isDevelopment) { | |||
49 | 52 | ||
50 | let mainWindow: BrowserWindow | null = null; | 53 | let mainWindow: BrowserWindow | null = null; |
51 | 54 | ||
52 | const store = rootStore.create({ | 55 | const store = createRootStore(); |
53 | shared: { | ||
54 | clickCount: 1, | ||
55 | }, | ||
56 | }); | ||
57 | 56 | ||
58 | function createWindow(): Promise<void> { | 57 | async function createWindow(): Promise<void> { |
59 | mainWindow = new BrowserWindow({ | 58 | mainWindow = new BrowserWindow({ |
60 | show: false, | 59 | show: false, |
61 | autoHideMenuBar: true, | 60 | autoHideMenuBar: true, |
62 | webPreferences: { | 61 | webPreferences: { |
63 | nativeWindowOpen: true, | ||
64 | webviewTag: false, | ||
65 | sandbox: true, | 62 | sandbox: true, |
66 | preload: join(__dirname, '../../preload/dist/index.cjs'), | 63 | preload: join(__dirname, '../../preload/dist/index.cjs'), |
67 | }, | 64 | }, |
@@ -78,16 +75,23 @@ function createWindow(): Promise<void> { | |||
78 | const { webContents } = mainWindow; | 75 | const { webContents } = mainWindow; |
79 | 76 | ||
80 | webContents.on('ipc-message', (_event, channel, ...args) => { | 77 | webContents.on('ipc-message', (_event, channel, ...args) => { |
81 | switch (channel) { | 78 | try { |
82 | case RendererToMainIpcMessage.SharedStoreSnapshotRequest: | 79 | switch (channel) { |
83 | webContents.send(MainToRendererIpcMessage.SharedStoreSnapshot, getSnapshot(store.shared)); | 80 | case RendererToMainIpcMessage.SharedStoreSnapshotRequest: |
84 | break; | 81 | webContents.send(MainToRendererIpcMessage.SharedStoreSnapshot, getSnapshot(store.shared)); |
85 | case RendererToMainIpcMessage.ButtonClick: | 82 | break; |
86 | store.buttonClick(); | 83 | case RendererToMainIpcMessage.SetBrowserViewBounds: |
87 | break; | 84 | store.setBrowserViewBounds(browserViewBounds.parse(args[0])); |
88 | default: | 85 | break; |
89 | console.warn('Unknown IPC message:', channel, args); | 86 | case RendererToMainIpcMessage.SetPaletteMode: |
90 | break; | 87 | store.setPaletteMode(paletteMode.parse(args[0])) |
88 | break; | ||
89 | default: | ||
90 | console.warn('Unknown IPC message:', channel, args); | ||
91 | break; | ||
92 | } | ||
93 | } catch (err) { | ||
94 | console.error('Error while processing IPC message:', channel, args, err); | ||
91 | } | 95 | } |
92 | }); | 96 | }); |
93 | 97 | ||
@@ -99,7 +103,24 @@ function createWindow(): Promise<void> { | |||
99 | ? import.meta.env.VITE_DEV_SERVER_URL | 103 | ? import.meta.env.VITE_DEV_SERVER_URL |
100 | : new URL('../renderer/dist/index.html', `file://${__dirname}`).toString(); | 104 | : new URL('../renderer/dist/index.html', `file://${__dirname}`).toString(); |
101 | 105 | ||
102 | return mainWindow.loadURL(pageUrl); | 106 | const browserView = new BrowserView({ |
107 | webPreferences: { | ||
108 | sandbox: true, | ||
109 | partition: 'service', | ||
110 | }, | ||
111 | }); | ||
112 | |||
113 | browserView.setBackgroundColor('#fff'); | ||
114 | |||
115 | autorun(() => { | ||
116 | browserView.setBounds(store.shared.browserViewBounds); | ||
117 | }); | ||
118 | |||
119 | await mainWindow.loadURL(pageUrl); | ||
120 | |||
121 | mainWindow.addBrowserView(browserView); | ||
122 | |||
123 | return browserView.webContents.loadURL('https://git.marussy.com/sophie/about'); | ||
103 | } | 124 | } |
104 | 125 | ||
105 | app.on('second-instance', () => { | 126 | app.on('second-instance', () => { |
diff --git a/packages/main/src/stores/RootStore.ts b/packages/main/src/stores/RootStore.ts index edc740c..9d138ce 100644 --- a/packages/main/src/stores/RootStore.ts +++ b/packages/main/src/stores/RootStore.ts | |||
@@ -18,15 +18,29 @@ | |||
18 | * SPDX-License-Identifier: AGPL-3.0-only | 18 | * SPDX-License-Identifier: AGPL-3.0-only |
19 | */ | 19 | */ |
20 | 20 | ||
21 | import { Instance, types } from 'mobx-state-tree'; | 21 | import { applySnapshot, Instance, types } from 'mobx-state-tree'; |
22 | import { sharedStore } from '@sophie/shared'; | 22 | import { |
23 | BrowserViewBounds, | ||
24 | emptySharedStore, | ||
25 | PaletteMode, | ||
26 | sharedStore, | ||
27 | } from '@sophie/shared'; | ||
23 | 28 | ||
24 | export const rootStore = types.model('RootStore', { | 29 | export const rootStore = types.model('RootStore', { |
25 | shared: sharedStore, | 30 | shared: sharedStore, |
26 | }).actions((self) => ({ | 31 | }).actions((self) => ({ |
27 | buttonClick() { | 32 | setBrowserViewBounds(bounds: BrowserViewBounds) { |
28 | self.shared.clickCount += 1; | 33 | applySnapshot(self.shared.browserViewBounds, bounds); |
34 | }, | ||
35 | setPaletteMode(mode: PaletteMode) { | ||
36 | self.shared.shouldUseDarkColors = mode === 'dark'; | ||
29 | }, | 37 | }, |
30 | })); | 38 | })); |
31 | 39 | ||
32 | export interface RootStore extends Instance<typeof rootStore> {} | 40 | export interface RootStore extends Instance<typeof rootStore> {} |
41 | |||
42 | export function createRootStore(): RootStore { | ||
43 | return rootStore.create({ | ||
44 | shared: emptySharedStore, | ||
45 | }); | ||
46 | } | ||
diff --git a/packages/preload/package.json b/packages/preload/package.json index e050b9a..bd2b412 100644 --- a/packages/preload/package.json +++ b/packages/preload/package.json | |||
@@ -12,7 +12,7 @@ | |||
12 | }, | 12 | }, |
13 | "dependencies": { | 13 | "dependencies": { |
14 | "@sophie/shared": "workspace:*", | 14 | "@sophie/shared": "workspace:*", |
15 | "electron": "^16.0.5", | 15 | "electron": "16.0.5", |
16 | "mobx": "^6.3.10", | 16 | "mobx": "^6.3.10", |
17 | "mobx-state-tree": "^5.1.0" | 17 | "mobx-state-tree": "^5.1.0" |
18 | }, | 18 | }, |
diff --git a/packages/preload/src/SophieRendererImpl.ts b/packages/preload/src/SophieRendererImpl.ts index 1ac8b65..6bffeae 100644 --- a/packages/preload/src/SophieRendererImpl.ts +++ b/packages/preload/src/SophieRendererImpl.ts | |||
@@ -21,12 +21,16 @@ | |||
21 | import { ipcRenderer } from 'electron'; | 21 | import { ipcRenderer } from 'electron'; |
22 | import type { IJsonPatch } from 'mobx-state-tree'; | 22 | import type { IJsonPatch } from 'mobx-state-tree'; |
23 | import { | 23 | import { |
24 | BrowserViewBounds, | ||
25 | browserViewBounds, | ||
24 | MainToRendererIpcMessage, | 26 | MainToRendererIpcMessage, |
27 | PaletteMode, | ||
28 | paletteMode, | ||
25 | RendererToMainIpcMessage, | 29 | RendererToMainIpcMessage, |
26 | sharedStore, | 30 | sharedStore, |
27 | SharedStoreListener, | 31 | SharedStoreListener, |
28 | SharedStoreSnapshotIn, | 32 | SharedStoreSnapshotIn, |
29 | SophieRenderer | 33 | SophieRenderer, |
30 | } from '@sophie/shared'; | 34 | } from '@sophie/shared'; |
31 | 35 | ||
32 | export type MessageSender = (channel: RendererToMainIpcMessage, ...args: unknown[]) => void; | 36 | export type MessageSender = (channel: RendererToMainIpcMessage, ...args: unknown[]) => void; |
@@ -71,8 +75,16 @@ export class SophieRendererImpl implements SophieRenderer { | |||
71 | this.#send(RendererToMainIpcMessage.SharedStoreSnapshotRequest); | 75 | this.#send(RendererToMainIpcMessage.SharedStoreSnapshotRequest); |
72 | } | 76 | } |
73 | 77 | ||
74 | buttonClick(): void { | 78 | setBrowserViewBounds(bounds: BrowserViewBounds): void { |
75 | this.#send(RendererToMainIpcMessage.ButtonClick); | 79 | if (browserViewBounds.safeParse(bounds).success) { |
80 | this.#send(RendererToMainIpcMessage.SetBrowserViewBounds, bounds); | ||
81 | } | ||
82 | } | ||
83 | |||
84 | setPaletteMode(mode: PaletteMode): void { | ||
85 | if (paletteMode.safeParse(mode).success) { | ||
86 | this.#send(RendererToMainIpcMessage.SetPaletteMode, mode); | ||
87 | } | ||
76 | } | 88 | } |
77 | } | 89 | } |
78 | 90 | ||
@@ -89,6 +101,7 @@ export function createSophieRenderer(): SophieRenderer { | |||
89 | 101 | ||
90 | return { | 102 | return { |
91 | setSharedStoreListener: impl.setSharedStoreListener.bind(impl), | 103 | setSharedStoreListener: impl.setSharedStoreListener.bind(impl), |
92 | buttonClick: impl.buttonClick.bind(impl), | 104 | setBrowserViewBounds: impl.setBrowserViewBounds.bind(impl), |
105 | setPaletteMode: impl.setPaletteMode.bind(impl), | ||
93 | }; | 106 | }; |
94 | } | 107 | } |
diff --git a/packages/renderer/package.json b/packages/renderer/package.json index 5f9bd57..4948f2b 100644 --- a/packages/renderer/package.json +++ b/packages/renderer/package.json | |||
@@ -16,6 +16,7 @@ | |||
16 | "@mui/icons-material": "^5.2.5", | 16 | "@mui/icons-material": "^5.2.5", |
17 | "@mui/material": "^5.2.5", | 17 | "@mui/material": "^5.2.5", |
18 | "@sophie/shared": "workspace:*", | 18 | "@sophie/shared": "workspace:*", |
19 | "lodash": "^4.17.21", | ||
19 | "mobx": "^6.3.10", | 20 | "mobx": "^6.3.10", |
20 | "mobx-react-lite": "^3.2.2", | 21 | "mobx-react-lite": "^3.2.2", |
21 | "mobx-state-tree": "^5.1.0", | 22 | "mobx-state-tree": "^5.1.0", |
@@ -23,6 +24,7 @@ | |||
23 | "react-dom": "^17.0.2" | 24 | "react-dom": "^17.0.2" |
24 | }, | 25 | }, |
25 | "devDependencies": { | 26 | "devDependencies": { |
27 | "@types/lodash": "^4.14.178", | ||
26 | "@types/react": "^17.0.38", | 28 | "@types/react": "^17.0.38", |
27 | "@types/react-dom": "^17.0.11", | 29 | "@types/react-dom": "^17.0.11", |
28 | "@vitejs/plugin-react": "^1.1.3", | 30 | "@vitejs/plugin-react": "^1.1.3", |
diff --git a/packages/renderer/src/components/App.tsx b/packages/renderer/src/components/App.tsx new file mode 100644 index 0000000..b627fa7 --- /dev/null +++ b/packages/renderer/src/components/App.tsx | |||
@@ -0,0 +1,42 @@ | |||
1 | /* | ||
2 | * Copyright (C) 2021-2022 Kristóf Marussy <kristof@marussy.com> | ||
3 | * | ||
4 | * This file is part of Sophie. | ||
5 | * | ||
6 | * Sophie is free software: you can redistribute it and/or modify | ||
7 | * it under the terms of the GNU Affero General Public License as | ||
8 | * published by the Free Software Foundation, version 3. | ||
9 | * | ||
10 | * This program is distributed in the hope that it will be useful, | ||
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
13 | * GNU Affero General Public License for more details. | ||
14 | * | ||
15 | * You should have received a copy of the GNU Affero General Public License | ||
16 | * along with this program. If not, see <https://www.gnu.org/licenses/>. | ||
17 | * | ||
18 | * SPDX-License-Identifier: AGPL-3.0-only | ||
19 | */ | ||
20 | |||
21 | import Box from '@mui/material/Box'; | ||
22 | import React from 'react'; | ||
23 | |||
24 | import { BrowserViewPlaceholder } from './BrowserViewPlaceholder'; | ||
25 | import { Sidebar } from './Sidebar'; | ||
26 | |||
27 | export function App(): JSX.Element { | ||
28 | return ( | ||
29 | <Box | ||
30 | sx={{ | ||
31 | display: 'flex', | ||
32 | flexDirection: 'row', | ||
33 | alignItems: 'stretch', | ||
34 | height: '100vh', | ||
35 | width: '100vw', | ||
36 | }} | ||
37 | > | ||
38 | <Sidebar /> | ||
39 | <BrowserViewPlaceholder /> | ||
40 | </Box> | ||
41 | ) | ||
42 | } | ||
diff --git a/packages/renderer/src/components/BrowserViewPlaceholder.tsx b/packages/renderer/src/components/BrowserViewPlaceholder.tsx new file mode 100644 index 0000000..06dc7fe --- /dev/null +++ b/packages/renderer/src/components/BrowserViewPlaceholder.tsx | |||
@@ -0,0 +1,128 @@ | |||
1 | /* | ||
2 | * Copyright (C) 2021-2022 Kristóf Marussy <kristof@marussy.com> | ||
3 | * | ||
4 | * This file is part of Sophie. | ||
5 | * | ||
6 | * Sophie is free software: you can redistribute it and/or modify | ||
7 | * it under the terms of the GNU Affero General Public License as | ||
8 | * published by the Free Software Foundation, version 3. | ||
9 | * | ||
10 | * This program is distributed in the hope that it will be useful, | ||
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
13 | * GNU Affero General Public License for more details. | ||
14 | * | ||
15 | * You should have received a copy of the GNU Affero General Public License | ||
16 | * along with this program. If not, see <https://www.gnu.org/licenses/>. | ||
17 | * | ||
18 | * SPDX-License-Identifier: AGPL-3.0-only | ||
19 | */ | ||
20 | |||
21 | import { throttle } from 'lodash'; | ||
22 | import { observer } from 'mobx-react-lite'; | ||
23 | import Box from '@mui/material/Box'; | ||
24 | import React, { | ||
25 | useCallback, | ||
26 | useEffect, | ||
27 | useRef, | ||
28 | useState, | ||
29 | } from 'react'; | ||
30 | import type { BrowserViewBounds } from '@sophie/shared'; | ||
31 | |||
32 | import { useStore } from './StoreProvider'; | ||
33 | |||
34 | export const BrowserViewPlaceholder = observer(function BrowserViewPlaceholder() { | ||
35 | const { | ||
36 | shared: { | ||
37 | browserViewBounds: { | ||
38 | x: storeX, | ||
39 | y: storeY, | ||
40 | width: storeWidth, | ||
41 | height: storeHeight, | ||
42 | }, | ||
43 | }, | ||
44 | setBrowserViewBounds, | ||
45 | } = useStore(); | ||
46 | |||
47 | const [ | ||
48 | { | ||
49 | x: currentX, | ||
50 | y: currentY, | ||
51 | width: currentWidth, | ||
52 | height: currentHeight, | ||
53 | }, | ||
54 | setBounds, | ||
55 | ] = useState<BrowserViewBounds>({ | ||
56 | x: 0, | ||
57 | y: 0, | ||
58 | width: 0, | ||
59 | height: 0, | ||
60 | }); | ||
61 | |||
62 | useEffect(() => { | ||
63 | if (storeX !== currentX | ||
64 | || storeY !== currentY | ||
65 | || storeWidth !== currentWidth | ||
66 | || storeHeight !== currentHeight) { | ||
67 | setBrowserViewBounds({ | ||
68 | x: currentX, | ||
69 | y: currentY, | ||
70 | width: currentWidth, | ||
71 | height: currentHeight, | ||
72 | }); | ||
73 | } | ||
74 | }, [ | ||
75 | storeX, | ||
76 | storeY, | ||
77 | storeWidth, | ||
78 | storeHeight, | ||
79 | setBrowserViewBounds, | ||
80 | currentX, | ||
81 | currentY, | ||
82 | currentWidth, | ||
83 | currentHeight, | ||
84 | ]); | ||
85 | |||
86 | const onResize = useCallback(throttle(([boxEntry]: ResizeObserverEntry[]) => { | ||
87 | if (boxEntry) { | ||
88 | const { | ||
89 | x, | ||
90 | y, | ||
91 | width, | ||
92 | height, | ||
93 | } = boxEntry.target.getBoundingClientRect(); | ||
94 | setBounds({ | ||
95 | x, | ||
96 | y, | ||
97 | width, | ||
98 | height, | ||
99 | }); | ||
100 | } | ||
101 | }, 100), [setBounds]); | ||
102 | |||
103 | const resizeObserverRef = useRef<ResizeObserver | null>(null); | ||
104 | |||
105 | const ref = useCallback((box: HTMLElement | null) => { | ||
106 | if (resizeObserverRef.current !== null) { | ||
107 | resizeObserverRef.current.disconnect(); | ||
108 | } | ||
109 | if (box === null) { | ||
110 | resizeObserverRef.current = null; | ||
111 | return; | ||
112 | } | ||
113 | resizeObserverRef.current = new ResizeObserver(onResize); | ||
114 | resizeObserverRef.current.observe(box); | ||
115 | }, [onResize, resizeObserverRef]); | ||
116 | |||
117 | return ( | ||
118 | <Box | ||
119 | sx={{ | ||
120 | flex: 1, | ||
121 | // Workaround: display a plain white background if we fail to set the BrowserView background color. | ||
122 | // https://github.com/electron/electron/issues/31019 | ||
123 | background: '#fff', | ||
124 | }} | ||
125 | ref={ref} | ||
126 | /> | ||
127 | ) | ||
128 | }); | ||
diff --git a/packages/renderer/src/components/Sidebar.tsx b/packages/renderer/src/components/Sidebar.tsx new file mode 100644 index 0000000..6c79932 --- /dev/null +++ b/packages/renderer/src/components/Sidebar.tsx | |||
@@ -0,0 +1,42 @@ | |||
1 | /* | ||
2 | * Copyright (C) 2021-2022 Kristóf Marussy <kristof@marussy.com> | ||
3 | * | ||
4 | * This file is part of Sophie. | ||
5 | * | ||
6 | * Sophie is free software: you can redistribute it and/or modify | ||
7 | * it under the terms of the GNU Affero General Public License as | ||
8 | * published by the Free Software Foundation, version 3. | ||
9 | * | ||
10 | * This program is distributed in the hope that it will be useful, | ||
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
13 | * GNU Affero General Public License for more details. | ||
14 | * | ||
15 | * You should have received a copy of the GNU Affero General Public License | ||
16 | * along with this program. If not, see <https://www.gnu.org/licenses/>. | ||
17 | * | ||
18 | * SPDX-License-Identifier: AGPL-3.0-only | ||
19 | */ | ||
20 | |||
21 | import Box from '@mui/material/Box'; | ||
22 | import React from 'react'; | ||
23 | |||
24 | import { ToggleDarkModeButton } from './ToggleDarkModeButton'; | ||
25 | |||
26 | export function Sidebar(): JSX.Element { | ||
27 | return ( | ||
28 | <Box | ||
29 | sx={(theme) => ({ | ||
30 | background: theme.palette.divider, | ||
31 | flex: 0, | ||
32 | display: 'flex', | ||
33 | flexDirection: 'column', | ||
34 | alignItems: 'center', | ||
35 | justifyContent: 'flex-end', | ||
36 | padding: 1, | ||
37 | })} | ||
38 | > | ||
39 | <ToggleDarkModeButton /> | ||
40 | </Box> | ||
41 | ); | ||
42 | } | ||
diff --git a/packages/renderer/src/components/ThemeProvider.tsx b/packages/renderer/src/components/ThemeProvider.tsx index 7173a9d..9215f5c 100644 --- a/packages/renderer/src/components/ThemeProvider.tsx +++ b/packages/renderer/src/components/ThemeProvider.tsx | |||
@@ -27,9 +27,9 @@ import React from 'react'; | |||
27 | 27 | ||
28 | import { useStore } from './StoreProvider'; | 28 | import { useStore } from './StoreProvider'; |
29 | 29 | ||
30 | export const ThemeProvider = observer(({ children }: { | 30 | export const ThemeProvider = observer(function ThemeProvider({ children }: { |
31 | children: JSX.Element | JSX.Element[], | 31 | children: JSX.Element | JSX.Element[], |
32 | }): JSX.Element => { | 32 | }) { |
33 | const { shared: { shouldUseDarkColors } } = useStore(); | 33 | const { shared: { shouldUseDarkColors } } = useStore(); |
34 | 34 | ||
35 | const theme = createTheme({ | 35 | const theme = createTheme({ |
diff --git a/packages/renderer/src/components/ToggleDarkModeButton.tsx b/packages/renderer/src/components/ToggleDarkModeButton.tsx new file mode 100644 index 0000000..1b6757e --- /dev/null +++ b/packages/renderer/src/components/ToggleDarkModeButton.tsx | |||
@@ -0,0 +1,40 @@ | |||
1 | /* | ||
2 | * Copyright (C) 2021-2022 Kristóf Marussy <kristof@marussy.com> | ||
3 | * | ||
4 | * This file is part of Sophie. | ||
5 | * | ||
6 | * Sophie is free software: you can redistribute it and/or modify | ||
7 | * it under the terms of the GNU Affero General Public License as | ||
8 | * published by the Free Software Foundation, version 3. | ||
9 | * | ||
10 | * This program is distributed in the hope that it will be useful, | ||
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
13 | * GNU Affero General Public License for more details. | ||
14 | * | ||
15 | * You should have received a copy of the GNU Affero General Public License | ||
16 | * along with this program. If not, see <https://www.gnu.org/licenses/>. | ||
17 | * | ||
18 | * SPDX-License-Identifier: AGPL-3.0-only | ||
19 | */ | ||
20 | |||
21 | import { observer } from 'mobx-react-lite'; | ||
22 | import DarkModeIcon from '@mui/icons-material/DarkMode'; | ||
23 | import LightModeIcon from '@mui/icons-material/LightMode'; | ||
24 | import IconButton from '@mui/material/IconButton'; | ||
25 | import React from 'react'; | ||
26 | |||
27 | import { useStore } from './StoreProvider'; | ||
28 | |||
29 | export const ToggleDarkModeButton = observer(function ToggleDarkModeButton() { | ||
30 | const { shared: { shouldUseDarkColors }, toggleDarkMode } = useStore(); | ||
31 | |||
32 | return ( | ||
33 | <IconButton | ||
34 | aria-label="Toggle dark mode" | ||
35 | onClick={() => toggleDarkMode()} | ||
36 | > | ||
37 | {shouldUseDarkColors ? <LightModeIcon /> : <DarkModeIcon />} | ||
38 | </IconButton> | ||
39 | ); | ||
40 | }); | ||
diff --git a/packages/renderer/src/index.tsx b/packages/renderer/src/index.tsx index 901d137..90cba2c 100644 --- a/packages/renderer/src/index.tsx +++ b/packages/renderer/src/index.tsx | |||
@@ -22,13 +22,12 @@ import '@fontsource/roboto/300.css'; | |||
22 | import '@fontsource/roboto/400.css'; | 22 | import '@fontsource/roboto/400.css'; |
23 | import '@fontsource/roboto/500.css'; | 23 | import '@fontsource/roboto/500.css'; |
24 | import '@fontsource/roboto/700.css'; | 24 | import '@fontsource/roboto/700.css'; |
25 | import { observer } from 'mobx-react-lite'; | ||
26 | import Button from "@mui/material/Button"; | ||
27 | import CssBaseline from "@mui/material/CssBaseline"; | 25 | import CssBaseline from "@mui/material/CssBaseline"; |
28 | import React from 'react'; | 26 | import React from 'react'; |
29 | import { render } from 'react-dom'; | 27 | import { render } from 'react-dom'; |
30 | 28 | ||
31 | import { StoreProvider, useStore } from './components/StoreProvider'; | 29 | import { App } from './components/App'; |
30 | import { StoreProvider } from './components/StoreProvider'; | ||
32 | import { ThemeProvider } from './components/ThemeProvider'; | 31 | import { ThemeProvider } from './components/ThemeProvider'; |
33 | import { exposeToReduxDevtools } from './devTools'; | 32 | import { exposeToReduxDevtools } from './devTools'; |
34 | import { createAndConnectRootStore } from './stores/RootStore'; | 33 | import { createAndConnectRootStore } from './stores/RootStore'; |
@@ -41,26 +40,13 @@ if (isDevelopment) { | |||
41 | exposeToReduxDevtools(store); | 40 | exposeToReduxDevtools(store); |
42 | } | 41 | } |
43 | 42 | ||
44 | const Example = observer(() => { | ||
45 | const { shared: { clickCount } } = useStore(); | ||
46 | |||
47 | return ( | ||
48 | <Button | ||
49 | variant="contained" | ||
50 | onClick={() => store.buttonClick()} | ||
51 | > | ||
52 | Hello Sophie! {clickCount} | ||
53 | </Button> | ||
54 | ); | ||
55 | }); | ||
56 | |||
57 | function Root(): JSX.Element { | 43 | function Root(): JSX.Element { |
58 | return ( | 44 | return ( |
59 | <React.StrictMode> | 45 | <React.StrictMode> |
60 | <StoreProvider store={store}> | 46 | <StoreProvider store={store}> |
61 | <ThemeProvider> | 47 | <ThemeProvider> |
62 | <CssBaseline enableColorScheme /> | 48 | <CssBaseline enableColorScheme /> |
63 | <Example /> | 49 | <App /> |
64 | </ThemeProvider> | 50 | </ThemeProvider> |
65 | </StoreProvider> | 51 | </StoreProvider> |
66 | </React.StrictMode> | 52 | </React.StrictMode> |
diff --git a/packages/renderer/src/stores/RootStore.ts b/packages/renderer/src/stores/RootStore.ts index f24ea4d..c6533ba 100644 --- a/packages/renderer/src/stores/RootStore.ts +++ b/packages/renderer/src/stores/RootStore.ts | |||
@@ -26,10 +26,16 @@ import { | |||
26 | Instance, | 26 | Instance, |
27 | types | 27 | types |
28 | } from 'mobx-state-tree'; | 28 | } from 'mobx-state-tree'; |
29 | import { sharedStore, SophieRenderer } from '@sophie/shared'; | 29 | import { |
30 | BrowserViewBounds, | ||
31 | emptySharedStore, | ||
32 | PaletteMode, | ||
33 | sharedStore, | ||
34 | SophieRenderer, | ||
35 | } from '@sophie/shared'; | ||
30 | 36 | ||
31 | export interface RootEnv { | 37 | export interface RootEnv { |
32 | ipc: SophieRenderer; | 38 | ipc: Omit<SophieRenderer, 'setSharedStoreListener'>; |
33 | } | 39 | } |
34 | 40 | ||
35 | /** | 41 | /** |
@@ -46,8 +52,18 @@ export function getEnv(model: IAnyStateTreeNode): RootEnv { | |||
46 | export const rootStore = types.model('RootStore', { | 52 | export const rootStore = types.model('RootStore', { |
47 | shared: sharedStore, | 53 | shared: sharedStore, |
48 | }).actions((self) => ({ | 54 | }).actions((self) => ({ |
49 | buttonClick() { | 55 | setBrowserViewBounds(bounds: BrowserViewBounds) { |
50 | getEnv(self).ipc.buttonClick(); | 56 | getEnv(self).ipc.setBrowserViewBounds(bounds); |
57 | }, | ||
58 | setPaletteMode(mode: PaletteMode) { | ||
59 | getEnv(self).ipc.setPaletteMode(mode); | ||
60 | }, | ||
61 | toggleDarkMode() { | ||
62 | if (self.shared.shouldUseDarkColors) { | ||
63 | this.setPaletteMode('light'); | ||
64 | } else { | ||
65 | this.setPaletteMode('dark'); | ||
66 | } | ||
51 | }, | 67 | }, |
52 | })); | 68 | })); |
53 | 69 | ||
@@ -63,7 +79,7 @@ export interface RootStore extends Instance<typeof rootStore> {} | |||
63 | */ | 79 | */ |
64 | export function createAndConnectRootStore(ipc: SophieRenderer): RootStore { | 80 | export function createAndConnectRootStore(ipc: SophieRenderer): RootStore { |
65 | const store = rootStore.create({ | 81 | const store = rootStore.create({ |
66 | shared: {}, | 82 | shared: emptySharedStore, |
67 | }, { | 83 | }, { |
68 | ipc, | 84 | ipc, |
69 | }); | 85 | }); |
diff --git a/packages/shared/package.json b/packages/shared/package.json index 83aec54..f5d4f90 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json | |||
@@ -17,6 +17,7 @@ | |||
17 | }, | 17 | }, |
18 | "dependencies": { | 18 | "dependencies": { |
19 | "mobx": "^6.3.10", | 19 | "mobx": "^6.3.10", |
20 | "mobx-state-tree": "^5.1.0" | 20 | "mobx-state-tree": "^5.1.0", |
21 | "zod": "^3.11.6" | ||
21 | } | 22 | } |
22 | } | 23 | } |
diff --git a/packages/shared/src/contextBridge/SophieRenderer.ts b/packages/shared/src/contextBridge/SophieRenderer.ts index 0d82d56..f2e2180 100644 --- a/packages/shared/src/contextBridge/SophieRenderer.ts +++ b/packages/shared/src/contextBridge/SophieRenderer.ts | |||
@@ -20,8 +20,12 @@ | |||
20 | 20 | ||
21 | import { SharedStoreListener } from '../stores/SharedStore'; | 21 | import { SharedStoreListener } from '../stores/SharedStore'; |
22 | 22 | ||
23 | import { BrowserViewBounds, PaletteMode } from '../schemas'; | ||
24 | |||
23 | export interface SophieRenderer { | 25 | export interface SophieRenderer { |
24 | setSharedStoreListener(listener: SharedStoreListener): void; | 26 | setSharedStoreListener(listener: SharedStoreListener): void; |
25 | 27 | ||
26 | buttonClick(): void; | 28 | setBrowserViewBounds(bounds: BrowserViewBounds): void; |
29 | |||
30 | setPaletteMode(mode: PaletteMode): void; | ||
27 | } | 31 | } |
diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 250700f..0a1ec40 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts | |||
@@ -24,9 +24,18 @@ export { MainToRendererIpcMessage } from './ipc/MainToRendererIpcMessage'; | |||
24 | export { RendererToMainIpcMessage } from './ipc/RendererToMainIpcMessage'; | 24 | export { RendererToMainIpcMessage } from './ipc/RendererToMainIpcMessage'; |
25 | 25 | ||
26 | export type { | 26 | export type { |
27 | BrowserViewBounds, | ||
28 | PaletteMode, | ||
29 | } from './schemas'; | ||
30 | export { | ||
31 | browserViewBounds, | ||
32 | paletteMode | ||
33 | } from './schemas'; | ||
34 | |||
35 | export type { | ||
27 | SharedStore, | 36 | SharedStore, |
28 | SharedStoreListener, | 37 | SharedStoreListener, |
29 | SharedStoreSnapshotIn, | 38 | SharedStoreSnapshotIn, |
30 | SharedStoreSnapshotOut, | 39 | SharedStoreSnapshotOut, |
31 | } from './stores/SharedStore'; | 40 | } from './stores/SharedStore'; |
32 | export { sharedStore } from './stores/SharedStore'; | 41 | export { emptySharedStore, sharedStore } from './stores/SharedStore'; |
diff --git a/packages/shared/src/ipc/MainToRendererIpcMessage.ts b/packages/shared/src/ipc/MainToRendererIpcMessage.ts index e68d676..92da489 100644 --- a/packages/shared/src/ipc/MainToRendererIpcMessage.ts +++ b/packages/shared/src/ipc/MainToRendererIpcMessage.ts | |||
@@ -19,6 +19,6 @@ | |||
19 | */ | 19 | */ |
20 | 20 | ||
21 | export enum MainToRendererIpcMessage { | 21 | export enum MainToRendererIpcMessage { |
22 | SharedStoreSnapshot = "sophie-shared-store-snapshot", | 22 | SharedStoreSnapshot = 'sophie-shared-store-snapshot', |
23 | SharedStorePatch = "sophie-shared-store-patch", | 23 | SharedStorePatch = 'sophie-shared-store-patch', |
24 | } | 24 | } |
diff --git a/packages/shared/src/ipc/RendererToMainIpcMessage.ts b/packages/shared/src/ipc/RendererToMainIpcMessage.ts index a7aa664..ba354d1 100644 --- a/packages/shared/src/ipc/RendererToMainIpcMessage.ts +++ b/packages/shared/src/ipc/RendererToMainIpcMessage.ts | |||
@@ -19,6 +19,7 @@ | |||
19 | */ | 19 | */ |
20 | 20 | ||
21 | export enum RendererToMainIpcMessage { | 21 | export enum RendererToMainIpcMessage { |
22 | SharedStoreSnapshotRequest = "sophie-shared-store-snapshot-request", | 22 | SharedStoreSnapshotRequest = 'sophie-shared-store-snapshot-request', |
23 | ButtonClick = "sophie-button-click" | 23 | SetBrowserViewBounds = 'sophie-set-browser-view-bounds', |
24 | SetPaletteMode = 'sophie-set-palette-mode', | ||
24 | } | 25 | } |
diff --git a/packages/shared/src/schemas.ts b/packages/shared/src/schemas.ts new file mode 100644 index 0000000..8827467 --- /dev/null +++ b/packages/shared/src/schemas.ts | |||
@@ -0,0 +1,34 @@ | |||
1 | /* | ||
2 | * Copyright (C) 2021-2022 Kristóf Marussy <kristof@marussy.com> | ||
3 | * | ||
4 | * This file is part of Sophie. | ||
5 | * | ||
6 | * Sophie is free software: you can redistribute it and/or modify | ||
7 | * it under the terms of the GNU Affero General Public License as | ||
8 | * published by the Free Software Foundation, version 3. | ||
9 | * | ||
10 | * This program is distributed in the hope that it will be useful, | ||
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
13 | * GNU Affero General Public License for more details. | ||
14 | * | ||
15 | * You should have received a copy of the GNU Affero General Public License | ||
16 | * along with this program. If not, see <https://www.gnu.org/licenses/>. | ||
17 | * | ||
18 | * SPDX-License-Identifier: AGPL-3.0-only | ||
19 | */ | ||
20 | |||
21 | import { z } from 'zod'; | ||
22 | |||
23 | export const browserViewBounds = z.object({ | ||
24 | x: z.number().nonnegative(), | ||
25 | y: z.number().nonnegative(), | ||
26 | width: z.number().nonnegative(), | ||
27 | height: z.number().nonnegative(), | ||
28 | }); | ||
29 | |||
30 | export type BrowserViewBounds = z.infer<typeof browserViewBounds>; | ||
31 | |||
32 | export const paletteMode = z.enum(['light', 'dark']); | ||
33 | |||
34 | export type PaletteMode = z.infer<typeof paletteMode>; | ||
diff --git a/packages/shared/src/stores/SharedStore.ts b/packages/shared/src/stores/SharedStore.ts index 7fd65b7..7c2b742 100644 --- a/packages/shared/src/stores/SharedStore.ts +++ b/packages/shared/src/stores/SharedStore.ts | |||
@@ -27,10 +27,20 @@ import { | |||
27 | } from 'mobx-state-tree'; | 27 | } from 'mobx-state-tree'; |
28 | 28 | ||
29 | export const sharedStore = types.model("SharedStore", { | 29 | export const sharedStore = types.model("SharedStore", { |
30 | browserViewBounds: types.model("BrowserViewBoundsStore", { | ||
31 | x: 0, | ||
32 | y: 0, | ||
33 | width: 0, | ||
34 | height: 0, | ||
35 | }), | ||
30 | shouldUseDarkColors: true, | 36 | shouldUseDarkColors: true, |
31 | clickCount: 0 | ||
32 | }); | 37 | }); |
33 | 38 | ||
39 | export const emptySharedStore: SharedStoreSnapshotIn = { | ||
40 | browserViewBounds: { | ||
41 | }, | ||
42 | }; | ||
43 | |||
34 | export interface SharedStore extends Instance<typeof sharedStore> {} | 44 | export interface SharedStore extends Instance<typeof sharedStore> {} |
35 | 45 | ||
36 | export interface SharedStoreSnapshotIn extends SnapshotIn<typeof sharedStore> {} | 46 | export interface SharedStoreSnapshotIn extends SnapshotIn<typeof sharedStore> {} |
diff --git a/packages/shared/vite.config.js b/packages/shared/vite.config.js index 76bd965..2867541 100644 --- a/packages/shared/vite.config.js +++ b/packages/shared/vite.config.js | |||
@@ -29,6 +29,7 @@ const config = makeConfig({ | |||
29 | external: [ | 29 | external: [ |
30 | 'mobx', | 30 | 'mobx', |
31 | 'mobx-state-tree', | 31 | 'mobx-state-tree', |
32 | 'zod', | ||
32 | ...builtinModules, | 33 | ...builtinModules, |
33 | ], | 34 | ], |
34 | }, | 35 | }, |