Implement web version of Heynote (#63)

* Move windows.darkMode -> window.heynote.themeMode

* Only add UpdateStatusItem if window.heynote.autoUpdate is set

* Update Vite

* Implement web version of Heynote.

Add a child vite project in ./webapp/ that is Heynote running within a browser. Imports almost all code from ../src/ and only adds a thin bridge that corresponds to the API between the Electron main process and the app code.

* Remove commented out tag

* Specify publicDir in vite config, instead of using a symlink

* Add webapp_dev npm command to package.json

* Add npm run command: webapp:build

* Add resolve alias '@' that points to project root.
Move assets file from public to assets in order to let Vite/Rollup handle bundling.
This commit is contained in:
Jonatan Heyman 2023-12-25 14:18:44 +01:00 committed by GitHub
parent bbb6387581
commit 079fa666d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
64 changed files with 221 additions and 31 deletions

View File

@ -0,0 +1,2 @@
$OpenSansPath: "@/assets/font/open-sans/fonts" !default;
$OpenSansVersion: "1.1.0" !default;

View File

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Before

Width:  |  Height:  |  Size: 583 B

After

Width:  |  Height:  |  Size: 583 B

View File

Before

Width:  |  Height:  |  Size: 727 B

After

Width:  |  Height:  |  Size: 727 B

View File

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

Before

Width:  |  Height:  |  Size: 267 B

After

Width:  |  Height:  |  Size: 267 B

View File

@ -1,5 +1,5 @@
const { contextBridge } = require('electron')
import darkMode from "./theme-mode"
import themeMode from "./theme-mode"
import { isMac, isWindows, isLinux } from "../detect-platform"
import { ipcRenderer } from "electron"
import {
@ -18,8 +18,6 @@ import {
import CONFIG from "../config"
import getCurrencyData from "./currency"
//contextBridge.exposeInMainWorld("platform", )
contextBridge.exposeInMainWorld("darkMode", darkMode)
contextBridge.exposeInMainWorld("heynote", {
platform: {
@ -28,6 +26,8 @@ contextBridge.exposeInMainWorld("heynote", {
isLinux,
},
themeMode: themeMode,
quit() {
console.log("quitting")
//ipcRenderer.invoke("app_quit")

View File

@ -2,7 +2,7 @@ const { 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
let themeModeChangeListener = null
export default {
set: (mode) => ipcRenderer.invoke('dark-mode:set', mode),
@ -14,14 +14,14 @@ export default {
}
},
onChange: (callback) => {
darkModeChangeListener = (event) => {
themeModeChangeListener = (event) => {
callback(event.matches ? "dark" : "light")
}
mediaMatch.addEventListener('change', darkModeChangeListener)
mediaMatch.addEventListener('change', themeModeChangeListener)
return mediaMatch
},
removeListener() {
mediaMatch.removeEventListener('change', darkModeChangeListener)
mediaMatch.removeEventListener('change', themeModeChangeListener)
},
initial: getComputedTheme(),
}

2
package-lock.json generated
View File

@ -48,7 +48,7 @@
"rollup-plugin-license": "^3.0.1",
"sass": "^1.57.1",
"typescript": "^4.9.4",
"vite": "^4.0.5",
"vite": "^4.5.1",
"vite-plugin-electron": "^0.11.1",
"vite-plugin-electron-renderer": "^0.11.4",
"vue": "^3.2.45",

View File

@ -24,7 +24,9 @@
"build": "vue-tsc --noEmit && vite build && electron-builder -c electron-builder.json5",
"prebuild": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"build_grammar": "lezer-generator src/editor/lang-heynote/heynote.grammar -o src/editor/lang-heynote/parser.js"
"build_grammar": "lezer-generator src/editor/lang-heynote/heynote.grammar -o src/editor/lang-heynote/parser.js",
"webapp:dev": "vite webapp",
"webapp:build": "vite build webapp"
},
"devDependencies": {
"@codemirror/autocomplete": "^6.11.1",
@ -63,7 +65,7 @@
"rollup-plugin-license": "^3.0.1",
"sass": "^1.57.1",
"typescript": "^4.9.4",
"vite": "^4.0.5",
"vite": "^4.5.1",
"vite-plugin-electron": "^0.11.1",
"vite-plugin-electron-renderer": "^0.11.4",
"vue": "^3.2.45",

View File

@ -1,2 +0,0 @@
$OpenSansPath: "font/open-sans/fonts" !default;
$OpenSansVersion: "1.1.0" !default;

View File

@ -19,8 +19,8 @@
selectionSize: 0,
language: "plaintext",
languageAuto: true,
theme: window.darkMode.initial,
initialTheme: window.darkMode.initial,
theme: window.heynote.themeMode.initial,
initialTheme: window.heynote.themeMode.initial,
systemTheme: 'system',
development: window.location.href.indexOf("dev=1") !== -1,
showLanguageSelector: false,
@ -30,13 +30,20 @@
},
mounted() {
window.darkMode.get().then((mode) => {
window.heynote.themeMode.get().then((mode) => {
this.theme = mode.computed
this.systemTheme = mode.theme
})
window.darkMode.onChange((theme) => {
const onChangeCallback = (theme) => {
this.theme = theme
})
if (theme === "system") {
document.body.setAttribute("theme", window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light")
} else {
document.body.setAttribute("theme", theme)
}
}
onChangeCallback(window.heynote.themeMode.initial)
window.heynote.themeMode.onChange(onChangeCallback)
window.heynote.onSettingsChange((settings) => {
this.settings = settings
})
@ -46,7 +53,7 @@
},
beforeUnmount() {
window.darkMode.removeListener()
window.heynote.themeMode.removeListener()
},
methods: {
@ -66,7 +73,7 @@
} else {
newTheme = this.systemTheme === "system" ? "light" : (this.systemTheme === "light" ? "dark" : "system")
}
window.darkMode.set(newTheme)
window.heynote.themeMode.set(newTheme)
this.systemTheme = newTheme
this.$refs.editor.focus()
},

View File

@ -50,6 +50,10 @@
changeLanguageTitle() {
return `Change language for current block (${this.cmdKey} + L)`
},
updatesEnabled() {
return !!window.heynote.autoUpdate
},
},
}
</script>
@ -80,7 +84,7 @@
>
<span class="icon icon-format"></span>
</div>
<UpdateStatusItem :allowBetaVersions="allowBetaVersions" />
<UpdateStatusItem v-if="updatesEnabled" :allowBetaVersions="allowBetaVersions" />
<div class="status-block theme clickable" @click="$emit('toggleTheme')" title="Toggle dark/light mode">
<span :class="'icon ' + systemTheme"></span>
</div>
@ -148,11 +152,11 @@
+dark-mode
opacity: 0.9
&.dark
background-image: url("icons/dark-mode.png")
background-image: url("@/assets/icons/dark-mode.png")
&.light
background-image: url("icons/light-mode.png")
background-image: url("@/assets/icons/light-mode.png")
&.system
background-image: url("icons/both-mode.png")
background-image: url("@/assets/icons/both-mode.png")
.format
padding-top: 0
@ -166,6 +170,6 @@
background-size: 16px
background-repeat: no-repeat
background-position: center center
background-image: url("icons/format.svg")
background-image: url("@/assets/icons/format.svg")
</style>

View File

@ -176,14 +176,14 @@
background-size: 14px
background-repeat: no-repeat
background-position: center center
background-image: url("icons/update.svg")
background-image: url("@/assets/icons/update.svg")
animation-name: spin
animation-duration: 2000ms
animation-iteration-count: infinite
animation-timing-function: linear
animation-play-state: paused
&.icon-download
background-image: url("icons/download.svg")
background-image: url("@/assets/icons/download.svg")
width: 16px
height: 16px
background-size: 16px

View File

@ -1,27 +1,27 @@
@font-face
font-family: 'Hack'
src: url('font/hack/hack-regular.woff2') format('woff2'), url('font/hack/hack-regular.woff') format('woff')
src: url('@/assets/font/hack/hack-regular.woff2') format('woff2'), url('@/assets/font/hack/hack-regular.woff') format('woff')
font-weight: 400
font-style: normal
@font-face
font-family: 'Hack'
src: url('font/hack/hack-bold.woff2') format('woff2'), url('font/hack/hack-bold.woff') format('woff')
src: url('@/assets/font/hack/hack-bold.woff2') format('woff2'), url('@/assets/font/hack/hack-bold.woff') format('woff')
font-weight: 700
font-style: normal
@font-face
font-family: 'Hack'
src: url('font/hack/hack-italic.woff2') format('woff2'), url('font/hack/hack-italic.woff') format('woff')
src: url('@/assets/font/hack/hack-italic.woff2') format('woff2'), url('@/assets/font/hack/hack-italic.woff') format('woff')
font-weight: 400
font-style: italic
@font-face
font-family: 'Hack'
src: url('font/hack/hack-bolditalic.woff2') format('woff2'), url('font/hack/hack-bolditalic.woff') format('woff')
src: url('@/assets/font/hack/hack-bolditalic.woff2') format('woff2'), url('@/assets/font/hack/hack-bolditalic.woff') format('woff')
font-weight: 700
font-style: italic
@import "~/../public/font/open-sans/open-sans"
@import "@/assets/font/open-sans/open-sans"

View File

@ -1,3 +1,5 @@
=dark-mode()
@media (prefers-color-scheme: dark)
@content
body[theme=dark] &
@content

View File

@ -6,6 +6,7 @@ import electron from 'vite-plugin-electron'
import renderer from 'vite-plugin-electron-renderer'
import license from 'rollup-plugin-license'
import pkg from './package.json'
import path from 'path'
rmSync('dist-electron', { recursive: true, force: true })
@ -14,6 +15,12 @@ const isProduction = process.env.NODE_ENV === "production"
// https://vitejs.dev/config/
export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname),
},
},
plugins: [
vue(),
electron([

80
webapp/bridge.js Normal file
View File

@ -0,0 +1,80 @@
const mediaMatch = window.matchMedia('(prefers-color-scheme: dark)')
let themeModeChangeListener = null
let autoUpdateCallbacks = null
let themeCallback = null
let currencyData = null
export default {
platform: {
isMac: true,
isWindows: false,
isLinux: false,
},
buffer: {
async load() {
const content = localStorage.getItem("buffer")
return content === null ? "\n∞∞∞text-a\n" : content
},
async save(content) {
console.log("saving buffer")
localStorage.setItem("buffer", content)
},
async saveAndQuit(content) {
},
},
onWindowClose(callback) {
//ipcRenderer.on(WINDOW_CLOSE_EVENT, callback)
},
onOpenSettings(callback) {
//ipcRenderer.on(OPEN_SETTINGS_EVENT, callback)
},
onSettingsChange(callback) {
//ipcRenderer.on(SETTINGS_CHANGE_EVENT, (event, settings) => callback(settings))
},
themeMode: {
set: (mode) => {
themeCallback(mode)
document.body.setAttribute("theme", mode === "dark" ? "dark" : "light")
},
get: async () => {
return {
theme: "light",
computed: "light",
}
},
onChange: (callback) => {
themeCallback = callback
themeModeChangeListener = (event) => {
callback(event.matches ? "dark" : "light")
}
mediaMatch.addEventListener('change', themeModeChangeListener)
return mediaMatch
},
removeListener() {
mediaMatch.removeEventListener('change', themeModeChangeListener)
},
initial: "light",
},
settings: {
keymap: "default",
},
getCurrencyData: async () => {
if (currencyData !== null) {
return currencyData
}
const response = await fetch("https://currencies.heynote.com/rates.json", {cache: "no-cache"})
currencyData = JSON.parse(await response.text())
return currencyData
},
}

21
webapp/index.html Normal file
View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="light dark">
<title>Heynote</title>
</head>
<body>
<div id="app"></div>
<script type="module">
import heynote from "./bridge.js";
window.heynote = heynote;
</script>
<script type="module" src="main.js"></script>
<script src="./math.js"></script>
</body>
</html>

13
webapp/main.js Normal file
View File

@ -0,0 +1,13 @@
import '../src/css/application.sass'
import { createApp } from 'vue'
import App from '../src/components/App.vue'
import { loadCurrencies } from '../src/currency'
const app = createApp(App)
app.mount('#app')
//console.log("test:", app.hej.test)
// load math.js currencies
loadCurrencies()
setInterval(loadCurrencies, 1000 * 3600 * 4)

54
webapp/vite.config.js Normal file
View File

@ -0,0 +1,54 @@
import path from 'path'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
//import { directoryPlugin } from "vite-plugin-list-directory-contents";
const middleware = () => {
return {
name: 'middleware',
apply: 'serve',
configureServer(viteDevServer) {
return () => {
viteDevServer.middlewares.use(async (req, res, next) => {
console.log("url:", req.originalUrl)
if (!req.originalUrl.endsWith(".html") && req.originalUrl !== "/") {
req.url = `/src` + req.originalUrl + ".html";
} else if (req.url === "/index.html") {
//req.url = `/src` + req.url;
}
next();
});
};
}
}
}
// https://vitejs.dev/config/
export default defineConfig({
publicDir: "../public",
plugins: [
vue(),
//directoryPlugin({ baseDir: __dirname }),
],
css: {
preprocessorOptions: {
sass: {
additionalData: `
@import "../src/css/include.sass"
`
},
},
},
resolve: {
alias: {
'@': path.resolve(__dirname, '..'),
},
},
})