Compare commits
7
Commits
main
..
08b3600836
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08b3600836 | ||
|
|
1614776afa | ||
|
|
3bc49ec40d | ||
|
|
990b0eaf92 | ||
|
|
b96403d46d | ||
|
|
3dc154ab16 | ||
|
|
71364b2fc0 |
+9
-136
@@ -73,67 +73,12 @@
|
|||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
.compose-wrapper textarea { resize: vertical; min-height: 60px; }
|
.compose-wrapper textarea { resize: vertical; min-height: 60px; }
|
||||||
input,textarea,tag-input {
|
|
||||||
padding: .5rem;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-radius: 4px;
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
padding: .5rem 1rem;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
/* Context Menu Styling */
|
|
||||||
.context-menu {
|
|
||||||
position: absolute;
|
|
||||||
background-color: #fff;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-radius: 5px;
|
|
||||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15);
|
|
||||||
padding: 5px;
|
|
||||||
z-index: 1000;
|
|
||||||
display: none; /* Initially hidden */
|
|
||||||
min-width: 150px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.context-menu-item {
|
|
||||||
padding: 8px 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.context-menu-item:hover {
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.context-menu-item span {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
/* Animations */
|
|
||||||
@keyframes fadeIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-5px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.context-menu.show {
|
|
||||||
animation: fadeIn 0.2s ease-out;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<aside id="sidebar">
|
<aside id="sidebar">
|
||||||
|
<h2>Tags</h2>
|
||||||
<!-- New search box -->
|
<!-- New search box -->
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -141,7 +86,6 @@
|
|||||||
placeholder="Search notes…"
|
placeholder="Search notes…"
|
||||||
style="width: calc(100% - 2rem); margin: .5rem 1rem; padding: .5rem;"
|
style="width: calc(100% - 2rem); margin: .5rem 1rem; padding: .5rem;"
|
||||||
/>
|
/>
|
||||||
<h2>Tags</h2>
|
|
||||||
<ul id="tag-list"></ul>
|
<ul id="tag-list"></ul>
|
||||||
</aside>
|
</aside>
|
||||||
<main>
|
<main>
|
||||||
@@ -150,68 +94,7 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="context-menu" class="context-menu">
|
|
||||||
<div class="context-menu-item"><span>Edit Tag</span></div>
|
|
||||||
<div class="context-menu-item"><span>Rename Tag</span></div>
|
|
||||||
<div class="context-menu-item"><span>Delete Tag</span></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
class ContextMenu extends HTMLElement {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.menu = document.createElement('div');
|
|
||||||
this.menu.classList.add('context-menu');
|
|
||||||
document.body.appendChild(this.menu);
|
|
||||||
|
|
||||||
document.body.addEventListener('contextmenu', (event) => {
|
|
||||||
event.preventDefault(); // Prevent default context menu
|
|
||||||
this.showContextMenu(event);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener('click', () => {
|
|
||||||
this.hideContextMenu();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
addItem(text) {
|
|
||||||
const item = document.createElement('div');
|
|
||||||
item.classList.add('context-menu-item');
|
|
||||||
item.innerHTML = `<span>${text}</span>`;
|
|
||||||
this.menu.appendChild(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
showContextMenu(event) {
|
|
||||||
|
|
||||||
let clicked = event.target.closest('.right-clickable');
|
|
||||||
if (!clicked){
|
|
||||||
clicked = event.target;
|
|
||||||
if (!clicked.classList.contains('right-clickable'))
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Get position
|
|
||||||
const x = event.clientX;
|
|
||||||
const y = event.clientY;
|
|
||||||
|
|
||||||
// Set the clicked tag's data to a property so it's available for the menu items.
|
|
||||||
this.currentElement = clicked;
|
|
||||||
|
|
||||||
this.menu.style.left = `${x}px`;
|
|
||||||
this.menu.style.top = `${y}px`;
|
|
||||||
this.menu.classList.add('show'); // Add show class for animation
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
hideContextMenu() {
|
|
||||||
this.menu.classList.remove('show');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define('context-menu', ContextMenu);
|
|
||||||
|
|
||||||
const contextMenu = new ContextMenu();
|
|
||||||
contextMenu.addItem('Edit Tag');
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||||
<!--
|
<!--
|
||||||
@@ -484,7 +367,7 @@ class NoteCard extends HTMLElement {
|
|||||||
? `<img src="${a.url}">`
|
? `<img src="${a.url}">`
|
||||||
: `<a href="${a.url}" target="_blank">${a.url}</a>`).join('')}
|
: `<a href="${a.url}" target="_blank">${a.url}</a>`).join('')}
|
||||||
</div>` : ''}
|
</div>` : ''}
|
||||||
${tags.length ? `<div class="tag-list">${tags.map(t => `<span class="tag right-clickable" data-tag="${t}">${t}</span>`).join('')}</div>` : ''}
|
${tags.length ? `<div class="tag-list">${tags.map(t => `<span class="tag">${t}</span>`).join('')}</div>` : ''}
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -496,15 +379,14 @@ class NoteCompose extends HTMLElement {
|
|||||||
super();
|
super();
|
||||||
this.noteId = null; // null = new
|
this.noteId = null; // null = new
|
||||||
this.innerHTML = `
|
this.innerHTML = `
|
||||||
<button class="compose-button">New note</button>
|
<form class="compose-wrapper">
|
||||||
<form class="compose-wrapper" style="display:none;">
|
|
||||||
<input name="title" placeholder="Title" required>
|
<input name="title" placeholder="Title" required>
|
||||||
<textarea name="body" placeholder="Take a note…" required></textarea>
|
<textarea name="body" placeholder="Take a note…" required></textarea>
|
||||||
<input name="files" type="file" multiple>
|
<input name="files" type="file" multiple>
|
||||||
<tag-input name="tags" placeholder="Tags (comma-separated)"></tag-input>
|
<tag-input name="tags" placeholder="Tags (comma-separated)"></tag-input>
|
||||||
<div style="display:flex;gap:.5rem;">
|
<div style="display:flex;gap:.5rem;">
|
||||||
<button type="submit">Save</button>
|
<button type="submit">Save</button>
|
||||||
<button type="button" class="cancel">Cancel</button>
|
<button type="button" id="cancel" style="display:none;">Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</form>`;
|
</form>`;
|
||||||
}
|
}
|
||||||
@@ -514,27 +396,18 @@ class NoteCompose extends HTMLElement {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.save();
|
this.save();
|
||||||
});
|
});
|
||||||
this.composeButton = this.querySelector('.compose-button')
|
|
||||||
this.composeButton.addEventListener('click', () => this.show());
|
|
||||||
this.querySelector('.cancel').addEventListener('click', () => this.reset());
|
this.querySelector('#cancel').addEventListener('click', () => this.reset());
|
||||||
document.addEventListener('edit-note', e => this.load(e.detail));
|
document.addEventListener('edit-note', e => this.load(e.detail));
|
||||||
}
|
}
|
||||||
show(){
|
|
||||||
this.form.style.display = 'block';
|
|
||||||
this.q('title').focus();
|
|
||||||
this.composeButton.style.display = "none";
|
|
||||||
}
|
|
||||||
hide(){
|
|
||||||
this.form.style.display = 'none';
|
|
||||||
this.composeButton.style.display = "block";
|
|
||||||
}
|
|
||||||
q(s) { return this.querySelector(s); }
|
q(s) { return this.querySelector(s); }
|
||||||
load(n) { /* pre-fill for edit */
|
load(n) { /* pre-fill for edit */
|
||||||
this.noteId = n.id;
|
this.noteId = n.id;
|
||||||
this.q('title').value = n.title;
|
this.q('title').value = n.title;
|
||||||
this.q('body').value = n.body;
|
this.q('body').value = n.body;
|
||||||
this.q('tags').value = (n.tags || []).join(', ');
|
this.q('tags').value = (n.tags || []).join(', ');
|
||||||
this.show();
|
this.q('#cancel').style.display = 'inline-block';
|
||||||
this.scrollIntoView({behavior:'smooth'});
|
this.scrollIntoView({behavior:'smooth'});
|
||||||
}
|
}
|
||||||
async save() {
|
async save() {
|
||||||
@@ -561,7 +434,7 @@ class NoteCompose extends HTMLElement {
|
|||||||
if (res.ok) { this.reset(); document.dispatchEvent(new CustomEvent('notes-changed')); }
|
if (res.ok) { this.reset(); document.dispatchEvent(new CustomEvent('notes-changed')); }
|
||||||
else alert('Save failed');
|
else alert('Save failed');
|
||||||
}
|
}
|
||||||
reset() { this.noteId=null; this.form.reset(); this.hide(); this.q('tags').value=''; }
|
reset() { this.noteId=null; this.form.reset(); this.q('#cancel').style.display='none'; this.q('tags').value=''; }
|
||||||
q(sel){ return this.querySelector(sel.includes('#') ? sel : `[name=\"${sel}\"]`); }
|
q(sel){ return this.querySelector(sel.includes('#') ? sel : `[name=\"${sel}\"]`); }
|
||||||
}
|
}
|
||||||
customElements.define('note-compose', NoteCompose);
|
customElements.define('note-compose', NoteCompose);
|
||||||
|
|||||||
@@ -77,43 +77,10 @@ app.mount("/static", StaticFiles(directory=UPLOAD_DIR), name="static")
|
|||||||
if pathlib.Path(FRONTEND_DIR).exists():
|
if pathlib.Path(FRONTEND_DIR).exists():
|
||||||
app.mount("/assets", StaticFiles(directory=FRONTEND_DIR), name="assets")
|
app.mount("/assets", StaticFiles(directory=FRONTEND_DIR), name="assets")
|
||||||
|
|
||||||
async def ensure_tables_exist():
|
|
||||||
async with db_session() as db:
|
|
||||||
# Check if 'notes' table exists
|
|
||||||
if 'notes' not in db.tables:
|
|
||||||
db.query("""
|
|
||||||
CREATE TABLE notes (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
title TEXT,
|
|
||||||
body TEXT,
|
|
||||||
created_at TEXT,
|
|
||||||
updated_at TEXT
|
|
||||||
)
|
|
||||||
""")
|
|
||||||
# Check if 'tags' table exists
|
|
||||||
if 'tags' not in db.tables:
|
|
||||||
db.query("""
|
|
||||||
CREATE TABLE tags (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
name TEXT UNIQUE
|
|
||||||
)
|
|
||||||
""")
|
|
||||||
# Check if 'note_tags' table exists
|
|
||||||
if 'note_tags' not in db.tables:
|
|
||||||
db.query("""
|
|
||||||
CREATE TABLE note_tags (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
note_id INTEGER,
|
|
||||||
tag TEXT,
|
|
||||||
FOREIGN KEY(note_id) REFERENCES notes(id)
|
|
||||||
)
|
|
||||||
""")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
async def init_search_index():
|
async def init_search_index():
|
||||||
await ensure_tables_exist()
|
|
||||||
db = dataset.connect(DB_URL)
|
db = dataset.connect(DB_URL)
|
||||||
# Create the FTS5 virtual table over notes.title and notes.body
|
# Create the FTS5 virtual table over notes.title and notes.body
|
||||||
db.query("""
|
db.query("""
|
||||||
@@ -273,8 +240,15 @@ async def _upsert_note(note_id: Optional[int], payload: Dict[str, Any]):
|
|||||||
db['notes'].update({"id": note_id, "title": title, "body": body, "updated_at": now}, ["id"])
|
db['notes'].update({"id": note_id, "title": title, "body": body, "updated_at": now}, ["id"])
|
||||||
db['attachments'].delete(note_id=note_id)
|
db['attachments'].delete(note_id=note_id)
|
||||||
db['note_tags'].delete(note_id=note_id)
|
db['note_tags'].delete(note_id=note_id)
|
||||||
|
db.query("DELETE FROM notes_fts WHERE note_id = :nid", nid=note_id)
|
||||||
|
|
||||||
# (Re‑)insert into FTS table
|
# (Re‑)insert into FTS table
|
||||||
|
db.query(
|
||||||
|
"INSERT INTO notes_fts (title, body, note_id) VALUES (:title, :body, :nid)",
|
||||||
|
title=title,
|
||||||
|
body=body,
|
||||||
|
nid=note_id,
|
||||||
|
)
|
||||||
for t in tags:
|
for t in tags:
|
||||||
t = t.strip()
|
t = t.strip()
|
||||||
if not t:
|
if not t:
|
||||||
|
|||||||
Reference in New Issue
Block a user