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.
2
assets/font/open-sans/sass/_variables.scss
Executable file
@ -0,0 +1,2 @@
|
||||
$OpenSansPath: "@/assets/font/open-sans/fonts" !default;
|
||||
$OpenSansVersion: "1.1.0" !default;
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 583 B After Width: | Height: | Size: 583 B |
Before Width: | Height: | Size: 727 B After Width: | Height: | Size: 727 B |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 267 B After Width: | Height: | Size: 267 B |
@ -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")
|
||||
|
@ -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
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -1,2 +0,0 @@
|
||||
$OpenSansPath: "font/open-sans/fonts" !default;
|
||||
$OpenSansVersion: "1.1.0" !default;
|
@ -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()
|
||||
},
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -1,3 +1,5 @@
|
||||
=dark-mode()
|
||||
@media (prefers-color-scheme: dark)
|
||||
@content
|
||||
body[theme=dark] &
|
||||
@content
|
||||
|
@ -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
@ -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
@ -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
@ -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
@ -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, '..'),
|
||||
},
|
||||
},
|
||||
})
|