mirror of
https://gitlab.com/ArkHost/HelixNotes.git
synced 2026-09-19 17:37: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_link_arrows: bool,
|
||||
default_view_mode: bool,
|
||||
new_notes_in_source_mode: bool,
|
||||
show_tray_icon: bool,
|
||||
close_to_tray: bool,
|
||||
enable_wiki_links: bool,
|
||||
@@ -1432,6 +1433,7 @@ pub fn set_general_settings(
|
||||
config.show_line_numbers = show_line_numbers;
|
||||
config.show_link_arrows = show_link_arrows;
|
||||
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.close_to_tray = close_to_tray;
|
||||
config.enable_wiki_links = enable_wiki_links;
|
||||
|
||||
@@ -197,6 +197,8 @@ pub struct AppConfig {
|
||||
#[serde(default)]
|
||||
pub default_view_mode: bool,
|
||||
#[serde(default)]
|
||||
pub new_notes_in_source_mode: bool,
|
||||
#[serde(default)]
|
||||
pub show_tray_icon: bool,
|
||||
#[serde(default)]
|
||||
pub close_to_tray: bool,
|
||||
@@ -315,6 +317,7 @@ impl Default for AppConfig {
|
||||
ai_model: "claude-sonnet-4-6".to_string(),
|
||||
ai_writing_style: None,
|
||||
default_view_mode: false,
|
||||
new_notes_in_source_mode: false,
|
||||
show_tray_icon: false,
|
||||
close_to_tray: false,
|
||||
enable_wiki_links: true,
|
||||
|
||||
@@ -265,6 +265,7 @@ export async function setGeneralSettings(
|
||||
showLineNumbers: boolean,
|
||||
showLinkArrows: boolean,
|
||||
defaultViewMode: boolean,
|
||||
newNotesInSourceMode: boolean,
|
||||
showTrayIcon: boolean,
|
||||
closeToTray: boolean,
|
||||
enableWikiLinks: boolean,
|
||||
@@ -291,6 +292,7 @@ export async function setGeneralSettings(
|
||||
showLineNumbers,
|
||||
showLinkArrows,
|
||||
defaultViewMode,
|
||||
newNotesInSourceMode,
|
||||
showTrayIcon,
|
||||
closeToTray,
|
||||
enableWikiLinks,
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
const appWindow = getCurrentWindow();
|
||||
const isMac = navigator.platform.startsWith('Mac');
|
||||
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 { debounce } from '$lib/utils/debounce';
|
||||
import { openNoteWindow } from '$lib/utils/window';
|
||||
@@ -449,6 +449,28 @@
|
||||
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) {
|
||||
if (e.button === 3) { 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.
|
||||
if (mod && e.shiftKey && code === 'Delete' && $activeNotePath) {
|
||||
e.preventDefault();
|
||||
noteList?.trashActiveNote();
|
||||
editor?.moveOpenNoteToTrash();
|
||||
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} />
|
||||
</div>
|
||||
<div class="mobile-panel" class:active={$mobileView === 'editor'}>
|
||||
<Editor bind:this={editor} />
|
||||
<Editor bind:this={editor} onMoveToTrash={trashOpenNote} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1028,7 +1050,7 @@
|
||||
{/if}
|
||||
|
||||
<div class="editor-panel">
|
||||
<Editor bind:this={editor} />
|
||||
<Editor bind:this={editor} onMoveToTrash={trashOpenNote} />
|
||||
{#if $viewMode === 'tasks' && !taskNoteOpened}
|
||||
<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">
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
import TextAlign from '@tiptap/extension-text-align';
|
||||
import { common, createLowlight } from 'lowlight';
|
||||
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 markdownItMark from 'markdown-it-mark';
|
||||
import markdownItSup from 'markdown-it-sup';
|
||||
@@ -55,7 +57,14 @@
|
||||
import { isMobile, isAndroid } from '$lib/platform';
|
||||
import ResizeHandle from './ResizeHandle.svelte';
|
||||
|
||||
let { onMoveToTrash = async () => false }: {
|
||||
onMoveToTrash?: (path: string) => Promise<boolean>;
|
||||
} = $props();
|
||||
|
||||
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
|
||||
let keyboardHeight = $state(0);
|
||||
@@ -68,6 +77,7 @@
|
||||
|
||||
let editorElement = $state<HTMLDivElement>(null!);
|
||||
let sourceElement = $state<HTMLTextAreaElement>(null!);
|
||||
let sourceHighlightElement = $state<HTMLPreElement>(null!);
|
||||
const LARGE_DOC_CHARS = 100_000;
|
||||
let isLargeDoc = $state(false);
|
||||
let editor: Editor | null = null;
|
||||
@@ -83,6 +93,10 @@
|
||||
});
|
||||
let editorReady = $state(false);
|
||||
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 sourceHistoryIndex = -1;
|
||||
let sourceHistoryTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
@@ -93,6 +107,7 @@
|
||||
let pendingContent = $state<string | null>(null);
|
||||
let ignoreNextUpdate = false;
|
||||
let isLoadingNote = false;
|
||||
let trashingNote = $state(false);
|
||||
let fixingBlobsPromise: Promise<void> = Promise.resolve();
|
||||
let hasPendingBlobs = false;
|
||||
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() {
|
||||
if (taskRevealTimer) clearTimeout(taskRevealTimer);
|
||||
taskRevealTimer = null;
|
||||
@@ -2971,8 +2998,16 @@
|
||||
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 () => {
|
||||
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;
|
||||
// Only fix blob images if a paste occurred (avoids full doc scan on every save)
|
||||
if (hasPendingBlobs) {
|
||||
@@ -2980,38 +3015,65 @@
|
||||
fixingBlobsPromise = fixBlobImages();
|
||||
}
|
||||
await fixingBlobsPromise;
|
||||
if (trashingNote || !$activeNote || !$activeNotePath) return;
|
||||
try {
|
||||
const path = $activeNotePath;
|
||||
const note = $activeNote;
|
||||
const body = $sourceMode
|
||||
? restoreTitleH1(sourceContent)
|
||||
: editorToMarkdown();
|
||||
// Safety: never save empty/near-empty body over a note that had real content
|
||||
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');
|
||||
return;
|
||||
}
|
||||
await saveNote($activeNotePath, $activeNote.meta, body);
|
||||
$editorDirty = false;
|
||||
await queueSave(() => saveNote(path, note.meta, body));
|
||||
if ($activeNotePath === path) $editorDirty = false;
|
||||
} catch (e) {
|
||||
console.error('Auto-save failed:', e);
|
||||
}
|
||||
}, isMobile ? 1500 : 500);
|
||||
|
||||
export async function forceSave() {
|
||||
if (get(viewerNote)) return; // viewer files are never written back
|
||||
if (!$activeNote || !$activeNotePath) return;
|
||||
export async function forceSave(allowWhileTrashing = false): Promise<boolean> {
|
||||
if (get(viewerNote) || (trashingNote && !allowWhileTrashing)) return false;
|
||||
if (!$activeNote || !$activeNotePath) return false;
|
||||
await fixingBlobsPromise;
|
||||
if (trashingNote && !allowWhileTrashing) return false;
|
||||
if (!$activeNote || !$activeNotePath) return false;
|
||||
try {
|
||||
const path = $activeNotePath;
|
||||
const note = $activeNote;
|
||||
const body = $sourceMode ? restoreTitleH1(sourceContent) : editorToMarkdown();
|
||||
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');
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
await saveNote($activeNotePath, $activeNote.meta, body);
|
||||
$editorDirty = false;
|
||||
await queueSave(() => saveNote(path, note.meta, body));
|
||||
if ($activeNotePath === path) $editorDirty = false;
|
||||
return true;
|
||||
} catch (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 revealTarget = taskTarget ? resolveTaskTarget(taskTarget, content) : null;
|
||||
loadedPath = path;
|
||||
lastSourceMode = $sourceMode;
|
||||
isLoadingNote = true;
|
||||
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.
|
||||
// Viewer mode (external file) always forces read-only. New notes stay editable and
|
||||
// can opt into source mode without changing how existing notes choose their mode.
|
||||
const isViewer = !!get(viewerNote);
|
||||
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));
|
||||
$readOnly = shouldBeReadOnly;
|
||||
if (editor) editor.setEditable(!shouldBeReadOnly);
|
||||
@@ -6025,6 +6090,13 @@
|
||||
}}
|
||||
/>
|
||||
</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}
|
||||
<div class="toolbar-actions">
|
||||
{#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" />
|
||||
</svg>
|
||||
</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>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -6240,9 +6317,10 @@
|
||||
<div class="editor-body">
|
||||
{#if isMobile}
|
||||
<!-- 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
|
||||
class="source-editor"
|
||||
style={$sourceMode ? '' : 'display:none'}
|
||||
bind:this={sourceElement}
|
||||
bind:value={sourceContent}
|
||||
readonly={$readOnly}
|
||||
@@ -6282,8 +6360,10 @@
|
||||
return;
|
||||
}
|
||||
}}
|
||||
onscroll={syncSourceEditorScroll}
|
||||
spellcheck="false"
|
||||
></textarea>
|
||||
</div>
|
||||
<!-- 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>
|
||||
{:else}
|
||||
@@ -6298,6 +6378,8 @@
|
||||
</div>
|
||||
</div>
|
||||
{/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
|
||||
class="source-editor"
|
||||
class:with-line-numbers={$appConfig?.show_line_numbers}
|
||||
@@ -6381,17 +6463,10 @@
|
||||
autoSave();
|
||||
}
|
||||
}}
|
||||
onscroll={() => {
|
||||
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)`;
|
||||
}
|
||||
}
|
||||
}}
|
||||
onscroll={syncSourceEditorScroll}
|
||||
spellcheck="false"
|
||||
></textarea>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- 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>
|
||||
@@ -8133,6 +8208,17 @@
|
||||
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 {
|
||||
color: var(--text-accent);
|
||||
background: var(--accent-light);
|
||||
@@ -8587,36 +8673,74 @@
|
||||
.outline-level-5 { padding-left: 62px; }
|
||||
.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 {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--text-primary);
|
||||
font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
|
||||
font-size: var(--editor-font-size, 14px);
|
||||
line-height: 1.3;
|
||||
resize: none;
|
||||
outline: none;
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
.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;
|
||||
/* 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) */
|
||||
white-space: pre;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
.source-editor.with-line-numbers {
|
||||
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 {
|
||||
@@ -8646,6 +8770,36 @@
|
||||
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 {
|
||||
height: 100%;
|
||||
user-select: text;
|
||||
@@ -11204,9 +11358,9 @@
|
||||
.editor-container.mobile .editor-toolbar {
|
||||
padding: 8px 16px 6px 16px;
|
||||
flex-shrink: 0;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 2px;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.toolbar-actions.mobile {
|
||||
@@ -11231,6 +11385,20 @@
|
||||
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 {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
@@ -11326,6 +11494,7 @@
|
||||
font-size: var(--editor-font-size, 16px) !important;
|
||||
}
|
||||
|
||||
.editor-container.mobile .source-highlight,
|
||||
.editor-container.mobile .source-editor {
|
||||
padding: 8px 16px 220px;
|
||||
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) {
|
||||
contextMenu = null;
|
||||
|
||||
@@ -717,6 +717,7 @@
|
||||
let showLineNumbers = $state($appConfig?.show_line_numbers ?? false);
|
||||
let showLinkArrows = $state($appConfig?.show_link_arrows ?? true);
|
||||
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 closeToTray = $state($appConfig?.close_to_tray ?? false);
|
||||
let enableWikiLinks = $state($appConfig?.enable_wiki_links ?? true);
|
||||
@@ -789,6 +790,7 @@
|
||||
$appConfig.show_line_numbers = showLineNumbers;
|
||||
$appConfig.show_link_arrows = showLinkArrows;
|
||||
$appConfig.default_view_mode = defaultViewMode;
|
||||
$appConfig.new_notes_in_source_mode = newNotesInSourceMode;
|
||||
$appConfig.show_tray_icon = showTrayIcon;
|
||||
$appConfig.close_to_tray = closeToTray;
|
||||
$appConfig.enable_wiki_links = enableWikiLinks;
|
||||
@@ -798,7 +800,7 @@
|
||||
$appConfig.show_daily_notes = showDailyNotes;
|
||||
$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));
|
||||
}
|
||||
|
||||
@@ -988,6 +990,7 @@
|
||||
pdfPreview = $appConfig.pdf_preview ?? false;
|
||||
pdfHeight = $appConfig.pdf_height ?? 600;
|
||||
titleMode = $appConfig.title_mode ?? 'input';
|
||||
newNotesInSourceMode = $appConfig.new_notes_in_source_mode ?? false;
|
||||
showAllNotes = $appConfig.show_all_notes ?? true;
|
||||
showQuickAccess = $appConfig.show_quick_access ?? true;
|
||||
showTasks = $appConfig.show_tasks ?? true;
|
||||
@@ -1309,8 +1312,18 @@
|
||||
<span class="toggle-knob"></span>
|
||||
</button>
|
||||
</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 class="settings-section">
|
||||
<h3>Wiki Links & Graph</h3>
|
||||
<label class="setting-toggle">
|
||||
|
||||
@@ -129,6 +129,7 @@ export interface AppConfig {
|
||||
ai_model: string;
|
||||
ai_writing_style: string | null;
|
||||
default_view_mode: boolean;
|
||||
new_notes_in_source_mode: boolean;
|
||||
show_tray_icon: boolean;
|
||||
close_to_tray: boolean;
|
||||
enable_wiki_links: boolean;
|
||||
|
||||
Reference in New Issue
Block a user