mirror of
https://github.com/heyman/heynote.git
synced 2024-12-22 14:40:37 +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
c14c700791
commit
d01c19fd72
@ -16,15 +16,15 @@ const untildify = (pathWithTilde) => {
|
|||||||
: pathWithTilde;
|
: pathWithTilde;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function constructBufferFilePath(directoryPath) {
|
export function constructBufferFilePath(directoryPath, path) {
|
||||||
return join(untildify(directoryPath), isDev ? "buffer-dev.txt" : "buffer.txt")
|
return join(untildify(directoryPath), path)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getBufferFilePath() {
|
export function getFullBufferFilePath(path) {
|
||||||
let defaultPath = app.getPath("userData")
|
let defaultPath = app.getPath("userData")
|
||||||
let configPath = CONFIG.get("settings.bufferPath")
|
let configPath = CONFIG.get("settings.bufferPath")
|
||||||
let bufferPath = configPath.length ? configPath : defaultPath
|
let bufferPath = configPath.length ? configPath : defaultPath
|
||||||
let bufferFilePath = constructBufferFilePath(bufferPath)
|
let bufferFilePath = constructBufferFilePath(bufferPath, path)
|
||||||
try {
|
try {
|
||||||
// use realpathSync to resolve a potential symlink
|
// use realpathSync to resolve a potential symlink
|
||||||
return fs.realpathSync(bufferFilePath)
|
return fs.realpathSync(bufferFilePath)
|
||||||
@ -103,39 +103,45 @@ export class Buffer {
|
|||||||
|
|
||||||
|
|
||||||
// Buffer
|
// Buffer
|
||||||
let buffer
|
let buffers = {}
|
||||||
export function loadBuffer() {
|
export function loadBuffer(path) {
|
||||||
if (buffer) {
|
if (buffers[path]) {
|
||||||
buffer.close()
|
buffers[path].close()
|
||||||
}
|
}
|
||||||
buffer = new Buffer({
|
buffers[path] = new Buffer({
|
||||||
filePath: getBufferFilePath(),
|
filePath: getFullBufferFilePath(path),
|
||||||
onChange: (content) => {
|
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 () => {
|
ipcMain.handle('buffer-content:load', async (event, path) => {
|
||||||
if (buffer.exists() && !(eraseInitialContent && isDev)) {
|
if (!buffers[path]) {
|
||||||
return await buffer.load()
|
loadBuffer(path)
|
||||||
|
}
|
||||||
|
if (buffers[path].exists() && !(eraseInitialContent && isDev)) {
|
||||||
|
return await buffers[path].load()
|
||||||
} else {
|
} else {
|
||||||
return isDev ? initialDevContent : initialContent
|
return isDev ? initialDevContent : initialContent
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function save(content) {
|
async function save(path, content) {
|
||||||
return await buffer.save(content)
|
return await buffers[path].save(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
ipcMain.handle('buffer-content:save', async (event, content) => {
|
ipcMain.handle('buffer-content:save', async (event, path, content) => {
|
||||||
return await save(content)
|
return await save(path, content)
|
||||||
});
|
});
|
||||||
|
|
||||||
export let contentSaved = false
|
export let contentSaved = false
|
||||||
ipcMain.handle('buffer-content:saveAndQuit', async (event, content) => {
|
ipcMain.handle('buffer-content:saveAndQuit', async (event, contents) => {
|
||||||
await save(content)
|
for (const [path, content] of contents) {
|
||||||
|
await save(path, content)
|
||||||
|
}
|
||||||
contentSaved = true
|
contentSaved = true
|
||||||
app.quit()
|
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 { initializeAutoUpdate, checkForUpdates } from './auto-update';
|
||||||
import { fixElectronCors } from './cors';
|
import { fixElectronCors } from './cors';
|
||||||
import { loadBuffer, contentSaved } from './buffer';
|
import { loadBuffer, contentSaved } from './buffer';
|
||||||
|
import { FileLibrary, setupFileLibraryEventHandlers } from './file-library';
|
||||||
|
|
||||||
|
|
||||||
// The built directory structure
|
// The built directory structure
|
||||||
@ -49,6 +50,7 @@ Menu.setApplicationMenu(menu)
|
|||||||
// process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true'
|
// process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true'
|
||||||
|
|
||||||
export let win: BrowserWindow | null = null
|
export let win: BrowserWindow | null = null
|
||||||
|
let fileLibrary: FileLibrary | null = null
|
||||||
let tray: Tray | null = null;
|
let tray: Tray | null = null;
|
||||||
let initErrors: string[] = []
|
let initErrors: string[] = []
|
||||||
// Here, you can also use other preload
|
// 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
|
// 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.
|
// send a message to the main process to save the current buffer and close the window.
|
||||||
if (!contentSaved) {
|
if (!!fileLibrary && !fileLibrary.contentSaved) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
win?.webContents.send(WINDOW_CLOSE_EVENT)
|
win?.webContents.send(WINDOW_CLOSE_EVENT)
|
||||||
} else {
|
} else {
|
||||||
@ -308,6 +310,7 @@ function registerAlwaysOnTop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
app.whenReady().then(createWindow).then(async () => {
|
app.whenReady().then(createWindow).then(async () => {
|
||||||
|
setupFileLibraryEventHandlers(fileLibrary, win)
|
||||||
initializeAutoUpdate(win)
|
initializeAutoUpdate(win)
|
||||||
registerGlobalHotkey()
|
registerGlobalHotkey()
|
||||||
registerShowInDock()
|
registerShowInDock()
|
||||||
@ -349,8 +352,16 @@ ipcMain.handle('dark-mode:set', (event, mode) => {
|
|||||||
|
|
||||||
ipcMain.handle('dark-mode:get', () => nativeTheme.themeSource)
|
ipcMain.handle('dark-mode:get', () => nativeTheme.themeSource)
|
||||||
|
|
||||||
// load buffer on app start
|
// Initialize note/file library
|
||||||
loadBuffer()
|
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", () => {
|
ipcMain.handle("getInitErrors", () => {
|
||||||
return initErrors
|
return initErrors
|
||||||
})
|
})
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
const { contextBridge } = require('electron')
|
const { contextBridge } = require('electron')
|
||||||
import themeMode from "./theme-mode"
|
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 { ipcRenderer } from "electron"
|
||||||
import {
|
import {
|
||||||
WINDOW_CLOSE_EVENT,
|
WINDOW_CLOSE_EVENT,
|
||||||
@ -29,9 +29,20 @@ contextBridge.exposeInMainWorld("heynote", {
|
|||||||
isLinux,
|
isLinux,
|
||||||
isWebApp: false,
|
isWebApp: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isDev: isDev,
|
||||||
themeMode: themeMode,
|
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() {
|
quit() {
|
||||||
console.log("quitting")
|
console.log("quitting")
|
||||||
//ipcRenderer.invoke("app_quit")
|
//ipcRenderer.invoke("app_quit")
|
||||||
@ -46,25 +57,52 @@ contextBridge.exposeInMainWorld("heynote", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
buffer: {
|
buffer: {
|
||||||
async load() {
|
async exists(path) {
|
||||||
return await ipcRenderer.invoke("buffer-content:load")
|
return await ipcRenderer.invoke("buffer:exists", path)
|
||||||
},
|
},
|
||||||
|
|
||||||
async save(content) {
|
async getList() {
|
||||||
return await ipcRenderer.invoke("buffer-content:save", content)
|
return await ipcRenderer.invoke("buffer:getList")
|
||||||
},
|
},
|
||||||
|
|
||||||
async saveAndQuit(content) {
|
async load(path) {
|
||||||
return await ipcRenderer.invoke("buffer-content:saveAndQuit", content)
|
return await ipcRenderer.invoke("buffer:load", path)
|
||||||
},
|
},
|
||||||
|
|
||||||
onChangeCallback(callback) {
|
async save(path, content) {
|
||||||
ipcRenderer.on("buffer-content:change", callback)
|
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() {
|
async selectLocation() {
|
||||||
return await ipcRenderer.invoke("buffer-content: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"),
|
settings: CONFIG.get("settings"),
|
||||||
|
@ -28,6 +28,7 @@ onmessage = (event) => {
|
|||||||
},
|
},
|
||||||
content: content,
|
content: content,
|
||||||
idx: event.data.idx,
|
idx: event.data.idx,
|
||||||
|
path: event.data.path,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -53,6 +54,7 @@ onmessage = (event) => {
|
|||||||
},
|
},
|
||||||
content: content,
|
content: content,
|
||||||
idx: event.data.idx,
|
idx: event.data.idx,
|
||||||
|
path: event.data.path,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -66,6 +68,7 @@ onmessage = (event) => {
|
|||||||
},
|
},
|
||||||
content: content,
|
content: content,
|
||||||
idx: event.data.idx,
|
idx: event.data.idx,
|
||||||
|
path: event.data.path,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,17 @@
|
|||||||
<script>
|
<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 StatusBar from './StatusBar.vue'
|
||||||
import Editor from './Editor.vue'
|
import Editor from './Editor.vue'
|
||||||
import LanguageSelector from './LanguageSelector.vue'
|
import LanguageSelector from './LanguageSelector.vue'
|
||||||
|
import NoteSelector from './NoteSelector.vue'
|
||||||
import Settings from './settings/Settings.vue'
|
import Settings from './settings/Settings.vue'
|
||||||
|
import ErrorMessages from './ErrorMessages.vue'
|
||||||
|
import NewNote from './NewNote.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -10,20 +19,17 @@
|
|||||||
StatusBar,
|
StatusBar,
|
||||||
LanguageSelector,
|
LanguageSelector,
|
||||||
Settings,
|
Settings,
|
||||||
|
NoteSelector,
|
||||||
|
ErrorMessages,
|
||||||
|
NewNote,
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
line: 1,
|
|
||||||
column: 1,
|
|
||||||
selectionSize: 0,
|
|
||||||
language: "plaintext",
|
|
||||||
languageAuto: true,
|
|
||||||
theme: window.heynote.themeMode.initial,
|
theme: window.heynote.themeMode.initial,
|
||||||
initialTheme: window.heynote.themeMode.initial,
|
initialTheme: window.heynote.themeMode.initial,
|
||||||
themeSetting: 'system',
|
themeSetting: 'system',
|
||||||
development: window.location.href.indexOf("dev=1") !== -1,
|
development: window.location.href.indexOf("dev=1") !== -1,
|
||||||
showLanguageSelector: false,
|
|
||||||
showSettings: false,
|
showSettings: false,
|
||||||
settings: window.heynote.settings,
|
settings: window.heynote.settings,
|
||||||
}
|
}
|
||||||
@ -56,13 +62,61 @@
|
|||||||
window.heynote.themeMode.removeListener()
|
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: {
|
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() {
|
openSettings() {
|
||||||
this.showSettings = true
|
this.showSettings = true
|
||||||
},
|
},
|
||||||
closeSettings() {
|
closeSettings() {
|
||||||
this.showSettings = false
|
this.showSettings = false
|
||||||
this.$refs.editor.focus()
|
this.focusEditor()
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleTheme() {
|
toggleTheme() {
|
||||||
@ -78,25 +132,8 @@
|
|||||||
this.$refs.editor.focus()
|
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) {
|
onSelectLanguage(language) {
|
||||||
this.showLanguageSelector = false
|
this.closeDialog()
|
||||||
this.$refs.editor.setLanguage(language)
|
this.$refs.editor.setLanguage(language)
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -111,7 +148,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<Editor
|
<Editor
|
||||||
@cursorChange="onCursorChange"
|
|
||||||
:theme="theme"
|
:theme="theme"
|
||||||
:development="development"
|
:development="development"
|
||||||
:debugSyntaxTree="false"
|
:debugSyntaxTree="false"
|
||||||
@ -124,37 +160,44 @@
|
|||||||
:fontSize="settings.fontSize"
|
:fontSize="settings.fontSize"
|
||||||
:defaultBlockLanguage="settings.defaultBlockLanguage || 'text'"
|
:defaultBlockLanguage="settings.defaultBlockLanguage || 'text'"
|
||||||
:defaultBlockLanguageAutoDetect="settings.defaultBlockLanguageAutoDetect === undefined ? true : settings.defaultBlockLanguageAutoDetect"
|
:defaultBlockLanguageAutoDetect="settings.defaultBlockLanguageAutoDetect === undefined ? true : settings.defaultBlockLanguageAutoDetect"
|
||||||
|
:inert="editorInert"
|
||||||
class="editor"
|
class="editor"
|
||||||
ref="editor"
|
ref="editor"
|
||||||
@openLanguageSelector="openLanguageSelector"
|
|
||||||
/>
|
/>
|
||||||
<StatusBar
|
<StatusBar
|
||||||
:line="line"
|
|
||||||
:column="column"
|
|
||||||
:selectionSize="selectionSize"
|
|
||||||
:language="language"
|
|
||||||
:languageAuto="languageAuto"
|
|
||||||
:theme="theme"
|
:theme="theme"
|
||||||
:themeSetting="themeSetting"
|
:themeSetting="themeSetting"
|
||||||
:autoUpdate="settings.autoUpdate"
|
:autoUpdate="settings.autoUpdate"
|
||||||
:allowBetaVersions="settings.allowBetaVersions"
|
:allowBetaVersions="settings.allowBetaVersions"
|
||||||
@toggleTheme="toggleTheme"
|
@toggleTheme="toggleTheme"
|
||||||
|
@openNoteSelector="openNoteSelector"
|
||||||
@openLanguageSelector="openLanguageSelector"
|
@openLanguageSelector="openLanguageSelector"
|
||||||
@formatCurrentBlock="formatCurrentBlock"
|
@formatCurrentBlock="formatCurrentBlock"
|
||||||
@openSettings="showSettings = true"
|
@openSettings="showSettings = true"
|
||||||
|
@click="() => {$refs.editor.focus()}"
|
||||||
class="status"
|
class="status"
|
||||||
/>
|
/>
|
||||||
<div class="overlay">
|
<div class="overlay">
|
||||||
<LanguageSelector
|
<LanguageSelector
|
||||||
v-if="showLanguageSelector"
|
v-if="showLanguageSelector"
|
||||||
@selectLanguage="onSelectLanguage"
|
@selectLanguage="onSelectLanguage"
|
||||||
@close="closeLanguageSelector"
|
@close="closeDialog"
|
||||||
|
/>
|
||||||
|
<NoteSelector
|
||||||
|
v-if="showNoteSelector"
|
||||||
|
@openNote="openNote"
|
||||||
|
@close="closeDialog"
|
||||||
/>
|
/>
|
||||||
<Settings
|
<Settings
|
||||||
v-if="showSettings"
|
v-if="showSettings"
|
||||||
:initialSettings="settings"
|
:initialSettings="settings"
|
||||||
@closeSettings="closeSettings"
|
@closeSettings="closeSettings"
|
||||||
/>
|
/>
|
||||||
|
<NewNote
|
||||||
|
v-if="showCreateNote"
|
||||||
|
@close="closeDialog"
|
||||||
|
/>
|
||||||
|
<ErrorMessages />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
<script>
|
<script>
|
||||||
import { HeynoteEditor, LANGUAGE_SELECTOR_EVENT } from '../editor/editor.js'
|
import { HeynoteEditor } from '../editor/editor.js'
|
||||||
import { syntaxTree } from "@codemirror/language"
|
import { syntaxTree } from "@codemirror/language"
|
||||||
|
import { toRaw } from 'vue';
|
||||||
|
import { mapState } from 'pinia'
|
||||||
|
import { useNotesStore } from "../stores/notes-store"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
@ -38,66 +41,23 @@
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
syntaxTreeDebugContent: null,
|
syntaxTreeDebugContent: null,
|
||||||
|
bufferFilePath: null,
|
||||||
|
editor: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$refs.editor.addEventListener("selectionChange", (e) => {
|
this.loadBuffer(this.currentNotePath)
|
||||||
//console.log("selectionChange:", e)
|
|
||||||
this.$emit("cursorChange", {
|
|
||||||
cursorLine: e.cursorLine,
|
|
||||||
selectionSize: e.selectionSize,
|
|
||||||
language: e.language,
|
|
||||||
languageAuto: e.languageAuto,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
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
|
// set up window close handler that will save the buffer and quit
|
||||||
window.heynote.onWindowClose(() => {
|
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 debugSyntaxTree prop is set, display syntax tree for debugging
|
||||||
if (this.debugSyntaxTree) {
|
if (this.debugSyntaxTree) {
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
@ -123,65 +83,108 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
|
currentNotePath(path) {
|
||||||
|
//console.log("currentNotePath changed to", path)
|
||||||
|
this.loadBuffer(path)
|
||||||
|
},
|
||||||
|
|
||||||
theme(newTheme) {
|
theme(newTheme) {
|
||||||
this.editor.setTheme(newTheme)
|
toRaw(this.editor).setTheme(newTheme)
|
||||||
},
|
},
|
||||||
|
|
||||||
keymap() {
|
keymap() {
|
||||||
this.editor.setKeymap(this.keymap, this.emacsMetaKey)
|
toRaw(this.editor).setKeymap(this.keymap, this.emacsMetaKey)
|
||||||
},
|
},
|
||||||
|
|
||||||
emacsMetaKey() {
|
emacsMetaKey() {
|
||||||
this.editor.setKeymap(this.keymap, this.emacsMetaKey)
|
toRaw(this.editor).setKeymap(this.keymap, this.emacsMetaKey)
|
||||||
},
|
},
|
||||||
|
|
||||||
showLineNumberGutter(show) {
|
showLineNumberGutter(show) {
|
||||||
this.editor.setLineNumberGutter(show)
|
toRaw(this.editor).setLineNumberGutter(show)
|
||||||
},
|
},
|
||||||
|
|
||||||
showFoldGutter(show) {
|
showFoldGutter(show) {
|
||||||
this.editor.setFoldGutter(show)
|
toRaw(this.editor).setFoldGutter(show)
|
||||||
},
|
},
|
||||||
|
|
||||||
bracketClosing(value) {
|
bracketClosing(value) {
|
||||||
this.editor.setBracketClosing(value)
|
toRaw(this.editor).setBracketClosing(value)
|
||||||
},
|
},
|
||||||
|
|
||||||
fontFamily() {
|
fontFamily() {
|
||||||
this.editor.setFont(this.fontFamily, this.fontSize)
|
toRaw(this.editor).setFont(this.fontFamily, this.fontSize)
|
||||||
},
|
},
|
||||||
fontSize() {
|
fontSize() {
|
||||||
this.editor.setFont(this.fontFamily, this.fontSize)
|
toRaw(this.editor).setFont(this.fontFamily, this.fontSize)
|
||||||
},
|
},
|
||||||
defaultBlockLanguage() {
|
defaultBlockLanguage() {
|
||||||
this.editor.setDefaultBlockLanguage(this.defaultBlockLanguage, this.defaultBlockLanguageAutoDetect)
|
toRaw(this.editor).setDefaultBlockLanguage(this.defaultBlockLanguage, this.defaultBlockLanguageAutoDetect)
|
||||||
},
|
},
|
||||||
defaultBlockLanguageAutoDetect() {
|
defaultBlockLanguageAutoDetect() {
|
||||||
this.editor.setDefaultBlockLanguage(this.defaultBlockLanguage, this.defaultBlockLanguageAutoDetect)
|
toRaw(this.editor).setDefaultBlockLanguage(this.defaultBlockLanguage, this.defaultBlockLanguageAutoDetect)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapState(useNotesStore, [
|
||||||
|
"currentNotePath",
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
setLanguage(language) {
|
loadBuffer(path) {
|
||||||
if (language === "auto") {
|
if (this.editor) {
|
||||||
this.editor.setCurrentLanguage(null, true)
|
this.editor.destroy()
|
||||||
} else {
|
|
||||||
this.editor.setCurrentLanguage(language, false)
|
|
||||||
}
|
}
|
||||||
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() {
|
formatCurrentBlock() {
|
||||||
this.editor.formatCurrentBlock()
|
const editor = toRaw(this.editor)
|
||||||
this.editor.focus()
|
editor.formatCurrentBlock()
|
||||||
|
editor.focus()
|
||||||
},
|
},
|
||||||
|
|
||||||
onCurrenciesLoaded() {
|
onCurrenciesLoaded() {
|
||||||
this.editor.currenciesLoaded()
|
if (this.editor) {
|
||||||
|
toRaw(this.editor).currenciesLoaded()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
focus() {
|
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>
|
<script>
|
||||||
|
import { mapState } from 'pinia'
|
||||||
import UpdateStatusItem from './UpdateStatusItem.vue'
|
import UpdateStatusItem from './UpdateStatusItem.vue'
|
||||||
import { LANGUAGES } from '../editor/languages.js'
|
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_MAP = Object.fromEntries(LANGUAGES.map(l => [l.token, l]))
|
||||||
const LANGUAGE_NAMES = Object.fromEntries(LANGUAGES.map(l => [l.token, l.name]))
|
const LANGUAGE_NAMES = Object.fromEntries(LANGUAGES.map(l => [l.token, l.name]))
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: [
|
props: [
|
||||||
"line",
|
|
||||||
"column",
|
|
||||||
"selectionSize",
|
|
||||||
"language",
|
|
||||||
"languageAuto",
|
|
||||||
"theme",
|
"theme",
|
||||||
"themeSetting",
|
"themeSetting",
|
||||||
"autoUpdate",
|
"autoUpdate",
|
||||||
@ -33,8 +30,17 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
...mapState(useNotesStore, [
|
||||||
|
"currentNoteName",
|
||||||
|
"currentCursorLine",
|
||||||
|
"currentLanguage",
|
||||||
|
"currentSelectionSize",
|
||||||
|
"currentLanguage",
|
||||||
|
"currentLanguageAuto",
|
||||||
|
]),
|
||||||
|
|
||||||
languageName() {
|
languageName() {
|
||||||
return LANGUAGE_NAMES[this.language] || this.language
|
return LANGUAGE_NAMES[this.currentLanguage] || this.currentLanguage
|
||||||
},
|
},
|
||||||
|
|
||||||
className() {
|
className() {
|
||||||
@ -42,7 +48,7 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
supportsFormat() {
|
supportsFormat() {
|
||||||
const lang = LANGUAGE_MAP[this.language]
|
const lang = LANGUAGE_MAP[this.currentLanguage]
|
||||||
return !!lang ? lang.supportsFormat : false
|
return !!lang ? lang.supportsFormat : false
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -54,6 +60,10 @@
|
|||||||
return `Format Block Content (Alt + Shift + F)`
|
return `Format Block Content (Alt + Shift + F)`
|
||||||
},
|
},
|
||||||
|
|
||||||
|
changeNoteTitle() {
|
||||||
|
return `Change Note (${this.cmdKey} + P)`
|
||||||
|
},
|
||||||
|
|
||||||
changeLanguageTitle() {
|
changeLanguageTitle() {
|
||||||
return `Change language for current block (${this.cmdKey} + L)`
|
return `Change language for current block (${this.cmdKey} + L)`
|
||||||
},
|
},
|
||||||
@ -68,24 +78,31 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="className">
|
<div :class="className">
|
||||||
<div class="status-block line-number">
|
<div class="status-block line-number">
|
||||||
Ln <span class="num">{{ line }}</span>
|
Ln <span class="num">{{ currentCursorLine?.line }}</span>
|
||||||
Col <span class="num">{{ column }}</span>
|
Col <span class="num">{{ currentCursorLine?.col }}</span>
|
||||||
<template v-if="selectionSize > 0">
|
<template v-if="currentSelectionSize > 0">
|
||||||
Sel <span class="num">{{ selectionSize }}</span>
|
Sel <span class="num">{{ currentSelectionSize }}</span>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
<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"
|
class="status-block lang clickable"
|
||||||
:title="changeLanguageTitle"
|
:title="changeLanguageTitle"
|
||||||
>
|
>
|
||||||
{{ languageName }}
|
{{ languageName }}
|
||||||
<span v-if="languageAuto" class="auto">(auto)</span>
|
<span v-if="currentLanguageAuto" class="auto">(auto)</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="supportsFormat"
|
v-if="supportsFormat"
|
||||||
@click="$emit('formatCurrentBlock')"
|
@click.stop="$emit('formatCurrentBlock')"
|
||||||
class="status-block format clickable"
|
class="status-block format clickable"
|
||||||
:title="formatBlockTitle"
|
:title="formatBlockTitle"
|
||||||
>
|
>
|
||||||
@ -100,7 +117,7 @@
|
|||||||
<span :class="'icon ' + themeSetting"></span>
|
<span :class="'icon ' + themeSetting"></span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@click="$emit('openSettings')"
|
@click.stop="$emit('openSettings')"
|
||||||
class="status-block settings clickable"
|
class="status-block settings clickable"
|
||||||
title="Settings"
|
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 { syntaxTree, ensureSyntaxTree, syntaxTreeAvailable } from "@codemirror/language"
|
||||||
import { Note, Document, NoteDelimiter } from "../lang-heynote/parser.terms.js"
|
import { Note, Document, NoteDelimiter } from "../lang-heynote/parser.terms.js"
|
||||||
import { IterMode } from "@lezer/common";
|
import { IterMode } from "@lezer/common";
|
||||||
|
import { useNotesStore } from "../../stores/notes-store.js"
|
||||||
import { heynoteEvent, LANGUAGE_CHANGE } from "../annotation.js";
|
import { heynoteEvent, LANGUAGE_CHANGE } from "../annotation.js";
|
||||||
import { SelectionChangeEvent } from "../event.js"
|
|
||||||
import { mathBlock } from "./math.js"
|
import { mathBlock } from "./math.js"
|
||||||
import { emptyBlockSelected } from "./select-all.js";
|
import { emptyBlockSelected } from "./select-all.js";
|
||||||
|
|
||||||
@ -404,32 +404,33 @@ function getSelectionSize(state, sel) {
|
|||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
const emitCursorChange = (editor) => ViewPlugin.fromClass(
|
const emitCursorChange = (editor) => {
|
||||||
class {
|
const notesStore = useNotesStore()
|
||||||
update(update) {
|
return ViewPlugin.fromClass(
|
||||||
// if the selection changed or the language changed (can happen without selection change),
|
class {
|
||||||
// emit a selection change event
|
update(update) {
|
||||||
const langChange = update.transactions.some(tr => tr.annotations.some(a => a.value == LANGUAGE_CHANGE))
|
// if the selection changed or the language changed (can happen without selection change),
|
||||||
if (update.selectionSet || langChange) {
|
// emit a selection change event
|
||||||
const cursorLine = getBlockLineFromPos(update.state, update.state.selection.main.head)
|
const langChange = update.transactions.some(tr => tr.annotations.some(a => a.value == LANGUAGE_CHANGE))
|
||||||
|
if (update.selectionSet || langChange) {
|
||||||
|
const cursorLine = getBlockLineFromPos(update.state, update.state.selection.main.head)
|
||||||
|
|
||||||
const selectionSize = update.state.selection.ranges.map(
|
const selectionSize = update.state.selection.ranges.map(
|
||||||
(sel) => getSelectionSize(update.state, sel)
|
(sel) => getSelectionSize(update.state, sel)
|
||||||
).reduce((a, b) => a + b, 0)
|
).reduce((a, b) => a + b, 0)
|
||||||
|
|
||||||
const block = getActiveNoteBlock(update.state)
|
const block = getActiveNoteBlock(update.state)
|
||||||
if (block && cursorLine) {
|
if (block && cursorLine) {
|
||||||
editor.element.dispatchEvent(new SelectionChangeEvent({
|
notesStore.currentCursorLine = cursorLine
|
||||||
cursorLine,
|
notesStore.currentSelectionSize = selectionSize
|
||||||
selectionSize,
|
notesStore.currentLanguage = block.language.name
|
||||||
language: block.language.name,
|
notesStore.currentLanguageAuto = block.language.auto
|
||||||
languageAuto: block.language.auto,
|
}
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
)
|
}
|
||||||
|
|
||||||
export const noteBlockExtension = (editor) => {
|
export const noteBlockExtension = (editor) => {
|
||||||
return [
|
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 { EditorView, keymap, drawSelection, ViewPlugin, lineNumbers } from "@codemirror/view"
|
||||||
import { indentUnit, forceParsing, foldGutter, ensureSyntaxTree } from "@codemirror/language"
|
import { indentUnit, forceParsing, foldGutter, ensureSyntaxTree } from "@codemirror/language"
|
||||||
import { markdown } from "@codemirror/lang-markdown"
|
import { markdown } from "@codemirror/lang-markdown"
|
||||||
@ -22,8 +22,8 @@ import { autoSaveContent } from "./save.js"
|
|||||||
import { todoCheckboxPlugin} from "./todo-checkbox.ts"
|
import { todoCheckboxPlugin} from "./todo-checkbox.ts"
|
||||||
import { links } from "./links.js"
|
import { links } from "./links.js"
|
||||||
import { NoteFormat } from "./note-format.js"
|
import { NoteFormat } from "./note-format.js"
|
||||||
|
import { useNotesStore } from "../stores/notes-store.js";
|
||||||
|
|
||||||
export const LANGUAGE_SELECTOR_EVENT = "openLanguageSelector"
|
|
||||||
|
|
||||||
function getKeymapExtensions(editor, keymap) {
|
function getKeymapExtensions(editor, keymap) {
|
||||||
if (keymap === "emacs") {
|
if (keymap === "emacs") {
|
||||||
@ -37,10 +37,10 @@ function getKeymapExtensions(editor, keymap) {
|
|||||||
export class HeynoteEditor {
|
export class HeynoteEditor {
|
||||||
constructor({
|
constructor({
|
||||||
element,
|
element,
|
||||||
|
path,
|
||||||
content,
|
content,
|
||||||
focus=true,
|
focus=true,
|
||||||
theme="light",
|
theme="light",
|
||||||
saveFunction=null,
|
|
||||||
keymap="default",
|
keymap="default",
|
||||||
emacsMetaKey,
|
emacsMetaKey,
|
||||||
showLineNumberGutter=true,
|
showLineNumberGutter=true,
|
||||||
@ -48,8 +48,11 @@ export class HeynoteEditor {
|
|||||||
bracketClosing=false,
|
bracketClosing=false,
|
||||||
fontFamily,
|
fontFamily,
|
||||||
fontSize,
|
fontSize,
|
||||||
|
defaultBlockToken,
|
||||||
|
defaultBlockAutoDetect,
|
||||||
}) {
|
}) {
|
||||||
this.element = element
|
this.element = element
|
||||||
|
this.path = path
|
||||||
this.themeCompartment = new Compartment
|
this.themeCompartment = new Compartment
|
||||||
this.keymapCompartment = new Compartment
|
this.keymapCompartment = new Compartment
|
||||||
this.lineNumberCompartmentPre = new Compartment
|
this.lineNumberCompartmentPre = new Compartment
|
||||||
@ -60,9 +63,10 @@ export class HeynoteEditor {
|
|||||||
this.deselectOnCopy = keymap === "emacs"
|
this.deselectOnCopy = keymap === "emacs"
|
||||||
this.emacsMetaKey = emacsMetaKey
|
this.emacsMetaKey = emacsMetaKey
|
||||||
this.fontTheme = new Compartment
|
this.fontTheme = new Compartment
|
||||||
this.defaultBlockToken = "text"
|
this.setDefaultBlockLanguage(defaultBlockToken, defaultBlockAutoDetect)
|
||||||
this.defaultBlockAutoDetect = true
|
this.contentLoaded = false
|
||||||
this.saveFunction = saveFunction
|
this.notesStore = useNotesStore()
|
||||||
|
|
||||||
|
|
||||||
const state = EditorState.create({
|
const state = EditorState.create({
|
||||||
doc: "",
|
doc: "",
|
||||||
@ -88,7 +92,7 @@ export class HeynoteEditor {
|
|||||||
}),
|
}),
|
||||||
heynoteLang(),
|
heynoteLang(),
|
||||||
noteBlockExtension(this),
|
noteBlockExtension(this),
|
||||||
languageDetection(() => this),
|
languageDetection(path, () => this),
|
||||||
|
|
||||||
// set cursor blink rate to 1 second
|
// set cursor blink rate to 1 second
|
||||||
drawSelection({cursorBlinkRate:1000}),
|
drawSelection({cursorBlinkRate:1000}),
|
||||||
@ -98,7 +102,7 @@ export class HeynoteEditor {
|
|||||||
return {class: view.state.facet(EditorView.darkTheme) ? "dark-theme" : "light-theme"}
|
return {class: view.state.facet(EditorView.darkTheme) ? "dark-theme" : "light-theme"}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
this.saveFunction ? autoSaveContent(this, 2000) : [],
|
autoSaveContent(this, 2000),
|
||||||
|
|
||||||
todoCheckboxPlugin,
|
todoCheckboxPlugin,
|
||||||
markdown(),
|
markdown(),
|
||||||
@ -107,34 +111,66 @@ export class HeynoteEditor {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// make sure saveFunction is called when page is unloaded
|
// make sure saveFunction is called when page is unloaded
|
||||||
if (saveFunction) {
|
window.addEventListener("beforeunload", () => {
|
||||||
window.addEventListener("beforeunload", () => {
|
this.save()
|
||||||
this.save()
|
})
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
this.view = new EditorView({
|
this.view = new EditorView({
|
||||||
state: state,
|
state: state,
|
||||||
parent: element,
|
parent: element,
|
||||||
})
|
})
|
||||||
|
|
||||||
this.setContent(content)
|
//this.setContent(content)
|
||||||
|
this.setReadOnly(true)
|
||||||
|
this.loadContent().then(() => {
|
||||||
|
this.setReadOnly(false)
|
||||||
|
})
|
||||||
|
|
||||||
if (focus) {
|
if (focus) {
|
||||||
this.view.focus()
|
this.view.focus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
save() {
|
async save() {
|
||||||
this.saveFunction(this.getContent())
|
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() {
|
getContent() {
|
||||||
this.note.content = this.view.state.sliceDoc()
|
this.note.content = this.view.state.sliceDoc()
|
||||||
this.note.cursors = this.view.state.selection.toJSON()
|
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()
|
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) {
|
setContent(content) {
|
||||||
try {
|
try {
|
||||||
this.note = NoteFormat.load(content)
|
this.note = NoteFormat.load(content)
|
||||||
@ -143,6 +179,7 @@ export class HeynoteEditor {
|
|||||||
this.setReadOnly(true)
|
this.setReadOnly(true)
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
|
this.notesStore.currentNoteName = this.note.metadata?.name || this.path
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
// set buffer content
|
// set buffer content
|
||||||
this.view.dispatch({
|
this.view.dispatch({
|
||||||
@ -151,7 +188,7 @@ export class HeynoteEditor {
|
|||||||
to: this.view.state.doc.length,
|
to: this.view.state.doc.length,
|
||||||
insert: this.note.content,
|
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
|
// 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() {
|
openLanguageSelector() {
|
||||||
this.element.dispatchEvent(new Event(LANGUAGE_SELECTOR_EVENT))
|
this.notesStore.openLanguageSelector()
|
||||||
|
}
|
||||||
|
|
||||||
|
openNoteSelector() {
|
||||||
|
this.notesStore.openNoteSelector()
|
||||||
|
}
|
||||||
|
|
||||||
|
openCreateNote() {
|
||||||
|
this.notesStore.openCreateNote()
|
||||||
}
|
}
|
||||||
|
|
||||||
setCurrentLanguage(lang, auto=false) {
|
setCurrentLanguage(lang, auto=false) {
|
||||||
@ -257,6 +302,15 @@ export class HeynoteEditor {
|
|||||||
currenciesLoaded() {
|
currenciesLoaded() {
|
||||||
triggerCurrenciesLoaded(this.view.state, this.view.dispatch)
|
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-ArrowUp", moveLineUp],
|
||||||
["Alt-ArrowDown", moveLineDown],
|
["Alt-ArrowDown", moveLineDown],
|
||||||
["Mod-l", () => editor.openLanguageSelector()],
|
["Mod-l", () => editor.openLanguageSelector()],
|
||||||
|
["Mod-p", () => editor.openNoteSelector()],
|
||||||
|
["Mod-s", () => editor.openCreateNote()],
|
||||||
["Alt-Shift-f", formatBlockContent],
|
["Alt-Shift-f", formatBlockContent],
|
||||||
["Mod-Alt-ArrowDown", newCursorBelow],
|
["Mod-Alt-ArrowDown", newCursorBelow],
|
||||||
["Mod-Alt-ArrowUp", newCursorAbove],
|
["Mod-Alt-ArrowUp", newCursorAbove],
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { EditorState } from "@codemirror/state";
|
import { EditorState } from "@codemirror/state";
|
||||||
import { EditorView } from "@codemirror/view";
|
import { EditorView, ViewPlugin } from "@codemirror/view";
|
||||||
import { redoDepth } from "@codemirror/commands";
|
import { redoDepth } from "@codemirror/commands";
|
||||||
import { getActiveNoteBlock, blockState } from "../block/block";
|
import { getActiveNoteBlock, blockState } from "../block/block";
|
||||||
import { levenshtein_distance } from "./levenshtein";
|
import { levenshtein_distance } from "./levenshtein";
|
||||||
@ -25,95 +25,112 @@ function cancelIdleCallbackCompat(id) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function languageDetection(getEditor) {
|
// we'll use a shared global web worker for the language detection, for multiple Editor instances
|
||||||
const previousBlockContent = {}
|
const editorInstances = {}
|
||||||
let idleCallbackId = null
|
const detectionWorker = new Worker('langdetect-worker.js?worker');
|
||||||
|
detectionWorker.onmessage = (event) => {
|
||||||
const detectionWorker = new Worker('langdetect-worker.js?worker');
|
//console.log("event:", event.data)
|
||||||
detectionWorker.onmessage = (event) => {
|
if (!event.data.guesslang.language) {
|
||||||
//console.log("event:", event.data)
|
return
|
||||||
if (!event.data.guesslang.language) {
|
}
|
||||||
return
|
|
||||||
}
|
const editor = editorInstances[event.data.path]
|
||||||
const editor = getEditor()
|
//const editor = getEditor()
|
||||||
const view = editor.view
|
const view = editor.view
|
||||||
const state = view.state
|
const state = view.state
|
||||||
const block = getActiveNoteBlock(state)
|
const block = getActiveNoteBlock(state)
|
||||||
const newLang = GUESSLANG_TO_TOKEN[event.data.guesslang.language]
|
const newLang = GUESSLANG_TO_TOKEN[event.data.guesslang.language]
|
||||||
if (block.language.auto === true && block.language.name !== newLang) {
|
if (block.language.auto === true && block.language.name !== newLang) {
|
||||||
console.log("New auto detected language:", newLang, "Confidence:", event.data.guesslang.confidence)
|
console.log("New auto detected language:", newLang, "Confidence:", event.data.guesslang.confidence)
|
||||||
let content = state.doc.sliceString(block.content.from, block.content.to)
|
let content = state.doc.sliceString(block.content.from, block.content.to)
|
||||||
const threshold = content.length * 0.1
|
const threshold = content.length * 0.1
|
||||||
if (levenshtein_distance(content, event.data.content) <= threshold) {
|
if (levenshtein_distance(content, event.data.content) <= threshold) {
|
||||||
// the content has not changed significantly so it's safe to change the language
|
// the content has not changed significantly so it's safe to change the language
|
||||||
if (redoDepth(state) === 0) {
|
if (redoDepth(state) === 0) {
|
||||||
console.log("Changing language to", newLang)
|
console.log("Changing language to", newLang)
|
||||||
changeLanguageTo(state, view.dispatch, block, newLang, true)
|
changeLanguageTo(state, view.dispatch, block, newLang, true)
|
||||||
} else {
|
|
||||||
console.log("Not changing language because the user has undo:ed and has redo history")
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
console.log("Content has changed significantly, not setting new language")
|
console.log("Not changing language because the user has undo:ed and has redo history")
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
console.log("Content has changed significantly, not setting new language")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const plugin = EditorView.updateListener.of(update => {
|
export function languageDetection(path, getEditor) {
|
||||||
if (update.docChanged) {
|
const previousBlockContent = {}
|
||||||
if (idleCallbackId !== null) {
|
let idleCallbackId = null
|
||||||
cancelIdleCallbackCompat(idleCallbackId)
|
const editor = getEditor()
|
||||||
idleCallbackId = null
|
editorInstances[path] = editor
|
||||||
|
|
||||||
|
//const plugin = EditorView.updateListener.of(update => {
|
||||||
|
const plugin = ViewPlugin.fromClass(
|
||||||
|
class {
|
||||||
|
update(update) {
|
||||||
|
if (update.docChanged) {
|
||||||
|
if (idleCallbackId !== null) {
|
||||||
|
cancelIdleCallbackCompat(idleCallbackId)
|
||||||
|
idleCallbackId = null
|
||||||
|
}
|
||||||
|
|
||||||
|
idleCallbackId = requestIdleCallbackCompat(() => {
|
||||||
|
idleCallbackId = null
|
||||||
|
|
||||||
|
const range = update.state.selection.asSingle().ranges[0]
|
||||||
|
const blocks = update.state.facet(blockState)
|
||||||
|
let block = null, idx = null;
|
||||||
|
for (let i=0; i<blocks.length; i++) {
|
||||||
|
if (blocks[i].content.from <= range.from && blocks[i].content.to >= range.from) {
|
||||||
|
block = blocks[i]
|
||||||
|
idx = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (block === null) {
|
||||||
|
return
|
||||||
|
} else if (block.language.auto === false) {
|
||||||
|
// if language is not auto, set it's previousBlockContent to null so that we'll trigger a language detection
|
||||||
|
// immediately if the user changes the language to auto
|
||||||
|
delete previousBlockContent[idx]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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 view = editor.view
|
||||||
|
const block = getActiveNoteBlock(view.state)
|
||||||
|
if (block.language.name !== editor.defaultBlockToken) {
|
||||||
|
changeLanguageTo(view.state, view.dispatch, block, editor.defaultBlockToken, true)
|
||||||
|
}
|
||||||
|
delete previousBlockContent[idx]
|
||||||
|
}
|
||||||
|
if (content.length <= 8) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const threshold = content.length * 0.1
|
||||||
|
if (!previousBlockContent[idx] || levenshtein_distance(previousBlockContent[idx], content) >= threshold) {
|
||||||
|
// the content has changed significantly, so schedule a language detection
|
||||||
|
//console.log("Scheduling language detection for block", idx, "with threshold", threshold)
|
||||||
|
detectionWorker.postMessage({
|
||||||
|
content: content,
|
||||||
|
idx: idx,
|
||||||
|
path: path,
|
||||||
|
})
|
||||||
|
previousBlockContent[idx] = content
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
idleCallbackId = requestIdleCallbackCompat(() => {
|
destroy() {
|
||||||
idleCallbackId = null
|
console.log("Removing editorInstance for:", path)
|
||||||
|
delete editorInstances[path]
|
||||||
const range = update.state.selection.asSingle().ranges[0]
|
}
|
||||||
const blocks = update.state.facet(blockState)
|
|
||||||
let block = null, idx = null;
|
|
||||||
for (let i=0; i<blocks.length; i++) {
|
|
||||||
if (blocks[i].content.from <= range.from && blocks[i].content.to >= range.from) {
|
|
||||||
block = blocks[i]
|
|
||||||
idx = i
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (block === null) {
|
|
||||||
return
|
|
||||||
} else if (block.language.auto === false) {
|
|
||||||
// if language is not auto, set it's previousBlockContent to null so that we'll trigger a language detection
|
|
||||||
// immediately if the user changes the language to auto
|
|
||||||
delete previousBlockContent[idx]
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
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 view = editor.view
|
|
||||||
const block = getActiveNoteBlock(view.state)
|
|
||||||
if (block.language.name !== editor.defaultBlockToken) {
|
|
||||||
changeLanguageTo(view.state, view.dispatch, block, editor.defaultBlockToken, true)
|
|
||||||
}
|
|
||||||
delete previousBlockContent[idx]
|
|
||||||
}
|
|
||||||
if (content.length <= 8) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const threshold = content.length * 0.1
|
|
||||||
if (!previousBlockContent[idx] || levenshtein_distance(previousBlockContent[idx], content) >= threshold) {
|
|
||||||
// the content has changed significantly, so schedule a language detection
|
|
||||||
//console.log("Scheduling language detection for block", idx, "with threshold", threshold)
|
|
||||||
detectionWorker.postMessage({
|
|
||||||
content: content,
|
|
||||||
idx: idx,
|
|
||||||
})
|
|
||||||
previousBlockContent[idx] = content
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
return plugin
|
return plugin
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { ViewPlugin } from "@codemirror/view"
|
import { ViewPlugin } from "@codemirror/view"
|
||||||
import { debounce } from "debounce"
|
import { debounce } from "debounce"
|
||||||
|
import { SET_CONTENT } from "./annotation"
|
||||||
|
|
||||||
|
|
||||||
export const autoSaveContent = (editor, interval) => {
|
export const autoSaveContent = (editor, interval) => {
|
||||||
@ -12,9 +13,12 @@ export const autoSaveContent = (editor, interval) => {
|
|||||||
class {
|
class {
|
||||||
update(update) {
|
update(update) {
|
||||||
if (update.docChanged) {
|
if (update.docChanged) {
|
||||||
save()
|
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 App from './components/App.vue'
|
||||||
import { loadCurrencies } from './currency'
|
import { loadCurrencies } from './currency'
|
||||||
import { useErrorStore } from './stores/error-store'
|
import { useErrorStore } from './stores/error-store'
|
||||||
|
import { useNotesStore, initNotesStore } from './stores/notes-store'
|
||||||
|
|
||||||
|
|
||||||
const pinia = createPinia()
|
const pinia = createPinia()
|
||||||
@ -18,10 +19,12 @@ app.mount('#app').$nextTick(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const errorStore = useErrorStore()
|
const errorStore = useErrorStore()
|
||||||
|
//errorStore.addError("test error")
|
||||||
window.heynote.getInitErrors().then((errors) => {
|
window.heynote.getInitErrors().then((errors) => {
|
||||||
errors.forEach((e) => errorStore.addError(e))
|
errors.forEach((e) => errorStore.addError(e))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
initNotesStore()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -29,3 +32,4 @@ window.heynote.getInitErrors().then((errors) => {
|
|||||||
loadCurrencies()
|
loadCurrencies()
|
||||||
setInterval(loadCurrencies, 1000 * 3600 * 4)
|
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,
|
defaultFontSize: isMobileDevice ? 16 : 12,
|
||||||
|
|
||||||
buffer: {
|
buffer: {
|
||||||
async load() {
|
async load(path) {
|
||||||
const content = localStorage.getItem("buffer")
|
const content = localStorage.getItem(path)
|
||||||
return content === null ? "\n∞∞∞text-a\n" : content
|
return content === null ? "\n∞∞∞text-a\n" : content
|
||||||
},
|
},
|
||||||
|
|
||||||
async save(content) {
|
async save(path, content) {
|
||||||
localStorage.setItem("buffer", 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