From 2f842612fbcdd7340bc7956c20f471ce3d450276 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Sun, 15 Jan 2023 11:59:17 +0100 Subject: [PATCH] Read dark/light mode from system, automatically listen for changes, and add button for toggling the mode to the status bar --- electron/main/index.ts | 11 +++++++-- electron/preload/index.ts | 29 +++++++++++++++++++++++ public/icons/both-mode.png | Bin 0 -> 2279 bytes public/icons/dark-mode.png | Bin 0 -> 2106 bytes public/icons/light-mode.png | Bin 0 -> 2148 bytes src/App.vue | 36 +++++++++++++++++++++++++---- src/components/StatusBar.vue | 43 +++++++++++++++++++++++++++-------- src/editor/editor.js | 2 +- 8 files changed, 105 insertions(+), 16 deletions(-) create mode 100644 public/icons/both-mode.png create mode 100644 public/icons/dark-mode.png create mode 100644 public/icons/light-mode.png diff --git a/electron/main/index.ts b/electron/main/index.ts index eb26828..56b50a7 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -54,7 +54,7 @@ async function createWindow() { // Consider using contextBridge.exposeInMainWorld // Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation nodeIntegration: true, - contextIsolation: false, + contextIsolation: true, }, }) @@ -119,5 +119,12 @@ ipcMain.handle('open-win', (_, arg) => { childWindow.loadURL(`${url}#${arg}`) } else { childWindow.loadFile(indexHtml, { hash: arg }) - } + } }) + + +ipcMain.handle('dark-mode:set', (event, mode) => { + nativeTheme.themeSource = mode +}) + +ipcMain.handle('dark-mode:get', () => nativeTheme.themeSource) diff --git a/electron/preload/index.ts b/electron/preload/index.ts index ebf1276..472e907 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -1,3 +1,32 @@ +const { contextBridge, ipcRenderer } = require('electron') + +const getComputedTheme = () => window.matchMedia('(prefers-color-scheme: dark)').matches ? "dark" : "light" +const mediaMatch = window.matchMedia('(prefers-color-scheme: dark)') +let darkModeChangeListener = null + +contextBridge.exposeInMainWorld('darkMode', { + set: (mode) => ipcRenderer.invoke('dark-mode:set', mode), + get: async () => { + const mode = await ipcRenderer.invoke('dark-mode:get') + return { + theme: mode, + computed: getComputedTheme(), + } + }, + onChange: (callback) => { + darkModeChangeListener = (event) => { + callback(event.matches ? "dark" : "light") + } + mediaMatch.addEventListener('change', darkModeChangeListener) + return mediaMatch + }, + removeListener() { + mediaMatch.removeEventListener('change', darkModeChangeListener) + }, + initial: getComputedTheme(), +}) + + function domReady(condition: DocumentReadyState[] = ['complete', 'interactive']) { return new Promise((resolve) => { if (condition.includes(document.readyState)) { diff --git a/public/icons/both-mode.png b/public/icons/both-mode.png new file mode 100644 index 0000000000000000000000000000000000000000..6b79e5bae32c75a14e74e5340ba0be16094fce80 GIT binary patch literal 2279 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 literal 0 HcmV?d00001 diff --git a/public/icons/light-mode.png b/public/icons/light-mode.png new file mode 100644 index 0000000000000000000000000000000000000000..a2a7d6524845d8cc44e8576901b775e49f7d237b GIT binary patch literal 2148 zcmai0X;c$e6i!^q;!;IL3og?E+;Wmhf*_0u7)dlKMj)W<9%3?q2_zGeK@wENU0gv$ zQ4ytwD$1q>MG=o62ufUfN^P|w;8M|6Tn;KKpnVAlBHI3#B=3FS{qBADzI!Lhz6<6J zv$nIQP$@vyBzlPu;%DdAfUoX zDWq4$sCB5G3!8OO@Y}SFz>t|jMswjHu`lFBXfeoz;lg0T9@Y>?D_5Wr{@iY3u;Ri| zB&k6WBtAZ#5%0_(w2=tQ-Q68wvJp0$4j6QuK}|~abhXaGB+;e8$8<6+t|4(k4Vg5h zONcm<3&Q}1y2VLc(W|c3bsrxj0x=;7i@`)brDJ;hD|8dmkFJR#NJ1AyXnGFN$3+j^ z&k&^V%O-lM=1XL;_`m5YRi9(%NWoH&i0-8JiRq&QU4TJ@AreeS#A#)iU@4|19n6_9 z|9syh6ccBv#6mR=T(f&kf_gD7?Cy%NdZFMiLZ(!s zzPKKX3F6}_OsxZ|JyG<6!CwKrh!{fa53V)Fg}Z$9)B!bPVnPleXLA_6kjTr+S4$}H z7=Y;{^X5PzftM@G-PM)OW^~DbqM^VRR~YzG60q1zwlkf{qO)8An9e9`COX5F#$=&P zrdip{2E8DclG6XSKUF>)?@UObf9XEGA<{U!(Xa%#`r2 z9uMgb4ly;|$LKgnzxX+3!Y5q{?7H?q_k%Hl^vn^k=$S;A8jK$;m@}=rFN~s4s7W}e zOuvdyC{t78QX6S&{KnOIy26vI)(ye&u&wJu>ZgXxje_7^wU zK9%gaoAdnWasQn|tQ+?wWM4rhPpR#|ezqN|ulSV_U)NL9!UT%60d~2L!X1^< zl#7qmI93a`Ul#f?+dq^PCM+kj!|ps>wSG#a{+|ui^fwH<>gFuz@=2>+jbyNIbUdMM zIk$Ono^soYsM|?J#$&d(R<(*BCHhu|oOTSpNPpp0mN`4x5-rR!oT)FW^{8nZ5w@qW zboCwXxRh2E&2#6~$aidJX&ZQ7SZRd8ItpcYmFc0T(T3ZD0vSboUVt$9+-{clK;DRT zjr+ne5)ypD4Vcf1iVNk} z%1UNkF5^xcIotkp(TxT9#9>?EpJ~Ox8xKl01u88|g=M23sx6BhTZBqsDS7Rjg;J4v z`cmDl-%2{8o9jb;C)^x(Eh#i&+~vmunopIcW@N=?P?zyo-t{H?q{xWTmvG92(5>a~ zFDY3Co$QLqnt-Y4>($iayw0rKK0_=fhAcBmk7NvJE4NFbawCR5DBqAj-tFYME81uI zOS5Jz{4v}iXz;dGReQwRJ&mD@K@X!Wf3h7j4b>8-O_r&Uu zH!GJ+_sBaX3lls&i!Odg>saY+Oq%pEV9Tg)-KM0Yw(etOeopCk!xgtGJP$u@DvDAW zl5bXN{|>gtA94>D815fD^8JTb9O}UL@2L}aS-pPS?z1IvZ-kL=abgk0VtMEdp5qaG zeCNUZgAOMPbB&LZId&j@t&GQTabS&ia+P=1wLNWV*)?-A*81EIZ`oz(XRSB=IS>gJ K@C)Wdto|1-LM>PT literal 0 HcmV?d00001 diff --git a/src/App.vue b/src/App.vue index 2cc720b..2cdb607 100644 --- a/src/App.vue +++ b/src/App.vue @@ -2,8 +2,7 @@ import HelloWorld from './components/HelloWorld.vue' import StatusBar from './components/StatusBar.vue' import Editor from './components/Editor.vue' - - console.log("[App.vue]", `Hello world from Electron ${process.versions.electron}!`) + export default { components: { @@ -18,11 +17,39 @@ column: 1, language: "plaintext", languageAuto: true, - theme: "dark", + theme: window.darkMode.initial, + initialTheme: window.darkMode.initial, + systemTheme: 'system', } }, + mounted() { + window.darkMode.get().then((mode) => { + this.theme = mode.computed + this.systemTheme = mode.theme + }) + window.darkMode.onChange((theme) => { + this.theme = theme + }) + }, + + beforeUnmount() { + window.darkMode.removeListener() + }, + methods: { + toggleTheme() { + let newTheme + // when the "system" theme is used, make sure that the first click always results in amn actual theme change + if (this.initialTheme === "light") { + newTheme = this.systemTheme === "system" ? "dark" : (this.systemTheme === "dark" ? "light" : "system") + } else { + newTheme = this.systemTheme === "system" ? "light" : (this.systemTheme === "light" ? "dark" : "system") + } + window.darkMode.set(newTheme) + this.systemTheme = newTheme + }, + onCursorChange(e) { //console.log("onCursorChange:", e) this.line = e.cursorLine.line @@ -47,7 +74,8 @@ :language="language" :languageAuto="languageAuto" :theme="theme" - @toggleTheme="theme = theme === 'dark' ? 'light' : 'dark'" + :systemTheme="systemTheme" + @toggleTheme="toggleTheme" class="status" /> diff --git a/src/components/StatusBar.vue b/src/components/StatusBar.vue index 27df416..d9cb9ac 100644 --- a/src/components/StatusBar.vue +++ b/src/components/StatusBar.vue @@ -19,6 +19,7 @@ "language", "languageAuto", "theme", + "systemTheme", ], mounted() { @@ -44,11 +45,13 @@ Col {{ column }}
-
{{ theme }}
{{ languageName }} (auto)
+
+ +
@@ -76,23 +79,45 @@ color: rgba(255, 255, 255, 0.75) .status-block.lang .auto color: rgba(255, 255, 255, 0.55) + .theme .icon + opacity: 0.9 .spacer flex-grow: 1 .status-block - padding: 2px 12px + padding: 2px 10px cursor: default + &:first-child + padding-left: 12px + &:last-child + padding-right: 12px &.clickable cursor: pointer &:hover - background: rgba(255,255,255, 0.1) - &.line-number + background-color: rgba(255,255,255, 0.1) + .line-number + color: rgba(255, 255, 255, 0.7) + .num + color: rgba(255, 255, 255, 1.0) + .lang + .auto color: rgba(255, 255, 255, 0.7) - .num - color: rgba(255, 255, 255, 1.0) - &.lang - .auto - color: rgba(255, 255, 255, 0.7) + .theme + padding-top: 0 + padding-bottom: 0 + .icon + display: block + width: 14px + height: 22px + background-size: 14px + background-repeat: no-repeat + background-position: center center + &.dark + background-image: url("/icons/dark-mode.png") + &.light + background-image: url("/icons/light-mode.png") + &.system + background-image: url("/icons/both-mode.png") diff --git a/src/editor/editor.js b/src/editor/editor.js index 1e7e36f..0fff526 100644 --- a/src/editor/editor.js +++ b/src/editor/editor.js @@ -24,7 +24,7 @@ export class HeynoteEditor { //minimalSetup, customSetup, - this.theme.of("dark" ? heynoteDark : heynoteLight), + this.theme.of(theme === "dark" ? heynoteDark : heynoteLight), heynoteBase, indentUnit.of(" "), EditorView.scrollMargins.of(f => {