mirror of
https://github.com/heyman/heynote.git
synced 2024-12-22 14:40:37 +01:00
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:
parent
29facb4787
commit
7be0a304d0
1
assets/icons/arrow-right-black.svg
Normal file
1
assets/icons/arrow-right-black.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="#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 |
Before Width: | Height: | Size: 522 B After Width: | Height: | Size: 522 B |
1
assets/icons/arrow-right-white.svg
Normal file
1
assets/icons/arrow-right-white.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="#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 |
@ -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
|
||||
|
@ -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! 👋
|
||||
|
||||
|
@ -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
|
||||
})
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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,15 +359,29 @@ ipcMain.handle('dark-mode:set', (event, mode) => {
|
||||
|
||||
ipcMain.handle('dark-mode:get', () => nativeTheme.themeSource)
|
||||
|
||||
|
||||
// Initialize note/file library
|
||||
async function initFileLibrary(win) {
|
||||
await migrateBufferFileToLibrary(app)
|
||||
|
||||
const customLibraryPath = CONFIG.get("settings.bufferPath")
|
||||
const libraryPath = customLibraryPath ? customLibraryPath : join(app.getPath("userData"), "notes")
|
||||
console.log("libraryPath", libraryPath)
|
||||
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)
|
||||
fileLibrary.setupWatcher(win)
|
||||
} catch (error) {
|
||||
initErrors.push(`Error: ${error.message}`)
|
||||
}
|
||||
setCurrentFileLibrary(fileLibrary)
|
||||
}
|
||||
|
||||
ipcMain.handle("getInitErrors", () => {
|
||||
return initErrors
|
||||
@ -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")
|
||||
}
|
||||
})
|
||||
|
@ -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)
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -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
1
src/common/constants.js
Normal file
@ -0,0 +1 @@
|
||||
export const SCRATCH_FILE_NAME = "scratch.txt"
|
@ -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"
|
||||
|
@ -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>
|
||||
|
@ -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: {
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 = []
|
||||
},
|
||||
},
|
||||
})
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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 = [];
|
||||
|
Loading…
Reference in New Issue
Block a user