mirror of
https://github.com/heyman/heynote.git
synced 2025-06-26 12:32:00 +02:00
Implement support for editing notes' metadata, and ability to move notes into other directories.
Create separate pinia store for the editor cache functionality.
This commit is contained in:
parent
0da3e32171
commit
7e1f01471a
1
assets/icons/arrow-right.svg
Normal file
1
assets/icons/arrow-right.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" ?><svg baseProfile="tiny" height="24px" version="1.2" viewBox="0 0 24 24" width="24px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g id="Layer_1"><path fill="#ddd" d="M13.293,7.293c-0.391,0.391-0.391,1.023,0,1.414L15.586,11H8c-0.552,0-1,0.448-1,1s0.448,1,1,1h7.586l-2.293,2.293 c-0.391,0.391-0.391,1.023,0,1.414C13.488,16.902,13.744,17,14,17s0.512-0.098,0.707-0.293L19.414,12l-4.707-4.707 C14.316,6.902,13.684,6.902,13.293,7.293z"/></g></svg>
|
After Width: | Height: | Size: 522 B |
@ -72,6 +72,15 @@ export class FileLibrary {
|
|||||||
await this.jetpack.writeAsync(fullPath, content)
|
await this.jetpack.writeAsync(fullPath, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async move(path, newPath) {
|
||||||
|
if (await this.exists(newPath)) {
|
||||||
|
throw new Error(`File already exists: ${newPath}`)
|
||||||
|
}
|
||||||
|
const fullOldPath = join(this.basePath, path)
|
||||||
|
const fullNewPath = join(this.basePath, newPath)
|
||||||
|
await this.jetpack.moveAsync(fullOldPath, fullNewPath)
|
||||||
|
}
|
||||||
|
|
||||||
async getList() {
|
async getList() {
|
||||||
console.log("Loading notes")
|
console.log("Loading notes")
|
||||||
const notes = {}
|
const notes = {}
|
||||||
@ -231,5 +240,9 @@ export function setupFileLibraryEventHandlers(library, win) {
|
|||||||
app.quit()
|
app.quit()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('buffer:move', async (event, path, newPath) => {
|
||||||
|
return await library.move(path, newPath)
|
||||||
|
});
|
||||||
|
|
||||||
library.setupWatcher(win)
|
library.setupWatcher(win)
|
||||||
}
|
}
|
||||||
|
@ -77,6 +77,10 @@ contextBridge.exposeInMainWorld("heynote", {
|
|||||||
return await ipcRenderer.invoke("buffer:save", path, content)
|
return await ipcRenderer.invoke("buffer:save", path, content)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async move(path, newPath) {
|
||||||
|
return await ipcRenderer.invoke("buffer:move", path, newPath)
|
||||||
|
},
|
||||||
|
|
||||||
async create(path, content) {
|
async create(path, content) {
|
||||||
return await ipcRenderer.invoke("buffer:create", path, content)
|
return await ipcRenderer.invoke("buffer:create", path, content)
|
||||||
},
|
},
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
import Settings from './settings/Settings.vue'
|
import Settings from './settings/Settings.vue'
|
||||||
import ErrorMessages from './ErrorMessages.vue'
|
import ErrorMessages from './ErrorMessages.vue'
|
||||||
import NewNote from './NewNote.vue'
|
import NewNote from './NewNote.vue'
|
||||||
|
import EditNote from './EditNote.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -22,6 +23,7 @@
|
|||||||
NoteSelector,
|
NoteSelector,
|
||||||
ErrorMessages,
|
ErrorMessages,
|
||||||
NewNote,
|
NewNote,
|
||||||
|
EditNote,
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
@ -67,6 +69,7 @@
|
|||||||
showLanguageSelector(value) { this.dialogWatcher(value) },
|
showLanguageSelector(value) { this.dialogWatcher(value) },
|
||||||
showNoteSelector(value) { this.dialogWatcher(value) },
|
showNoteSelector(value) { this.dialogWatcher(value) },
|
||||||
showCreateNote(value) { this.dialogWatcher(value) },
|
showCreateNote(value) { this.dialogWatcher(value) },
|
||||||
|
showEditNote(value) { this.dialogWatcher(value) },
|
||||||
|
|
||||||
currentNotePath() {
|
currentNotePath() {
|
||||||
this.focusEditor()
|
this.focusEditor()
|
||||||
@ -79,10 +82,11 @@
|
|||||||
"showLanguageSelector",
|
"showLanguageSelector",
|
||||||
"showNoteSelector",
|
"showNoteSelector",
|
||||||
"showCreateNote",
|
"showCreateNote",
|
||||||
|
"showEditNote",
|
||||||
]),
|
]),
|
||||||
|
|
||||||
editorInert() {
|
editorInert() {
|
||||||
return this.showCreateNote || this.showSettings
|
return this.showCreateNote || this.showSettings || this.showEditNote
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -92,6 +96,7 @@
|
|||||||
"openNoteSelector",
|
"openNoteSelector",
|
||||||
"openCreateNote",
|
"openCreateNote",
|
||||||
"closeDialog",
|
"closeDialog",
|
||||||
|
"closeNoteSelector",
|
||||||
"openNote",
|
"openNote",
|
||||||
]),
|
]),
|
||||||
|
|
||||||
@ -186,7 +191,7 @@
|
|||||||
<NoteSelector
|
<NoteSelector
|
||||||
v-if="showNoteSelector"
|
v-if="showNoteSelector"
|
||||||
@openNote="openNote"
|
@openNote="openNote"
|
||||||
@close="closeDialog"
|
@close="closeNoteSelector"
|
||||||
/>
|
/>
|
||||||
<Settings
|
<Settings
|
||||||
v-if="showSettings"
|
v-if="showSettings"
|
||||||
@ -197,6 +202,10 @@
|
|||||||
v-if="showCreateNote"
|
v-if="showCreateNote"
|
||||||
@close="closeDialog"
|
@close="closeDialog"
|
||||||
/>
|
/>
|
||||||
|
<EditNote
|
||||||
|
v-if="showEditNote"
|
||||||
|
@close="closeDialog"
|
||||||
|
/>
|
||||||
<ErrorMessages />
|
<ErrorMessages />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
274
src/components/EditNote.vue
Normal file
274
src/components/EditNote.vue
Normal file
@ -0,0 +1,274 @@
|
|||||||
|
<script>
|
||||||
|
import slugify from '@sindresorhus/slugify';
|
||||||
|
|
||||||
|
import { toRaw } from 'vue';
|
||||||
|
import { mapState, mapActions } from 'pinia'
|
||||||
|
import { useNotesStore } from "../stores/notes-store"
|
||||||
|
|
||||||
|
import FolderSelector from './folder-selector/FolderSelector.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
name: "",
|
||||||
|
filename: "",
|
||||||
|
tags: [],
|
||||||
|
directoryTree: null,
|
||||||
|
parentPath: "",
|
||||||
|
errors: {
|
||||||
|
name: null,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
FolderSelector
|
||||||
|
},
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
this.$refs.nameInput.focus()
|
||||||
|
this.updateNotes()
|
||||||
|
|
||||||
|
console.log("EditNote mounted", this.currentNote)
|
||||||
|
this.name = this.currentNote.name
|
||||||
|
|
||||||
|
// build directory tree
|
||||||
|
const directories = await window.heynote.buffer.getDirectoryList()
|
||||||
|
const rootNode = {
|
||||||
|
name: "Heynote Root",
|
||||||
|
path: "",
|
||||||
|
children: [],
|
||||||
|
open: true,
|
||||||
|
}
|
||||||
|
const getNodeFromList = (list, part) => list.find(node => node.name === part)
|
||||||
|
|
||||||
|
directories.forEach((path) => {
|
||||||
|
const parts = path.split("/")
|
||||||
|
let currentLevel = rootNode
|
||||||
|
let currentParts = []
|
||||||
|
parts.forEach(part => {
|
||||||
|
currentParts.push(part)
|
||||||
|
let node = getNodeFromList(currentLevel.children, part)
|
||||||
|
if (node) {
|
||||||
|
currentLevel = node
|
||||||
|
} else {
|
||||||
|
const currentPath = currentParts.join("/")
|
||||||
|
node = {
|
||||||
|
name: part,
|
||||||
|
children: [],
|
||||||
|
path: currentPath,
|
||||||
|
open: this.currentNotePath.startsWith(currentPath),
|
||||||
|
}
|
||||||
|
currentLevel.children.push(node)
|
||||||
|
currentLevel = node
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
//console.log("tree:", rootNode)
|
||||||
|
this.directoryTree = rootNode
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapState(useNotesStore, [
|
||||||
|
"notes",
|
||||||
|
"currentNotePath",
|
||||||
|
]),
|
||||||
|
|
||||||
|
currentNote() {
|
||||||
|
return this.notes[this.currentNotePath]
|
||||||
|
},
|
||||||
|
|
||||||
|
currentNoteDirectory() {
|
||||||
|
return this.currentNotePath.split("/").slice(0, -1).join("/")
|
||||||
|
},
|
||||||
|
|
||||||
|
nameInputClass() {
|
||||||
|
return {
|
||||||
|
"name-input": true,
|
||||||
|
"error": this.errors.name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
...mapActions(useNotesStore, [
|
||||||
|
"updateNotes",
|
||||||
|
"updateNoteMetadata",
|
||||||
|
]),
|
||||||
|
|
||||||
|
onKeydown(event) {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
this.$emit("close")
|
||||||
|
event.preventDefault()
|
||||||
|
} if (event.key === "Enter") {
|
||||||
|
this.submit()
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onInputKeydown(event) {
|
||||||
|
// redirect arrow keys and page up/down to folder selector
|
||||||
|
const redirectKeys = ["ArrowDown", "ArrowUp", "PageDown", "PageUp"]
|
||||||
|
if (redirectKeys.includes(event.key)) {
|
||||||
|
this.$refs.folderSelect.$el.dispatchEvent(new KeyboardEvent("keydown", {key: event.key}))
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
submit() {
|
||||||
|
let slug = slugify(this.name)
|
||||||
|
if (slug === "") {
|
||||||
|
this.errors.name = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const parentPathPrefix = this.parentPath === "" ? "" : this.parentPath + "/"
|
||||||
|
let path;
|
||||||
|
for (let i=0; i<1000; i++) {
|
||||||
|
let filename = slug + ".txt"
|
||||||
|
path = parentPathPrefix + filename
|
||||||
|
if (path === this.currentNotePath || !this.notes[path]) {
|
||||||
|
// file name is ok if it's the current note, or if it doesn't exist
|
||||||
|
break
|
||||||
|
}
|
||||||
|
slug = slugify(this.name + "-" + i)
|
||||||
|
}
|
||||||
|
if (path !== this.currentNotePath && this.notes[path]) {
|
||||||
|
console.error("Failed to edit note, path already exists", path)
|
||||||
|
this.errors.name = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.log("Update note", path)
|
||||||
|
this.updateNoteMetadata(this.currentNotePath, this.name, path)
|
||||||
|
this.$emit("close")
|
||||||
|
//this.$emit("create", this.$refs.input.value)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="fader" @keydown.stop="onKeydown" tabindex="-1">
|
||||||
|
<form class="new-note" tabindex="-1" @focusout="onFocusOut" ref="container" @submit.prevent="submit">
|
||||||
|
<div class="container">
|
||||||
|
<h1>Edit Note</h1>
|
||||||
|
<input
|
||||||
|
placeholder="Name"
|
||||||
|
type="text"
|
||||||
|
v-model="name"
|
||||||
|
:class="nameInputClass"
|
||||||
|
ref="nameInput"
|
||||||
|
@keydown="onInputKeydown"
|
||||||
|
@input="errors.name = false"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label for="folder-select">Move to</label>
|
||||||
|
<FolderSelector
|
||||||
|
v-if="directoryTree"
|
||||||
|
:directoryTree="directoryTree"
|
||||||
|
:selectedPath="currentNoteDirectory"
|
||||||
|
id="folder-select"
|
||||||
|
v-model="parentPath"
|
||||||
|
ref="folderSelect"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="bottom-bar">
|
||||||
|
<button type="submit">Create Note</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="sass">
|
||||||
|
.fader
|
||||||
|
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
|
||||||
|
width: 420px
|
||||||
|
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)
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
max-height: 100%
|
||||||
|
&:focus
|
||||||
|
outline: none
|
||||||
|
+dark-mode
|
||||||
|
background: #151516
|
||||||
|
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
|
||||||
|
min-height: 0
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
|
||||||
|
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: 100%
|
||||||
|
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
|
||||||
|
&.error
|
||||||
|
background: #ffe9e9
|
||||||
|
+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
|
||||||
|
padding-top: 0
|
||||||
|
display: flex
|
||||||
|
justify-content: flex-end
|
||||||
|
button
|
||||||
|
font-size: 12px
|
||||||
|
height: 28px
|
||||||
|
border: 1px solid #c5c5c5
|
||||||
|
border-radius: 3px
|
||||||
|
padding-left: 10px
|
||||||
|
padding-right: 10px
|
||||||
|
&:focus
|
||||||
|
outline-color: #48b57e
|
||||||
|
+dark-mode
|
||||||
|
background: #444
|
||||||
|
border: none
|
||||||
|
color: rgba(255,255,255, 0.75)
|
||||||
|
|
||||||
|
</style>
|
@ -5,6 +5,7 @@
|
|||||||
import { mapState, mapWritableState, mapActions } from 'pinia'
|
import { mapState, mapWritableState, mapActions } from 'pinia'
|
||||||
import { useErrorStore } from "../stores/error-store"
|
import { useErrorStore } from "../stores/error-store"
|
||||||
import { useNotesStore } from "../stores/notes-store"
|
import { useNotesStore } from "../stores/notes-store"
|
||||||
|
import { useEditorCacheStore } from "../stores/editor-cache"
|
||||||
|
|
||||||
const NUM_EDITOR_INSTANCES = 5
|
const NUM_EDITOR_INSTANCES = 5
|
||||||
|
|
||||||
@ -45,10 +46,6 @@
|
|||||||
return {
|
return {
|
||||||
syntaxTreeDebugContent: null,
|
syntaxTreeDebugContent: null,
|
||||||
editor: null,
|
editor: null,
|
||||||
editorCache: {
|
|
||||||
lru: [],
|
|
||||||
cache: {}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -164,36 +161,21 @@
|
|||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(useErrorStore, ["addError"]),
|
...mapActions(useErrorStore, ["addError"]),
|
||||||
|
...mapActions(useEditorCacheStore, ["getEditor", "addEditor", "eachEditor"]),
|
||||||
|
|
||||||
loadBuffer(path) {
|
loadBuffer(path) {
|
||||||
|
console.log("loadBuffer", path)
|
||||||
if (this.editor) {
|
if (this.editor) {
|
||||||
this.editor.hide()
|
this.editor.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.editorCache.cache[path]) {
|
let cachedEditor = this.getEditor(path)
|
||||||
// editor is already loaded, just switch to it
|
if (cachedEditor) {
|
||||||
console.log("Switching to cached editor", path)
|
console.log("show cached editor")
|
||||||
toRaw(this.editor).hide()
|
this.editor = cachedEditor
|
||||||
this.editor = this.editorCache.cache[path]
|
|
||||||
toRaw(this.editor).show()
|
toRaw(this.editor).show()
|
||||||
//toRaw(this.editor).currenciesLoaded()
|
|
||||||
this.currentEditor = toRaw(this.editor)
|
|
||||||
window._heynote_editor = toRaw(this.editor)
|
|
||||||
// move to end of LRU
|
|
||||||
this.editorCache.lru = this.editorCache.lru.filter(p => p !== path)
|
|
||||||
this.editorCache.lru.push(path)
|
|
||||||
} else {
|
} else {
|
||||||
// check if we need to free up a slot
|
console.log("create new editor")
|
||||||
if (this.editorCache.lru.length >= NUM_EDITOR_INSTANCES) {
|
|
||||||
const pathToFree = this.editorCache.lru.shift()
|
|
||||||
console.log("Freeing up editor slot", pathToFree)
|
|
||||||
this.editorCache.cache[pathToFree].destroy()
|
|
||||||
delete this.editorCache.cache[pathToFree]
|
|
||||||
this.editorCache.lru = this.editorCache.lru.filter(p => p !== pathToFree)
|
|
||||||
}
|
|
||||||
|
|
||||||
// create new Editor instance
|
|
||||||
console.log("Loading new editor", path)
|
|
||||||
try {
|
try {
|
||||||
this.editor = new HeynoteEditor({
|
this.editor = new HeynoteEditor({
|
||||||
element: this.$refs.editor,
|
element: this.$refs.editor,
|
||||||
@ -209,15 +191,15 @@
|
|||||||
defaultBlockToken: this.defaultBlockLanguage,
|
defaultBlockToken: this.defaultBlockLanguage,
|
||||||
defaultBlockAutoDetect: this.defaultBlockLanguageAutoDetect,
|
defaultBlockAutoDetect: this.defaultBlockLanguageAutoDetect,
|
||||||
})
|
})
|
||||||
this.currentEditor = toRaw(this.editor)
|
|
||||||
window._heynote_editor = toRaw(this.editor)
|
|
||||||
this.editorCache.cache[path] = this.editor
|
|
||||||
this.editorCache.lru.push(path)
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.addError("Error! " + e.message)
|
this.addError("Error! " + e.message)
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
|
this.addEditor(path, toRaw(this.editor))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.currentEditor = toRaw(this.editor)
|
||||||
|
window._heynote_editor = toRaw(this.editor)
|
||||||
},
|
},
|
||||||
|
|
||||||
setLanguage(language) {
|
setLanguage(language) {
|
||||||
@ -245,10 +227,6 @@
|
|||||||
focus() {
|
focus() {
|
||||||
toRaw(this.editor).focus()
|
toRaw(this.editor).focus()
|
||||||
},
|
},
|
||||||
|
|
||||||
eachEditor(fn) {
|
|
||||||
Object.values(toRaw(this.editorCache).cache).forEach(fn)
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -3,14 +3,16 @@
|
|||||||
|
|
||||||
import { mapState, mapActions } from 'pinia'
|
import { mapState, mapActions } from 'pinia'
|
||||||
import { toRaw } from 'vue';
|
import { toRaw } from 'vue';
|
||||||
import { useNotesStore } from "../stores/notes-store"
|
import { useNotesStore, SCRATCH_FILE } from "../stores/notes-store"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
selected: 0,
|
selected: 0,
|
||||||
|
actionButton: 0,
|
||||||
filter: "",
|
filter: "",
|
||||||
items: [],
|
items: [],
|
||||||
|
SCRATCH_FILE: SCRATCH_FILE,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -23,7 +25,7 @@
|
|||||||
"path": path,
|
"path": path,
|
||||||
"name": metadata?.name || path,
|
"name": metadata?.name || path,
|
||||||
"folder": path.split("/").slice(0, -1).join("/"),
|
"folder": path.split("/").slice(0, -1).join("/"),
|
||||||
"scratch": path === "buffer-dev.txt",
|
"scratch": path === SCRATCH_FILE,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (this.items.length > 1) {
|
if (this.items.length > 1) {
|
||||||
@ -84,9 +86,11 @@
|
|||||||
methods: {
|
methods: {
|
||||||
...mapActions(useNotesStore, [
|
...mapActions(useNotesStore, [
|
||||||
"updateNotes",
|
"updateNotes",
|
||||||
|
"editNote",
|
||||||
]),
|
]),
|
||||||
|
|
||||||
onKeydown(event) {
|
onKeydown(event) {
|
||||||
|
const path = this.filteredItems[this.selected].path
|
||||||
if (event.key === "ArrowDown") {
|
if (event.key === "ArrowDown") {
|
||||||
if (this.selected === this.filteredItems.length - 1) {
|
if (this.selected === this.filteredItems.length - 1) {
|
||||||
this.selected = 0
|
this.selected = 0
|
||||||
@ -99,7 +103,7 @@
|
|||||||
} else {
|
} else {
|
||||||
this.$refs.item[this.selected].scrollIntoView({block: "nearest"})
|
this.$refs.item[this.selected].scrollIntoView({block: "nearest"})
|
||||||
}
|
}
|
||||||
|
this.actionButton = 0
|
||||||
} else if (event.key === "ArrowUp") {
|
} else if (event.key === "ArrowUp") {
|
||||||
if (this.selected === 0) {
|
if (this.selected === 0) {
|
||||||
this.selected = this.filteredItems.length - 1
|
this.selected = this.filteredItems.length - 1
|
||||||
@ -112,9 +116,23 @@
|
|||||||
} else {
|
} else {
|
||||||
this.$refs.item[this.selected].scrollIntoView({block: "nearest"})
|
this.$refs.item[this.selected].scrollIntoView({block: "nearest"})
|
||||||
}
|
}
|
||||||
} else if (event.key === "Enter") {
|
this.actionButton = 0
|
||||||
this.selectItem(this.filteredItems[this.selected].path)
|
} else if (event.key === "ArrowRight" && path !== SCRATCH_FILE) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
this.actionButton = Math.min(2, this.actionButton + 1)
|
||||||
|
} else if (event.key === "ArrowLeft" && path !== SCRATCH_FILE) {
|
||||||
|
event.preventDefault()
|
||||||
|
this.actionButton = Math.max(0, this.actionButton - 1)
|
||||||
|
} else if (event.key === "Enter") {
|
||||||
|
event.preventDefault()
|
||||||
|
if (this.actionButton === 1) {
|
||||||
|
console.log("edit file:", path)
|
||||||
|
this.editNote(path)
|
||||||
|
} else if (this.actionButton === 2) {
|
||||||
|
console.log("delete file:", path)
|
||||||
|
} else {
|
||||||
|
this.selectItem(path)
|
||||||
|
}
|
||||||
} else if (event.key === "Escape") {
|
} else if (event.key === "Escape") {
|
||||||
this.$emit("close")
|
this.$emit("close")
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
@ -140,9 +158,16 @@
|
|||||||
getItemClass(item, idx) {
|
getItemClass(item, idx) {
|
||||||
return {
|
return {
|
||||||
"selected": idx === this.selected,
|
"selected": idx === this.selected,
|
||||||
|
"action-buttons-visible": this.actionButton > 0,
|
||||||
"scratch": item.scratch,
|
"scratch": item.scratch,
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
|
showActionButtons(idx) {
|
||||||
|
this.selected = idx
|
||||||
|
this.actionButton = 1
|
||||||
|
this.$refs.input.focus()
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@ -167,6 +192,21 @@
|
|||||||
>
|
>
|
||||||
<span class="name" v-html="item.name" />
|
<span class="name" v-html="item.name" />
|
||||||
<span class="path" v-html="item.folder" />
|
<span class="path" v-html="item.folder" />
|
||||||
|
<span class="action-buttons">
|
||||||
|
<button
|
||||||
|
v-if="actionButton > 0 && idx === selected"
|
||||||
|
:class="{'selected':actionButton === 1}"
|
||||||
|
>Edit</button>
|
||||||
|
<button
|
||||||
|
v-if="actionButton > 0 && idx === selected"
|
||||||
|
:class="{'delete':true, 'selected':actionButton === 2}"
|
||||||
|
>Delete</button>
|
||||||
|
<button
|
||||||
|
class="show-actions"
|
||||||
|
v-if="item.path !== SCRATCH_FILE && (actionButton === 0 || idx !== selected)"
|
||||||
|
@click.stop.prevent="showActionButtons(idx)"
|
||||||
|
></button>
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</form>
|
</form>
|
||||||
@ -228,16 +268,20 @@
|
|||||||
.items
|
.items
|
||||||
overflow-y: auto
|
overflow-y: auto
|
||||||
> li
|
> li
|
||||||
|
position: relative
|
||||||
border-radius: 3px
|
border-radius: 3px
|
||||||
padding: 5px 12px
|
padding: 5px 12px
|
||||||
cursor: pointer
|
|
||||||
display: flex
|
display: flex
|
||||||
align-items: center
|
align-items: center
|
||||||
&:hover
|
&:hover
|
||||||
background: #e2e2e2
|
background: #e2e2e2
|
||||||
|
.action-buttons .show-actions
|
||||||
|
display: inline-block
|
||||||
&.selected
|
&.selected
|
||||||
background: #48b57e
|
background: #48b57e
|
||||||
color: #fff
|
color: #fff
|
||||||
|
.action-buttons .show-actions
|
||||||
|
display: inline-block
|
||||||
&.scratch
|
&.scratch
|
||||||
font-weight: 600
|
font-weight: 600
|
||||||
+dark-mode
|
+dark-mode
|
||||||
@ -247,6 +291,10 @@
|
|||||||
&.selected
|
&.selected
|
||||||
background: #1b6540
|
background: #1b6540
|
||||||
color: rgba(255,255,255, 0.87)
|
color: rgba(255,255,255, 0.87)
|
||||||
|
&.action-buttons-visible
|
||||||
|
background: none
|
||||||
|
border: 1px solid #1b6540
|
||||||
|
padding: 4px 11px
|
||||||
.name
|
.name
|
||||||
margin-right: 12px
|
margin-right: 12px
|
||||||
flex-shrink: 0
|
flex-shrink: 0
|
||||||
@ -264,4 +312,44 @@
|
|||||||
text-wrap: nowrap
|
text-wrap: nowrap
|
||||||
::v-deep(b)
|
::v-deep(b)
|
||||||
font-weight: 700
|
font-weight: 700
|
||||||
|
.action-buttons
|
||||||
|
position: absolute
|
||||||
|
top: 1px
|
||||||
|
right: 1px
|
||||||
|
button
|
||||||
|
padding: 1px 10px
|
||||||
|
font-size: 12px
|
||||||
|
background: none
|
||||||
|
border: none
|
||||||
|
border-radius: 2px
|
||||||
|
margin-right: 2px
|
||||||
|
cursor: pointer
|
||||||
|
&:last-child
|
||||||
|
margin-right: 0
|
||||||
|
&:hover
|
||||||
|
background: rgba(255,255,255, 0.1)
|
||||||
|
+dark-mode
|
||||||
|
//background: #1b6540
|
||||||
|
//&:hover
|
||||||
|
// background:
|
||||||
|
&.selected
|
||||||
|
background: #1b6540
|
||||||
|
&:hover
|
||||||
|
background: #1f7449
|
||||||
|
&.delete
|
||||||
|
background: #ae1e1e
|
||||||
|
&:hover
|
||||||
|
background: #bf2222
|
||||||
|
&.show-actions
|
||||||
|
display: none
|
||||||
|
position: relative
|
||||||
|
top: 1px
|
||||||
|
padding: 1px 8px
|
||||||
|
//cursor: default
|
||||||
|
background-image: url(@/assets/icons/arrow-right.svg)
|
||||||
|
width: 22px
|
||||||
|
height: 19px
|
||||||
|
background-size: 19px
|
||||||
|
background-position: center center
|
||||||
|
background-repeat: no-repeat
|
||||||
</style>
|
</style>
|
||||||
|
@ -217,6 +217,12 @@ export class HeynoteEditor {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setName(name) {
|
||||||
|
this.note.metadata.name = name
|
||||||
|
this.name = name
|
||||||
|
triggerCursorChange(this.view)
|
||||||
|
}
|
||||||
|
|
||||||
getBlocks() {
|
getBlocks() {
|
||||||
return this.view.state.facet(blockState)
|
return this.view.state.facet(blockState)
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ 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'
|
import { useNotesStore, initNotesStore } from './stores/notes-store'
|
||||||
|
import { useEditorCacheStore } from './stores/editor-cache'
|
||||||
|
|
||||||
|
|
||||||
const pinia = createPinia()
|
const pinia = createPinia()
|
||||||
@ -19,6 +20,7 @@ app.mount('#app').$nextTick(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const errorStore = useErrorStore()
|
const errorStore = useErrorStore()
|
||||||
|
const editorCacheStore = useEditorCacheStore()
|
||||||
//errorStore.addError("test error")
|
//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))
|
||||||
|
48
src/stores/editor-cache.js
Normal file
48
src/stores/editor-cache.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { toRaw } from 'vue';
|
||||||
|
import { defineStore } from "pinia"
|
||||||
|
import { NoteFormat } from "../editor/note-format"
|
||||||
|
|
||||||
|
const NUM_EDITOR_INSTANCES = 5
|
||||||
|
|
||||||
|
export const useEditorCacheStore = defineStore("editorCache", {
|
||||||
|
state: () => ({
|
||||||
|
editorCache: {
|
||||||
|
lru: [],
|
||||||
|
cache: {},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
getEditor(path) {
|
||||||
|
// move to end of LRU
|
||||||
|
this.editorCache.lru = this.editorCache.lru.filter(p => p !== path)
|
||||||
|
this.editorCache.lru.push(path)
|
||||||
|
|
||||||
|
if (this.editorCache.cache[path]) {
|
||||||
|
return this.editorCache.cache[path]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
addEditor(path, editor) {
|
||||||
|
if (this.editorCache.lru.length >= NUM_EDITOR_INSTANCES) {
|
||||||
|
const pathToFree = this.editorCache.lru.shift()
|
||||||
|
this.freeEditor(pathToFree)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.editorCache.cache[path] = editor
|
||||||
|
},
|
||||||
|
|
||||||
|
freeEditor(pathToFree) {
|
||||||
|
if (!this.editorCache.cache[pathToFree]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.editorCache.cache[pathToFree].destroy()
|
||||||
|
delete this.editorCache.cache[pathToFree]
|
||||||
|
this.editorCache.lru = this.editorCache.lru.filter(p => p !== pathToFree)
|
||||||
|
},
|
||||||
|
|
||||||
|
eachEditor(fn) {
|
||||||
|
Object.values(this.editorCache.cache).forEach(fn)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
@ -1,8 +1,9 @@
|
|||||||
import { toRaw } from 'vue';
|
import { toRaw } from 'vue';
|
||||||
import { defineStore } from "pinia"
|
import { defineStore } from "pinia"
|
||||||
import { NoteFormat } from "../editor/note-format"
|
import { NoteFormat } from "../editor/note-format"
|
||||||
|
import { useEditorCacheStore } from "./editor-cache"
|
||||||
|
|
||||||
const SCRATCH_FILE = window.heynote.isDev ? "buffer-dev.txt" : "buffer.txt"
|
export const SCRATCH_FILE = window.heynote.isDev ? "buffer-dev.txt" : "buffer.txt"
|
||||||
|
|
||||||
export const useNotesStore = defineStore("notes", {
|
export const useNotesStore = defineStore("notes", {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
@ -20,6 +21,7 @@ export const useNotesStore = defineStore("notes", {
|
|||||||
showNoteSelector: false,
|
showNoteSelector: false,
|
||||||
showLanguageSelector: false,
|
showLanguageSelector: false,
|
||||||
showCreateNote: false,
|
showCreateNote: false,
|
||||||
|
showEditNote: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
@ -32,9 +34,7 @@ export const useNotesStore = defineStore("notes", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
openNote(path) {
|
openNote(path) {
|
||||||
this.showNoteSelector = false
|
this.closeDialog()
|
||||||
this.showLanguageSelector = false
|
|
||||||
this.showCreateNote = false
|
|
||||||
this.currentNotePath = path
|
this.currentNotePath = path
|
||||||
|
|
||||||
const recent = this.recentNotePaths.filter((p) => p !== path)
|
const recent = this.recentNotePaths.filter((p) => p !== path)
|
||||||
@ -43,30 +43,49 @@ export const useNotesStore = defineStore("notes", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
openLanguageSelector() {
|
openLanguageSelector() {
|
||||||
|
this.closeDialog()
|
||||||
this.showLanguageSelector = true
|
this.showLanguageSelector = true
|
||||||
this.showNoteSelector = false
|
|
||||||
this.showCreateNote = false
|
|
||||||
},
|
},
|
||||||
openNoteSelector() {
|
openNoteSelector() {
|
||||||
|
this.closeDialog()
|
||||||
this.showNoteSelector = true
|
this.showNoteSelector = true
|
||||||
this.showLanguageSelector = false
|
|
||||||
this.showCreateNote = false
|
|
||||||
},
|
},
|
||||||
openCreateNote() {
|
openCreateNote() {
|
||||||
|
this.closeDialog()
|
||||||
this.showCreateNote = true
|
this.showCreateNote = true
|
||||||
this.showNoteSelector = false
|
|
||||||
this.showLanguageSelector = false
|
|
||||||
},
|
},
|
||||||
closeDialog() {
|
closeDialog() {
|
||||||
this.showCreateNote = false
|
this.showCreateNote = false
|
||||||
this.showNoteSelector = false
|
this.showNoteSelector = false
|
||||||
this.showLanguageSelector = false
|
this.showLanguageSelector = false
|
||||||
|
this.showEditNote = false
|
||||||
},
|
},
|
||||||
|
|
||||||
|
closeNoteSelector() {
|
||||||
|
this.showNoteSelector = false
|
||||||
|
},
|
||||||
|
|
||||||
|
editNote(path) {
|
||||||
|
if (this.currentNotePath !== path) {
|
||||||
|
this.openNote(path)
|
||||||
|
}
|
||||||
|
this.closeDialog()
|
||||||
|
this.showEditNote = true
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new note file at `path` with name `name` from the current block of the current open editor
|
||||||
|
*/
|
||||||
async createNewNoteFromActiveBlock(path, name) {
|
async createNewNoteFromActiveBlock(path, name) {
|
||||||
await toRaw(this.currentEditor).createNewNoteFromActiveBlock(path, name)
|
await toRaw(this.currentEditor).createNewNoteFromActiveBlock(path, name)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new note file at path, with name `name`, and content content
|
||||||
|
* @param {*} path: File path relative to Heynote root
|
||||||
|
* @param {*} name Name of the note
|
||||||
|
* @param {*} content Contents (without metadata)
|
||||||
|
*/
|
||||||
async saveNewNote(path, name, content) {
|
async saveNewNote(path, name, content) {
|
||||||
//window.heynote.buffer.save(path, content)
|
//window.heynote.buffer.save(path, content)
|
||||||
//this.updateNotes()
|
//this.updateNotes()
|
||||||
@ -82,6 +101,24 @@ export const useNotesStore = defineStore("notes", {
|
|||||||
await window.heynote.buffer.create(path, note.serialize())
|
await window.heynote.buffer.create(path, note.serialize())
|
||||||
this.updateNotes()
|
this.updateNotes()
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async updateNoteMetadata(path, name, newPath) {
|
||||||
|
const editorCacheStore = useEditorCacheStore()
|
||||||
|
|
||||||
|
if (this.currentEditor.path !== path) {
|
||||||
|
throw new Error(`Can't update note (${path}) since it's not the active one (${this.currentEditor.path})`)
|
||||||
|
}
|
||||||
|
console.log("currentEditor", this.currentEditor)
|
||||||
|
toRaw(this.currentEditor).setName(name)
|
||||||
|
await (toRaw(this.currentEditor)).save()
|
||||||
|
if (newPath && path !== newPath) {
|
||||||
|
console.log("moving note", path, newPath)
|
||||||
|
editorCacheStore.freeEditor(path)
|
||||||
|
await window.heynote.buffer.move(path, newPath)
|
||||||
|
this.openNote(newPath)
|
||||||
|
this.updateNotes()
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user