WIP: Multiple notes support

Add support for migrating old buffer file to new library.

Add support for changing location for the notes library.

Replace theme toggle in status bar with a dropdown in Appearance settings.

Improve New Note and Update Note dialogs.

Implement UI for confirming note delete (the actualal deltion is still to be implemented).
This commit is contained in:
Jonatan Heyman 2024-09-10 13:34:23 +02:00
parent 29facb4787
commit 7be0a304d0
24 changed files with 341 additions and 269 deletions

View 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="#222" 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

View File

Before

Width:  |  Height:  |  Size: 522 B

After

Width:  |  Height:  |  Size: 522 B

View 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="#fff" 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

View File

@ -36,6 +36,8 @@ Available for Mac, Windows, and Linux.
⌥ + Shift + Enter Add new block at the start of the buffer
⌘ + ⌥ + Enter Split the current block at cursor position
⌘ + L Change block language
⌘ + S Create a new note from the current block
⌘ + P Open note selector
⌘ + Down Goto next block
⌘ + Up Goto previous block
⌘ + A Select all text in a note block. Press again to select the whole buffer
@ -52,6 +54,8 @@ Ctrl + Shift + Enter Add new block at the end of the buffer
Alt + Shift + Enter Add new block at the start of the buffer
Ctrl + Alt + Enter Split the current block at cursor position
Ctrl + L Change block language
Ctrl + S Create a new note from the current block
Ctrl + P Open note selector
Ctrl + Down Goto next block
Ctrl + Up Goto previous block
Ctrl + A Select all text in a note block. Press again to select the whole buffer

View File

