mirror of
https://gitlab.com/ArkHost/HelixNotes.git
synced 2026-09-19 17:37:29 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f688cf4bd4 | ||
|
|
a95e240bcd | ||
|
|
bbcca553ce | ||
|
|
6a40f553f1 |
@@ -6,11 +6,12 @@ Your notes are stored as standard Markdown files on your local filesystem. No cl
|
||||
|
||||
## Download
|
||||
|
||||
| Platform | Download |
|
||||
|----------|----------|
|
||||
| Linux | [HelixNotes_1.0.1_amd64.AppImage](https://codeberg.org/ArkHost/HelixNotes/releases/download/v1.0.1/HelixNotes_1.0.1_amd64.AppImage) |
|
||||
| Windows | [HelixNotes_1.0.1_x64-setup.exe](https://codeberg.org/ArkHost/HelixNotes/releases/download/v1.0.1/HelixNotes_1.0.1_x64-setup.exe) |
|
||||
| macOS | Coming soon |
|
||||
| Platform | Download | Notes |
|
||||
|----------|----------|-------|
|
||||
| Linux (Arch/rolling) | [HelixNotes_1.0.3_amd64.AppImage](https://codeberg.org/ArkHost/HelixNotes/releases/download/v1.0.3/HelixNotes_1.0.3_amd64.AppImage) | Best for Arch, Fedora, openSUSE |
|
||||
| Linux (Debian/Ubuntu/Mint) | [HelixNotes_1.0.3_amd64.deb](https://codeberg.org/ArkHost/HelixNotes/releases/download/v1.0.3/HelixNotes_1.0.3_amd64.deb) | Ubuntu 22.04+ |
|
||||
| Windows | [HelixNotes_1.0.3_x64-setup.exe](https://codeberg.org/ArkHost/HelixNotes/releases/download/v1.0.3/HelixNotes_1.0.3_x64-setup.exe) | Windows 10/11 |
|
||||
| macOS | Coming soon | |
|
||||
|
||||
See all releases: [codeberg.org/ArkHost/HelixNotes/releases](https://codeberg.org/ArkHost/HelixNotes/releases)
|
||||
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"name": "helixnotes",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"version": "1.0.1",
|
||||
"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.1"
|
||||
version = "1.0.4"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dirs",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "helixnotes"
|
||||
version = "1.0.1"
|
||||
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())?;
|
||||
@@ -1341,3 +1380,18 @@ fn save_app_config(config: &AppConfig) -> Result<(), String> {
|
||||
std::fs::write(path, data).map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ── Install Type Detection ──
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_install_type() -> String {
|
||||
if cfg!(target_os = "windows") {
|
||||
"windows".to_string()
|
||||
} else if std::env::var("APPIMAGE").is_ok() {
|
||||
"appimage".to_string()
|
||||
} else if std::path::Path::new("/var/lib/dpkg/info/helix-notes.list").exists() {
|
||||
"deb".to_string()
|
||||
} else {
|
||||
"native".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,
|
||||
@@ -94,6 +95,7 @@ pub fn run() {
|
||||
commands::set_ai_settings,
|
||||
commands::test_ai_connection,
|
||||
commands::ai_ask,
|
||||
commands::get_install_type,
|
||||
]);
|
||||
|
||||
if close_to_tray {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"productName": "HelixNotes",
|
||||
"version": "1.0.1",
|
||||
"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");
|
||||
}
|
||||
@@ -309,3 +313,7 @@ export async function aiAsk(
|
||||
): Promise<void> {
|
||||
return invoke("ai_ask", { action, text, customPrompt, requestId });
|
||||
}
|
||||
|
||||
export async function getInstallType(): Promise<string> {
|
||||
return invoke("get_install_type");
|
||||
}
|
||||
|
||||
@@ -20,11 +20,15 @@
|
||||
showCommandPalette,
|
||||
theme,
|
||||
focusMode,
|
||||
activeNote
|
||||
activeNote,
|
||||
activeNotePath,
|
||||
editorDirty,
|
||||
showInfo,
|
||||
showSettings
|
||||
} from '$lib/stores/app';
|
||||
|
||||
const appWindow = getCurrentWindow();
|
||||
import { loadVaultState, saveVaultState } from '$lib/api';
|
||||
import { loadVaultState, saveVaultState, readNote } from '$lib/api';
|
||||
import { debounce } from '$lib/utils/debounce';
|
||||
import type { VaultState, FileEvent } from '$lib/types';
|
||||
|
||||
@@ -32,6 +36,39 @@
|
||||
let noteList: NoteList;
|
||||
let editor: Editor;
|
||||
let unlistenFileChange: (() => void) | null = null;
|
||||
let navigatingFromHistory = false;
|
||||
let noteHistory: string[] = [];
|
||||
let noteHistoryIndex = -1;
|
||||
|
||||
// Track note navigation in history stack
|
||||
$effect(() => {
|
||||
const path = $activeNotePath;
|
||||
if (navigatingFromHistory) {
|
||||
navigatingFromHistory = false;
|
||||
return;
|
||||
}
|
||||
if (path) {
|
||||
// Trim forward history and push
|
||||
noteHistory = [...noteHistory.slice(0, noteHistoryIndex + 1), path];
|
||||
noteHistoryIndex = noteHistory.length - 1;
|
||||
}
|
||||
});
|
||||
|
||||
function navigateHistory(direction: -1 | 1) {
|
||||
const newIndex = noteHistoryIndex + direction;
|
||||
if (newIndex < 0 || newIndex >= noteHistory.length) return;
|
||||
const path = noteHistory[newIndex];
|
||||
noteHistoryIndex = newIndex;
|
||||
navigatingFromHistory = true;
|
||||
readNote(path).then((content) => {
|
||||
$activeNote = content;
|
||||
$activeNotePath = path;
|
||||
$editorDirty = false;
|
||||
editor?.loadNote(path, content.content);
|
||||
}).catch(() => {
|
||||
// Note may have been deleted, ignore
|
||||
});
|
||||
}
|
||||
|
||||
const persistState = debounce(async () => {
|
||||
const state: VaultState = {
|
||||
@@ -69,7 +106,22 @@
|
||||
editor?.focusTitle();
|
||||
}
|
||||
|
||||
function handleMouseDown(e: MouseEvent) {
|
||||
if (e.button === 3) { e.preventDefault(); navigateHistory(-1); }
|
||||
if (e.button === 4) { e.preventDefault(); navigateHistory(1); }
|
||||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.altKey && e.key === 'ArrowLeft') {
|
||||
e.preventDefault();
|
||||
navigateHistory(-1);
|
||||
return;
|
||||
}
|
||||
if (e.altKey && e.key === 'ArrowRight') {
|
||||
e.preventDefault();
|
||||
navigateHistory(1);
|
||||
return;
|
||||
}
|
||||
if (e.ctrlKey && !e.shiftKey && e.key === 'n') {
|
||||
e.preventDefault();
|
||||
createAndFocusNote();
|
||||
@@ -77,9 +129,19 @@
|
||||
if (e.ctrlKey && e.shiftKey && e.key === 'N') {
|
||||
e.preventDefault();
|
||||
}
|
||||
if (e.ctrlKey && e.key === 'f') {
|
||||
if (e.ctrlKey && e.shiftKey && e.key === 'F') {
|
||||
e.preventDefault();
|
||||
$showSearch = true;
|
||||
return;
|
||||
}
|
||||
if (e.ctrlKey && !e.shiftKey && e.key === 'f') {
|
||||
e.preventDefault();
|
||||
if ($activeNotePath) {
|
||||
editor?.openNoteSearch();
|
||||
} else {
|
||||
$showSearch = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (e.ctrlKey && e.key === 'p') {
|
||||
e.preventDefault();
|
||||
@@ -90,7 +152,9 @@
|
||||
editor?.forceSave();
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
if ($focusMode) $focusMode = false;
|
||||
if ($showSettings) $showSettings = false;
|
||||
else if ($showInfo) $showInfo = false;
|
||||
else if ($focusMode) $focusMode = false;
|
||||
else if ($showSearch) $showSearch = false;
|
||||
else if ($showCommandPalette) $showCommandPalette = false;
|
||||
}
|
||||
@@ -142,7 +206,7 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
<svelte:window onkeydown={handleKeydown} onmousedown={handleMouseDown} />
|
||||
|
||||
<div class="app-shell">
|
||||
{#if $focusMode}
|
||||
|
||||
@@ -93,6 +93,14 @@
|
||||
let historySelected = $state<VersionEntry | null>(null);
|
||||
let historyLoading = $state(false);
|
||||
|
||||
// In-note search
|
||||
let noteSearchOpen = $state(false);
|
||||
let noteSearchQuery = $state('');
|
||||
let noteSearchIndex = $state(0);
|
||||
let noteSearchResults = $state<{from: number, to: number}[]>([]);
|
||||
let noteSearchInput = $state<HTMLInputElement>(null!);
|
||||
const noteSearchPluginKey = new PluginKey('noteSearch');
|
||||
|
||||
// Slash commands
|
||||
let slashMenu = $state<{ x: number; y: number; query: string; from: number; to: number } | null>(null);
|
||||
let slashSelectedIndex = $state(0);
|
||||
@@ -267,6 +275,31 @@
|
||||
codeLangDropdown = null;
|
||||
}
|
||||
|
||||
// ── In-note search extension ──
|
||||
const NoteSearchExtension = Extension.create({
|
||||
name: 'noteSearch',
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
key: noteSearchPluginKey,
|
||||
state: {
|
||||
init() { return DecorationSet.empty; },
|
||||
apply(tr, old) {
|
||||
const meta = tr.getMeta(noteSearchPluginKey);
|
||||
if (meta !== undefined) return meta;
|
||||
return old.map(tr.mapping, tr.doc);
|
||||
},
|
||||
},
|
||||
props: {
|
||||
decorations(state) {
|
||||
return this.getState(state);
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
const CodeBlockLanguageSelect = Extension.create({
|
||||
name: 'codeBlockLanguageSelect',
|
||||
addProseMirrorPlugins() {
|
||||
@@ -397,7 +430,14 @@
|
||||
closeSlashMenu();
|
||||
return true;
|
||||
}
|
||||
if (event.key === 'ArrowRight' || event.key === 'Tab') {
|
||||
if (event.key === 'Tab') {
|
||||
event.preventDefault();
|
||||
if (slashTableHover.rows > 0 && slashTableHover.cols > 0) {
|
||||
slashInsertTable(slashTableHover.rows, slashTableHover.cols);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (event.key === 'ArrowRight') {
|
||||
event.preventDefault();
|
||||
slashTableHover = { rows: Math.max(1, slashTableHover.rows), cols: Math.min(10, (slashTableHover.cols || 0) + 1) };
|
||||
return true;
|
||||
@@ -436,7 +476,7 @@
|
||||
slashSelectedIndex = (slashSelectedIndex - 1 + slashFiltered.length) % Math.max(1, slashFiltered.length);
|
||||
return true;
|
||||
}
|
||||
if (event.key === 'Enter') {
|
||||
if (event.key === 'Enter' || event.key === 'Tab') {
|
||||
if (slashFiltered.length > 0) {
|
||||
event.preventDefault();
|
||||
executeSlashCommand(slashSelectedIndex);
|
||||
@@ -1038,9 +1078,12 @@
|
||||
return '```' + lang + '\n' + code + '\n```\n';
|
||||
}
|
||||
case 'blockquote': {
|
||||
const inner: string[] = [];
|
||||
node.forEach((child: any) => inner.push(serializeNode(child)));
|
||||
return inner.join('').split('\n').filter((l: string) => l !== '').map((l: string) => '> ' + l).join('\n') + '\n';
|
||||
const blocks: string[] = [];
|
||||
node.forEach((child: any) => {
|
||||
const lines = serializeNode(child).replace(/\n$/, '').split('\n');
|
||||
blocks.push(lines.map((l: string) => '> ' + l).join('\n'));
|
||||
});
|
||||
return blocks.join('\n>\n') + '\n';
|
||||
}
|
||||
case 'bulletList': {
|
||||
const items: string[] = [];
|
||||
@@ -1144,6 +1187,91 @@
|
||||
return parts.join('');
|
||||
}
|
||||
|
||||
function autofocus(el: HTMLElement) {
|
||||
requestAnimationFrame(() => el.focus());
|
||||
}
|
||||
|
||||
// ── 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 = [];
|
||||
noteSearchIndex = 0;
|
||||
const tr = editor.state.tr.setMeta(noteSearchPluginKey, DecorationSet.empty);
|
||||
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) => {
|
||||
if (!node.isText || !node.text) return;
|
||||
const text = node.text.toLowerCase();
|
||||
let idx = text.indexOf(lowerQuery);
|
||||
while (idx !== -1) {
|
||||
results.push({ from: pos + idx, to: pos + idx + query.length });
|
||||
idx = text.indexOf(lowerQuery, idx + 1);
|
||||
}
|
||||
});
|
||||
noteSearchResults = results;
|
||||
if (noteSearchIndex >= results.length) noteSearchIndex = 0;
|
||||
applySearchDecorations();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function applySearchDecorations() {
|
||||
if (!editor) return;
|
||||
const decorations = noteSearchResults.map((m, i) =>
|
||||
Decoration.inline(m.from, m.to, { class: i === noteSearchIndex ? 'note-search-match note-search-active' : 'note-search-match' })
|
||||
);
|
||||
const decoSet = DecorationSet.create(editor.state.doc, decorations);
|
||||
const tr = editor.state.tr.setMeta(noteSearchPluginKey, decoSet);
|
||||
editor.view.dispatch(tr);
|
||||
scrollToCurrentMatch();
|
||||
}
|
||||
|
||||
function scrollToCurrentMatch() {
|
||||
if (!editor || noteSearchResults.length === 0) return;
|
||||
requestAnimationFrame(() => {
|
||||
const el = editor?.view.dom.querySelector('.note-search-active');
|
||||
if (el) {
|
||||
el.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function noteSearchNext() {
|
||||
if (noteSearchResults.length === 0) return;
|
||||
noteSearchIndex = (noteSearchIndex + 1) % noteSearchResults.length;
|
||||
applySearchDecorations();
|
||||
}
|
||||
|
||||
function noteSearchPrev() {
|
||||
if (noteSearchResults.length === 0) return;
|
||||
noteSearchIndex = (noteSearchIndex - 1 + noteSearchResults.length) % noteSearchResults.length;
|
||||
applySearchDecorations();
|
||||
}
|
||||
|
||||
export function openNoteSearch() {
|
||||
noteSearchOpen = true;
|
||||
}
|
||||
|
||||
function closeNoteSearch() {
|
||||
noteSearchOpen = false;
|
||||
noteSearchQuery = '';
|
||||
noteSearchResults = [];
|
||||
noteSearchIndex = 0;
|
||||
if (editor) {
|
||||
const tr = editor.state.tr.setMeta(noteSearchPluginKey, DecorationSet.empty);
|
||||
editor.view.dispatch(tr);
|
||||
editor.commands.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function stripAssetSrc(src: string): string {
|
||||
// blob: URLs are not persistable — they were temporary browser references
|
||||
if (src.startsWith('blob:')) return '';
|
||||
@@ -1418,6 +1546,7 @@
|
||||
DetailsContent,
|
||||
TextAlign.configure({ types: ['heading', 'paragraph'] }),
|
||||
SlashCommands,
|
||||
NoteSearchExtension,
|
||||
...($appConfig?.enable_wiki_links ? [WikiLink, WikiLinkAutocomplete] : []),
|
||||
],
|
||||
content: html,
|
||||
@@ -2294,6 +2423,16 @@
|
||||
{#if readOnly}
|
||||
<span class="readonly-indicator">View Mode</span>
|
||||
{/if}
|
||||
<button
|
||||
class="icon-btn"
|
||||
class:active={noteSearchOpen}
|
||||
onclick={() => noteSearchOpen ? closeNoteSearch() : openNoteSearch()}
|
||||
title="Find in note (Ctrl+F)"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="icon-btn"
|
||||
class:active={readOnly}
|
||||
@@ -2401,6 +2540,40 @@
|
||||
</div>
|
||||
|
||||
<div class="editor-body-wrapper">
|
||||
{#if noteSearchOpen}
|
||||
<div class="note-search-bar">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="opacity:0.5;flex-shrink:0"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
<input
|
||||
bind:this={noteSearchInput}
|
||||
type="text"
|
||||
class="note-search-input"
|
||||
placeholder="Find in note..."
|
||||
bind:value={noteSearchQuery}
|
||||
oninput={() => updateNoteSearch(noteSearchQuery)}
|
||||
onkeydown={(e) => {
|
||||
if (e.key === 'Enter') { e.preventDefault(); e.shiftKey ? noteSearchPrev() : noteSearchNext(); }
|
||||
if (e.key === 'Escape') { e.preventDefault(); closeNoteSearch(); }
|
||||
}}
|
||||
use:autofocus
|
||||
/>
|
||||
<span class="note-search-count">
|
||||
{#if noteSearchQuery && noteSearchResults.length > 0}
|
||||
{noteSearchIndex + 1} / {noteSearchResults.length}
|
||||
{:else if noteSearchQuery}
|
||||
No results
|
||||
{/if}
|
||||
</span>
|
||||
<button class="note-search-btn" onclick={noteSearchPrev} title="Previous (Shift+Enter)">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"/></svg>
|
||||
</button>
|
||||
<button class="note-search-btn" onclick={noteSearchNext} title="Next (Enter)">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
|
||||
</button>
|
||||
<button class="note-search-btn" onclick={closeNoteSearch} title="Close (Esc)">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="editor-body">
|
||||
{#if $sourceMode}
|
||||
<textarea
|
||||
@@ -3184,6 +3357,7 @@
|
||||
placeholder="Tell AI what to do with the selected text..."
|
||||
bind:value={aiCustomPrompt}
|
||||
onkeydown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); runAiAction('custom', aiCustomPrompt); } }}
|
||||
use:autofocus
|
||||
></textarea>
|
||||
<button class="ai-custom-submit" onclick={() => runAiAction('custom', aiCustomPrompt)} disabled={!aiCustomPrompt.trim()}>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13" /><polygon points="22 2 15 22 11 13 2 9 22 2" /></svg>
|
||||
@@ -3212,6 +3386,7 @@
|
||||
placeholder="Describe the note you want to create..."
|
||||
bind:value={aiCustomPrompt}
|
||||
onkeydown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); runAiAction('custom', aiCustomPrompt); } }}
|
||||
use:autofocus
|
||||
></textarea>
|
||||
<button class="ai-custom-submit" onclick={() => runAiAction('custom', aiCustomPrompt)} disabled={!aiCustomPrompt.trim()}>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 8V4l-2-2"/><rect x="4" y="8" width="16" height="12" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M9 13v2"/><path d="M15 13v2"/></svg>
|
||||
@@ -3600,9 +3775,58 @@
|
||||
.editor-body-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.note-search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.note-search-input {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-primary);
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
min-width: 0;
|
||||
}
|
||||
.note-search-input::placeholder {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.note-search-count {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.note-search-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
padding: 2px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.note-search-btn:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
:global(.note-search-match) {
|
||||
background: rgba(255, 200, 0, 0.3);
|
||||
border-radius: 2px;
|
||||
}
|
||||
:global(.note-search-active) {
|
||||
background: rgba(255, 150, 0, 0.6);
|
||||
}
|
||||
|
||||
.editor-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
||||
@@ -114,13 +114,16 @@
|
||||
<h4 class="shortcuts-group-title">Keyboard Shortcuts</h4>
|
||||
<div class="shortcut-row"><span class="shortcut-desc">New note</span><span class="shortcut-keys"><kbd>Ctrl</kbd>+<kbd>N</kbd></span></div>
|
||||
<div class="shortcut-row"><span class="shortcut-desc">Quick open</span><span class="shortcut-keys"><kbd>Ctrl</kbd>+<kbd>P</kbd></span></div>
|
||||
<div class="shortcut-row"><span class="shortcut-desc">Search</span><span class="shortcut-keys"><kbd>Ctrl</kbd>+<kbd>F</kbd></span></div>
|
||||
<div class="shortcut-row"><span class="shortcut-desc">Find in note</span><span class="shortcut-keys"><kbd>Ctrl</kbd>+<kbd>F</kbd></span></div>
|
||||
<div class="shortcut-row"><span class="shortcut-desc">Search vault</span><span class="shortcut-keys"><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>F</kbd></span></div>
|
||||
<div class="shortcut-row"><span class="shortcut-desc">Save</span><span class="shortcut-keys"><kbd>Ctrl</kbd>+<kbd>S</kbd></span></div>
|
||||
<div class="shortcut-row"><span class="shortcut-desc">Bold</span><span class="shortcut-keys"><kbd>Ctrl</kbd>+<kbd>B</kbd></span></div>
|
||||
<div class="shortcut-row"><span class="shortcut-desc">Italic</span><span class="shortcut-keys"><kbd>Ctrl</kbd>+<kbd>I</kbd></span></div>
|
||||
<div class="shortcut-row"><span class="shortcut-desc">Underline</span><span class="shortcut-keys"><kbd>Ctrl</kbd>+<kbd>U</kbd></span></div>
|
||||
<div class="shortcut-row"><span class="shortcut-desc">Undo</span><span class="shortcut-keys"><kbd>Ctrl</kbd>+<kbd>Z</kbd></span></div>
|
||||
<div class="shortcut-row"><span class="shortcut-desc">Redo</span><span class="shortcut-keys"><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>Z</kbd></span></div>
|
||||
<div class="shortcut-row"><span class="shortcut-desc">Go back</span><span class="shortcut-keys"><kbd>Alt</kbd>+<kbd>←</kbd></span></div>
|
||||
<div class="shortcut-row"><span class="shortcut-desc">Go forward</span><span class="shortcut-keys"><kbd>Alt</kbd>+<kbd>→</kbd></span></div>
|
||||
<div class="shortcut-row"><span class="shortcut-desc">Exit focus mode</span><span class="shortcut-keys"><kbd>Esc</kbd></span></div>
|
||||
|
||||
<h4 class="shortcuts-group-title">Editor Commands</h4>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { showSettings, theme, appConfig } from '$lib/stores/app';
|
||||
import { showSettings, theme, appConfig, updateAvailable as globalUpdateAvailable, installType, settingsTab } from '$lib/stores/app';
|
||||
import { setTheme, setAccentColor, setFontSize, setFontFamily, setGeneralSettings, importObsidian, createBackup, listBackups, restoreBackup, deleteBackup, setBackupSettings, setAiSettings, testAiConnection } from '$lib/api';
|
||||
import { open as openDialog } from '@tauri-apps/plugin-dialog';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
@@ -24,6 +24,23 @@
|
||||
}
|
||||
loadAppVersion();
|
||||
|
||||
// Switch to requested tab if set externally (e.g. from update badge)
|
||||
$effect(() => {
|
||||
const tab = $settingsTab;
|
||||
if (tab) {
|
||||
activeTab = tab as Tab;
|
||||
$settingsTab = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Pre-populate from global store if update was already detected at startup
|
||||
$effect(() => {
|
||||
const global = $globalUpdateAvailable;
|
||||
if (global && !updateAvailable) {
|
||||
updateAvailable = { version: global.version, body: global.body };
|
||||
}
|
||||
});
|
||||
|
||||
async function handleCheckUpdate() {
|
||||
updateChecking = true;
|
||||
updateMessage = null;
|
||||
@@ -33,6 +50,7 @@
|
||||
if (update) {
|
||||
updateObj = update;
|
||||
updateAvailable = { version: update.version, body: update.body, date: update.date };
|
||||
globalUpdateAvailable.set({ version: update.version, body: update.body });
|
||||
updateMessage = { type: 'info', text: `Version ${update.version} is available!` };
|
||||
} else {
|
||||
updateMessage = { type: 'success', text: 'You are on the latest version.' };
|
||||
@@ -1034,6 +1052,7 @@
|
||||
<div class="update-notes">{updateAvailable.body}</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if $installType === 'appimage' || $installType === 'windows'}
|
||||
<button class="update-install-btn" onclick={handleDownloadAndInstall} disabled={updateDownloading}>
|
||||
{#if updateDownloading}
|
||||
<svg class="spinner-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10" opacity="0.25" /><path d="M12 2a10 10 0 019.95 9" /></svg>
|
||||
@@ -1050,6 +1069,19 @@
|
||||
<div class="update-progress-fill" style="width: {updateProgress}%"></div>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if $installType === 'deb'}
|
||||
<div class="update-apt-info">
|
||||
<p>Update via your package manager:</p>
|
||||
<code>sudo apt update && sudo apt upgrade helix-notes</code>
|
||||
</div>
|
||||
{:else}
|
||||
<a class="update-install-btn" href="https://codeberg.org/ArkHost/HelixNotes/releases" target="_blank" rel="noopener">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/>
|
||||
</svg>
|
||||
Download from Codeberg
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1918,6 +1950,23 @@
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.update-apt-info {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.update-apt-info p {
|
||||
margin: 0 0 6px 0;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.update-apt-info code {
|
||||
display: block;
|
||||
padding: 8px 12px;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
user-select: all;
|
||||
}
|
||||
|
||||
.update-progress-bar {
|
||||
margin-top: 10px;
|
||||
height: 6px;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||
import { vaultReady, focusMode } from '$lib/stores/app';
|
||||
import { vaultReady, focusMode, updateAvailable, showSettings, settingsTab } from '$lib/stores/app';
|
||||
|
||||
let { onNewNote = () => {} }: {
|
||||
onNewNote?: () => void;
|
||||
@@ -71,6 +71,12 @@
|
||||
<line x1="29" y1="18" x2="19" y2="30" stroke="white" stroke-width="2" stroke-linecap="round" opacity="0.7" />
|
||||
</svg>
|
||||
<span class="titlebar-title">HelixNotes</span>
|
||||
{#if $updateAvailable}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<button class="update-badge" onmousedown={(e) => e.stopPropagation()} onclick={() => { $settingsTab = 'updates'; $showSettings = true; }}>
|
||||
v{$updateAvailable.version} available
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="titlebar-actions">
|
||||
<button class="switch-vault-btn" onclick={() => ($vaultReady = false)} title="Switch Vault">
|
||||
@@ -144,6 +150,25 @@
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.update-badge {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: var(--accent);
|
||||
background: color-mix(in srgb, var(--accent) 12%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--accent) 25%, transparent);
|
||||
border-radius: 10px;
|
||||
padding: 1px 8px;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
-webkit-app-region: no-drag;
|
||||
transition: background 0.15s, border-color 0.15s;
|
||||
}
|
||||
|
||||
.update-badge:hover {
|
||||
background: color-mix(in srgb, var(--accent) 20%, transparent);
|
||||
border-color: color-mix(in srgb, var(--accent) 40%, transparent);
|
||||
}
|
||||
|
||||
.titlebar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
+28
-2
@@ -23,6 +23,7 @@ export const searchQuery = writable("");
|
||||
export const showCommandPalette = writable(false);
|
||||
export const showSearch = writable(false);
|
||||
export const showSettings = writable(false);
|
||||
export const settingsTab = writable<string | null>(null);
|
||||
export const showInfo = writable(false);
|
||||
export const notebookIcons = writable<Record<string, string>>({});
|
||||
export const quickAccessPaths = writable<string[]>([]);
|
||||
@@ -45,8 +46,32 @@ export const focusMode = writable(false);
|
||||
// Theme
|
||||
export const theme = writable<string>("system");
|
||||
|
||||
// Update state
|
||||
export const updateAvailable = writable<{
|
||||
version: string;
|
||||
body?: string;
|
||||
} | null>(null);
|
||||
export const installType = writable<string>("native");
|
||||
|
||||
export async function checkForUpdate() {
|
||||
try {
|
||||
const { check } = await import("@tauri-apps/plugin-updater");
|
||||
const update = await check();
|
||||
if (update) {
|
||||
updateAvailable.set({ version: update.version, body: update.body });
|
||||
}
|
||||
} catch {
|
||||
// Silent fail — don't disrupt app startup
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
@@ -69,7 +94,8 @@ export const sortedNotes = derived([notes, sortMode], ([$notes, $sortMode]) => {
|
||||
};
|
||||
|
||||
return [...pinned.sort(sortFn), ...unpinned.sort(sortFn)];
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
export const vaultState = derived(
|
||||
[
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import '../app.css';
|
||||
import { theme, appConfig, activeNotePath } from '$lib/stores/app';
|
||||
import { theme, appConfig, activeNotePath, installType, checkForUpdate } from '$lib/stores/app';
|
||||
import { openUrl } from '@tauri-apps/plugin-opener';
|
||||
import { openFile } from '$lib/api';
|
||||
import { openFile, getInstallType } from '$lib/api';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
let { children } = $props();
|
||||
@@ -31,6 +31,12 @@
|
||||
return resolved.join('/');
|
||||
}
|
||||
|
||||
// Detect install type and check for updates on startup
|
||||
onMount(() => {
|
||||
getInstallType().then(t => installType.set(t)).catch(() => {});
|
||||
checkForUpdate();
|
||||
});
|
||||
|
||||
// Intercept all link clicks in capture phase to prevent webview navigation
|
||||
onMount(() => {
|
||||
function handleLinkClick(e: MouseEvent) {
|
||||
|
||||
Reference in New Issue
Block a user