Read dark/light mode from system, automatically listen for changes, and add button for toggling the mode to the status bar

This commit is contained in:
Jonatan Heyman 2023-01-15 11:59:17 +01:00
parent fbac07d338
commit 2f842612fb
8 changed files with 105 additions and 16 deletions

View File

@ -54,7 +54,7 @@ async function createWindow() {
// Consider using contextBridge.exposeInMainWorld // Consider using contextBridge.exposeInMainWorld
// Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation // Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation
nodeIntegration: true, nodeIntegration: true,
contextIsolation: false, contextIsolation: true,
}, },
}) })
@ -119,5 +119,12 @@ ipcMain.handle('open-win', (_, arg) => {
childWindow.loadURL(`${url}#${arg}`) childWindow.loadURL(`${url}#${arg}`)
} else { } else {
childWindow.loadFile(indexHtml, { hash: arg }) childWindow.loadFile(indexHtml, { hash: arg })
} }
}) })
ipcMain.handle('dark-mode:set', (event, mode) => {
nativeTheme.themeSource = mode
})
ipcMain.handle('dark-mode:get', () => nativeTheme.themeSource)

View File

@ -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']) { function domReady(condition: DocumentReadyState[] = ['complete', 'interactive']) {
return new Promise((resolve) => { return new Promise((resolve) => {
if (condition.includes(document.readyState)) { if (condition.includes(document.readyState)) {

BIN
public/icons/both-mode.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
public/icons/dark-mode.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
public/icons/light-mode.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -2,8 +2,7 @@
import HelloWorld from './components/HelloWorld.vue' import HelloWorld from './components/HelloWorld.vue'
import StatusBar from './components/StatusBar.vue' import StatusBar from './components/StatusBar.vue'
import Editor from './components/Editor.vue' import Editor from './components/Editor.vue'
console.log("[App.vue]", `Hello world from Electron ${process.versions.electron}!`)
export default { export default {
components: { components: {
@ -18,11 +17,39 @@
column: 1, column: 1,
language: "plaintext", language: "plaintext",
languageAuto: true, 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: { 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) { onCursorChange(e) {
//console.log("onCursorChange:", e) //console.log("onCursorChange:", e)
this.line = e.cursorLine.line this.line = e.cursorLine.line
@ -47,7 +74,8 @@
:language="language" :language="language"
:languageAuto="languageAuto" :languageAuto="languageAuto"
:theme="theme" :theme="theme"
@toggleTheme="theme = theme === 'dark' ? 'light' : 'dark'" :systemTheme="systemTheme"
@toggleTheme="toggleTheme"
class="status" class="status"
/> />
</template> </template>

View File

@ -19,6 +19,7 @@
"language", "language",
"languageAuto", "languageAuto",
"theme", "theme",
"systemTheme",
], ],
mounted() { mounted() {
@ -44,11 +45,13 @@
Col <span class="num">{{ column }}</span> Col <span class="num">{{ column }}</span>
</div> </div>
<div class="spacer"></div> <div class="spacer"></div>
<div class="status-block theme clickable" @click="$emit('toggleTheme')">{{ theme }}</div>
<div class="status-block lang clickable"> <div class="status-block lang clickable">
{{ languageName }} {{ languageName }}
<span v-if="languageAuto" class="auto">(auto)</span> <span v-if="languageAuto" class="auto">(auto)</span>
</div> </div>
<div class="status-block theme clickable" @click="$emit('toggleTheme')">
<span :class="'icon ' + systemTheme"></span>
</div>
</div> </div>
</template> </template>
@ -76,23 +79,45 @@
color: rgba(255, 255, 255, 0.75) color: rgba(255, 255, 255, 0.75)
.status-block.lang .auto .status-block.lang .auto
color: rgba(255, 255, 255, 0.55) color: rgba(255, 255, 255, 0.55)
.theme .icon
opacity: 0.9
.spacer .spacer
flex-grow: 1 flex-grow: 1
.status-block .status-block
padding: 2px 12px padding: 2px 10px
cursor: default cursor: default
&:first-child
padding-left: 12px
&:last-child
padding-right: 12px
&.clickable &.clickable
cursor: pointer cursor: pointer
&:hover &:hover
background: rgba(255,255,255, 0.1) background-color: rgba(255,255,255, 0.1)
&.line-number .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) color: rgba(255, 255, 255, 0.7)
.num .theme
color: rgba(255, 255, 255, 1.0) padding-top: 0
&.lang padding-bottom: 0
.auto .icon
color: rgba(255, 255, 255, 0.7) 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")
</style> </style>

View File

@ -24,7 +24,7 @@ export class HeynoteEditor {
//minimalSetup, //minimalSetup,
customSetup, customSetup,
this.theme.of("dark" ? heynoteDark : heynoteLight), this.theme.of(theme === "dark" ? heynoteDark : heynoteLight),
heynoteBase, heynoteBase,
indentUnit.of(" "), indentUnit.of(" "),
EditorView.scrollMargins.of(f => { EditorView.scrollMargins.of(f => {