search WIP
This commit is contained in:
@@ -98,8 +98,7 @@ class NotesAPI {
|
||||
this.encryptionKey = encryptionKey || process.env.NOTES_ENCRYPTION_KEY;
|
||||
this._sodiumReady = false;
|
||||
this.index = new Index({
|
||||
tokenize: "tolerant",
|
||||
resolution: 9
|
||||
tokenize: "forward"
|
||||
});
|
||||
}
|
||||
async _initSodium() {
|
||||
@@ -179,12 +178,23 @@ class NotesAPI {
|
||||
try {
|
||||
const note = this._decrypt(encryptedNote.data || encryptedNote);
|
||||
this.notesCache.set(note.id, note);
|
||||
this.index.add(note.id, note.title + "\n" + note.content);
|
||||
const searchText = note.plainText || this._extractPlainText(note.content);
|
||||
this.index.add(note.id, note.title + "\n" + searchText);
|
||||
} catch (error) {
|
||||
console.error("Failed to decrypt note:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
_extractPlainText(content) {
|
||||
if (!content) return "";
|
||||
if (typeof content === "string") return content;
|
||||
const extractText = (node) => {
|
||||
if (typeof node === "string") return node;
|
||||
if (!node || !node.content) return "";
|
||||
return node.content.map(extractText).join(" ");
|
||||
};
|
||||
return extractText(content);
|
||||
}
|
||||
/* -----------------------
|
||||
Public API
|
||||
------------------------*/
|
||||
@@ -203,7 +213,7 @@ class NotesAPI {
|
||||
getNote(id) {
|
||||
return this.notesCache.get(id) ?? null;
|
||||
}
|
||||
async createNote(metadata = {}, content = "") {
|
||||
async createNote(metadata = {}, content = "", plainText = "") {
|
||||
const id = crypto.randomUUID();
|
||||
const now = (/* @__PURE__ */ new Date()).toISOString();
|
||||
const note = {
|
||||
@@ -212,14 +222,15 @@ class NotesAPI {
|
||||
category: metadata.category || null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
content
|
||||
content,
|
||||
plainText
|
||||
};
|
||||
const encryptedNote = {
|
||||
id: note.id,
|
||||
data: this._encrypt(note)
|
||||
};
|
||||
this.notesCache.set(id, note);
|
||||
this.index.add(id, note.title + "\n" + content);
|
||||
this.index.add(id, note.title + "\n" + plainText);
|
||||
await this.adapter.create(encryptedNote);
|
||||
return note;
|
||||
}
|
||||
@@ -228,16 +239,17 @@ class NotesAPI {
|
||||
this.notesCache.delete(id);
|
||||
this.index.remove(id);
|
||||
}
|
||||
async updateNote(id, content) {
|
||||
async updateNote(id, content, plainText = "") {
|
||||
const note = this.notesCache.get(id);
|
||||
if (!note) throw new Error("Note not found");
|
||||
note.content = content;
|
||||
note.plainText = plainText;
|
||||
note.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
||||
const encryptedNote = {
|
||||
id: note.id,
|
||||
data: this._encrypt(note)
|
||||
};
|
||||
this.index.update(id, note.title + "\n" + content);
|
||||
this.index.update(id, note.title + "\n" + plainText);
|
||||
await this.adapter.update(encryptedNote);
|
||||
return note;
|
||||
}
|
||||
@@ -261,12 +273,18 @@ class NotesAPI {
|
||||
id: note.id,
|
||||
data: this._encrypt(note)
|
||||
};
|
||||
this.index.update(id, note.title + "\n" + note.content);
|
||||
this.index.update(
|
||||
id,
|
||||
note.title + "\n" + (note.plainText || this._extractPlainText(note.content))
|
||||
);
|
||||
await this.adapter.update(encryptedNote);
|
||||
return note;
|
||||
}
|
||||
search(query) {
|
||||
const ids = this.index.search(query);
|
||||
const ids = this.index.search(query, {
|
||||
limit: 50,
|
||||
suggest: true
|
||||
});
|
||||
return ids.map((id) => this.notesCache.get(id));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,7 @@ export default class NotesAPI {
|
||||
this._sodiumReady = false
|
||||
|
||||
this.index = new Index({
|
||||
tokenize: 'tolerant',
|
||||
resolution: 9,
|
||||
tokenize: 'forward',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -118,13 +117,28 @@ export default class NotesAPI {
|
||||
const note = this._decrypt(encryptedNote.data || encryptedNote)
|
||||
|
||||
this.notesCache.set(note.id, note)
|
||||
this.index.add(note.id, note.title + '\n' + note.content)
|
||||
const searchText =
|
||||
note.plainText || this._extractPlainText(note.content)
|
||||
this.index.add(note.id, note.title + '\n' + searchText)
|
||||
} catch (error) {
|
||||
console.error('Failed to decrypt note:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_extractPlainText(content) {
|
||||
if (!content) return ''
|
||||
if (typeof content === 'string') return content
|
||||
|
||||
const extractText = (node) => {
|
||||
if (typeof node === 'string') return node
|
||||
if (!node || !node.content) return ''
|
||||
return node.content.map(extractText).join(' ')
|
||||
}
|
||||
|
||||
return extractText(content)
|
||||
}
|
||||
|
||||
/* -----------------------
|
||||
Public API
|
||||
------------------------*/
|
||||
@@ -150,7 +164,7 @@ export default class NotesAPI {
|
||||
return this.notesCache.get(id) ?? null
|
||||
}
|
||||
|
||||
async createNote(metadata = {}, content = '') {
|
||||
async createNote(metadata = {}, content = '', plainText = '') {
|
||||
const id = crypto.randomUUID()
|
||||
const now = new Date().toISOString()
|
||||
|
||||
@@ -161,6 +175,7 @@ export default class NotesAPI {
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
content,
|
||||
plainText,
|
||||
}
|
||||
|
||||
const encryptedNote = {
|
||||
@@ -169,7 +184,7 @@ export default class NotesAPI {
|
||||
}
|
||||
|
||||
this.notesCache.set(id, note)
|
||||
this.index.add(id, note.title + '\n' + content)
|
||||
this.index.add(id, note.title + '\n' + plainText)
|
||||
|
||||
await this.adapter.create(encryptedNote)
|
||||
|
||||
@@ -183,11 +198,12 @@ export default class NotesAPI {
|
||||
this.index.remove(id)
|
||||
}
|
||||
|
||||
async updateNote(id, content) {
|
||||
async updateNote(id, content, plainText = '') {
|
||||
const note = this.notesCache.get(id)
|
||||
if (!note) throw new Error('Note not found')
|
||||
|
||||
note.content = content
|
||||
note.plainText = plainText
|
||||
note.updatedAt = new Date().toISOString()
|
||||
|
||||
const encryptedNote = {
|
||||
@@ -195,7 +211,7 @@ export default class NotesAPI {
|
||||
data: this._encrypt(note),
|
||||
}
|
||||
|
||||
this.index.update(id, note.title + '\n' + content)
|
||||
this.index.update(id, note.title + '\n' + plainText)
|
||||
|
||||
await this.adapter.update(encryptedNote)
|
||||
|
||||
@@ -228,7 +244,12 @@ export default class NotesAPI {
|
||||
data: this._encrypt(note),
|
||||
}
|
||||
|
||||
this.index.update(id, note.title + '\n' + note.content)
|
||||
this.index.update(
|
||||
id,
|
||||
note.title +
|
||||
'\n' +
|
||||
(note.plainText || this._extractPlainText(note.content)),
|
||||
)
|
||||
|
||||
await this.adapter.update(encryptedNote)
|
||||
|
||||
@@ -236,7 +257,10 @@ export default class NotesAPI {
|
||||
}
|
||||
|
||||
search(query) {
|
||||
const ids = this.index.search(query)
|
||||
const ids = this.index.search(query, {
|
||||
limit: 50,
|
||||
suggest: true,
|
||||
})
|
||||
return ids.map((id) => this.notesCache.get(id))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<template>
|
||||
<nav class="nav layout-block">
|
||||
<button @click="toggleMenu">Menu</button>
|
||||
|
||||
<router-link to="/search">Search</router-link>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
@@ -29,6 +31,9 @@ onMounted(() => {
|
||||
|
||||
<style lang="scss">
|
||||
.nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding-top: 9px;
|
||||
color: var(--grey-100);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ const formatDate = (date) => {
|
||||
.note-row {
|
||||
grid-template-columns: auto 1fr;
|
||||
display: grid;
|
||||
width: 100%;
|
||||
gap: 20px;
|
||||
cursor: pointer;
|
||||
|
||||
|
||||
@@ -33,8 +33,9 @@ const { loadNote, updateNoteContent, updateNoteMetadata } = useNotes()
|
||||
|
||||
const updateNote = _debounce(async ({ editor }) => {
|
||||
const json = editor.getJSON()
|
||||
const text = editor.getText()
|
||||
|
||||
await updateNoteContent(props.id, json)
|
||||
await updateNoteContent(props.id, json, text)
|
||||
|
||||
updateTitle(editor)
|
||||
}, 300)
|
||||
|
||||
@@ -44,8 +44,13 @@ export default () => {
|
||||
/* -------------------------
|
||||
Create
|
||||
--------------------------*/
|
||||
async function createNote(metadata, content) {
|
||||
const note = await window.notesAPI.call('createNote', metadata, content)
|
||||
async function createNote(metadata, content, plainText = '') {
|
||||
const note = await window.notesAPI.call(
|
||||
'createNote',
|
||||
metadata,
|
||||
content,
|
||||
plainText,
|
||||
)
|
||||
await loadCategories()
|
||||
return note
|
||||
}
|
||||
@@ -53,8 +58,13 @@ export default () => {
|
||||
/* -------------------------
|
||||
Update
|
||||
--------------------------*/
|
||||
async function updateNoteContent(id, content) {
|
||||
const note = await window.notesAPI.call('updateNote', id, content)
|
||||
async function updateNoteContent(id, content, plainText = '') {
|
||||
const note = await window.notesAPI.call(
|
||||
'updateNote',
|
||||
id,
|
||||
content,
|
||||
plainText,
|
||||
)
|
||||
return note
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import Note from '@/views/Note.vue'
|
||||
import CreateCategory from '@/views/CreateCategory.vue'
|
||||
import Category from '@/views/Category.vue'
|
||||
import Instructions from '@/views/Instructions.vue'
|
||||
import Search from '@/views/Search.vue'
|
||||
|
||||
const routes = [
|
||||
{ path: '/', name: 'directory', component: Directory },
|
||||
@@ -12,6 +13,7 @@ const routes = [
|
||||
{ path: '/category', name: 'create-category', component: CreateCategory },
|
||||
{ path: '/category/:id', name: 'category', component: Category },
|
||||
{ path: '/instructions', name: 'instructions', component: Instructions },
|
||||
{ path: '/search', name: 'search', component: Search },
|
||||
]
|
||||
|
||||
export const router = createRouter({
|
||||
|
||||
100
src/renderer/src/views/Search.vue
Normal file
100
src/renderer/src/views/Search.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<main class="search layout-block">
|
||||
<router-link class="back" to="/"><- Back</router-link>
|
||||
|
||||
<form @submit.prevent="onSearch">
|
||||
<div class="input-wrap">
|
||||
<input
|
||||
v-model="query"
|
||||
type="text"
|
||||
placeholder="Search"
|
||||
ref="searchInput"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="results">
|
||||
<note-row
|
||||
v-for="note in searchResults"
|
||||
:key="note.id"
|
||||
:note="note"
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import useNotes from '@/composables/useNotes'
|
||||
import NoteRow from '@/components/NoteRow.vue'
|
||||
|
||||
const query = ref('')
|
||||
const searchInput = ref()
|
||||
|
||||
const { search, searchResults } = useNotes()
|
||||
|
||||
onMounted(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
searchInput.value?.focus()
|
||||
})
|
||||
|
||||
const onSearch = async () => {
|
||||
await search(query.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
main.search {
|
||||
.back {
|
||||
display: block;
|
||||
opacity: 0.25;
|
||||
margin-top: 9px;
|
||||
}
|
||||
.input-wrap {
|
||||
margin-top: 19px;
|
||||
position: relative;
|
||||
|
||||
input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
padding: 5px 15px 6px;
|
||||
background: var(--theme-bg);
|
||||
--clip-start: 16px;
|
||||
clip-path: polygon(
|
||||
var(--clip-start) 1px,
|
||||
calc(100% - var(--clip-start)) 1px,
|
||||
calc(100% - 1.5px) 50%,
|
||||
calc(100% - var(--clip-start)) calc(100% - 1px),
|
||||
var(--clip-start) calc(100% - 1px),
|
||||
1.5px 50%
|
||||
);
|
||||
}
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--theme-fg);
|
||||
--clip-start: 15px;
|
||||
clip-path: polygon(
|
||||
var(--clip-start) 0,
|
||||
calc(100% - var(--clip-start)) 0,
|
||||
100% 50%,
|
||||
calc(100% - var(--clip-start)) 100%,
|
||||
var(--clip-start) 100%,
|
||||
0% 50%
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.results {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user