mirror of
https://gitlab.com/ArkHost/HelixNotes.git
synced 2026-07-24 07:45:57 +02:00
v1.0.4 - Quick Access reorder, performance improvements, search fixes
- Quick Access drag-to-reorder: drag notes to rearrange, order persists - Quick Access preserves order when moving notes between notebooks - In-note search: debounced input, proper scroll-to-match - Panel resize: RAF-batched updates, editor containment during resize - Thinner resize handles (3px) - Note list: removed transition, added CSS containment for 1200+ notes - Escape closes Settings and Info panels - Fix Quick Access cache invalidation on remove
This commit is contained in:
+1
-1
@@ -2,7 +2,7 @@
|
||||
"name": "helixnotes",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.4",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
|
||||
Generated
+1
-1
@@ -1778,7 +1778,7 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "helixnotes"
|
||||
version = "1.0.3"
|
||||
version = "1.0.4"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dirs",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "helixnotes"
|
||||
version = "1.0.3"
|
||||
version = "1.0.4"
|
||||
description = "Local-first markdown note-taking app"
|
||||
authors = ["HelixNotes"]
|
||||
license = "AGPL-3.0-or-later"
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::search::SearchIndex;
|
||||
use crate::state::AppState;
|
||||
use crate::types::*;
|
||||
use crate::vault::{operations, watcher};
|
||||
use std::path::Path;
|
||||
use tauri::{AppHandle, Manager, State};
|
||||
|
||||
// ── Vault Management ──
|
||||
@@ -195,8 +196,39 @@ pub fn delete_note(state: State<'_, AppState>, path: String) -> Result<(), Strin
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn move_note(note_path: String, dest_notebook: String) -> Result<String, String> {
|
||||
operations::move_note(¬e_path, &dest_notebook)
|
||||
pub fn move_note(
|
||||
state: State<'_, AppState>,
|
||||
note_path: String,
|
||||
dest_notebook: String,
|
||||
) -> Result<String, String> {
|
||||
let config = state.config.lock().map_err(|e| e.to_string())?;
|
||||
let vault_path = config.active_vault.as_ref().ok_or("No active vault")?;
|
||||
|
||||
// Compute old relative path before move
|
||||
let old_relative = Path::new(¬e_path)
|
||||
.strip_prefix(vault_path)
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
let new_full_path = operations::move_note(¬e_path, &dest_notebook)?;
|
||||
|
||||
// Update quick access if the moved note was in it
|
||||
if !old_relative.is_empty() {
|
||||
if let Ok(mut qa) = operations::load_quick_access(vault_path) {
|
||||
if let Some(pos) = qa.iter().position(|p| *p == old_relative) {
|
||||
let new_relative = Path::new(&new_full_path)
|
||||
.strip_prefix(vault_path)
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_default();
|
||||
if !new_relative.is_empty() {
|
||||
qa[pos] = new_relative;
|
||||
let _ = operations::save_quick_access(vault_path, &qa);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(new_full_path)
|
||||
}
|
||||
|
||||
// ── Tags ──
|
||||
@@ -437,6 +469,13 @@ pub fn remove_quick_access(
|
||||
operations::remove_quick_access(vault_path, ¬e_relative)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn reorder_quick_access(state: State<'_, AppState>, paths: Vec<String>) -> Result<(), String> {
|
||||
let config = state.config.lock().map_err(|e| e.to_string())?;
|
||||
let vault_path = config.active_vault.as_ref().ok_or("No active vault")?;
|
||||
operations::save_quick_access(vault_path, &paths)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_vault_stats(state: State<'_, AppState>) -> Result<VaultStats, String> {
|
||||
let config = state.config.lock().map_err(|e| e.to_string())?;
|
||||
|
||||
@@ -79,6 +79,7 @@ pub fn run() {
|
||||
commands::get_quick_access,
|
||||
commands::add_quick_access,
|
||||
commands::remove_quick_access,
|
||||
commands::reorder_quick_access,
|
||||
commands::get_vault_stats,
|
||||
commands::import_obsidian,
|
||||
commands::open_file,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"productName": "HelixNotes",
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.4",
|
||||
"identifier": "com.helixnotes.app",
|
||||
"build": {
|
||||
"frontendDist": "../build",
|
||||
|
||||
+10
-1
@@ -31,7 +31,7 @@
|
||||
--shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
--sidebar-width: 220px;
|
||||
--notelist-width: 280px;
|
||||
--panel-resize-handle: 4px;
|
||||
--panel-resize-handle: 3px;
|
||||
}
|
||||
|
||||
:root.dark {
|
||||
@@ -149,3 +149,12 @@ body {
|
||||
.resize-handle.active {
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
body.resizing .ProseMirror {
|
||||
pointer-events: none;
|
||||
contain: strict;
|
||||
}
|
||||
|
||||
body.resizing {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@@ -213,6 +213,10 @@ export async function removeQuickAccess(noteRelative: string): Promise<void> {
|
||||
return invoke("remove_quick_access", { noteRelative });
|
||||
}
|
||||
|
||||
export async function reorderQuickAccess(paths: string[]): Promise<void> {
|
||||
return invoke("reorder_quick_access", { paths });
|
||||
}
|
||||
|
||||
export async function getVaultStats(): Promise<VaultStats> {
|
||||
return invoke("get_vault_stats");
|
||||
}
|
||||
|
||||
@@ -1192,7 +1192,10 @@
|
||||
}
|
||||
|
||||
// ── In-note search functions ──
|
||||
let noteSearchTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
function updateNoteSearch(query: string) {
|
||||
if (noteSearchTimer) clearTimeout(noteSearchTimer);
|
||||
if (!editor) return;
|
||||
if (!query) {
|
||||
noteSearchResults = [];
|
||||
@@ -1201,6 +1204,8 @@
|
||||
editor.view.dispatch(tr);
|
||||
return;
|
||||
}
|
||||
noteSearchTimer = setTimeout(() => {
|
||||
if (!editor) return;
|
||||
const results: {from: number, to: number}[] = [];
|
||||
const lowerQuery = query.toLowerCase();
|
||||
editor.state.doc.descendants((node, pos) => {
|
||||
@@ -1215,6 +1220,7 @@
|
||||
noteSearchResults = results;
|
||||
if (noteSearchIndex >= results.length) noteSearchIndex = 0;
|
||||
applySearchDecorations();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function applySearchDecorations() {
|
||||
@@ -1230,9 +1236,12 @@
|
||||
|
||||
function scrollToCurrentMatch() {
|
||||
if (!editor || noteSearchResults.length === 0) return;
|
||||
const match = noteSearchResults[noteSearchIndex];
|
||||
editor.commands.setTextSelection(match.from);
|
||||
editor.commands.scrollIntoView();
|
||||
requestAnimationFrame(() => {
|
||||
const el = editor?.view.dom.querySelector('.note-search-active');
|
||||
if (el) {
|
||||
el.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function noteSearchNext() {
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
getQuickAccess,
|
||||
addQuickAccess,
|
||||
removeQuickAccess,
|
||||
reorderQuickAccess,
|
||||
moveNote
|
||||
} from '$lib/api';
|
||||
import { formatRelativeTime } from '$lib/utils/time';
|
||||
@@ -48,6 +49,11 @@
|
||||
let lastClickedPath = $state<string | null>(null);
|
||||
let batchMovePicker = $state(false);
|
||||
|
||||
// Quick Access drag-to-reorder
|
||||
let qaDragFrom = $state<number | null>(null);
|
||||
let qaDragOver = $state<number | null>(null);
|
||||
let qaDragHalf = $state<'top' | 'bottom'>('bottom');
|
||||
|
||||
function clearSelection() {
|
||||
selectedPaths = new Set();
|
||||
lastClickedPath = null;
|
||||
@@ -260,12 +266,33 @@
|
||||
await removeQuickAccess(note.relative_path);
|
||||
const qaNotes = await getQuickAccess();
|
||||
$quickAccessPaths = qaNotes.map(n => n.relative_path);
|
||||
if ($viewMode === 'quickaccess') await refresh();
|
||||
if ($viewMode === 'quickaccess') await refresh(true);
|
||||
} catch (e) {
|
||||
console.error('Failed to remove from quick access:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleQaDrop(targetIndex: number) {
|
||||
if (qaDragFrom === null) { qaDragFrom = null; qaDragOver = null; return; }
|
||||
// Compute the actual insert position based on which half we're hovering
|
||||
let insertAt = qaDragHalf === 'bottom' ? targetIndex + 1 : targetIndex;
|
||||
if (qaDragFrom === insertAt || qaDragFrom + 1 === insertAt) {
|
||||
qaDragFrom = null; qaDragOver = null; return;
|
||||
}
|
||||
const arr = [...$sortedNotes];
|
||||
const [moved] = arr.splice(qaDragFrom, 1);
|
||||
if (insertAt > qaDragFrom) insertAt--;
|
||||
arr.splice(insertAt, 0, moved);
|
||||
$notes = arr;
|
||||
qaDragFrom = null;
|
||||
qaDragOver = null;
|
||||
try {
|
||||
await reorderQuickAccess(arr.map(n => n.relative_path));
|
||||
} catch (e) {
|
||||
console.error('Failed to reorder quick access:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleMoveNote(note: NoteEntry, destPath: string) {
|
||||
contextMenu = null;
|
||||
movePickerNote = null;
|
||||
@@ -491,7 +518,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#each $sortedNotes as note (note.path)}
|
||||
{#each $sortedNotes as note, noteIndex (note.path)}
|
||||
{#if editingNote === note.path}
|
||||
<div class="note-item active">
|
||||
<input
|
||||
@@ -512,6 +539,8 @@
|
||||
class:selected={selectedPaths.has(note.path)}
|
||||
class:pinned={note.meta.pinned}
|
||||
class:compact={compact}
|
||||
class:qa-drag-above={$viewMode === 'quickaccess' && qaDragOver === noteIndex && qaDragFrom !== null && qaDragHalf === 'top'}
|
||||
class:qa-drag-below={$viewMode === 'quickaccess' && qaDragOver === noteIndex && qaDragFrom !== null && qaDragHalf === 'bottom'}
|
||||
onclick={(e) => handleNoteClick(e, note)}
|
||||
oncontextmenu={(e) => {
|
||||
e.preventDefault();
|
||||
@@ -528,6 +557,12 @@
|
||||
}}
|
||||
draggable="true"
|
||||
ondragstart={(e) => {
|
||||
if ($viewMode === 'quickaccess') {
|
||||
qaDragFrom = noteIndex;
|
||||
e.dataTransfer!.setData('text/plain', note.path);
|
||||
e.dataTransfer!.effectAllowed = 'move';
|
||||
return;
|
||||
}
|
||||
if (selectedPaths.size > 1 && selectedPaths.has(note.path)) {
|
||||
e.dataTransfer!.setData('text/plain', [...selectedPaths].join('\n'));
|
||||
} else {
|
||||
@@ -535,6 +570,25 @@
|
||||
}
|
||||
e.dataTransfer!.effectAllowed = 'move';
|
||||
}}
|
||||
ondragover={(e) => {
|
||||
if ($viewMode === 'quickaccess' && qaDragFrom !== null) {
|
||||
e.preventDefault();
|
||||
e.dataTransfer!.dropEffect = 'move';
|
||||
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
||||
qaDragHalf = e.clientY < rect.top + rect.height / 2 ? 'top' : 'bottom';
|
||||
qaDragOver = noteIndex;
|
||||
}
|
||||
}}
|
||||
ondragleave={() => {
|
||||
if (qaDragOver === noteIndex) qaDragOver = null;
|
||||
}}
|
||||
ondrop={(e) => {
|
||||
if ($viewMode === 'quickaccess' && qaDragFrom !== null) {
|
||||
e.preventDefault();
|
||||
handleQaDrop(noteIndex);
|
||||
}
|
||||
}}
|
||||
ondragend={() => { qaDragFrom = null; qaDragOver = null; }}
|
||||
>
|
||||
{#if compact}
|
||||
<div class="note-compact-row">
|
||||
@@ -761,8 +815,8 @@
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: background 0.1s;
|
||||
margin-bottom: 1px;
|
||||
contain: content;
|
||||
}
|
||||
|
||||
.note-item:hover {
|
||||
@@ -1133,4 +1187,12 @@
|
||||
color: var(--accent);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.note-item.qa-drag-above {
|
||||
border-top: 2px solid var(--accent);
|
||||
}
|
||||
|
||||
.note-item.qa-drag-below {
|
||||
border-bottom: 2px solid var(--accent);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,23 +2,35 @@
|
||||
let { onResize }: { onResize: (delta: number) => void } = $props();
|
||||
let active = $state(false);
|
||||
let startX = 0;
|
||||
let rafId = 0;
|
||||
let pendingDelta = 0;
|
||||
|
||||
function onMouseDown(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
active = true;
|
||||
startX = e.clientX;
|
||||
document.body.classList.add('resizing');
|
||||
window.addEventListener('mousemove', onMouseMove);
|
||||
window.addEventListener('mouseup', onMouseUp);
|
||||
}
|
||||
|
||||
function onMouseMove(e: MouseEvent) {
|
||||
const delta = e.clientX - startX;
|
||||
pendingDelta += e.clientX - startX;
|
||||
startX = e.clientX;
|
||||
onResize(delta);
|
||||
if (!rafId) {
|
||||
rafId = requestAnimationFrame(() => {
|
||||
onResize(pendingDelta);
|
||||
pendingDelta = 0;
|
||||
rafId = 0;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function onMouseUp() {
|
||||
active = false;
|
||||
document.body.classList.remove('resizing');
|
||||
if (rafId) { cancelAnimationFrame(rafId); rafId = 0; }
|
||||
if (pendingDelta) { onResize(pendingDelta); pendingDelta = 0; }
|
||||
window.removeEventListener('mousemove', onMouseMove);
|
||||
window.removeEventListener('mouseup', onMouseUp);
|
||||
}
|
||||
|
||||
@@ -66,7 +66,12 @@ export async function checkForUpdate() {
|
||||
}
|
||||
|
||||
// Derived
|
||||
export const sortedNotes = derived([notes, sortMode], ([$notes, $sortMode]) => {
|
||||
export const sortedNotes = derived(
|
||||
[notes, sortMode, viewMode],
|
||||
([$notes, $sortMode, $viewMode]) => {
|
||||
// Quick Access preserves stored order
|
||||
if ($viewMode === "quickaccess") return $notes;
|
||||
|
||||
const pinned = $notes.filter((n) => n.meta.pinned);
|
||||
const unpinned = $notes.filter((n) => !n.meta.pinned);
|
||||
|
||||
@@ -89,7 +94,8 @@ export const sortedNotes = derived([notes, sortMode], ([$notes, $sortMode]) => {
|
||||
};
|
||||
|
||||
return [...pinned.sort(sortFn), ...unpinned.sort(sortFn)];
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
export const vaultState = derived(
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user