@ -1,9 +1,8 @@
import os from "os";
import { keyHelpStr } from "../shared-utils/key-helper";
export const eraseInitialContent = !!process.env.ERASE_INITIAL_CONTENT
export const initialContent = `
{"formatVersion":"1.0.0","name":"Scratch"}
markdown
Welcome to Heynote! 👋

View File

@ -1,172 +0,0 @@
import fs from "fs"
import os from "node:os"
import { join, dirname, basename } from "path"
import { app, ipcMain, dialog } from "electron"
import * as jetpack from "fs-jetpack";
import CONFIG from "../config"
import { isDev } from "../detect-platform"
import { win } from "./index"
import { eraseInitialContent, initialContent, initialDevContent } from '../initial-content'
const untildify = (pathWithTilde) => {
const homeDirectory = os.homedir();
return homeDirectory
? pathWithTilde.replace(/^~(?=$|\/|\\)/, homeDirectory)
: pathWithTilde;
}
export function constructBufferFilePath(directoryPath, path) {
return join(untildify(directoryPath), path)
}
export function getFullBufferFilePath(path) {
let defaultPath = app.getPath("userData")
let configPath = CONFIG.get("settings.bufferPath")
let bufferPath = configPath.length ? configPath : defaultPath
let bufferFilePath = constructBufferFilePath(bufferPath, path)
try {
// use realpathSync to resolve a potential symlink
return fs.realpathSync(bufferFilePath)
} catch (err) {
// realpathSync will fail if the file does not exist, but that doesn't matter since the file will be created
if (err.code !== "ENOENT") {
throw err
}
return bufferFilePath
}
}
export class Buffer {
constructor({filePath, onChange}) {
this.filePath = filePath
this.onChange = onChange
this.watcher = null
this.setupWatcher()
this._lastSavedContent = null
}
async load() {
const content = await jetpack.read(this.filePath, 'utf8')
this.setupWatcher()
return content
}
async save(content) {
this._lastSavedContent = content
const saveResult = await jetpack.write(this.filePath, content, {
atomic: true,
mode: '600',
})
return saveResult
}
exists() {
return jetpack.exists(this.filePath) === "file"
}
setupWatcher() {
if (!this.watcher && this.exists()) {
this.watcher = fs.watch(
dirname(this.filePath),
{
persistent: true,
recursive: false,
encoding: "utf8",
},
async (eventType, filename) => {
if (filename !== basename(this.filePath)) {
return
}
// read the file content and compare it to the last saved content
// (if the content is the same, then we can ignore the event)
const content = await jetpack.read(this.filePath, 'utf8')
if (this._lastSavedContent !== content) {
// file has changed on disk, trigger onChange
this.onChange(content)
}
}
)
}
}
close() {
if (this.watcher) {
this.watcher.close()
this.watcher = null
}
}
}
// Buffer
let buffers = {}
export function loadBuffer(path) {
if (buffers[path]) {
buffers[path].close()
}
buffers[path] = new Buffer({
filePath: getFullBufferFilePath(path),
onChange: (content) => {
console.log("Old buffer.js onChange")
win?.webContents.send("buffer-content:change", path, content)
},
})
return buffers[path]
}
ipcMain.handle('buffer-content:load', async (event, path) => {
if (!buffers[path]) {
loadBuffer(path)
}
if (buffers[path].exists() && !(eraseInitialContent && isDev)) {
return await buffers[path].load()
} else {
return isDev ? initialDevContent : initialContent
}
});
async function save(path, content) {
return await buffers[path].save(content)
}
ipcMain.handle('buffer-content:save', async (event, path, content) => {
return await save(path, content)
});
export let contentSaved = false
ipcMain.handle('buffer-content:saveAndQuit', async (event, contents) => {
for (const [path, content] of contents) {
await save(path, content)
}
contentSaved = true
app.quit()
})
ipcMain.handle("buffer-content:selectLocation", async () => {
let result = await dialog.showOpenDialog({
title: "Select directory to store buffer",
properties: [
"openDirectory",
"createDirectory",
"noResolveAliases",
],
})
if (result.canceled) {
return
}
const filePath = result.filePaths[0]
if (fs.existsSync(constructBufferFilePath(filePath))) {
if (dialog.showMessageBoxSync({
type: "question",
message: "The selected directory already contains a buffer file. It will be loaded. Do you want to continue?",
buttons: ["Cancel", "Continue"],
}) === 0) {
return
}
}
return filePath
})

View File

@ -5,6 +5,16 @@ import { join, dirname, basename } from "path"
import * as jetpack from "fs-jetpack";
import { app, ipcMain, dialog } from "electron"
import CONFIG from "../config"
import { SCRATCH_FILE_NAME } from "../../src/common/constants"
import { NoteFormat } from "../../src/common/note-format"
import { isDev } from '../detect-platform';
import { initialContent, initialDevContent } from '../initial-content'
export const NOTES_DIR_NAME = isDev ? "notes-dev" : "notes"
let library
const untildify = (pathWithTilde) => {
const homeDir = os.homedir()
@ -42,6 +52,11 @@ export class FileLibrary {
this.watcher = null;
this.contentSaved = false
this.onChangeCallback = null
// create scratch.txt if it doesn't exist
if (!this.jetpack.exists(SCRATCH_FILE_NAME)) {
this.jetpack.write(SCRATCH_FILE_NAME, isDev ? initialDevContent : initialContent)
}
}
async exists(path) {
@ -82,7 +97,7 @@ export class FileLibrary {
}
async getList() {
console.log("Loading notes")
//console.log("Listing notes")
const notes = {}
const files = await this.jetpack.findAsync(".", {
matching: "*.txt",
@ -199,10 +214,13 @@ export class NoteBuffer {
}
}
export function setCurrentFileLibrary(lib) {
library = lib
}
export function setupFileLibraryEventHandlers(library, win) {
export function setupFileLibraryEventHandlers(win) {
ipcMain.handle('buffer:load', async (event, path) => {
console.log("buffer:load", path)
//console.log("buffer:load", path)
return await library.load(path)
});
@ -244,5 +262,71 @@ export function setupFileLibraryEventHandlers(library, win) {
return await library.move(path, newPath)
});
library.setupWatcher(win)
ipcMain.handle("library:selectLocation", async () => {
let result = await dialog.showOpenDialog({
title: "Select directory to store buffer",
properties: [
"openDirectory",
"createDirectory",
"noResolveAliases",
],
})
if (result.canceled) {
return
}
const filePath = result.filePaths[0]
return filePath
})
}
export async function migrateBufferFileToLibrary(app) {
async function ensureBufferFileMetadata(filePath) {
const metadata = await readNoteMetadata(filePath)
//console.log("Metadata", metadata)
if (!metadata || !metadata.name) {
console.log("Adding metadata to", filePath)
const note = NoteFormat.load(jetpack.read(filePath))
note.metadata.name = "Scratch"
jetpack.write(filePath, note.serialize())
} else {
console.log("Metadata already exists for", filePath)
}
}
const defaultLibraryPath = join(app.getPath("userData"), NOTES_DIR_NAME)
const customBufferPath = CONFIG.get("settings.bufferPath")
const oldBufferFile = isDev ? "buffer-dev.txt" : "buffer.txt"
if (customBufferPath) {
// if the new buffer file exists, no need to migrate
if (jetpack.exists(join(customBufferPath, SCRATCH_FILE_NAME)) === "file") {
return
}
const oldBufferFileFullPath = join(customBufferPath, oldBufferFile)
if (jetpack.exists(oldBufferFileFullPath) === "file") {
const newFileFullPath = join(customBufferPath, SCRATCH_FILE_NAME);
console.log(`Migrating file ${oldBufferFileFullPath} to ${newFileFullPath}`)
// rename buffer file to scratch.txt
jetpack.move(oldBufferFileFullPath, newFileFullPath)
// add metadata to scratch.txt (just to be sure, we'll double check that it's needed first)
await ensureBufferFileMetadata(newFileFullPath)
}
} else {
// if the new buffer file exists, no need to migrate
if (jetpack.exists(join(defaultLibraryPath, SCRATCH_FILE_NAME)) === "file") {
return
}
// check if the old buffer file exists, while the default *library* path doesn't exist
const oldBufferFileFullPath = join(app.getPath("userData"), oldBufferFile)
if (jetpack.exists(oldBufferFileFullPath) === "file" && jetpack.exists(defaultLibraryPath) !== "dir") {
const newFileFullPath = join(defaultLibraryPath, SCRATCH_FILE_NAME);
console.log(`Migrating buffer file ${oldBufferFileFullPath} to ${newFileFullPath}`)
// create the default library path
jetpack.dir(defaultLibraryPath)
// move the buffer file to the library path
jetpack.move(oldBufferFileFullPath, newFileFullPath)
// add metadata to scratch.txt
await ensureBufferFileMetadata(newFileFullPath)
}
}
}

View File

@ -9,8 +9,13 @@ import CONFIG from "../config"
import { isDev, isLinux, isMac, isWindows } from '../detect-platform';
import { initializeAutoUpdate, checkForUpdates } from './auto-update';
import { fixElectronCors } from './cors';
import { loadBuffer, contentSaved } from './buffer';
import { FileLibrary, setupFileLibraryEventHandlers } from './file-library';
import {
FileLibrary,
setupFileLibraryEventHandlers,
setCurrentFileLibrary,
migrateBufferFileToLibrary,
NOTES_DIR_NAME
} from './file-library';
// The built directory structure
@ -310,7 +315,9 @@ function registerAlwaysOnTop() {
}
app.whenReady().then(createWindow).then(async () => {
setupFileLibraryEventHandlers(fileLibrary, win)
initFileLibrary(win).then(() => {
setupFileLibraryEventHandlers(win)
})
initializeAutoUpdate(win)
registerGlobalHotkey()
registerShowInDock()
@ -352,14 +359,28 @@ ipcMain.handle('dark-mode:set', (event, mode) => {
ipcMain.handle('dark-mode:get', () => nativeTheme.themeSource)
// Initialize note/file library
const customLibraryPath = CONFIG.get("settings.bufferPath")
const libraryPath = customLibraryPath ? customLibraryPath : join(app.getPath("userData"), "notes")
console.log("libraryPath", libraryPath)
try {
async function initFileLibrary(win) {
await migrateBufferFileToLibrary(app)
const customLibraryPath = CONFIG.get("settings.bufferPath")
const defaultLibraryPath = join(app.getPath("userData"), NOTES_DIR_NAME)
const libraryPath = customLibraryPath ? customLibraryPath : defaultLibraryPath
//console.log("libraryPath", libraryPath)
// if we're using the default library path, and it doesn't exist (e.g. first time run), create it
if (!customLibraryPath && !fs.existsSync(defaultLibraryPath)) {
fs.mkdirSync(defaultLibraryPath)
}
try {
fileLibrary = new FileLibrary(libraryPath)
} catch (error) {
fileLibrary.setupWatcher(win)
} catch (error) {
initErrors.push(`Error: ${error.message}`)
}
setCurrentFileLibrary(fileLibrary)
}
ipcMain.handle("getInitErrors", () => {
@ -393,9 +414,10 @@ ipcMain.handle('settings:set', async (event, settings) => {
registerAlwaysOnTop()
}
if (bufferPathChanged) {
const buffer = loadBuffer()
if (buffer.exists()) {
win?.webContents.send("buffer-content:change", await buffer.load())
}
console.log("bufferPath changed, closing existing file library")
fileLibrary.close()
console.log("initializing new file library")
initFileLibrary(win)
await win.webContents.send("library:pathChanged")
}
})

View File

@ -108,12 +108,11 @@ contextBridge.exposeInMainWorld("heynote", {
},
async selectLocation() {
return await ipcRenderer.invoke("buffer-content:selectLocation")
return await ipcRenderer.invoke("library:selectLocation")
},
callbacks(callbacks) {
ipcRenderer.on("buffer:noteMetadataChanged", (event, path, info) => callbacks?.noteMetadataChanged(path, info))
ipcRenderer.on("buffer:noteRemoved", (event, path) => callbacks?.noteRemoved(path))
setLibraryPathChangeCallback(callback) {
ipcRenderer.on("library:pathChanged", callback)
},
},

View File

@ -9,6 +9,8 @@ export const keyHelpStr = (platform: string) => {
[`${altChar} + Shift + Enter`, "Add new block at the start of the buffer"],
[`${modChar} + ${altChar} + Enter`, "Split the current block at cursor position"],
[`${modChar} + L`, "Change block language"],
[`${modChar} + S`, "Create a new note from the current block"],
[`${modChar} + P`, "Open note selector"],
[`${modChar} + Down`, "Goto next block"],
[`${modChar} + Up`, "Goto previous block"],
[`${modChar} + A`, "Select all text in a note block. Press again to select the whole buffer"],

1
src/common/constants.js Normal file
View File

@ -0,0 +1 @@
export const SCRATCH_FILE_NAME = "scratch.txt"

View File

@ -137,6 +137,11 @@
this.$refs.editor.focus()
},
setTheme(theme) {
window.heynote.themeMode.set(theme)
this.themeSetting = theme
},
onSelectLanguage(language) {
this.closeDialog()
this.$refs.editor.setLanguage(language)
@ -170,11 +175,8 @@
ref="editor"
/>
<StatusBar
:theme="theme"
:themeSetting="themeSetting"
:autoUpdate="settings.autoUpdate"
:allowBetaVersions="settings.allowBetaVersions"
@toggleTheme="toggleTheme"
@openNoteSelector="openNoteSelector"
@openLanguageSelector="openLanguageSelector"
@formatCurrentBlock="formatCurrentBlock"
@ -196,7 +198,9 @@
<Settings
v-if="showSettings"
:initialSettings="settings"
:themeSetting="themeSetting"
@closeSettings="closeSettings"
@setTheme="setTheme"
/>
<NewNote
v-if="showCreateNote"

View File

@ -97,14 +97,26 @@
onKeydown(event) {
if (event.key === "Escape") {
this.$emit("close")
event.preventDefault()
this.cancel()
} if (event.key === "Enter") {
this.submit()
event.preventDefault()
this.submit()
}
},
onCancelKeydown(event) {
if (event.key === "Enter") {
event.preventDefault()
event.stopPropagation()
this.cancel()
}
},
cancel() {
this.$emit("close")
},
onInputKeydown(event) {
// redirect arrow keys and page up/down to folder selector
const redirectKeys = ["ArrowDown", "ArrowUp", "PageDown", "PageUp"]
@ -171,7 +183,12 @@
/>
</div>
<div class="bottom-bar">
<button type="submit">Create Note</button>
<button type="submit">Update Note</button>
<button
class="cancel"
@keydown="onCancelKeydown"
@click.stop.prevent="cancel"
>Cancel</button>
</div>
</form>
</div>
@ -256,7 +273,7 @@
padding: 10px
padding-top: 0
display: flex
justify-content: flex-end
justify-content: space-between
button
font-size: 12px
height: 28px
@ -270,5 +287,9 @@
background: #444
border: none
color: rgba(255,255,255, 0.75)
&[type="submit"]
order: 1
&.cancel
order: 0
</style>

View File

@ -86,9 +86,9 @@
},
watch: {
currentNotePath(path) {
loadNewEditor() {
//console.log("currentNotePath changed to", path)
this.loadBuffer(path)
this.loadBuffer(this.currentNotePath)
},
theme(newTheme) {
@ -152,11 +152,16 @@
computed: {
...mapState(useNotesStore, [
"currentNotePath",
"libraryId",
]),
...mapWritableState(useNotesStore, [
"currentEditor",
"currentNoteName",
]),
loadNewEditor() {
return `${this.currentNotePath}|${this.libraryId}`
},
},
methods: {

View File

@ -96,6 +96,18 @@
}
},
onCancelKeydown(event) {
if (event.key === "Enter") {
event.preventDefault()
event.stopPropagation()
this.cancel()
}
},
cancel() {
this.$emit("close")
},
onInputKeydown(event) {
// redirect arrow keys and page up/down to folder selector
const redirectKeys = ["ArrowDown", "ArrowUp", "PageDown", "PageUp"]
@ -162,6 +174,11 @@
</div>
<div class="bottom-bar">
<button type="submit">Create Note</button>
<button
class="cancel"
@keydown="onCancelKeydown"
@click.stop.prevent="cancel"
>Cancel</button>
</div>
</form>
</div>
@ -246,7 +263,7 @@
padding: 10px
padding-top: 0
display: flex
justify-content: flex-end
justify-content: space-between
button
font-size: 12px
height: 28px
@ -260,5 +277,9 @@
background: #444
border: none
color: rgba(255,255,255, 0.75)
&[type="submit"]
order: 1
&.cancel
order: 0
</style>

View File

@ -3,7 +3,8 @@
import { mapState, mapActions } from 'pinia'
import { toRaw } from 'vue';
import { useNotesStore, SCRATCH_FILE } from "../stores/notes-store"
import { SCRATCH_FILE_NAME } from "../common/constants"
import { useNotesStore } from "../stores/notes-store"
export default {
data() {
@ -12,7 +13,8 @@
actionButton: 0,
filter: "",
items: [],
SCRATCH_FILE: SCRATCH_FILE,
SCRATCH_FILE_NAME: SCRATCH_FILE_NAME,
deleteConfirm: false,
}
},
@ -25,7 +27,7 @@
"path": path,
"name": metadata?.name || path,
"folder": path.split("/").slice(0, -1).join("/"),
"scratch": path === SCRATCH_FILE,
"scratch": path === SCRATCH_FILE_NAME,
}
})
if (this.items.length > 1) {
@ -120,25 +122,30 @@
this.$refs.item[this.selected].scrollIntoView({block: "nearest"})
}
this.actionButton = 0
} else if (event.key === "ArrowRight" && path !== SCRATCH_FILE) {
} else if (event.key === "ArrowRight" && path !== SCRATCH_FILE_NAME) {
event.preventDefault()
this.actionButton = Math.min(2, this.actionButton + 1)
} else if (event.key === "ArrowLeft" && path !== SCRATCH_FILE) {
} else if (event.key === "ArrowLeft" && path !== SCRATCH_FILE_NAME) {
event.preventDefault()
this.actionButton = Math.max(0, this.actionButton - 1)
this.deleteConfirm = false
} 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)
this.deleteConfirmNote(path)
} else {
this.selectItem(path)
}
} else if (event.key === "Escape") {
this.$emit("close")
event.preventDefault()
if (this.actionButton !== 0) {
this.hideActionButtons()
} else {
this.$emit("close")
}
}
},
@ -169,8 +176,24 @@
showActionButtons(idx) {
this.selected = idx
this.actionButton = 1
this.deleteConfirm = false
this.$refs.input.focus()
},
hideActionButtons() {
this.actionButton = 0
this.deleteConfirm = false
},
deleteConfirmNote(path) {
if (this.deleteConfirm) {
console.log("delete file:", path)
} else {
this.deleteConfirm = true
this.actionButton = 2
this.$refs.input.focus()
}
},
}
}
</script>
@ -195,18 +218,27 @@
>
<span class="name" v-html="item.name" />
<span class="path" v-html="item.folder" />
<span class="action-buttons">
<span :class="{'action-buttons':true, 'visible':actionButton > 0 && idx === selected}">
<button
v-if="actionButton > 0 && idx === selected"
:class="{'selected':actionButton === 1}"
@click.stop.prevent="editNote(item.path)"
>Edit</button>
<button
v-if="actionButton > 0 && idx === selected"
:class="{'delete':true, 'selected':actionButton === 2}"
>Delete</button>
:class="{'delete':true, 'selected':actionButton === 2, 'confirm':deleteConfirm}"
@click.stop.prevent="deleteConfirmNote(item.path)"
>
<template v-if="deleteConfirm">
Really Delete?
</template>
<template v-else>
Delete
</template>
</button>
<button
class="show-actions"
v-if="item.path !== SCRATCH_FILE && (actionButton === 0 || idx !== selected)"
v-if="item.path !== SCRATCH_FILE_NAME && (actionButton === 0 || idx !== selected)"
@click.stop.prevent="showActionButtons(idx)"
></button>
</span>
@ -273,31 +305,40 @@
> li
position: relative
border-radius: 3px
padding: 5px 12px
padding: 3px 12px
line-height: 18px
display: flex
align-items: center
&:hover
background: #e2e2e2
.action-buttons .show-actions
display: inline-block
&.selected
background: #48b57e
color: #fff
.action-buttons .show-actions
display: inline-block
&.scratch
font-weight: 600
background-image: url(@/assets/icons/arrow-right-black.svg)
&.selected .action-buttons .show-actions
background-image: url(@/assets/icons/arrow-right-white.svg)
+dark-mode
color: rgba(255,255,255, 0.65)
&:hover
background: #29292a
&.selected
background: #48b57e
color: #fff
&.action-buttons-visible
background: none
border: 1px solid #48b57e
padding: 2px 11px
color: #444
.action-buttons .show-actions
display: inline-block
+dark-mode
background: #1b6540
color: rgba(255,255,255, 0.87)
&.action-buttons-visible
background: none
border: 1px solid #1b6540
padding: 4px 11px
color: rgba(255,255,255, 0.65)
&.scratch
font-weight: 600
.name
margin-right: 12px
flex-shrink: 0
@ -318,9 +359,15 @@
.action-buttons
position: absolute
top: 1px
right: 1px
right: 0px
padding: 0 1px
&.visible
background: #efefef
+dark-mode
background: #151516
button
padding: 1px 10px
padding: 0 10px
height: 20px
font-size: 12px
background: none
border: none
@ -330,12 +377,20 @@
&:last-child
margin-right: 0
&:hover
background: rgba(255,255,255, 0.1)
background: rgba(0,0,0, 0.1)
+dark-mode
//background: #1b6540
//&:hover
// background:
&:hover
background-color: rgba(255,255,255, 0.1)
&.selected
background: #48b57e
color: #fff
&:hover
background: #3ea471
&.delete
background: #e95050
&:hover
background: #ce4848
+dark-mode
background: #1b6540
&:hover
background: #1f7449
@ -343,16 +398,20 @@
background: #ae1e1e
&:hover
background: #bf2222
&.confirm
font-weight: 600
&.show-actions
display: none
position: relative
top: 1px
padding: 1px 8px
//cursor: default
background-image: url(@/assets/icons/arrow-right.svg)
background-image: url(@/assets/icons/arrow-right-white.svg)
width: 22px
height: 19px
background-size: 19px
background-position: center center
background-repeat: no-repeat
+dark-mode
background-image: url(@/assets/icons/arrow-right-grey.svg)
</style>

View File

@ -9,8 +9,6 @@
export default {
props: [
"theme",
"themeSetting",
"autoUpdate",
"allowBetaVersions",
],
@ -113,9 +111,6 @@
:autoUpdate="autoUpdate"
:allowBetaVersions="allowBetaVersions"
/>
<div class="status-block theme clickable" @click="$emit('toggleTheme')" title="Toggle dark/light mode">
<span :class="'icon ' + themeSetting"></span>
</div>
<div
@click.stop="$emit('openSettings')"
class="status-block settings clickable"
@ -176,19 +171,6 @@
color: rgba(255, 255, 255, 0.7)
+dark-mode
color: rgba(255, 255, 255, 0.55)
.theme
padding-top: 0
padding-bottom: 0
.icon
background-size: 14px
background-repeat: no-repeat
background-position: center center
&.dark
background-image: url("@/assets/icons/dark-mode.png")
&.light
background-image: url("@/assets/icons/light-mode.png")
&.system
background-image: url("@/assets/icons/both-mode.png")
.format
padding-top: 0

View File

@ -14,6 +14,7 @@
props: {
initialKeymap: String,
initialSettings: Object,
themeSetting: String,
},
components: {
KeyboardHotkey,
@ -60,6 +61,7 @@
systemFonts: [[defaultFontFamily, defaultFontFamily + " (default)"]],
defaultFontSize: defaultFontSize,
appVersion: "",
theme: this.themeSetting,
}
},
@ -109,6 +111,9 @@
if (!this.showInDock) {
this.showInMenu = true
}
if (this.theme != this.themeSetting) {
this.$emit("setTheme", this.theme)
}
},
async selectBufferLocation() {
@ -293,6 +298,16 @@
</TabContent>
<TabContent tab="appearance" :activeTab="activeTab">
<div class="row">
<div class="entry">
<h2>Color Theme</h2>
<select v-model="theme" @change="updateSettings" class="theme">
<option :selected="theme === 'system'" value="system">System</option>
<option :selected="theme === 'light'" value="light">Light</option>
<option :selected="theme === 'dark'" value="dark">Dark</option>
</select>
</div>
</div>
<div class="row">
<div class="entry">
<h2>Gutters</h2>

View File

@ -21,7 +21,7 @@ import { languageDetection } from "./language-detection/autodetect.js"
import { autoSaveContent } from "./save.js"
import { todoCheckboxPlugin} from "./todo-checkbox.ts"
import { links } from "./links.js"
import { NoteFormat } from "./note-format.js"
import { NoteFormat } from "../common/note-format.js"
import { useNotesStore } from "../stores/notes-store.js";
@ -140,7 +140,7 @@ export class HeynoteEditor {
if (content === this.diskContent) {
return
}
console.log("saving:", this.path)
//console.log("saving:", this.path)
this.diskContent = content
await window.heynote.buffer.save(this.path, content)
}
@ -158,7 +158,7 @@ export class HeynoteEditor {
}
async loadContent() {
console.log("loading content", this.path)
//console.log("loading content", this.path)
const content = await window.heynote.buffer.load(this.path)
this.diskContent = content
this.contentLoaded = true
@ -328,11 +328,13 @@ export class HeynoteEditor {
triggerCurrenciesLoaded(this.view.state, this.view.dispatch)
}
destroy() {
destroy(save=true) {
if (this.onChange) {
window.heynote.buffer.removeOnChangeCallback(this.path, this.onChange)
}
if (save) {
this.save()
}
this.view.destroy()
window.heynote.buffer.close(this.path)
}

View File

@ -1,6 +1,6 @@
import { toRaw } from 'vue';
import { defineStore } from "pinia"
import { NoteFormat } from "../editor/note-format"
import { NoteFormat } from "../common/note-format"
const NUM_EDITOR_INSTANCES = 5
@ -44,5 +44,14 @@ export const useEditorCacheStore = defineStore("editorCache", {
eachEditor(fn) {
Object.values(toRaw(this.editorCache.cache)).forEach(fn)
},
clearCache(save=true) {
console.log("Clearing editor cache")
this.eachEditor((editor) => {
editor.destroy(save=save)
})
this.editorCache.cache = {}
this.editorCache.lru = []
},
},
})

View File

@ -1,22 +1,23 @@
import { toRaw } from 'vue';
import { defineStore } from "pinia"
import { NoteFormat } from "../editor/note-format"
import { NoteFormat } from "../common/note-format"
import { useEditorCacheStore } from "./editor-cache"
import { SCRATCH_FILE_NAME } from "../common/constants"
export const SCRATCH_FILE = window.heynote.isDev ? "buffer-dev.txt" : "buffer.txt"
export const useNotesStore = defineStore("notes", {
state: () => ({
notes: {},
recentNotePaths: [SCRATCH_FILE],
recentNotePaths: [SCRATCH_FILE_NAME],
currentEditor: null,
currentNotePath: SCRATCH_FILE,
currentNotePath: SCRATCH_FILE_NAME,
currentNoteName: null,
currentLanguage: null,
currentLanguageAuto: null,
currentCursorLine: null,
currentSelectionSize: null,
libraryId: 0,
showNoteSelector: false,
showLanguageSelector: false,
@ -119,10 +120,22 @@ export const useNotesStore = defineStore("notes", {
this.updateNotes()
}
},
async reloadLibrary() {
const editorCacheStore = useEditorCacheStore()
await this.updateNotes()
editorCacheStore.clearCache(false)
this.currentEditor = null
this.currentNotePath = SCRATCH_FILE_NAME
this.libraryId++
},
},
})
export async function initNotesStore() {
const notesStore = useNotesStore()
window.heynote.buffer.setLibraryPathChangeCallback(() => {
notesStore.reloadLibrary()
})
await notesStore.updateNotes()
}

View File

@ -1,6 +1,6 @@
import { test, expect } from "@playwright/test";
import { HeynotePage } from "./test-utils.js";
import { NoteFormat } from "../src/editor/note-format.js";
import { NoteFormat } from "../src/common/note-format.js";
let heynotePage

View File

@ -1,5 +1,5 @@
import { test, expect } from '@playwright/test';
import { NoteFormat } from '../src/editor/note-format.js';
import { NoteFormat } from '../src/common/note-format.js';
export function pageErrorGetter(page) {
let messages = [];