mirror of
https://github.com/heyman/heynote.git
synced 2024-11-25 09:23:17 +01:00
WIP: Implement support for multiple notes
Refactor Vue <-> Editor <-> CodeMirror code. Introduce Pinia store to keep global state, in order to get rid of a lot of event juggling between Editor class/child components and the root App component.
This commit is contained in:
parent
f156320601
commit
f11f360496
@ -16,15 +16,15 @@ const untildify = (pathWithTilde) => {
|
||||
: pathWithTilde;
|
||||
}
|
||||
|
||||
export function constructBufferFilePath(directoryPath) {
|
||||
return join(untildify(directoryPath), isDev ? "buffer-dev.txt" : "buffer.txt")
|
||||
export function constructBufferFilePath(directoryPath, path) {
|
||||
return join(untildify(directoryPath), path)
|
||||
}
|
||||
|
||||
export function getBufferFilePath() {
|
||||
export function getFullBufferFilePath(path) {
|
||||
let defaultPath = app.getPath("userData")
|
||||
let configPath = CONFIG.get("settings.bufferPath")
|
||||
let bufferPath = configPath.length ? configPath : defaultPath
|
||||
let bufferFilePath = constructBufferFilePath(bufferPath)
|
||||
let bufferFilePath = constructBufferFilePath(bufferPath, path)
|
||||
try {
|
||||
// use realpathSync to resolve a potential symlink
|
||||
return fs.realpathSync(bufferFilePath)
|
||||
@ -103,39 +103,45 @@ export class Buffer {
|
||||
|
||||
|
||||
// Buffer
|
||||
let buffer
|
||||
export function loadBuffer() {
|
||||
if (buffer) {
|
||||
buffer.close()
|
||||
let buffers = {}
|
||||
export function loadBuffer(path) {
|
||||
if (buffers[path]) {
|
||||
buffers[path].close()
|
||||
}
|
||||
buffer = new Buffer({
|
||||
filePath: getBufferFilePath(),
|
||||
buffers[path] = new Buffer({
|
||||
filePath: getFullBufferFilePath(path),
|
||||
onChange: (content) => {
|
||||
win?.webContents.send("buffer-content:change", content)
|
||||
console.log("Old buffer.js onChange")
|
||||
win?.webContents.send("buffer-content:change", path, content)
|
||||
},
|
||||
})
|
||||
return buffer
|
||||
return buffers[path]
|
||||
}
|
||||
|
||||
ipcMain.handle('buffer-content:load', async () => {
|
||||
if (buffer.exists() && !(eraseInitialContent && isDev)) {
|
||||
return await buffer.load()
|
||||
ipcMain.handle('buffer-content:load', async (event, path) => {
|
||||
if (!buffers[path]) {
|
||||
loadBuffer(path)
|
||||
}
|
||||
if (buffers[path].exists() && !(eraseInitialContent && isDev)) {
|
||||
return await buffers[path].load()
|
||||
} else {
|
||||
return isDev ? initialDevContent : initialContent
|
||||
}
|
||||
});
|
||||
|
||||
async function save(content) {
|
||||
return await buffer.save(content)
|
||||
async function save(path, content) {
|
||||
return await buffers[path].save(content)
|
||||
}
|
||||
|
||||
ipcMain.handle('buffer-content:save', async (event, content) => {
|
||||
return await save(content)
|
||||
ipcMain.handle('buffer-content:save', async (event, path, content) => {
|
||||
return await save(path, content)
|
||||
});
|
||||
|
||||
export let contentSaved = false
|
||||
ipcMain.handle('buffer-content:saveAndQuit', async (event, content) => {
|
||||
await save(content)
|
||||
ipcMain.handle('buffer-content:saveAndQuit', async (event, contents) => {
|
||||
for (const [path, content] of contents) {
|
||||
await save(path, content)
|
||||
}
|
||||
contentSaved = true
|
||||
app.quit()
|
||||
})
|
||||
|
210
electron/main/file-library.js
Normal file
210
electron/main/file-library.js
Normal file
@ -0,0 +1,210 @@
|
||||
import fs from "fs"
|
||||
import os from "node:os"
|
||||
import { join, dirname, basename } from "path"
|
||||
|
||||
import * as jetpack from "fs-jetpack";
|
||||
import { app, ipcMain, dialog } from "electron"
|
||||
|
||||
|
||||
const untildify = (pathWithTilde) => {
|
||||
const homeDir = os.homedir()
|
||||
return homeDir ? pathWithTilde.replace(/^~(?=$|\/|\\)/, homeDir) : pathWithTilde
|
||||
}
|
||||
|
||||
async function readNoteMetadata(filePath) {
|
||||
const chunks = []
|
||||
for await (let chunk of fs.createReadStream(filePath, { start: 0, end:4000 })) {
|
||||
chunks.push(chunk)
|
||||
}
|
||||
const headContent = Buffer.concat(chunks).toString("utf8")
|
||||
const firstSeparator = headContent.indexOf("\n∞∞∞")
|
||||
if (firstSeparator === -1) {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
const metadata = JSON.parse(headContent.slice(0, firstSeparator).trim())
|
||||
return {"name": metadata.name, "tags": metadata.tags}
|
||||
} catch (e) {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class FileLibrary {
|
||||
constructor(basePath) {
|
||||
basePath = untildify(basePath)
|
||||
if (jetpack.exists(basePath) !== "dir") {
|
||||
throw new Error(`Path directory does not exist: ${basePath}`)
|
||||
}
|
||||
this.basePath = fs.realpathSync(basePath)
|
||||
this.jetpack = jetpack.cwd(this.basePath)
|
||||
this.files = {};
|
||||
this.watcher = null;
|
||||
this.contentSaved = false
|
||||
this.onChangeCallback = null
|
||||
}
|
||||
|
||||
async exists(path) {
|
||||
return this.jetpack.exists(path) === "file"
|
||||
}
|
||||
|
||||
async load(path) {
|
||||
if (this.files[path]) {
|
||||
return this.files[path].read()
|
||||
}
|
||||
const fullPath = fs.realpathSync(join(this.basePath, path))
|
||||
this.files[path] = new NoteBuffer({fullPath, library:this})
|
||||
return await this.files[path].read()
|
||||
}
|
||||
|
||||
async save(path, content) {
|
||||
if (!this.files[path]) {
|
||||
throw new Error(`File not loaded: ${path}`)
|
||||
}
|
||||
return await this.files[path].save(content)
|
||||
}
|
||||
|
||||
async getList() {
|
||||
console.log("Loading notes")
|
||||
const notes = {}
|
||||
const files = await this.jetpack.findAsync(this.basePath, {
|
||||
matching: "*.txt",
|
||||
recursive: true,
|
||||
})
|
||||
const promises = []
|
||||
for (const file of files) {
|
||||
promises.push(readNoteMetadata(join(this.basePath, file)))
|
||||
}
|
||||
const metadataList = await Promise.all(promises)
|
||||
metadataList.forEach((metadata, i) => {
|
||||
const path = files[i]
|
||||
notes[path] = metadata
|
||||
})
|
||||
return notes
|
||||
}
|
||||
|
||||
setupWatcher(win) {
|
||||
if (!this.watcher) {
|
||||
this.watcher = fs.watch(
|
||||
this.basePath,
|
||||
{
|
||||
persistent: true,
|
||||
recursive: true,
|
||||
encoding: "utf8",
|
||||
},
|
||||
async (eventType, changedPath) => {
|
||||
console.log("File changed", eventType, changedPath)
|
||||
//if (changedPath.toLowerCase().endsWith(".txt")) {
|
||||
// console.log("txt", this.notes)
|
||||
// if (await this.exists(changedPath)) {
|
||||
// console.log("file exists!")
|
||||
// const newMetadata = await readNoteMetadata(join(this.basePath, changedPath))
|
||||
// if (!(changedPath in this.notes) || newMetadata.name !== this.notes[changedPath].name) {
|
||||
// this.notes[changedPath] = newMetadata
|
||||
// win.webContents.send("buffer:noteMetadataChanged", changedPath, newMetadata)
|
||||
// console.log("metadata changed")
|
||||
// } else {
|
||||
// console.log("no metadata change")
|
||||
// }
|
||||
// } else if (changedPath in this.notes) {
|
||||
// console.log("note removed", changedPath)
|
||||
// delete this.notes[changedPath]
|
||||
// win.webContents.send("buffer:noteRemoved", changedPath)
|
||||
// }
|
||||
//}
|
||||
for (const [path, buffer] of Object.entries(this.files)) {
|
||||
if (changedPath === basename(path)) {
|
||||
const content = await buffer.read()
|
||||
if (buffer._lastSavedContent !== content) {
|
||||
win.webContents.send("buffer:change", path, content)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
closeFile(path) {
|
||||
if (this.files[path]) {
|
||||
delete this.files[path]
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
for (const buffer of Object.values(this.files)) {
|
||||
this.closeFile(buffer.filePath)
|
||||
}
|
||||
this.stopWatcher()
|
||||
}
|
||||
|
||||
stopWatcher() {
|
||||
if (this.watcher) {
|
||||
this.watcher.close()
|
||||
this.watcher = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export class NoteBuffer {
|
||||
constructor({fullPath, library}) {
|
||||
this.fullPath = fullPath
|
||||
this._lastSavedContent = null
|
||||
this.library = library
|
||||
}
|
||||
|
||||
async read() {
|
||||
return await this.library.jetpack.read(this.fullPath, 'utf8')
|
||||
}
|
||||
|
||||
async save(content) {
|
||||
this._lastSavedContent = content
|
||||
const saveResult = await this.library.jetpack.write(this.fullPath, content, {
|
||||
atomic: true,
|
||||
mode: '600',
|
||||
})
|
||||
return saveResult
|
||||
}
|
||||
|
||||
exists() {
|
||||
return jetpack.exists(this.fullPath) === "file"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function setupFileLibraryEventHandlers(library, win) {
|
||||
ipcMain.handle('buffer:load', async (event, path) => {
|
||||
console.log("buffer:load", path)
|
||||
return await library.load(path)
|
||||
});
|
||||
|
||||
|
||||
ipcMain.handle('buffer:save', async (event, path, content) => {
|
||||
return await library.save(path, content)
|
||||
});
|
||||
|
||||
ipcMain.handle('buffer:getList', async (event) => {
|
||||
return await library.getList()
|
||||
});
|
||||
|
||||
ipcMain.handle('buffer:exists', async (event, path) => {
|
||||
return await library.exists(path)
|
||||
});
|
||||
|
||||
ipcMain.handle('buffer:close', async (event, path) => {
|
||||
return await library.closeFile(path)
|
||||
});
|
||||
|
||||
ipcMain.handle('buffer:saveAndQuit', async (event, contents) => {
|
||||
library.stopWatcher()
|
||||
for (const [path, content] of contents) {
|
||||
await library.save(path, content)
|
||||
}
|
||||
library.contentSaved = true
|
||||
app.quit()
|
||||
})
|
||||
|
||||
library.setupWatcher(win)
|
||||
}
|
@ -10,6 +10,7 @@ import { isDev, isLinux, isMac, isWindows } from '../detect-platform';
|
||||
import { initializeAutoUpdate, checkForUpdates } from './auto-update';
|
||||
import { fixElectronCors } from './cors';
|
||||
import { loadBuffer, contentSaved } from './buffer';
|
||||
import { FileLibrary, setupFileLibraryEventHandlers } from './file-library';
|
||||
|
||||
|
||||
// The built directory structure
|
||||
@ -49,6 +50,7 @@ Menu.setApplicationMenu(menu)
|
||||
// process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true'
|
||||
|
||||
export let win: BrowserWindow | null = null
|
||||
let fileLibrary: FileLibrary | null = null
|
||||
let tray: Tray | null = null;
|
||||
let initErrors: string[] = []
|
||||
// Here, you can also use other preload
|
||||
@ -139,7 +141,7 @@ async function createWindow() {
|
||||
}
|
||||
// Prevent the window from closing, and send a message to the renderer which will in turn
|
||||
// send a message to the main process to save the current buffer and close the window.
|
||||
if (!contentSaved) {
|
||||
if (!!fileLibrary && !fileLibrary.contentSaved) {
|
||||
event.preventDefault()
|
||||
win?.webContents.send(WINDOW_CLOSE_EVENT)
|
||||
} else {
|
||||
@ -303,6 +305,7 @@ function registerAlwaysOnTop() {
|
||||
}
|
||||
|
||||
app.whenReady().then(createWindow).then(async () => {
|
||||
setupFileLibraryEventHandlers(fileLibrary, win)
|
||||
initializeAutoUpdate(win)
|
||||
registerGlobalHotkey()
|
||||
registerShowInDock()
|
||||
@ -344,8 +347,16 @@ ipcMain.handle('dark-mode:set', (event, mode) => {
|
||||
|
||||
ipcMain.handle('dark-mode:get', () => nativeTheme.themeSource)
|
||||
|
||||
// load buffer on app start
|
||||
loadBuffer()
|
||||
// Initialize note/file library
|
||||
const customLibraryPath = CONFIG.get("settings.bufferPath")
|
||||
const libraryPath = customLibraryPath ? customLibraryPath : join(app.getPath("userData"), "notes")
|
||||
console.log("libraryPath", libraryPath)
|
||||
try {
|
||||
fileLibrary = new FileLibrary(libraryPath)
|
||||
} catch (error) {
|
||||
initErrors.push(`Error: ${error.message}`)
|
||||
}
|
||||
|
||||
ipcMain.handle("getInitErrors", () => {
|
||||
return initErrors
|
||||
})
|
||||
|
@ -1,6 +1,6 @@
|
||||
const { contextBridge } = require('electron')
|
||||
import themeMode from "./theme-mode"
|
||||
import { isMac, isWindows, isLinux } from "../detect-platform"
|
||||
import { isMac, isWindows, isLinux, isDev } from "../detect-platform"
|
||||
import { ipcRenderer } from "electron"
|
||||
import {
|
||||
WINDOW_CLOSE_EVENT,
|
||||
@ -30,8 +30,19 @@ contextBridge.exposeInMainWorld("heynote", {
|
||||
isWebApp: false,
|
||||
},
|
||||
|
||||
isDev: isDev,
|
||||
themeMode: themeMode,
|
||||
|
||||
init() {
|
||||
ipcRenderer.on("buffer:change", (event, path, content) => {
|
||||
// called on all changes to open buffer files
|
||||
// go through all registered callbacks for this path and call them
|
||||
if (this.buffer._onChangeCallbacks[path]) {
|
||||
this.buffer._onChangeCallbacks[path].forEach(callback => callback(content))
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
quit() {
|
||||
console.log("quitting")
|
||||
//ipcRenderer.invoke("app_quit")
|
||||
@ -46,25 +57,52 @@ contextBridge.exposeInMainWorld("heynote", {
|
||||
},
|
||||
|
||||
buffer: {
|
||||
async load() {
|
||||
return await ipcRenderer.invoke("buffer-content:load")
|
||||
async exists(path) {
|
||||
return await ipcRenderer.invoke("buffer:exists", path)
|
||||
},
|
||||
|
||||
async save(content) {
|
||||
return await ipcRenderer.invoke("buffer-content:save", content)
|
||||
async getList() {
|
||||
return await ipcRenderer.invoke("buffer:getList")
|
||||
},
|
||||
|
||||
async saveAndQuit(content) {
|
||||
return await ipcRenderer.invoke("buffer-content:saveAndQuit", content)
|
||||
async load(path) {
|
||||
return await ipcRenderer.invoke("buffer:load", path)
|
||||
},
|
||||
|
||||
onChangeCallback(callback) {
|
||||
ipcRenderer.on("buffer-content:change", callback)
|
||||
async save(path, content) {
|
||||
return await ipcRenderer.invoke("buffer:save", path, content)
|
||||
},
|
||||
|
||||
async saveAndQuit(contents) {
|
||||
return await ipcRenderer.invoke("buffer:saveAndQuit", contents)
|
||||
},
|
||||
|
||||
async close(path) {
|
||||
return await ipcRenderer.invoke("buffer:close", path)
|
||||
},
|
||||
|
||||
_onChangeCallbacks: {},
|
||||
addOnChangeCallback(path, callback) {
|
||||
// register a callback to be called when the buffer content changes for a specific file
|
||||
if (!this._onChangeCallbacks[path]) {
|
||||
this._onChangeCallbacks[path] = []
|
||||
}
|
||||
this._onChangeCallbacks[path].push(callback)
|
||||
},
|
||||
removeOnChangeCallback(path, callback) {
|
||||
if (this._onChangeCallbacks[path]) {
|
||||
this._onChangeCallbacks[path] = this._onChangeCallbacks[path].filter(cb => cb !== callback)
|
||||
}
|
||||
},
|
||||
|
||||
async selectLocation() {
|
||||
return await ipcRenderer.invoke("buffer-content:selectLocation")
|
||||
}
|
||||
},
|
||||
|
||||
callbacks(callbacks) {
|
||||
ipcRenderer.on("buffer:noteMetadataChanged", (event, path, info) => callbacks?.noteMetadataChanged(path, info))
|
||||
ipcRenderer.on("buffer:noteRemoved", (event, path) => callbacks?.noteRemoved(path))
|
||||
},
|
||||
},
|
||||
|
||||
settings: CONFIG.get("settings"),
|
||||
|
@ -28,6 +28,7 @@ onmessage = (event) => {
|
||||
},
|
||||
content: content,
|
||||
idx: event.data.idx,
|
||||
path: event.data.path,
|
||||
})
|
||||
return
|
||||
}
|
||||
@ -53,6 +54,7 @@ onmessage = (event) => {
|
||||
},
|
||||
content: content,
|
||||
idx: event.data.idx,
|
||||
path: event.data.path,
|
||||
})
|
||||
return
|
||||
}
|
||||
@ -66,6 +68,7 @@ onmessage = (event) => {
|
||||
},
|
||||
content: content,
|
||||
idx: event.data.idx,
|
||||
path: event.data.path,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
@ -1,8 +1,17 @@
|
||||
<script>
|
||||
import { mapState, mapActions } from 'pinia'
|
||||
|
||||
import { mapWritableState } from 'pinia'
|
||||
import { useNotesStore } from "../stores/notes-store"
|
||||
import { useErrorStore } from "../stores/error-store"
|
||||
|
||||
import StatusBar from './StatusBar.vue'
|
||||
import Editor from './Editor.vue'
|
||||
import LanguageSelector from './LanguageSelector.vue'
|
||||
import NoteSelector from './NoteSelector.vue'
|
||||
import Settings from './settings/Settings.vue'
|
||||
import ErrorMessages from './ErrorMessages.vue'
|
||||
import NewNote from './NewNote.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -10,20 +19,17 @@
|
||||
StatusBar,
|
||||
LanguageSelector,
|
||||
Settings,
|
||||
NoteSelector,
|
||||
ErrorMessages,
|
||||
NewNote,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
line: 1,
|
||||
column: 1,
|
||||
selectionSize: 0,
|
||||
language: "plaintext",
|
||||
languageAuto: true,
|
||||
theme: window.heynote.themeMode.initial,
|
||||
initialTheme: window.heynote.themeMode.initial,
|
||||
themeSetting: 'system',
|
||||
development: window.location.href.indexOf("dev=1") !== -1,
|
||||
showLanguageSelector: false,
|
||||
showSettings: false,
|
||||
settings: window.heynote.settings,
|
||||
}
|
||||
@ -56,13 +62,61 @@
|
||||
window.heynote.themeMode.removeListener()
|
||||
},
|
||||
|
||||
watch: {
|
||||
// when a dialog is closed, we want to focus the editor
|
||||
showLanguageSelector(value) { this.dialogWatcher(value) },
|
||||
showNoteSelector(value) { this.dialogWatcher(value) },
|
||||
showCreateNote(value) { this.dialogWatcher(value) },
|
||||
|
||||
currentNotePath() {
|
||||
this.focusEditor()
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState(useNotesStore, [
|
||||
"currentNotePath",
|
||||
"showLanguageSelector",
|
||||
"showNoteSelector",
|
||||
"showCreateNote",
|
||||
]),
|
||||
|
||||
editorInert() {
|
||||
return this.showCreateNote || this.showSettings
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapActions(useNotesStore, [
|
||||
"openLanguageSelector",
|
||||
"openNoteSelector",
|
||||
"openCreateNote",
|
||||
"closeDialog",
|
||||
"openNote",
|
||||
]),
|
||||
|
||||
// Used as a watcher for the booleans that control the visibility of editor dialogs.
|
||||
// When a dialog is closed, we want to focus the editor
|
||||
dialogWatcher(value) {
|
||||
if (!value) {
|
||||
this.focusEditor()
|
||||
}
|
||||
},
|
||||
|
||||
focusEditor() {
|
||||
// we need to wait for the next tick for the cases when we set the inert attribute on the editor
|
||||
// in which case issuing a focus() call immediately would not work
|
||||
this.$nextTick(() => {
|
||||
this.$refs.editor.focus()
|
||||
})
|
||||
},
|
||||
|
||||
openSettings() {
|
||||
this.showSettings = true
|
||||
},
|
||||
closeSettings() {
|
||||
this.showSettings = false
|
||||
this.$refs.editor.focus()
|
||||
this.focusEditor()
|
||||
},
|
||||
|
||||
toggleTheme() {
|
||||
@ -78,25 +132,8 @@
|
||||
this.$refs.editor.focus()
|
||||
},
|
||||
|
||||
onCursorChange(e) {
|
||||
this.line = e.cursorLine.line
|
||||
this.column = e.cursorLine.col
|
||||
this.selectionSize = e.selectionSize
|
||||
this.language = e.language
|
||||
this.languageAuto = e.languageAuto
|
||||
},
|
||||
|
||||
openLanguageSelector() {
|
||||
this.showLanguageSelector = true
|
||||
},
|
||||
|
||||
closeLanguageSelector() {
|
||||
this.showLanguageSelector = false
|
||||
this.$refs.editor.focus()
|
||||
},
|
||||
|
||||
onSelectLanguage(language) {
|
||||
this.showLanguageSelector = false
|
||||
this.closeDialog()
|
||||
this.$refs.editor.setLanguage(language)
|
||||
},
|
||||
|
||||
@ -111,7 +148,6 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<Editor
|
||||
@cursorChange="onCursorChange"
|
||||
:theme="theme"
|
||||
:development="development"
|
||||
:debugSyntaxTree="false"
|
||||
@ -124,37 +160,44 @@
|
||||
:fontSize="settings.fontSize"
|
||||
:defaultBlockLanguage="settings.defaultBlockLanguage || 'text'"
|
||||
:defaultBlockLanguageAutoDetect="settings.defaultBlockLanguageAutoDetect === undefined ? true : settings.defaultBlockLanguageAutoDetect"
|
||||
:inert="editorInert"
|
||||
class="editor"
|
||||
ref="editor"
|
||||
@openLanguageSelector="openLanguageSelector"
|
||||
/>
|
||||
<StatusBar
|
||||
:line="line"
|
||||
:column="column"
|
||||
:selectionSize="selectionSize"
|
||||
:language="language"
|
||||
:languageAuto="languageAuto"
|
||||
:theme="theme"
|
||||
:themeSetting="themeSetting"
|
||||
:autoUpdate="settings.autoUpdate"
|
||||
:allowBetaVersions="settings.allowBetaVersions"
|
||||
@toggleTheme="toggleTheme"
|
||||
@openNoteSelector="openNoteSelector"
|
||||
@openLanguageSelector="openLanguageSelector"
|
||||
@formatCurrentBlock="formatCurrentBlock"
|
||||
@openSettings="showSettings = true"
|
||||
@click="() => {$refs.editor.focus()}"
|
||||
class="status"
|
||||
/>
|
||||
<div class="overlay">
|
||||
<LanguageSelector
|
||||
v-if="showLanguageSelector"
|
||||
@selectLanguage="onSelectLanguage"
|
||||
@close="closeLanguageSelector"
|
||||
@close="closeDialog"
|
||||
/>
|
||||
<NoteSelector
|
||||
v-if="showNoteSelector"
|
||||
@openNote="openNote"
|
||||
@close="closeDialog"
|
||||
/>
|
||||
<Settings
|
||||
v-if="showSettings"
|
||||
:initialSettings="settings"
|
||||
@closeSettings="closeSettings"
|
||||
/>
|
||||
<NewNote
|
||||
v-if="showCreateNote"
|
||||
@close="closeDialog"
|
||||
/>
|
||||
<ErrorMessages />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -1,6 +1,9 @@
|
||||
<script>
|
||||
import { HeynoteEditor, LANGUAGE_SELECTOR_EVENT } from '../editor/editor.js'
|
||||
import { HeynoteEditor } from '../editor/editor.js'
|
||||
import { syntaxTree } from "@codemirror/language"
|
||||
import { toRaw } from 'vue';
|
||||
import { mapState } from 'pinia'
|
||||
import { useNotesStore } from "../stores/notes-store"
|
||||
|
||||
export default {
|
||||
props: {
|
||||
@ -38,66 +41,23 @@
|
||||
data() {
|
||||
return {
|
||||
syntaxTreeDebugContent: null,
|
||||
bufferFilePath: null,
|
||||
editor: null,
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$refs.editor.addEventListener("selectionChange", (e) => {
|
||||
//console.log("selectionChange:", e)
|
||||
this.$emit("cursorChange", {
|
||||
cursorLine: e.cursorLine,
|
||||
selectionSize: e.selectionSize,
|
||||
language: e.language,
|
||||
languageAuto: e.languageAuto,
|
||||
})
|
||||
})
|
||||
this.loadBuffer(this.currentNotePath)
|
||||
|
||||
this.$refs.editor.addEventListener(LANGUAGE_SELECTOR_EVENT, (e) => {
|
||||
this.$emit("openLanguageSelector")
|
||||
})
|
||||
|
||||
// load buffer content and create editor
|
||||
window.heynote.buffer.load().then((content) => {
|
||||
try {
|
||||
let diskContent = content
|
||||
this.editor = new HeynoteEditor({
|
||||
element: this.$refs.editor,
|
||||
content: content,
|
||||
theme: this.theme,
|
||||
saveFunction: (content) => {
|
||||
if (content === diskContent) {
|
||||
return
|
||||
}
|
||||
diskContent = content
|
||||
window.heynote.buffer.save(content)
|
||||
},
|
||||
keymap: this.keymap,
|
||||
emacsMetaKey: this.emacsMetaKey,
|
||||
showLineNumberGutter: this.showLineNumberGutter,
|
||||
showFoldGutter: this.showFoldGutter,
|
||||
bracketClosing: this.bracketClosing,
|
||||
fontFamily: this.fontFamily,
|
||||
fontSize: this.fontSize,
|
||||
})
|
||||
window._heynote_editor = this.editor
|
||||
window.document.addEventListener("currenciesLoaded", this.onCurrenciesLoaded)
|
||||
this.editor.setDefaultBlockLanguage(this.defaultBlockLanguage, this.defaultBlockLanguageAutoDetect)
|
||||
|
||||
// set up buffer change listener
|
||||
window.heynote.buffer.onChangeCallback((event, content) => {
|
||||
diskContent = content
|
||||
this.editor.setContent(content)
|
||||
})
|
||||
} catch (e) {
|
||||
alert("Error! " + e.message)
|
||||
throw e
|
||||
}
|
||||
})
|
||||
// set up window close handler that will save the buffer and quit
|
||||
window.heynote.onWindowClose(() => {
|
||||
window.heynote.buffer.saveAndQuit(this.editor.getContent())
|
||||
window.heynote.buffer.saveAndQuit([
|
||||
[this.editor.path, this.editor.getContent()],
|
||||
])
|
||||
})
|
||||
|
||||
window.document.addEventListener("currenciesLoaded", this.onCurrenciesLoaded)
|
||||
|
||||
// if debugSyntaxTree prop is set, display syntax tree for debugging
|
||||
if (this.debugSyntaxTree) {
|
||||
setInterval(() => {
|
||||
@ -123,65 +83,108 @@
|
||||
},
|
||||
|
||||
watch: {
|
||||
currentNotePath(path) {
|
||||
//console.log("currentNotePath changed to", path)
|
||||
this.loadBuffer(path)
|
||||
},
|
||||
|
||||
theme(newTheme) {
|
||||
this.editor.setTheme(newTheme)
|
||||
toRaw(this.editor).setTheme(newTheme)
|
||||
},
|
||||
|
||||
keymap() {
|
||||
this.editor.setKeymap(this.keymap, this.emacsMetaKey)
|
||||
toRaw(this.editor).setKeymap(this.keymap, this.emacsMetaKey)
|
||||
},
|
||||
|
||||
emacsMetaKey() {
|
||||
this.editor.setKeymap(this.keymap, this.emacsMetaKey)
|
||||
toRaw(this.editor).setKeymap(this.keymap, this.emacsMetaKey)
|
||||
},
|
||||
|
||||
showLineNumberGutter(show) {
|
||||
this.editor.setLineNumberGutter(show)
|
||||
toRaw(this.editor).setLineNumberGutter(show)
|
||||
},
|
||||
|
||||
showFoldGutter(show) {
|
||||
this.editor.setFoldGutter(show)
|
||||
toRaw(this.editor).setFoldGutter(show)
|
||||
},
|
||||
|
||||
bracketClosing(value) {
|
||||
this.editor.setBracketClosing(value)
|
||||
toRaw(this.editor).setBracketClosing(value)
|
||||
},
|
||||
|
||||
fontFamily() {
|
||||
this.editor.setFont(this.fontFamily, this.fontSize)
|
||||
toRaw(this.editor).setFont(this.fontFamily, this.fontSize)
|
||||
},
|
||||
fontSize() {
|
||||
this.editor.setFont(this.fontFamily, this.fontSize)
|
||||
toRaw(this.editor).setFont(this.fontFamily, this.fontSize)
|
||||
},
|
||||
defaultBlockLanguage() {
|
||||
this.editor.setDefaultBlockLanguage(this.defaultBlockLanguage, this.defaultBlockLanguageAutoDetect)
|
||||
toRaw(this.editor).setDefaultBlockLanguage(this.defaultBlockLanguage, this.defaultBlockLanguageAutoDetect)
|
||||
},
|
||||
defaultBlockLanguageAutoDetect() {
|
||||
this.editor.setDefaultBlockLanguage(this.defaultBlockLanguage, this.defaultBlockLanguageAutoDetect)
|
||||
toRaw(this.editor).setDefaultBlockLanguage(this.defaultBlockLanguage, this.defaultBlockLanguageAutoDetect)
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState(useNotesStore, [
|
||||
"currentNotePath",
|
||||
]),
|
||||
},
|
||||
|
||||
methods: {
|
||||
setLanguage(language) {
|
||||
if (language === "auto") {
|
||||
this.editor.setCurrentLanguage(null, true)
|
||||
} else {
|
||||
this.editor.setCurrentLanguage(language, false)
|
||||
loadBuffer(path) {
|
||||
if (this.editor) {
|
||||
this.editor.destroy()
|
||||
}
|
||||
this.editor.focus()
|
||||
// load buffer content and create editor
|
||||
this.bufferFilePath = path
|
||||
try {
|
||||
this.editor = new HeynoteEditor({
|
||||
element: this.$refs.editor,
|
||||
path: this.bufferFilePath,
|
||||
theme: this.theme,
|
||||
keymap: this.keymap,
|
||||
emacsMetaKey: this.emacsMetaKey,
|
||||
showLineNumberGutter: this.showLineNumberGutter,
|
||||
showFoldGutter: this.showFoldGutter,
|
||||
bracketClosing: this.bracketClosing,
|
||||
fontFamily: this.fontFamily,
|
||||
fontSize: this.fontSize,
|
||||
defaultBlockToken: this.defaultBlockLanguage,
|
||||
defaultBlockAutoDetect: this.defaultBlockLanguageAutoDetect,
|
||||
})
|
||||
window._heynote_editor = toRaw(this.editor)
|
||||
} catch (e) {
|
||||
alert("Error! " + e.message)
|
||||
throw e
|
||||
}
|
||||
},
|
||||
|
||||
setLanguage(language) {
|
||||
const editor = toRaw(this.editor)
|
||||
if (language === "auto") {
|
||||
editor.setCurrentLanguage(null, true)
|
||||
} else {
|
||||
editor.setCurrentLanguage(language, false)
|
||||
}
|
||||
editor.focus()
|
||||
},
|
||||
|
||||
formatCurrentBlock() {
|
||||
this.editor.formatCurrentBlock()
|
||||
this.editor.focus()
|
||||
const editor = toRaw(this.editor)
|
||||
editor.formatCurrentBlock()
|
||||
editor.focus()
|
||||
},
|
||||
|
||||
onCurrenciesLoaded() {
|
||||
this.editor.currenciesLoaded()
|
||||
if (this.editor) {
|
||||
toRaw(this.editor).currenciesLoaded()
|
||||
}
|
||||
},
|
||||
|
||||
focus() {
|
||||
this.editor.focus()
|
||||
toRaw(this.editor).focus()
|
||||
},
|
||||
},
|
||||
}
|
||||
|
158
src/components/NewNote.vue
Normal file
158
src/components/NewNote.vue
Normal file
@ -0,0 +1,158 @@
|
||||
<script>
|
||||
import { mapState, mapActions } from 'pinia'
|
||||
import { useNotesStore } from "../stores/notes-store"
|
||||
|
||||
import FolderSelect from './form/FolderSelect.vue'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
name: "",
|
||||
filename: "",
|
||||
tags: [],
|
||||
}
|
||||
},
|
||||
components: {
|
||||
FolderSelect
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
await this.updateNotes()
|
||||
this.$refs.nameInput.focus()
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState(useNotesStore, [
|
||||
"notes",
|
||||
]),
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapActions(useNotesStore, [
|
||||
"updateNotes",
|
||||
]),
|
||||
|
||||
onKeydown(event) {
|
||||
if (event.key === "Escape") {
|
||||
this.$emit("close")
|
||||
event.preventDefault()
|
||||
}
|
||||
},
|
||||
|
||||
onSubmit(event) {
|
||||
event.preventDefault()
|
||||
console.log("Creating note", this.name)
|
||||
this.$emit("close")
|
||||
//this.$emit("create", this.$refs.input.value)
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="scroller" @keydown="onKeydown" tabindex="-1">
|
||||
<form class="new-note" tabindex="-1" @focusout="onFocusOut" ref="container" @submit="onSubmit">
|
||||
<div class="container">
|
||||
<h1>New Note from Block</h1>
|
||||
<input
|
||||
placeholder="Name"
|
||||
type="text"
|
||||
v-model="name"
|
||||
class="name-input"
|
||||
ref="nameInput"
|
||||
/>
|
||||
|
||||
<label for="folder-select">Create in</label>
|
||||
<FolderSelect id="folder-select" />
|
||||
</div>
|
||||
<div class="bottom-bar">
|
||||
<button type="submit">Create Note</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="sass">
|
||||
.scroller
|
||||
overflow: auto
|
||||
position: fixed
|
||||
top: 0
|
||||
left: 0
|
||||
bottom: 0
|
||||
right: 0
|
||||
background: rgba(0,0,0, 0.2)
|
||||
.new-note
|
||||
font-size: 13px
|
||||
//background: #48b57e
|
||||
background: #efefef
|
||||
position: absolute
|
||||
top: 0
|
||||
left: 50%
|
||||
transform: translateX(-50%)
|
||||
border-radius: 0 0 5px 5px
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.3)
|
||||
&:focus
|
||||
outline: none
|
||||
+dark-mode
|
||||
background: #333
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.5)
|
||||
color: rgba(255,255,255, 0.7)
|
||||
+webapp-mobile
|
||||
max-width: calc(100% - 80px)
|
||||
|
||||
.container
|
||||
padding: 10px
|
||||
|
||||
h1
|
||||
font-weight: bold
|
||||
margin-bottom: 14px
|
||||
|
||||
label
|
||||
display: block
|
||||
margin-bottom: 6px
|
||||
//padding-left: 2px
|
||||
font-size: 12px
|
||||
font-weight: 600
|
||||
|
||||
|
||||
.name-input
|
||||
width: 400px
|
||||
background: #fff
|
||||
padding: 4px 5px
|
||||
border: 1px solid #ccc
|
||||
box-sizing: border-box
|
||||
border-radius: 2px
|
||||
margin-bottom: 16px
|
||||
&:focus
|
||||
outline: none
|
||||
border: 1px solid #fff
|
||||
outline: 2px solid #48b57e
|
||||
+dark-mode
|
||||
background: #3b3b3b
|
||||
color: rgba(255,255,255, 0.9)
|
||||
border: 1px solid #5a5a5a
|
||||
&:focus
|
||||
border: 1px solid #3b3b3b
|
||||
+webapp-mobile
|
||||
font-size: 16px
|
||||
max-width: 100%
|
||||
|
||||
.bottom-bar
|
||||
border-radius: 0 0 5px 5px
|
||||
background: #e3e3e3
|
||||
padding: 10px
|
||||
display: flex
|
||||
justify-content: flex-end
|
||||
+dark-mode
|
||||
background: #222
|
||||
button
|
||||
font-size: 12px
|
||||
height: 28px
|
||||
border: 1px solid #c5c5c5
|
||||
border-radius: 3px
|
||||
padding-left: 10px
|
||||
padding-right: 10px
|
||||
&:focus
|
||||
outline-color: #48b57e
|
||||
|
||||
</style>
|
186
src/components/NoteSelector.vue
Normal file
186
src/components/NoteSelector.vue
Normal file
@ -0,0 +1,186 @@
|
||||
<script>
|
||||
import { mapState, mapActions } from 'pinia'
|
||||
import { useNotesStore } from "../stores/notes-store"
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
selected: 0,
|
||||
filter: "",
|
||||
items: [],
|
||||
}
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
await this.updateNotes()
|
||||
this.$refs.container.focus()
|
||||
this.$refs.input.focus()
|
||||
this.items = Object.entries(this.notes).map(([path, metadata]) => {
|
||||
return {
|
||||
"path": path,
|
||||
"name": metadata?.name || path,
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState(useNotesStore, [
|
||||
"notes",
|
||||
]),
|
||||
|
||||
filteredItems() {
|
||||
return this.items.filter((lang) => {
|
||||
return lang.name.toLowerCase().indexOf(this.filter.toLowerCase()) !== -1
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapActions(useNotesStore, [
|
||||
"updateNotes",
|
||||
]),
|
||||
|
||||
onKeydown(event) {
|
||||
if (event.key === "ArrowDown") {
|
||||
this.selected = Math.min(this.selected + 1, this.filteredItems.length - 1)
|
||||
event.preventDefault()
|
||||
if (this.selected === this.filteredItems.length - 1) {
|
||||
this.$refs.container.scrollIntoView({block: "end"})
|
||||
} else {
|
||||
this.$refs.item[this.selected].scrollIntoView({block: "nearest"})
|
||||
}
|
||||
|
||||
} else if (event.key === "ArrowUp") {
|
||||
this.selected = Math.max(this.selected - 1, 0)
|
||||
event.preventDefault()
|
||||
if (this.selected === 0) {
|
||||
this.$refs.container.scrollIntoView({block: "start"})
|
||||
} else {
|
||||
this.$refs.item[this.selected].scrollIntoView({block: "nearest"})
|
||||
}
|
||||
} else if (event.key === "Enter") {
|
||||
this.selectItem(this.filteredItems[this.selected].path)
|
||||
event.preventDefault()
|
||||
} else if (event.key === "Escape") {
|
||||
this.$emit("close")
|
||||
event.preventDefault()
|
||||
}
|
||||
},
|
||||
|
||||
selectItem(path) {
|
||||
this.$emit("openNote", path)
|
||||
},
|
||||
|
||||
onInput(event) {
|
||||
// reset selection
|
||||
this.selected = 0
|
||||
},
|
||||
|
||||
onFocusOut(event) {
|
||||
let container = this.$refs.container
|
||||
if (container !== event.relatedTarget && !container.contains(event.relatedTarget)) {
|
||||
this.$emit("close")
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="scroller">
|
||||
<form class="note-selector" tabindex="-1" @focusout="onFocusOut" ref="container">
|
||||
<input
|
||||
type="text"
|
||||
ref="input"
|
||||
@keydown="onKeydown"
|
||||
@input="onInput"
|
||||
v-model="filter"
|
||||
/>
|
||||
<ul class="items">
|
||||
<li
|
||||
v-for="item, idx in filteredItems"
|
||||
:key="item.path"
|
||||
:class="idx === selected ? 'selected' : ''"
|
||||
@click="selectItem(item.path)"
|
||||
ref="item"
|
||||
>
|
||||
<span class="name">{{ item.name }}</span>
|
||||
<span class="path">{{ item.path }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="sass">
|
||||
.scroller
|
||||
overflow: auto
|
||||
position: fixed
|
||||
top: 0
|
||||
left: 0
|
||||
bottom: 0
|
||||
right: 0
|
||||
.note-selector
|
||||
font-size: 13px
|
||||
padding: 10px
|
||||
//background: #48b57e
|
||||
background: #efefef
|
||||
position: absolute
|
||||
top: 0
|
||||
left: 50%
|
||||
transform: translateX(-50%)
|
||||
border-radius: 0 0 5px 5px
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.3)
|
||||
+dark-mode
|
||||
background: #151516
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.5)
|
||||
+webapp-mobile
|
||||
max-width: calc(100% - 80px)
|
||||
|
||||
input
|
||||
background: #fff
|
||||
padding: 4px 5px
|
||||
border: 1px solid #ccc
|
||||
box-sizing: border-box
|
||||
border-radius: 2px
|
||||
width: 400px
|
||||
margin-bottom: 10px
|
||||
&:focus
|
||||
outline: none
|
||||
border: 1px solid #fff
|
||||
outline: 2px solid #48b57e
|
||||
+dark-mode
|
||||
background: #3b3b3b
|
||||
color: rgba(255,255,255, 0.9)
|
||||
border: 1px solid #5a5a5a
|
||||
&:focus
|
||||
border: 1px solid #3b3b3b
|
||||
+webapp-mobile
|
||||
font-size: 16px
|
||||
max-width: 100%
|
||||
|
||||
.items
|
||||
> li
|
||||
border-radius: 3px
|
||||
padding: 5px 12px
|
||||
cursor: pointer
|
||||
display: flex
|
||||
align-items: center
|
||||
&:hover
|
||||
background: #e2e2e2
|
||||
&.selected
|
||||
background: #48b57e
|
||||
color: #fff
|
||||
+dark-mode
|
||||
color: rgba(255,255,255, 0.53)
|
||||
&:hover
|
||||
background: #29292a
|
||||
&.selected
|
||||
background: #1b6540
|
||||
color: rgba(255,255,255, 0.87)
|
||||
.name
|
||||
margin-right: 12px
|
||||
.path
|
||||
opacity: 0.6
|
||||
font-size: 12px
|
||||
</style>
|
@ -1,17 +1,14 @@
|
||||
<script>
|
||||
import { mapState } from 'pinia'
|
||||
import UpdateStatusItem from './UpdateStatusItem.vue'
|
||||
import { LANGUAGES } from '../editor/languages.js'
|
||||
import { useNotesStore } from "../stores/notes-store"
|
||||
|
||||
const LANGUAGE_MAP = Object.fromEntries(LANGUAGES.map(l => [l.token, l]))
|
||||
const LANGUAGE_NAMES = Object.fromEntries(LANGUAGES.map(l => [l.token, l.name]))
|
||||
|
||||
export default {
|
||||
props: [
|
||||
"line",
|
||||
"column",
|
||||
"selectionSize",
|
||||
"language",
|
||||
"languageAuto",
|
||||
"theme",
|
||||
"themeSetting",
|
||||
"autoUpdate",
|
||||
@ -33,8 +30,17 @@
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState(useNotesStore, [
|
||||
"currentNoteName",
|
||||
"currentCursorLine",
|
||||
"currentLanguage",
|
||||
"currentSelectionSize",
|
||||
"currentLanguage",
|
||||
"currentLanguageAuto",
|
||||
]),
|
||||
|
||||
languageName() {
|
||||
return LANGUAGE_NAMES[this.language] || this.language
|
||||
return LANGUAGE_NAMES[this.currentLanguage] || this.currentLanguage
|
||||
},
|
||||
|
||||
className() {
|
||||
@ -42,7 +48,7 @@
|
||||
},
|
||||
|
||||
supportsFormat() {
|
||||
const lang = LANGUAGE_MAP[this.language]
|
||||
const lang = LANGUAGE_MAP[this.currentLanguage]
|
||||
return !!lang ? lang.supportsFormat : false
|
||||
},
|
||||
|
||||
@ -54,6 +60,10 @@
|
||||
return `Format Block Content (Alt + Shift + F)`
|
||||
},
|
||||
|
||||
changeNoteTitle() {
|
||||
return `Change Note (${this.cmdKey} + P)`
|
||||
},
|
||||
|
||||
changeLanguageTitle() {
|
||||
return `Change language for current block (${this.cmdKey} + L)`
|
||||
},
|
||||
@ -68,24 +78,31 @@
|
||||
<template>
|
||||
<div :class="className">
|
||||
<div class="status-block line-number">
|
||||
Ln <span class="num">{{ line }}</span>
|
||||
Col <span class="num">{{ column }}</span>
|
||||
<template v-if="selectionSize > 0">
|
||||
Sel <span class="num">{{ selectionSize }}</span>
|
||||
Ln <span class="num">{{ currentCursorLine?.line }}</span>
|
||||
Col <span class="num">{{ currentCursorLine?.col }}</span>
|
||||
<template v-if="currentSelectionSize > 0">
|
||||
Sel <span class="num">{{ currentSelectionSize }}</span>
|
||||
</template>
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
<div
|
||||
@click="$emit('openLanguageSelector')"
|
||||
@click.stop="$emit('openNoteSelector')"
|
||||
class="status-block note clickable"
|
||||
:title="changeNoteTitle"
|
||||
>
|
||||
{{ currentNoteName }}
|
||||
</div>
|
||||
<div
|
||||
@click.stop="$emit('openLanguageSelector')"
|
||||
class="status-block lang clickable"
|
||||
:title="changeLanguageTitle"
|
||||
>
|
||||
{{ languageName }}
|
||||
<span v-if="languageAuto" class="auto">(auto)</span>
|
||||
<span v-if="currentLanguageAuto" class="auto">(auto)</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="supportsFormat"
|
||||
@click="$emit('formatCurrentBlock')"
|
||||
@click.stop="$emit('formatCurrentBlock')"
|
||||
class="status-block format clickable"
|
||||
:title="formatBlockTitle"
|
||||
>
|
||||
@ -100,7 +117,7 @@
|
||||
<span :class="'icon ' + themeSetting"></span>
|
||||
</div>
|
||||
<div
|
||||
@click="$emit('openSettings')"
|
||||
@click.stop="$emit('openSettings')"
|
||||
class="status-block settings clickable"
|
||||
title="Settings"
|
||||
>
|
||||
|
53
src/components/form/FolderSelect.vue
Normal file
53
src/components/form/FolderSelect.vue
Normal file
@ -0,0 +1,53 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
tree: Array,
|
||||
},
|
||||
|
||||
methods: {
|
||||
onKeyDown(event) {
|
||||
console.log("Keydown", event.key)
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault()
|
||||
this.$emit("click")
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="folder-select-container"
|
||||
@keydown="onKeyDown"
|
||||
@click.stop.prevent="()=>{}"
|
||||
>
|
||||
<div class="folder root selected">
|
||||
Heynote Root
|
||||
</div>
|
||||
<div class="folder indent">New Folder…</div>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style lang="sass" scoped>
|
||||
.folder-select-container
|
||||
width: 100%
|
||||
background: #fff
|
||||
border: 1px solid #ccc
|
||||
border-radius: 2px
|
||||
padding: 5px 5px
|
||||
text-align: left
|
||||
&:focus
|
||||
outline: none
|
||||
border: 1px solid #fff
|
||||
outline: 2px solid #48b57e
|
||||
|
||||
.folder
|
||||
padding: 3px 6px
|
||||
font-size: 13px
|
||||
&.selected
|
||||
background: #48b57e
|
||||
color: #fff
|
||||
&.indent
|
||||
padding-left: 16px
|
||||
</style>
|
@ -4,8 +4,8 @@ import { EditorState, RangeSetBuilder, StateField, Facet , StateEffect, RangeSet
|
||||
import { syntaxTree, ensureSyntaxTree, syntaxTreeAvailable } from "@codemirror/language"
|
||||
import { Note, Document, NoteDelimiter } from "../lang-heynote/parser.terms.js"
|
||||
import { IterMode } from "@lezer/common";
|
||||
import { useNotesStore } from "../../stores/notes-store.js"
|
||||
import { heynoteEvent, LANGUAGE_CHANGE } from "../annotation.js";
|
||||
import { SelectionChangeEvent } from "../event.js"
|
||||
import { mathBlock } from "./math.js"
|
||||
import { emptyBlockSelected } from "./select-all.js";
|
||||
|
||||
@ -404,7 +404,9 @@ function getSelectionSize(state, sel) {
|
||||
return count
|
||||
}
|
||||
|
||||
const emitCursorChange = (editor) => ViewPlugin.fromClass(
|
||||
const emitCursorChange = (editor) => {
|
||||
const notesStore = useNotesStore()
|
||||
return ViewPlugin.fromClass(
|
||||
class {
|
||||
update(update) {
|
||||
// if the selection changed or the language changed (can happen without selection change),
|
||||
@ -419,17 +421,16 @@ const emitCursorChange = (editor) => ViewPlugin.fromClass(
|
||||
|
||||
const block = getActiveNoteBlock(update.state)
|
||||
if (block && cursorLine) {
|
||||
editor.element.dispatchEvent(new SelectionChangeEvent({
|
||||
cursorLine,
|
||||
selectionSize,
|
||||
language: block.language.name,
|
||||
languageAuto: block.language.auto,
|
||||
}))
|
||||
notesStore.currentCursorLine = cursorLine
|
||||
notesStore.currentSelectionSize = selectionSize
|
||||
notesStore.currentLanguage = block.language.name
|
||||
notesStore.currentLanguageAuto = block.language.auto
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export const noteBlockExtension = (editor) => {
|
||||
return [
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Annotation, EditorState, Compartment, Facet, EditorSelection } from "@codemirror/state"
|
||||
import { Annotation, EditorState, Compartment, Facet, EditorSelection, Transaction } from "@codemirror/state"
|
||||
import { EditorView, keymap, drawSelection, ViewPlugin, lineNumbers } from "@codemirror/view"
|
||||
import { indentUnit, forceParsing, foldGutter, ensureSyntaxTree } from "@codemirror/language"
|
||||
import { markdown } from "@codemirror/lang-markdown"
|
||||
@ -22,8 +22,8 @@ import { autoSaveContent } from "./save.js"
|
||||
import { todoCheckboxPlugin} from "./todo-checkbox.ts"
|
||||
import { links } from "./links.js"
|
||||
import { NoteFormat } from "./note-format.js"
|
||||
import { useNotesStore } from "../stores/notes-store.js";
|
||||
|
||||
export const LANGUAGE_SELECTOR_EVENT = "openLanguageSelector"
|
||||
|
||||
function getKeymapExtensions(editor, keymap) {
|
||||
if (keymap === "emacs") {
|
||||
@ -37,10 +37,10 @@ function getKeymapExtensions(editor, keymap) {
|
||||
export class HeynoteEditor {
|
||||
constructor({
|
||||
element,
|
||||
path,
|
||||
content,
|
||||
focus=true,
|
||||
theme="light",
|
||||
saveFunction=null,
|
||||
keymap="default",
|
||||
emacsMetaKey,
|
||||
showLineNumberGutter=true,
|
||||
@ -48,8 +48,11 @@ export class HeynoteEditor {
|
||||
bracketClosing=false,
|
||||
fontFamily,
|
||||
fontSize,
|
||||
defaultBlockToken,
|
||||
defaultBlockAutoDetect,
|
||||
}) {
|
||||
this.element = element
|
||||
this.path = path
|
||||
this.themeCompartment = new Compartment
|
||||
this.keymapCompartment = new Compartment
|
||||
this.lineNumberCompartmentPre = new Compartment
|
||||
@ -60,9 +63,10 @@ export class HeynoteEditor {
|
||||
this.deselectOnCopy = keymap === "emacs"
|
||||
this.emacsMetaKey = emacsMetaKey
|
||||
this.fontTheme = new Compartment
|
||||
this.defaultBlockToken = "text"
|
||||
this.defaultBlockAutoDetect = true
|
||||
this.saveFunction = saveFunction
|
||||
this.setDefaultBlockLanguage(defaultBlockToken, defaultBlockAutoDetect)
|
||||
this.contentLoaded = false
|
||||
this.notesStore = useNotesStore()
|
||||
|
||||
|
||||
const state = EditorState.create({
|
||||
doc: "",
|
||||
@ -88,7 +92,7 @@ export class HeynoteEditor {
|
||||
}),
|
||||
heynoteLang(),
|
||||
noteBlockExtension(this),
|
||||
languageDetection(() => this),
|
||||
languageDetection(path, () => this),
|
||||
|
||||
// set cursor blink rate to 1 second
|
||||
drawSelection({cursorBlinkRate:1000}),
|
||||
@ -98,7 +102,7 @@ export class HeynoteEditor {
|
||||
return {class: view.state.facet(EditorView.darkTheme) ? "dark-theme" : "light-theme"}
|
||||
}),
|
||||
|
||||
this.saveFunction ? autoSaveContent(this, 2000) : [],
|
||||
autoSaveContent(this, 2000),
|
||||
|
||||
todoCheckboxPlugin,
|
||||
markdown(),
|
||||
@ -107,34 +111,66 @@ export class HeynoteEditor {
|
||||
})
|
||||
|
||||
// make sure saveFunction is called when page is unloaded
|
||||
if (saveFunction) {
|
||||
window.addEventListener("beforeunload", () => {
|
||||
this.save()
|
||||
})
|
||||
}
|
||||
|
||||
this.view = new EditorView({
|
||||
state: state,
|
||||
parent: element,
|
||||
})
|
||||
|
||||
this.setContent(content)
|
||||
//this.setContent(content)
|
||||
this.setReadOnly(true)
|
||||
this.loadContent().then(() => {
|
||||
this.setReadOnly(false)
|
||||
})
|
||||
|
||||
if (focus) {
|
||||
this.view.focus()
|
||||
}
|
||||
}
|
||||
|
||||
save() {
|
||||
this.saveFunction(this.getContent())
|
||||
async save() {
|
||||
if (!this.contentLoaded) {
|
||||
return
|
||||
}
|
||||
const content = this.getContent()
|
||||
if (content === this.diskContent) {
|
||||
return
|
||||
}
|
||||
console.log("saving:", this.path)
|
||||
this.diskContent = content
|
||||
await window.heynote.buffer.save(this.path, content)
|
||||
}
|
||||
|
||||
getContent() {
|
||||
this.note.content = this.view.state.sliceDoc()
|
||||
this.note.cursors = this.view.state.selection.toJSON()
|
||||
|
||||
const ranges = this.note.cursors.ranges
|
||||
if (ranges.length == 1 && ranges[0].anchor == 0 && ranges[0].head == 0) {
|
||||
console.log("DEBUG!! Cursor is at 0,0")
|
||||
console.trace()
|
||||
}
|
||||
return this.note.serialize()
|
||||
}
|
||||
|
||||
async loadContent() {
|
||||
console.log("loading content", this.path)
|
||||
const content = await window.heynote.buffer.load(this.path)
|
||||
this.diskContent = content
|
||||
this.contentLoaded = true
|
||||
this.setContent(content)
|
||||
|
||||
// set up content change listener
|
||||
this.onChange = (content) => {
|
||||
this.diskContent = content
|
||||
this.setContent(content)
|
||||
}
|
||||
window.heynote.buffer.addOnChangeCallback(this.path, this.onChange)
|
||||
}
|
||||
|
||||
setContent(content) {
|
||||
try {
|
||||
this.note = NoteFormat.load(content)
|
||||
@ -143,6 +179,7 @@ export class HeynoteEditor {
|
||||
this.setReadOnly(true)
|
||||
throw e
|
||||
}
|
||||
this.notesStore.currentNoteName = this.note.metadata?.name || this.path
|
||||
return new Promise((resolve) => {
|
||||
// set buffer content
|
||||
this.view.dispatch({
|
||||
@ -151,7 +188,7 @@ export class HeynoteEditor {
|
||||
to: this.view.state.doc.length,
|
||||
insert: this.note.content,
|
||||
},
|
||||
annotations: [heynoteEvent.of(SET_CONTENT)],
|
||||
annotations: [heynoteEvent.of(SET_CONTENT), Transaction.addToHistory.of(false)],
|
||||
})
|
||||
|
||||
// Ensure we have a parsed syntax tree when buffer is loaded. This prevents errors for large buffers
|
||||
@ -217,7 +254,15 @@ export class HeynoteEditor {
|
||||
}
|
||||
|
||||
openLanguageSelector() {
|
||||
this.element.dispatchEvent(new Event(LANGUAGE_SELECTOR_EVENT))
|
||||
this.notesStore.openLanguageSelector()
|
||||
}
|
||||
|
||||
openNoteSelector() {
|
||||
this.notesStore.openNoteSelector()
|
||||
}
|
||||
|
||||
openCreateNote() {
|
||||
this.notesStore.openCreateNote()
|
||||
}
|
||||
|
||||
setCurrentLanguage(lang, auto=false) {
|
||||
@ -257,6 +302,15 @@ export class HeynoteEditor {
|
||||
currenciesLoaded() {
|
||||
triggerCurrenciesLoaded(this.view.state, this.view.dispatch)
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.onChange) {
|
||||
window.heynote.buffer.removeOnChangeCallback(this.path, this.onChange)
|
||||
}
|
||||
this.save()
|
||||
this.view.destroy()
|
||||
window.heynote.buffer.close(this.path)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,9 +0,0 @@
|
||||
export class SelectionChangeEvent extends Event {
|
||||
constructor({cursorLine, language, languageAuto, selectionSize}) {
|
||||
super("selectionChange")
|
||||
this.cursorLine = cursorLine
|
||||
this.selectionSize = selectionSize
|
||||
this.language = language
|
||||
this.languageAuto = languageAuto
|
||||
}
|
||||
}
|
@ -57,6 +57,8 @@ export function heynoteKeymap(editor) {
|
||||
["Alt-ArrowUp", moveLineUp],
|
||||
["Alt-ArrowDown", moveLineDown],
|
||||
["Mod-l", () => editor.openLanguageSelector()],
|
||||
["Mod-p", () => editor.openNoteSelector()],
|
||||
["Mod-s", () => editor.openCreateNote()],
|
||||
["Alt-Shift-f", formatBlockContent],
|
||||
["Mod-Alt-ArrowDown", newCursorBelow],
|
||||
["Mod-Alt-ArrowUp", newCursorAbove],
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { EditorState } from "@codemirror/state";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import { EditorView, ViewPlugin } from "@codemirror/view";
|
||||
import { redoDepth } from "@codemirror/commands";
|
||||
import { getActiveNoteBlock, blockState } from "../block/block";
|
||||
import { levenshtein_distance } from "./levenshtein";
|
||||
@ -25,17 +25,17 @@ function cancelIdleCallbackCompat(id) {
|
||||
}
|
||||
}
|
||||
|
||||
export function languageDetection(getEditor) {
|
||||
const previousBlockContent = {}
|
||||
let idleCallbackId = null
|
||||
|
||||
// we'll use a shared global web worker for the language detection, for multiple Editor instances
|
||||
const editorInstances = {}
|
||||
const detectionWorker = new Worker('langdetect-worker.js?worker');
|
||||
detectionWorker.onmessage = (event) => {
|
||||
//console.log("event:", event.data)
|
||||
if (!event.data.guesslang.language) {
|
||||
return
|
||||
}
|
||||
const editor = getEditor()
|
||||
|
||||
const editor = editorInstances[event.data.path]
|
||||
//const editor = getEditor()
|
||||
const view = editor.view
|
||||
const state = view.state
|
||||
const block = getActiveNoteBlock(state)
|
||||
@ -58,7 +58,16 @@ export function languageDetection(getEditor) {
|
||||
}
|
||||
}
|
||||
|
||||
const plugin = EditorView.updateListener.of(update => {
|
||||
export function languageDetection(path, getEditor) {
|
||||
const previousBlockContent = {}
|
||||
let idleCallbackId = null
|
||||
const editor = getEditor()
|
||||
editorInstances[path] = editor
|
||||
|
||||
//const plugin = EditorView.updateListener.of(update => {
|
||||
const plugin = ViewPlugin.fromClass(
|
||||
class {
|
||||
update(update) {
|
||||
if (update.docChanged) {
|
||||
if (idleCallbackId !== null) {
|
||||
cancelIdleCallbackCompat(idleCallbackId)
|
||||
@ -90,7 +99,7 @@ export function languageDetection(getEditor) {
|
||||
const content = update.state.doc.sliceString(block.content.from, block.content.to)
|
||||
if (content === "" && redoDepth(update.state) === 0) {
|
||||
// if content is cleared, set language to default
|
||||
const editor = getEditor()
|
||||
//const editor = getEditor()
|
||||
const view = editor.view
|
||||
const block = getActiveNoteBlock(view.state)
|
||||
if (block.language.name !== editor.defaultBlockToken) {
|
||||
@ -108,12 +117,20 @@ export function languageDetection(getEditor) {
|
||||
detectionWorker.postMessage({
|
||||
content: content,
|
||||
idx: idx,
|
||||
path: path,
|
||||
})
|
||||
previousBlockContent[idx] = content
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
destroy() {
|
||||
console.log("Removing editorInstance for:", path)
|
||||
delete editorInstances[path]
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return plugin
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { ViewPlugin } from "@codemirror/view"
|
||||
import { debounce } from "debounce"
|
||||
import { SET_CONTENT } from "./annotation"
|
||||
|
||||
|
||||
export const autoSaveContent = (editor, interval) => {
|
||||
@ -12,9 +13,12 @@ export const autoSaveContent = (editor, interval) => {
|
||||
class {
|
||||
update(update) {
|
||||
if (update.docChanged) {
|
||||
const initialSetContent = update.transactions.flatMap(t => t.annotations).some(a => a.value === SET_CONTENT)
|
||||
if (!initialSetContent) {
|
||||
save()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
@ -6,6 +6,7 @@ import { createPinia } from 'pinia'
|
||||
import App from './components/App.vue'
|
||||
import { loadCurrencies } from './currency'
|
||||
import { useErrorStore } from './stores/error-store'
|
||||
import { useNotesStore, initNotesStore } from './stores/notes-store'
|
||||
|
||||
|
||||
const pinia = createPinia()
|
||||
@ -18,10 +19,12 @@ app.mount('#app').$nextTick(() => {
|
||||
})
|
||||
|
||||
const errorStore = useErrorStore()
|
||||
//errorStore.addError("test error")
|
||||
window.heynote.getInitErrors().then((errors) => {
|
||||
errors.forEach((e) => errorStore.addError(e))
|
||||
})
|
||||
|
||||
initNotesStore()
|
||||
|
||||
|
||||
|
||||
@ -29,3 +32,4 @@ window.heynote.getInitErrors().then((errors) => {
|
||||
loadCurrencies()
|
||||
setInterval(loadCurrencies, 1000 * 3600 * 4)
|
||||
|
||||
window.heynote.init()
|
||||
|
65
src/stores/notes-store.js
Normal file
65
src/stores/notes-store.js
Normal file
@ -0,0 +1,65 @@
|
||||
import { defineStore } from "pinia"
|
||||
|
||||
export const useNotesStore = defineStore("notes", {
|
||||
state: () => ({
|
||||
notes: {},
|
||||
currentNotePath: window.heynote.isDev ? "buffer-dev.txt" : "buffer.txt",
|
||||
currentNoteName: null,
|
||||
currentLanguage: null,
|
||||
currentLanguageAuto: null,
|
||||
currentCursorLine: null,
|
||||
currentSelectionSize: null,
|
||||
|
||||
showNoteSelector: false,
|
||||
showLanguageSelector: false,
|
||||
showCreateNote: false,
|
||||
}),
|
||||
|
||||
actions: {
|
||||
async updateNotes() {
|
||||
this.setNotes(await window.heynote.buffer.getList())
|
||||
},
|
||||
|
||||
setNotes(notes) {
|
||||
this.notes = notes
|
||||
},
|
||||
|
||||
createNewNote(path, content) {
|
||||
//window.heynote.buffer.save(path, content)
|
||||
this.updateNotes()
|
||||
},
|
||||
|
||||
openNote(path) {
|
||||
this.showNoteSelector = false
|
||||
this.showLanguageSelector = false
|
||||
this.showCreateNote = false
|
||||
this.currentNotePath = path
|
||||
},
|
||||
|
||||
openLanguageSelector() {
|
||||
this.showLanguageSelector = true
|
||||
this.showNoteSelector = false
|
||||
this.showCreateNote = false
|
||||
},
|
||||
openNoteSelector() {
|
||||
this.showNoteSelector = true
|
||||
this.showLanguageSelector = false
|
||||
this.showCreateNote = false
|
||||
},
|
||||
openCreateNote() {
|
||||
this.showCreateNote = true
|
||||
this.showNoteSelector = false
|
||||
this.showLanguageSelector = false
|
||||
},
|
||||
closeDialog() {
|
||||
this.showCreateNote = false
|
||||
this.showNoteSelector = false
|
||||
this.showLanguageSelector = false
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export async function initNotesStore() {
|
||||
const notesStore = useNotesStore()
|
||||
await notesStore.updateNotes()
|
||||
}
|
@ -80,20 +80,38 @@ const Heynote = {
|
||||
defaultFontSize: isMobileDevice ? 16 : 12,
|
||||
|
||||
buffer: {
|
||||
async load() {
|
||||
const content = localStorage.getItem("buffer")
|
||||
async load(path) {
|
||||
const content = localStorage.getItem(path)
|
||||
return content === null ? "\n∞∞∞text-a\n" : content
|
||||
},
|
||||
|
||||
async save(content) {
|
||||
localStorage.setItem("buffer", content)
|
||||
async save(path, content) {
|
||||
console.log("saving", path, content)
|
||||
localStorage.setItem(path, content)
|
||||
},
|
||||
|
||||
async saveAndQuit(content) {
|
||||
async saveAndQuit(contents) {
|
||||
|
||||
},
|
||||
|
||||
onChangeCallback(callback) {
|
||||
|
||||
async exists(path) {
|
||||
return true
|
||||
},
|
||||
|
||||
async getList(path) {
|
||||
return [{"path":"buffer.txt", "metadata":{}}]
|
||||
},
|
||||
|
||||
async close(path) {
|
||||
|
||||
},
|
||||
|
||||
_onChangeCallbacks: {},
|
||||
addOnChangeCallback(path, callback) {
|
||||
|
||||
},
|
||||
removeOnChangeCallback(path, callback) {
|
||||
|
||||
},
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user