mirror of
https://gitlab.com/ArkHost/HelixNotes.git
synced 2026-09-19 09:27:29 +02:00
Compare commits
2
Commits
4a1045b41e
...
1a880a4f96
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a880a4f96 | ||
|
|
f4e05ed9c4 |
@@ -1398,6 +1398,7 @@ pub fn set_general_settings(
|
|||||||
show_line_numbers: bool,
|
show_line_numbers: bool,
|
||||||
show_link_arrows: bool,
|
show_link_arrows: bool,
|
||||||
default_view_mode: bool,
|
default_view_mode: bool,
|
||||||
|
new_notes_in_source_mode: bool,
|
||||||
show_tray_icon: bool,
|
show_tray_icon: bool,
|
||||||
close_to_tray: bool,
|
close_to_tray: bool,
|
||||||
enable_wiki_links: bool,
|
enable_wiki_links: bool,
|
||||||
@@ -1432,6 +1433,7 @@ pub fn set_general_settings(
|
|||||||
config.show_line_numbers = show_line_numbers;
|
config.show_line_numbers = show_line_numbers;
|
||||||
config.show_link_arrows = show_link_arrows;
|
config.show_link_arrows = show_link_arrows;
|
||||||
config.default_view_mode = default_view_mode;
|
config.default_view_mode = default_view_mode;
|
||||||
|
config.new_notes_in_source_mode = new_notes_in_source_mode;
|
||||||
config.show_tray_icon = show_tray_icon;
|
config.show_tray_icon = show_tray_icon;
|
||||||
config.close_to_tray = close_to_tray;
|
config.close_to_tray = close_to_tray;
|
||||||
config.enable_wiki_links = enable_wiki_links;
|
config.enable_wiki_links = enable_wiki_links;
|
||||||
|
|||||||
@@ -197,6 +197,8 @@ pub struct AppConfig {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub default_view_mode: bool,
|
pub default_view_mode: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
pub new_notes_in_source_mode: bool,
|
||||||
|
#[serde(default)]
|
||||||
pub show_tray_icon: bool,
|
pub show_tray_icon: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub close_to_tray: bool,
|
pub close_to_tray: bool,
|
||||||
@@ -315,6 +317,7 @@ impl Default for AppConfig {
|
|||||||
ai_model: "claude-sonnet-4-6".to_string(),
|
ai_model: "claude-sonnet-4-6".to_string(),
|
||||||
ai_writing_style: None,
|
ai_writing_style: None,
|
||||||
default_view_mode: false,
|
default_view_mode: false,
|
||||||
|
new_notes_in_source_mode: false,
|
||||||
show_tray_icon: false,
|
show_tray_icon: false,
|
||||||
close_to_tray: false,
|
close_to_tray: false,
|
||||||
enable_wiki_links: true,
|
enable_wiki_links: true,
|
||||||
|
|||||||
@@ -265,6 +265,7 @@ export async function setGeneralSettings(
|
|||||||
showLineNumbers: boolean,
|
showLineNumbers: boolean,
|
||||||
showLinkArrows: boolean,
|
showLinkArrows: boolean,
|
||||||
defaultViewMode: boolean,
|
defaultViewMode: boolean,
|
||||||
|
newNotesInSourceMode: boolean,
|
||||||
showTrayIcon: boolean,
|
showTrayIcon: boolean,
|
||||||
closeToTray: boolean,
|
closeToTray: boolean,
|
||||||
enableWikiLinks: boolean,
|
enableWikiLinks: boolean,
|
||||||
@@ -291,6 +292,7 @@ export async function setGeneralSettings(
|
|||||||
showLineNumbers,
|
showLineNumbers,
|
||||||
showLinkArrows,
|
showLinkArrows,
|
||||||
defaultViewMode,
|
defaultViewMode,
|
||||||
|
newNotesInSourceMode,
|
||||||
showTrayIcon,
|
showTrayIcon,
|
||||||
closeToTray,
|
closeToTray,
|
||||||
enableWikiLinks,
|
enableWikiLinks,
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
const appWindow = getCurrentWindow();
|
const appWindow = getCurrentWindow();
|
||||||
const isMac = navigator.platform.startsWith('Mac');
|
const isMac = navigator.platform.startsWith('Mac');
|
||||||
const isMobile = $derived($platformIsMobile);
|
const isMobile = $derived($platformIsMobile);
|
||||||
import { loadVaultState, saveVaultState, readNote, createDailyNote, createBackup, getPendingOpenFile, addQuickAccess, removeQuickAccess, getQuickAccess, setTheme, syncNow, getAppConfig, setTaskDone, setTaskPriority, setTaskDue, findOrphanedAttachments, trashOrphanedAttachments } from '$lib/api';
|
import { loadVaultState, saveVaultState, readNote, deleteNote, createDailyNote, createBackup, getPendingOpenFile, addQuickAccess, removeQuickAccess, getQuickAccess, setTheme, syncNow, getAppConfig, setTaskDone, setTaskPriority, setTaskDue, findOrphanedAttachments, trashOrphanedAttachments } from '$lib/api';
|
||||||
import { darkThemes, isAndroid } from '$lib/platform';
|
import { darkThemes, isAndroid } from '$lib/platform';
|
||||||
import { debounce } from '$lib/utils/debounce';
|
import { debounce } from '$lib/utils/debounce';
|
||||||
import { openNoteWindow } from '$lib/utils/window';
|
import { openNoteWindow } from '$lib/utils/window';
|
||||||
@@ -449,6 +449,28 @@
|
|||||||
else if ($mobileView === 'notelist') $mobileView = 'sidebar';
|
else if ($mobileView === 'notelist') $mobileView = 'sidebar';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function trashOpenNote(path: string): Promise<boolean> {
|
||||||
|
if (path !== $activeNotePath || $viewerNote || $viewMode === 'trash') return false;
|
||||||
|
try {
|
||||||
|
await deleteNote(path);
|
||||||
|
if (Object.hasOwn($noteOrder, path)) {
|
||||||
|
const { [path]: _, ...rest } = $noteOrder;
|
||||||
|
$noteOrder = rest;
|
||||||
|
}
|
||||||
|
$notes = $notes.filter((note) => note.path !== path);
|
||||||
|
if ($activeNotePath === path) {
|
||||||
|
$activeNote = null;
|
||||||
|
$activeNotePath = null;
|
||||||
|
}
|
||||||
|
noteList?.refresh(true).catch((error) => console.error('Failed to refresh notes after trashing:', error));
|
||||||
|
if (isMobile) $mobileView = 'notelist';
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to move open note to Trash:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleMouseDown(e: MouseEvent) {
|
function handleMouseDown(e: MouseEvent) {
|
||||||
if (e.button === 3) { e.preventDefault(); navigateHistory(-1); }
|
if (e.button === 3) { e.preventDefault(); navigateHistory(-1); }
|
||||||
if (e.button === 4) { e.preventDefault(); navigateHistory(1); }
|
if (e.button === 4) { e.preventDefault(); navigateHistory(1); }
|
||||||
@@ -467,7 +489,7 @@
|
|||||||
// Ctrl/Cmd+Shift+Delete: move the open note to the trash.
|
// Ctrl/Cmd+Shift+Delete: move the open note to the trash.
|
||||||
if (mod && e.shiftKey && code === 'Delete' && $activeNotePath) {
|
if (mod && e.shiftKey && code === 'Delete' && $activeNotePath) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
noteList?.trashActiveNote();
|
editor?.moveOpenNoteToTrash();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -951,7 +973,7 @@
|
|||||||
<NoteList bind:this={noteList} onNoteSelected={handleNoteSelected} onBeforeNoteSwitch={() => editor?.flushSave()} onNoteMoved={() => sidebar?.refresh()} onNoteCreated={() => { editor?.focusTitle(); }} onToggleTask={toggleTask} onSetTaskPriority={changeTaskPriority} onSetTaskDue={changeTaskDue} />
|
<NoteList bind:this={noteList} onNoteSelected={handleNoteSelected} onBeforeNoteSwitch={() => editor?.flushSave()} onNoteMoved={() => sidebar?.refresh()} onNoteCreated={() => { editor?.focusTitle(); }} onToggleTask={toggleTask} onSetTaskPriority={changeTaskPriority} onSetTaskDue={changeTaskDue} />
|
||||||
</div>
|
</div>
|
||||||
<div class="mobile-panel" class:active={$mobileView === 'editor'}>
|
<div class="mobile-panel" class:active={$mobileView === 'editor'}>
|
||||||
<Editor bind:this={editor} />
|
<Editor bind:this={editor} onMoveToTrash={trashOpenNote} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1028,7 +1050,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="editor-panel">
|
<div class="editor-panel">
|
||||||
<Editor bind:this={editor} />
|
<Editor bind:this={editor} onMoveToTrash={trashOpenNote} />
|
||||||
{#if $viewMode === 'tasks' && !taskNoteOpened}
|
{#if $viewMode === 'tasks' && !taskNoteOpened}
|
||||||
<div class="tasks-editor-placeholder">
|
<div class="tasks-editor-placeholder">
|
||||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
|||||||
@@ -24,6 +24,8 @@
|
|||||||
import TextAlign from '@tiptap/extension-text-align';
|
import TextAlign from '@tiptap/extension-text-align';
|
||||||
import { common, createLowlight } from 'lowlight';
|
import { common, createLowlight } from 'lowlight';
|
||||||
import powershell from 'highlight.js/lib/languages/powershell';
|
import powershell from 'highlight.js/lib/languages/powershell';
|
||||||
|
import hljs from 'highlight.js/lib/core';
|
||||||
|
import markdownLanguage from 'highlight.js/lib/languages/markdown';
|
||||||
import MarkdownIt from 'markdown-it';
|
import MarkdownIt from 'markdown-it';
|
||||||
import markdownItMark from 'markdown-it-mark';
|
import markdownItMark from 'markdown-it-mark';
|
||||||
import markdownItSup from 'markdown-it-sup';
|
import markdownItSup from 'markdown-it-sup';
|
||||||
@@ -55,7 +57,14 @@
|
|||||||
import { isMobile, isAndroid } from '$lib/platform';
|
import { isMobile, isAndroid } from '$lib/platform';
|
||||||
import ResizeHandle from './ResizeHandle.svelte';
|
import ResizeHandle from './ResizeHandle.svelte';
|
||||||
|
|
||||||
|
let { onMoveToTrash = async () => false }: {
|
||||||
|
onMoveToTrash?: (path: string) => Promise<boolean>;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
const modKey = navigator.platform.startsWith('Mac') ? '⌘' : 'Ctrl';
|
const modKey = navigator.platform.startsWith('Mac') ? '⌘' : 'Ctrl';
|
||||||
|
const sourceHighlighter = hljs.newInstance();
|
||||||
|
sourceHighlighter.registerLanguage('markdown', markdownLanguage);
|
||||||
|
const SOURCE_HIGHLIGHT_MAX_CHARS = 32_000;
|
||||||
|
|
||||||
// Track virtual keyboard height on mobile via visualViewport
|
// Track virtual keyboard height on mobile via visualViewport
|
||||||
let keyboardHeight = $state(0);
|
let keyboardHeight = $state(0);
|
||||||
@@ -68,6 +77,7 @@
|
|||||||
|
|
||||||
let editorElement = $state<HTMLDivElement>(null!);
|
let editorElement = $state<HTMLDivElement>(null!);
|
||||||
let sourceElement = $state<HTMLTextAreaElement>(null!);
|
let sourceElement = $state<HTMLTextAreaElement>(null!);
|
||||||
|
let sourceHighlightElement = $state<HTMLPreElement>(null!);
|
||||||
const LARGE_DOC_CHARS = 100_000;
|
const LARGE_DOC_CHARS = 100_000;
|
||||||
let isLargeDoc = $state(false);
|
let isLargeDoc = $state(false);
|
||||||
let editor: Editor | null = null;
|
let editor: Editor | null = null;
|
||||||
@@ -83,6 +93,10 @@
|
|||||||
});
|
});
|
||||||
let editorReady = $state(false);
|
let editorReady = $state(false);
|
||||||
let sourceContent = $state('');
|
let sourceContent = $state('');
|
||||||
|
let sourceHighlightHtml = $derived.by(() => {
|
||||||
|
if (sourceContent.length > SOURCE_HIGHLIGHT_MAX_CHARS) return escapeHtml(sourceContent);
|
||||||
|
return sourceHighlighter.highlight(sourceContent, { language: 'markdown', ignoreIllegals: true }).value;
|
||||||
|
});
|
||||||
let sourceHistory: Array<{ content: string; cursor: number }> = [];
|
let sourceHistory: Array<{ content: string; cursor: number }> = [];
|
||||||
let sourceHistoryIndex = -1;
|
let sourceHistoryIndex = -1;
|
||||||
let sourceHistoryTimer: ReturnType<typeof setTimeout> | null = null;
|
let sourceHistoryTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
@@ -93,6 +107,7 @@
|
|||||||
let pendingContent = $state<string | null>(null);
|
let pendingContent = $state<string | null>(null);
|
||||||
let ignoreNextUpdate = false;
|
let ignoreNextUpdate = false;
|
||||||
let isLoadingNote = false;
|
let isLoadingNote = false;
|
||||||
|
let trashingNote = $state(false);
|
||||||
let fixingBlobsPromise: Promise<void> = Promise.resolve();
|
let fixingBlobsPromise: Promise<void> = Promise.resolve();
|
||||||
let hasPendingBlobs = false;
|
let hasPendingBlobs = false;
|
||||||
let lastSourceMode = $sourceMode;
|
let lastSourceMode = $sourceMode;
|
||||||
@@ -123,6 +138,18 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function syncSourceEditorScroll() {
|
||||||
|
if (sourceHighlightElement && sourceElement) {
|
||||||
|
sourceHighlightElement.scrollTop = sourceElement.scrollTop;
|
||||||
|
sourceHighlightElement.scrollLeft = sourceElement.scrollLeft;
|
||||||
|
}
|
||||||
|
if ($appConfig?.show_line_numbers && sourceElement) {
|
||||||
|
const clip = sourceElement.closest('.editor-body')?.querySelector('.line-numbers-clip');
|
||||||
|
const gutter = clip?.firstElementChild as HTMLElement | null;
|
||||||
|
if (gutter) gutter.style.transform = `translateY(-${sourceElement.scrollTop}px)`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function clearTaskReveal() {
|
function clearTaskReveal() {
|
||||||
if (taskRevealTimer) clearTimeout(taskRevealTimer);
|
if (taskRevealTimer) clearTimeout(taskRevealTimer);
|
||||||
taskRevealTimer = null;
|
taskRevealTimer = null;
|
||||||
@@ -2971,8 +2998,16 @@
|
|||||||
return src;
|
return src;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let saveQueue: Promise<void> = Promise.resolve();
|
||||||
|
|
||||||
|
function queueSave(task: () => Promise<void>): Promise<void> {
|
||||||
|
const queued = saveQueue.then(task);
|
||||||
|
saveQueue = queued.catch(() => {});
|
||||||
|
return queued;
|
||||||
|
}
|
||||||
|
|
||||||
const autoSave = debounce(async () => {
|
const autoSave = debounce(async () => {
|
||||||
if (get(viewerNote)) return; // never autosave external viewer files
|
if (get(viewerNote) || trashingNote) return; // never autosave external viewer files or a note being trashed
|
||||||
if (!$activeNote || !$activeNotePath || !$editorDirty) return;
|
if (!$activeNote || !$activeNotePath || !$editorDirty) return;
|
||||||
// Only fix blob images if a paste occurred (avoids full doc scan on every save)
|
// Only fix blob images if a paste occurred (avoids full doc scan on every save)
|
||||||
if (hasPendingBlobs) {
|
if (hasPendingBlobs) {
|
||||||
@@ -2980,38 +3015,65 @@
|
|||||||
fixingBlobsPromise = fixBlobImages();
|
fixingBlobsPromise = fixBlobImages();
|
||||||
}
|
}
|
||||||
await fixingBlobsPromise;
|
await fixingBlobsPromise;
|
||||||
|
if (trashingNote || !$activeNote || !$activeNotePath) return;
|
||||||
try {
|
try {
|
||||||
|
const path = $activeNotePath;
|
||||||
|
const note = $activeNote;
|
||||||
const body = $sourceMode
|
const body = $sourceMode
|
||||||
? restoreTitleH1(sourceContent)
|
? restoreTitleH1(sourceContent)
|
||||||
: editorToMarkdown();
|
: editorToMarkdown();
|
||||||
// Safety: never save empty/near-empty body over a note that had real content
|
// Safety: never save empty/near-empty body over a note that had real content
|
||||||
const trimmed = body.replace(/^#.*\n?/, '').trim();
|
const trimmed = body.replace(/^#.*\n?/, '').trim();
|
||||||
if (!trimmed && $activeNote.content && $activeNote.content.trim().length > 10) {
|
if (!trimmed && note.content && note.content.trim().length > 10) {
|
||||||
console.warn('Auto-save blocked: refusing to overwrite note with empty content');
|
console.warn('Auto-save blocked: refusing to overwrite note with empty content');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await saveNote($activeNotePath, $activeNote.meta, body);
|
await queueSave(() => saveNote(path, note.meta, body));
|
||||||
$editorDirty = false;
|
if ($activeNotePath === path) $editorDirty = false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Auto-save failed:', e);
|
console.error('Auto-save failed:', e);
|
||||||
}
|
}
|
||||||
}, isMobile ? 1500 : 500);
|
}, isMobile ? 1500 : 500);
|
||||||
|
|
||||||
export async function forceSave() {
|
export async function forceSave(allowWhileTrashing = false): Promise<boolean> {
|
||||||
if (get(viewerNote)) return; // viewer files are never written back
|
if (get(viewerNote) || (trashingNote && !allowWhileTrashing)) return false;
|
||||||
if (!$activeNote || !$activeNotePath) return;
|
if (!$activeNote || !$activeNotePath) return false;
|
||||||
await fixingBlobsPromise;
|
await fixingBlobsPromise;
|
||||||
|
if (trashingNote && !allowWhileTrashing) return false;
|
||||||
|
if (!$activeNote || !$activeNotePath) return false;
|
||||||
try {
|
try {
|
||||||
|
const path = $activeNotePath;
|
||||||
|
const note = $activeNote;
|
||||||
const body = $sourceMode ? restoreTitleH1(sourceContent) : editorToMarkdown();
|
const body = $sourceMode ? restoreTitleH1(sourceContent) : editorToMarkdown();
|
||||||
const trimmed = body.replace(/^#.*\n?/, '').trim();
|
const trimmed = body.replace(/^#.*\n?/, '').trim();
|
||||||
if (!trimmed && $activeNote.content && $activeNote.content.trim().length > 10) {
|
if (!trimmed && note.content && note.content.trim().length > 10) {
|
||||||
console.warn('Force-save blocked: refusing to overwrite note with empty content');
|
console.warn('Force-save blocked: refusing to overwrite note with empty content');
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
await saveNote($activeNotePath, $activeNote.meta, body);
|
await queueSave(() => saveNote(path, note.meta, body));
|
||||||
$editorDirty = false;
|
if ($activeNotePath === path) $editorDirty = false;
|
||||||
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Save failed:', e);
|
console.error('Save failed:', e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function moveOpenNoteToTrash(): Promise<boolean> {
|
||||||
|
if (trashingNote || !$activeNotePath || $viewerNote) return false;
|
||||||
|
const path = $activeNotePath;
|
||||||
|
const wasDirty = $editorDirty;
|
||||||
|
trashingNote = true;
|
||||||
|
try {
|
||||||
|
// Drain any save already in flight, then persist the latest editor state.
|
||||||
|
await saveQueue;
|
||||||
|
if (wasDirty && !(await forceSave(true))) return false;
|
||||||
|
if ($activeNotePath !== path) return false;
|
||||||
|
// A queued debounce observes this flag and cannot recreate the moved note.
|
||||||
|
$editorDirty = false;
|
||||||
|
return await onMoveToTrash(path);
|
||||||
|
} finally {
|
||||||
|
trashingNote = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3271,13 +3333,16 @@
|
|||||||
const revealRequest = ++taskRevealRequest;
|
const revealRequest = ++taskRevealRequest;
|
||||||
const revealTarget = taskTarget ? resolveTaskTarget(taskTarget, content) : null;
|
const revealTarget = taskTarget ? resolveTaskTarget(taskTarget, content) : null;
|
||||||
loadedPath = path;
|
loadedPath = path;
|
||||||
lastSourceMode = $sourceMode;
|
|
||||||
isLoadingNote = true;
|
isLoadingNote = true;
|
||||||
isLargeDoc = content.length > LARGE_DOC_CHARS;
|
isLargeDoc = content.length > LARGE_DOC_CHARS;
|
||||||
// Apply default view mode when switching notes - but new notes always open in edit mode.
|
// Viewer mode (external file) always forces read-only. New notes stay editable and
|
||||||
// Viewer mode (external file) always forces read-only.
|
// can opt into source mode without changing how existing notes choose their mode.
|
||||||
const isViewer = !!get(viewerNote);
|
const isViewer = !!get(viewerNote);
|
||||||
const isNewNote = $activeNote?.meta.title === 'Untitled' && !content.replace(/^---[\s\S]*?---\s*/, '').trim();
|
const isNewNote = $activeNote?.meta.title === 'Untitled' && !content.replace(/^---[\s\S]*?---\s*/, '').trim();
|
||||||
|
if (isNewNote && ($appConfig?.new_notes_in_source_mode ?? false)) {
|
||||||
|
$sourceMode = true;
|
||||||
|
}
|
||||||
|
lastSourceMode = $sourceMode;
|
||||||
const shouldBeReadOnly = isViewer ? true : (isNewNote ? false : ($appConfig?.default_view_mode ?? false));
|
const shouldBeReadOnly = isViewer ? true : (isNewNote ? false : ($appConfig?.default_view_mode ?? false));
|
||||||
$readOnly = shouldBeReadOnly;
|
$readOnly = shouldBeReadOnly;
|
||||||
if (editor) editor.setEditable(!shouldBeReadOnly);
|
if (editor) editor.setEditable(!shouldBeReadOnly);
|
||||||
@@ -6025,6 +6090,13 @@
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{#if isMobile}
|
||||||
|
<button type="button" class="icon-btn editor-trash-btn mobile" onclick={moveOpenNoteToTrash} disabled={trashingNote} title="Move to Trash" aria-label="Move note to Trash">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M3 6h18"/><path d="M19 6v14H5V6"/><path d="M8 6V4h8v2"/><path d="M10 11v5"/><path d="M14 11v5"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
{#if !isMobile}
|
{#if !isMobile}
|
||||||
<div class="toolbar-actions">
|
<div class="toolbar-actions">
|
||||||
{#if $canGoBack || $canGoForward}
|
{#if $canGoBack || $canGoForward}
|
||||||
@@ -6173,6 +6245,11 @@
|
|||||||
<path d="M5.854 4.854a.5.5 0 10-.708-.708l-3.5 3.5a.5.5 0 000 .708l3.5 3.5a.5.5 0 00.708-.708L2.707 8l3.147-3.146zm4.292 0a.5.5 0 01.708-.708l3.5 3.5a.5.5 0 010 .708l-3.5 3.5a.5.5 0 01-.708-.708L13.293 8l-3.147-3.146z" />
|
<path d="M5.854 4.854a.5.5 0 10-.708-.708l-3.5 3.5a.5.5 0 000 .708l3.5 3.5a.5.5 0 00.708-.708L2.707 8l3.147-3.146zm4.292 0a.5.5 0 01.708-.708l3.5 3.5a.5.5 0 010 .708l-3.5 3.5a.5.5 0 01-.708-.708L13.293 8l-3.147-3.146z" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" class="icon-btn editor-trash-btn" onclick={moveOpenNoteToTrash} disabled={trashingNote} title="Move to Trash" aria-label="Move note to Trash">
|
||||||
|
<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="M3 6h18"/><path d="M19 6v14H5V6"/><path d="M8 6V4h8v2"/><path d="M10 11v5"/><path d="M14 11v5"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@@ -6240,9 +6317,10 @@
|
|||||||
<div class="editor-body">
|
<div class="editor-body">
|
||||||
{#if isMobile}
|
{#if isMobile}
|
||||||
<!-- Mobile: both views always in DOM, toggled via display to avoid slow editor re-creation -->
|
<!-- Mobile: both views always in DOM, toggled via display to avoid slow editor re-creation -->
|
||||||
|
<div class="source-editor-layer" style={$sourceMode ? '' : 'display:none'}>
|
||||||
|
<pre class="source-highlight" aria-hidden="true" bind:this={sourceHighlightElement}><code>{@html sourceHighlightHtml}</code></pre>
|
||||||
<textarea
|
<textarea
|
||||||
class="source-editor"
|
class="source-editor"
|
||||||
style={$sourceMode ? '' : 'display:none'}
|
|
||||||
bind:this={sourceElement}
|
bind:this={sourceElement}
|
||||||
bind:value={sourceContent}
|
bind:value={sourceContent}
|
||||||
readonly={$readOnly}
|
readonly={$readOnly}
|
||||||
@@ -6282,8 +6360,10 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onscroll={syncSourceEditorScroll}
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
></textarea>
|
></textarea>
|
||||||
|
</div>
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
<div class="tiptap-wrapper" class:large-doc={isLargeDoc} style={$sourceMode ? 'display:none' : ''} spellcheck="false" bind:this={editorElement} onclick={(e) => { closeLinkContextMenu(); handleEditorClick(e); }}></div>
|
<div class="tiptap-wrapper" class:large-doc={isLargeDoc} style={$sourceMode ? 'display:none' : ''} spellcheck="false" bind:this={editorElement} onclick={(e) => { closeLinkContextMenu(); handleEditorClick(e); }}></div>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -6298,6 +6378,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
<div class="source-editor-layer" class:with-line-numbers={$appConfig?.show_line_numbers}>
|
||||||
|
<pre class="source-highlight" aria-hidden="true" bind:this={sourceHighlightElement}><code>{@html sourceHighlightHtml}</code></pre>
|
||||||
<textarea
|
<textarea
|
||||||
class="source-editor"
|
class="source-editor"
|
||||||
class:with-line-numbers={$appConfig?.show_line_numbers}
|
class:with-line-numbers={$appConfig?.show_line_numbers}
|
||||||
@@ -6381,17 +6463,10 @@
|
|||||||
autoSave();
|
autoSave();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onscroll={() => {
|
onscroll={syncSourceEditorScroll}
|
||||||
if ($appConfig?.show_line_numbers) {
|
|
||||||
const clip = sourceElement?.previousElementSibling as HTMLElement;
|
|
||||||
const gutter = clip?.firstElementChild as HTMLElement;
|
|
||||||
if (gutter) {
|
|
||||||
gutter.style.transform = `translateY(-${sourceElement.scrollTop}px)`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
></textarea>
|
></textarea>
|
||||||
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
<div class="tiptap-wrapper" class:large-doc={isLargeDoc} spellcheck="false" bind:this={editorElement} onclick={(e) => { closeLinkContextMenu(); handleEditorClick(e); }} oncontextmenu={handleEditorContextMenu}></div>
|
<div class="tiptap-wrapper" class:large-doc={isLargeDoc} spellcheck="false" bind:this={editorElement} onclick={(e) => { closeLinkContextMenu(); handleEditorClick(e); }} oncontextmenu={handleEditorContextMenu}></div>
|
||||||
@@ -8133,6 +8208,17 @@
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editor-trash-btn:hover,
|
||||||
|
.editor-trash-btn:focus-visible {
|
||||||
|
color: var(--danger);
|
||||||
|
background: color-mix(in srgb, var(--danger) 12%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-trash-btn:disabled {
|
||||||
|
opacity: 0.45;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
.icon-btn.active {
|
.icon-btn.active {
|
||||||
color: var(--text-accent);
|
color: var(--text-accent);
|
||||||
background: var(--accent-light);
|
background: var(--accent-light);
|
||||||
@@ -8587,36 +8673,74 @@
|
|||||||
.outline-level-5 { padding-left: 62px; }
|
.outline-level-5 { padding-left: 62px; }
|
||||||
.outline-level-6 { padding-left: 74px; }
|
.outline-level-6 { padding-left: 74px; }
|
||||||
|
|
||||||
|
.source-editor-layer {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-width: var(--editor-content-width, none);
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-highlight,
|
||||||
.source-editor {
|
.source-editor {
|
||||||
|
box-sizing: border-box;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border: none;
|
border: none;
|
||||||
background: none;
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
|
font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
|
||||||
font-size: var(--editor-font-size, 14px);
|
font-size: var(--editor-font-size, 14px);
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
resize: none;
|
|
||||||
outline: none;
|
|
||||||
padding: 0 0 var(--editor-scroll-past-end, 65vh);
|
padding: 0 0 var(--editor-scroll-past-end, 65vh);
|
||||||
margin: 0 auto;
|
|
||||||
max-width: var(--editor-content-width, none);
|
|
||||||
user-select: text;
|
|
||||||
/* Wrap long lines instead of horizontal-scrolling (matches mobile). (issue #100) */
|
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-highlight {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 0;
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-highlight code {
|
||||||
|
font: inherit;
|
||||||
|
white-space: inherit;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-editor {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 1;
|
||||||
|
background: transparent;
|
||||||
|
color: transparent;
|
||||||
|
caret-color: var(--text-primary);
|
||||||
|
resize: none;
|
||||||
|
outline: none;
|
||||||
|
user-select: text;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.source-editor.with-line-numbers {
|
.source-editor-layer.with-line-numbers {
|
||||||
|
max-width: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-editor.with-line-numbers,
|
||||||
|
.source-editor-layer.with-line-numbers .source-highlight {
|
||||||
padding-left: 48px;
|
padding-left: 48px;
|
||||||
/* The line-number gutter has one fixed row per line, so wrapping would desync it.
|
/* The line-number gutter has one fixed row per line, so wrapping would desync it.
|
||||||
Keep no-wrap (horizontal scroll) whenever line numbers are on. (issue #100) */
|
Keep no-wrap (horizontal scroll) whenever line numbers are on. (issue #100) */
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
|
word-break: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-editor.with-line-numbers {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
/* The line-number gutter is pinned to the left edge, so don't center/cap here. (#137) */
|
|
||||||
max-width: none;
|
|
||||||
margin: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.line-numbers-clip {
|
.line-numbers-clip {
|
||||||
@@ -8646,6 +8770,36 @@
|
|||||||
padding-right: 12px;
|
padding-right: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Markdown source highlighting stays color-only so glyph metrics match the textarea caret. */
|
||||||
|
:global(.source-highlight .hljs-section),
|
||||||
|
:global(.source-highlight .hljs-strong) {
|
||||||
|
color: var(--accent);
|
||||||
|
font-weight: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.source-highlight .hljs-string),
|
||||||
|
:global(.source-highlight .hljs-link) {
|
||||||
|
color: color-mix(in srgb, var(--accent) 78%, var(--text-primary));
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.source-highlight .hljs-bullet),
|
||||||
|
:global(.source-highlight .hljs-symbol),
|
||||||
|
:global(.source-highlight .hljs-meta),
|
||||||
|
:global(.source-highlight .hljs-attr) {
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.source-highlight .hljs-quote),
|
||||||
|
:global(.source-highlight .hljs-code) {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.source-highlight .hljs-emphasis) {
|
||||||
|
color: color-mix(in srgb, var(--accent) 60%, var(--text-primary));
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
.tiptap-wrapper {
|
.tiptap-wrapper {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
user-select: text;
|
user-select: text;
|
||||||
@@ -11204,9 +11358,9 @@
|
|||||||
.editor-container.mobile .editor-toolbar {
|
.editor-container.mobile .editor-toolbar {
|
||||||
padding: 8px 16px 6px 16px;
|
padding: 8px 16px 6px 16px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
flex-direction: column;
|
flex-direction: row;
|
||||||
align-items: stretch;
|
align-items: center;
|
||||||
gap: 2px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar-actions.mobile {
|
.toolbar-actions.mobile {
|
||||||
@@ -11231,6 +11385,20 @@
|
|||||||
padding: 4px 0;
|
padding: 4px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editor-container.mobile .editor-trash-btn.mobile {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
padding: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-container.mobile .editor-trash-btn.mobile:active {
|
||||||
|
color: var(--danger);
|
||||||
|
background: color-mix(in srgb, var(--danger) 12%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
.editor-container.mobile .editor-body-wrapper {
|
.editor-container.mobile .editor-body-wrapper {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
@@ -11326,6 +11494,7 @@
|
|||||||
font-size: var(--editor-font-size, 16px) !important;
|
font-size: var(--editor-font-size, 16px) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editor-container.mobile .source-highlight,
|
||||||
.editor-container.mobile .source-editor {
|
.editor-container.mobile .source-editor {
|
||||||
padding: 8px 16px 220px;
|
padding: 8px 16px 220px;
|
||||||
font-size: var(--editor-font-size, 15px);
|
font-size: var(--editor-font-size, 15px);
|
||||||
|
|||||||
@@ -519,12 +519,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move the currently open note to the trash (keyboard shortcut from the editor).
|
|
||||||
export async function trashActiveNote() {
|
|
||||||
if ($viewMode === 'trash' || !$activeNotePath) return;
|
|
||||||
const note = $notes.find(n => n.path === $activeNotePath);
|
|
||||||
if (note) await handleDelete(note);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleRestore(note: NoteEntry) {
|
async function handleRestore(note: NoteEntry) {
|
||||||
contextMenu = null;
|
contextMenu = null;
|
||||||
|
|||||||
@@ -717,6 +717,7 @@
|
|||||||
let showLineNumbers = $state($appConfig?.show_line_numbers ?? false);
|
let showLineNumbers = $state($appConfig?.show_line_numbers ?? false);
|
||||||
let showLinkArrows = $state($appConfig?.show_link_arrows ?? true);
|
let showLinkArrows = $state($appConfig?.show_link_arrows ?? true);
|
||||||
let defaultViewMode = $state($appConfig?.default_view_mode ?? false);
|
let defaultViewMode = $state($appConfig?.default_view_mode ?? false);
|
||||||
|
let newNotesInSourceMode = $state($appConfig?.new_notes_in_source_mode ?? false);
|
||||||
let showTrayIcon = $state($appConfig?.show_tray_icon ?? false);
|
let showTrayIcon = $state($appConfig?.show_tray_icon ?? false);
|
||||||
let closeToTray = $state($appConfig?.close_to_tray ?? false);
|
let closeToTray = $state($appConfig?.close_to_tray ?? false);
|
||||||
let enableWikiLinks = $state($appConfig?.enable_wiki_links ?? true);
|
let enableWikiLinks = $state($appConfig?.enable_wiki_links ?? true);
|
||||||
@@ -789,6 +790,7 @@
|
|||||||
$appConfig.show_line_numbers = showLineNumbers;
|
$appConfig.show_line_numbers = showLineNumbers;
|
||||||
$appConfig.show_link_arrows = showLinkArrows;
|
$appConfig.show_link_arrows = showLinkArrows;
|
||||||
$appConfig.default_view_mode = defaultViewMode;
|
$appConfig.default_view_mode = defaultViewMode;
|
||||||
|
$appConfig.new_notes_in_source_mode = newNotesInSourceMode;
|
||||||
$appConfig.show_tray_icon = showTrayIcon;
|
$appConfig.show_tray_icon = showTrayIcon;
|
||||||
$appConfig.close_to_tray = closeToTray;
|
$appConfig.close_to_tray = closeToTray;
|
||||||
$appConfig.enable_wiki_links = enableWikiLinks;
|
$appConfig.enable_wiki_links = enableWikiLinks;
|
||||||
@@ -798,7 +800,7 @@
|
|||||||
$appConfig.show_daily_notes = showDailyNotes;
|
$appConfig.show_daily_notes = showDailyNotes;
|
||||||
$appConfig.show_trash = showTrash;
|
$appConfig.show_trash = showTrash;
|
||||||
}
|
}
|
||||||
setGeneralSettings(compactNotes, timeFormat, weekStart, dailyTitleFormat, gpuAcceleration, autostart, pdfPreview, pdfHeight, titleMode, hideTitleInBody, showLineNumbers, showLinkArrows, defaultViewMode, showTrayIcon, closeToTray, enableWikiLinks, showNoteDates, startupView, restoreLastSession, showAllNotes, showQuickAccess, showTasks, showDailyNotes, showTrash)
|
setGeneralSettings(compactNotes, timeFormat, weekStart, dailyTitleFormat, gpuAcceleration, autostart, pdfPreview, pdfHeight, titleMode, hideTitleInBody, showLineNumbers, showLinkArrows, defaultViewMode, newNotesInSourceMode, showTrayIcon, closeToTray, enableWikiLinks, showNoteDates, startupView, restoreLastSession, showAllNotes, showQuickAccess, showTasks, showDailyNotes, showTrash)
|
||||||
.catch((e) => console.error('Failed to save general settings:', e));
|
.catch((e) => console.error('Failed to save general settings:', e));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -988,6 +990,7 @@
|
|||||||
pdfPreview = $appConfig.pdf_preview ?? false;
|
pdfPreview = $appConfig.pdf_preview ?? false;
|
||||||
pdfHeight = $appConfig.pdf_height ?? 600;
|
pdfHeight = $appConfig.pdf_height ?? 600;
|
||||||
titleMode = $appConfig.title_mode ?? 'input';
|
titleMode = $appConfig.title_mode ?? 'input';
|
||||||
|
newNotesInSourceMode = $appConfig.new_notes_in_source_mode ?? false;
|
||||||
showAllNotes = $appConfig.show_all_notes ?? true;
|
showAllNotes = $appConfig.show_all_notes ?? true;
|
||||||
showQuickAccess = $appConfig.show_quick_access ?? true;
|
showQuickAccess = $appConfig.show_quick_access ?? true;
|
||||||
showTasks = $appConfig.show_tasks ?? true;
|
showTasks = $appConfig.show_tasks ?? true;
|
||||||
@@ -1309,8 +1312,18 @@
|
|||||||
<span class="toggle-knob"></span>
|
<span class="toggle-knob"></span>
|
||||||
</button>
|
</button>
|
||||||
</label>
|
</label>
|
||||||
|
<label class="setting-toggle" style="margin-top: 12px;">
|
||||||
|
<span class="setting-label">
|
||||||
|
<span class="setting-name">Open new notes in source mode</span>
|
||||||
|
<span class="setting-desc">Start empty notes in the plain-text Markdown editor instead of rich-text mode</span>
|
||||||
|
</span>
|
||||||
|
<button class="toggle-switch" class:on={newNotesInSourceMode} onclick={() => { newNotesInSourceMode = !newNotesInSourceMode; saveGeneralSettings(); }}>
|
||||||
|
<span class="toggle-knob"></span>
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<h3>Wiki Links & Graph</h3>
|
<h3>Wiki Links & Graph</h3>
|
||||||
<label class="setting-toggle">
|
<label class="setting-toggle">
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ export interface AppConfig {
|
|||||||
ai_model: string;
|
ai_model: string;
|
||||||
ai_writing_style: string | null;
|
ai_writing_style: string | null;
|
||||||
default_view_mode: boolean;
|
default_view_mode: boolean;
|
||||||
|
new_notes_in_source_mode: boolean;
|
||||||
show_tray_icon: boolean;
|
show_tray_icon: boolean;
|
||||||
close_to_tray: boolean;
|
close_to_tray: boolean;
|
||||||
enable_wiki_links: boolean;
|
enable_wiki_links: boolean;
|
||||||
|
|||||||
Reference in New Issue
Block a user