mirror of
https://gitlab.com/ArkHost/HelixNotes.git
synced 2026-07-24 07:45:57 +02:00
v1.1.6 - Multi-window, single-instance file handling, line height setting
This commit is contained in:
@@ -38,6 +38,10 @@ export async function setFontFamily(family: string): Promise<void> {
|
||||
return invoke("set_font_family", { family });
|
||||
}
|
||||
|
||||
export async function setLineHeight(height: number): Promise<void> {
|
||||
return invoke("set_line_height", { height });
|
||||
}
|
||||
|
||||
export async function getNotebooks(): Promise<NotebookEntry[]> {
|
||||
return invoke("get_notebooks");
|
||||
}
|
||||
@@ -330,3 +334,7 @@ export async function aiAsk(
|
||||
export async function getInstallType(): Promise<string> {
|
||||
return invoke("get_install_type");
|
||||
}
|
||||
|
||||
export async function getPendingOpenFile(): Promise<string | null> {
|
||||
return invoke("get_pending_open_file");
|
||||
}
|
||||
|
||||
@@ -28,14 +28,16 @@
|
||||
showInfo,
|
||||
showSettings,
|
||||
sourceMode,
|
||||
mobileView
|
||||
mobileView,
|
||||
appConfig
|
||||
} from '$lib/stores/app';
|
||||
|
||||
const appWindow = getCurrentWindow();
|
||||
const isMac = navigator.platform.startsWith('Mac');
|
||||
const isMobile = /android|ios/i.test(navigator.userAgent);
|
||||
import { loadVaultState, saveVaultState, readNote, createDailyNote, createBackup } from '$lib/api';
|
||||
import { loadVaultState, saveVaultState, readNote, createDailyNote, createBackup, getPendingOpenFile } from '$lib/api';
|
||||
import { debounce } from '$lib/utils/debounce';
|
||||
import { openNoteWindow } from '$lib/utils/window';
|
||||
import { get } from 'svelte/store';
|
||||
import type { VaultState, FileEvent } from '$lib/types';
|
||||
|
||||
@@ -43,6 +45,7 @@
|
||||
let noteList: NoteList;
|
||||
let editor: Editor;
|
||||
let unlistenFileChange: (() => void) | null = null;
|
||||
let unlistenOpenFile: (() => void) | null = null;
|
||||
let backupInterval: ReturnType<typeof setInterval> | null = null;
|
||||
let navigatingFromHistory = false;
|
||||
let noteHistory: string[] = [];
|
||||
@@ -104,6 +107,22 @@
|
||||
});
|
||||
}
|
||||
|
||||
async function handleOpenFile(filePath: string) {
|
||||
if (!filePath || !filePath.endsWith('.md')) return;
|
||||
const config = get(appConfig);
|
||||
const vaultRoot = config?.active_vault;
|
||||
if (!vaultRoot || !filePath.startsWith(vaultRoot)) return;
|
||||
try {
|
||||
const content = await readNote(filePath);
|
||||
$activeNote = content;
|
||||
$activeNotePath = filePath;
|
||||
$editorDirty = false;
|
||||
editor?.loadNote(filePath, content.content);
|
||||
} catch (e) {
|
||||
console.error('Failed to open file:', e);
|
||||
}
|
||||
}
|
||||
|
||||
const persistState = debounce(async () => {
|
||||
const state: VaultState = {
|
||||
last_open_note: null,
|
||||
@@ -219,6 +238,12 @@
|
||||
e.preventDefault();
|
||||
$sourceMode = !$sourceMode;
|
||||
}
|
||||
if (mod && e.shiftKey && e.key === 'W') {
|
||||
e.preventDefault();
|
||||
if ($activeNotePath && $activeNote) {
|
||||
openNoteWindow($activeNotePath, $activeNote.meta.title);
|
||||
}
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
if ($showSettings) $showSettings = false;
|
||||
else if ($showInfo) $showInfo = false;
|
||||
@@ -268,6 +293,17 @@
|
||||
await noteList?.refresh(true);
|
||||
});
|
||||
|
||||
// Listen for file open events (from single-instance or OS file associations)
|
||||
unlistenOpenFile = await listen<string>('open-file', async (event) => {
|
||||
await handleOpenFile(event.payload);
|
||||
});
|
||||
|
||||
// Check for pending file from first-launch CLI args
|
||||
try {
|
||||
const pending = await getPendingOpenFile();
|
||||
if (pending) await handleOpenFile(pending);
|
||||
} catch (_) {}
|
||||
|
||||
// Scheduled backup: check on startup and every 5 minutes
|
||||
checkScheduledBackup();
|
||||
backupInterval = setInterval(checkScheduledBackup, 5 * 60 * 1000);
|
||||
@@ -275,6 +311,7 @@
|
||||
|
||||
onDestroy(() => {
|
||||
unlistenFileChange?.();
|
||||
unlistenOpenFile?.();
|
||||
if (backupInterval) clearInterval(backupInterval);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
import { openFile, copyFileTo } from '$lib/api';
|
||||
import { save as saveDialog } from '@tauri-apps/plugin-dialog';
|
||||
import { activeNote, activeNotePath, appConfig, editorDirty, sourceMode, focusMode, readOnly, quickAccessPaths, notes } from '$lib/stores/app';
|
||||
import { saveNote, saveImage, saveAttachment, addQuickAccess, removeQuickAccess, getQuickAccess, getNoteVersions, getNoteVersionContent, createVersion, aiAsk, getAllNoteTitles, readNote } from '$lib/api';
|
||||
import { saveNote, saveImage, saveAttachment, addQuickAccess, removeQuickAccess, getQuickAccess, getNoteVersions, getNoteVersionContent, createVersion, aiAsk, getAllNoteTitles, readNote, renameNote } from '$lib/api';
|
||||
import type { VersionEntry, AiStreamEvent, NoteTitleEntry } from '$lib/types';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import { debounce } from '$lib/utils/debounce';
|
||||
@@ -2941,16 +2941,37 @@
|
||||
editor?.commands.focus('start');
|
||||
}
|
||||
}}
|
||||
onchange={(e) => {
|
||||
if ($activeNote) {
|
||||
const newTitle = (e.target as HTMLInputElement).value;
|
||||
onchange={async (e) => {
|
||||
if ($activeNote && $activeNotePath) {
|
||||
const newTitle = (e.target as HTMLInputElement).value.trim();
|
||||
if (!newTitle) return;
|
||||
const oldPath = $activeNotePath;
|
||||
$activeNote.meta.title = newTitle;
|
||||
// Update the note list entry so the 2nd panel reflects the change
|
||||
notes.update(list => list.map(n =>
|
||||
n.path === $activeNotePath ? { ...n, meta: { ...n.meta, title: newTitle } } : n
|
||||
));
|
||||
$editorDirty = true;
|
||||
autoSave();
|
||||
// Rename file on disk if filename doesn't match the new title
|
||||
const filename = oldPath.split('/').pop() ?? '';
|
||||
const stem = filename.replace(/\.md$/, '');
|
||||
if (stem !== newTitle) {
|
||||
try {
|
||||
const newPath = await renameNote(oldPath, newTitle);
|
||||
$activeNotePath = newPath;
|
||||
notes.update(list => list.map(n =>
|
||||
n.path === oldPath
|
||||
? { ...n, path: newPath, relative_path: n.relative_path.replace(/[^/]+$/, newTitle + '.md'), meta: { ...n.meta, title: newTitle } }
|
||||
: n
|
||||
));
|
||||
} catch (err) {
|
||||
console.error('Failed to rename note file:', err);
|
||||
notes.update(list => list.map(n =>
|
||||
n.path === oldPath ? { ...n, meta: { ...n.meta, title: newTitle } } : n
|
||||
));
|
||||
}
|
||||
} else {
|
||||
notes.update(list => list.map(n =>
|
||||
n.path === oldPath ? { ...n, meta: { ...n.meta, title: newTitle } } : n
|
||||
));
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -4817,7 +4838,7 @@
|
||||
color: var(--text-primary);
|
||||
font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
|
||||
font-size: var(--editor-font-size, 14px);
|
||||
line-height: 1.6;
|
||||
line-height: var(--editor-line-height, 1.6);
|
||||
resize: none;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
@@ -4845,7 +4866,7 @@
|
||||
padding-top: 8px;
|
||||
font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
|
||||
font-size: var(--editor-font-size, 14px);
|
||||
line-height: 1.6;
|
||||
line-height: var(--editor-line-height, 1.6);
|
||||
color: var(--text-secondary);
|
||||
opacity: 0.5;
|
||||
text-align: right;
|
||||
@@ -4874,7 +4895,7 @@
|
||||
|
||||
:global(.tiptap-wrapper .tiptap p) {
|
||||
margin: 0 0 0.75em;
|
||||
line-height: 1.65;
|
||||
line-height: var(--editor-line-height, 1.65);
|
||||
}
|
||||
|
||||
:global(.tiptap-wrapper .tiptap h1) {
|
||||
@@ -5192,7 +5213,7 @@
|
||||
|
||||
:global(.tiptap-wrapper .tiptap li) {
|
||||
margin: 0.25em 0;
|
||||
line-height: 1.65;
|
||||
line-height: var(--editor-line-height, 1.65);
|
||||
}
|
||||
|
||||
:global(.tiptap-wrapper .tiptap li p) {
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
getAllTags
|
||||
} from '$lib/api';
|
||||
import { formatRelativeTime } from '$lib/utils/time';
|
||||
import { openNoteWindow } from '$lib/utils/window';
|
||||
import type { NoteEntry, SortMode } from '$lib/types';
|
||||
|
||||
let { onNoteSelected = (_path: string, _content: string) => {}, onNoteMoved = () => {} }: {
|
||||
@@ -936,6 +937,12 @@
|
||||
Add to Quick Access
|
||||
</button>
|
||||
{/if}
|
||||
<button onclick={() => { const n = contextMenu!.note; contextMenu = null; openNoteWindow(n.path, n.meta.title); }}>
|
||||
<svg width="12" height="12" 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>
|
||||
Open in New Window
|
||||
</button>
|
||||
<button onclick={() => startRename(contextMenu!.note)}>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M17 3a2.85 2.85 0 114 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/>
|
||||
|
||||
@@ -0,0 +1,326 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||
import Editor from './Editor.svelte';
|
||||
import {
|
||||
activeNote,
|
||||
activeNotePath,
|
||||
editorDirty,
|
||||
readOnly,
|
||||
sourceMode,
|
||||
theme
|
||||
} from '$lib/stores/app';
|
||||
import { readNote } from '$lib/api';
|
||||
import type { FileEvent } from '$lib/types';
|
||||
|
||||
let { notePath }: { notePath: string } = $props();
|
||||
|
||||
const appWindow = getCurrentWindow();
|
||||
const isMac = navigator.platform.startsWith('Mac');
|
||||
let editor = $state<Editor>(null!);
|
||||
let unlistenFileChange: (() => void) | null = null;
|
||||
let maximized = $state(false);
|
||||
let loadError = $state<string | null>(null);
|
||||
|
||||
async function checkMaximized() {
|
||||
maximized = await appWindow.isMaximized();
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
checkMaximized();
|
||||
const unlisten = appWindow.onResized(() => checkMaximized());
|
||||
return () => { unlisten.then(fn => fn()); };
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
const themeValue = $theme || 'system';
|
||||
const root = document.documentElement;
|
||||
root.classList.remove('dark');
|
||||
if (themeValue === 'dark' || (themeValue === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
root.classList.add('dark');
|
||||
}
|
||||
});
|
||||
|
||||
let lastMouseDown = 0;
|
||||
const RESIZE_EDGE = 6;
|
||||
|
||||
function handleMouseDown(e: MouseEvent) {
|
||||
if (e.button !== 0) return;
|
||||
const target = e.target as HTMLElement;
|
||||
if (target.closest('.titlebar-controls') || target.closest('.titlebar-actions')) return;
|
||||
|
||||
if (!maximized) {
|
||||
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
||||
if (e.clientY - rect.top < RESIZE_EDGE || e.clientX - rect.left < RESIZE_EDGE) return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
if (now - lastMouseDown < 300) {
|
||||
appWindow.toggleMaximize();
|
||||
lastMouseDown = 0;
|
||||
return;
|
||||
}
|
||||
lastMouseDown = now;
|
||||
appWindow.startDragging();
|
||||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
const mod = e.ctrlKey || e.metaKey;
|
||||
if (mod && e.key === 's') {
|
||||
e.preventDefault();
|
||||
editor?.forceSave();
|
||||
}
|
||||
if (mod && e.shiftKey && e.key === 'M') {
|
||||
e.preventDefault();
|
||||
$sourceMode = !$sourceMode;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const content = await readNote(notePath);
|
||||
$activeNote = content;
|
||||
$activeNotePath = notePath;
|
||||
$editorDirty = false;
|
||||
setTimeout(() => {
|
||||
editor?.loadNote(notePath, content.content);
|
||||
appWindow.setTitle(`${content.meta.title} - HelixNotes`);
|
||||
}, 50);
|
||||
} catch (e) {
|
||||
loadError = String(e);
|
||||
}
|
||||
|
||||
unlistenFileChange = await listen<FileEvent>('file-changed', async (event) => {
|
||||
if (event.payload.path === notePath && event.payload.event_type === 'modify') {
|
||||
if (!$editorDirty) {
|
||||
try {
|
||||
const content = await readNote(notePath);
|
||||
$activeNote = content;
|
||||
editor?.loadNote(notePath, content.content);
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
unlistenFileChange?.();
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<div class="note-window">
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="nw-titlebar" class:macos={isMac} onmousedown={handleMouseDown}>
|
||||
<div class="nw-titlebar-brand">
|
||||
<svg width="14" height="14" viewBox="0 0 48 48" fill="none">
|
||||
<rect width="48" height="48" rx="12" fill="var(--accent)" />
|
||||
<circle cx="16" cy="16" r="3.5" fill="white" opacity="0.9" />
|
||||
<circle cx="32" cy="16" r="3.5" fill="white" opacity="0.9" />
|
||||
<circle cx="16" cy="32" r="3.5" fill="white" opacity="0.9" />
|
||||
<circle cx="32" cy="32" r="3.5" fill="white" opacity="0.9" />
|
||||
<line x1="19" y1="18" x2="29" y2="30" stroke="white" stroke-width="2" stroke-linecap="round" opacity="0.7" />
|
||||
<line x1="29" y1="18" x2="19" y2="30" stroke="white" stroke-width="2" stroke-linecap="round" opacity="0.7" />
|
||||
</svg>
|
||||
<span class="nw-title">{$activeNote?.meta.title || 'Loading...'}</span>
|
||||
{#if $editorDirty}
|
||||
<span class="nw-dirty-dot" title="Unsaved changes"></span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="titlebar-actions">
|
||||
<button class="nw-btn" class:active={$sourceMode} onclick={() => ($sourceMode = !$sourceMode)} title="Toggle Source Mode">
|
||||
<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="16 18 22 12 16 6" /><polyline points="8 6 2 12 8 18" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="nw-btn" class:active={$readOnly} onclick={() => ($readOnly = !$readOnly)} title={$readOnly ? 'Switch to Edit Mode' : 'Switch to View Mode'}>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
{#if $readOnly}
|
||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
{:else}
|
||||
<path d="M17.94 17.94A10.07 10.07 0 0112 20c-7 0-11-8-11-8a18.45 18.45 0 015.06-5.94" />
|
||||
<path d="M9.9 4.24A9.12 9.12 0 0112 4c7 0 11 8 11 8a18.5 18.5 0 01-2.16 3.19" />
|
||||
<line x1="1" y1="1" x2="23" y2="23" />
|
||||
{/if}
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{#if !isMac}
|
||||
<div class="titlebar-controls">
|
||||
<button class="titlebar-btn" onclick={() => appWindow.minimize()} title="Minimize">
|
||||
<svg width="10" height="10" viewBox="0 0 10 10">
|
||||
<line x1="1" y1="5" x2="9" y2="5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="titlebar-btn" onclick={() => appWindow.toggleMaximize()} title={maximized ? 'Restore' : 'Maximize'}>
|
||||
{#if maximized}
|
||||
<svg width="10" height="10" viewBox="0 0 10 10">
|
||||
<rect x="2.5" y="0.5" width="7" height="7" rx="1" fill="none" stroke="currentColor" stroke-width="1.2" />
|
||||
<rect x="0.5" y="2.5" width="7" height="7" rx="1" fill="var(--bg-secondary)" stroke="currentColor" stroke-width="1.2" />
|
||||
</svg>
|
||||
{:else}
|
||||
<svg width="10" height="10" viewBox="0 0 10 10">
|
||||
<rect x="1" y="1" width="8" height="8" rx="1" fill="none" stroke="currentColor" stroke-width="1.2" />
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
<button class="titlebar-btn titlebar-close" onclick={() => appWindow.close()} title="Close">
|
||||
<svg width="10" height="10" viewBox="0 0 10 10">
|
||||
<line x1="1.5" y1="1.5" x2="8.5" y2="8.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" />
|
||||
<line x1="8.5" y1="1.5" x2="1.5" y2="8.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if loadError}
|
||||
<div class="nw-error">
|
||||
<p>Failed to load note</p>
|
||||
<p class="nw-error-detail">{loadError}</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="nw-editor">
|
||||
<Editor bind:this={editor} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.note-window {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.nw-titlebar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 34px;
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
user-select: none;
|
||||
flex-shrink: 0;
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
.nw-titlebar.macos {
|
||||
padding-left: 78px;
|
||||
}
|
||||
|
||||
.nw-titlebar-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding-left: 14px;
|
||||
pointer-events: none;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.nw-title {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--text-tertiary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.nw-dirty-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.titlebar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-right: 8px;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.nw-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
border-radius: 7px;
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
|
||||
.nw-btn:hover {
|
||||
background: var(--bg-tertiary, var(--bg-hover));
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.nw-btn.active {
|
||||
background: color-mix(in srgb, var(--accent) 18%, transparent);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.titlebar-controls {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.titlebar-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 38px;
|
||||
height: 100%;
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--text-tertiary);
|
||||
cursor: pointer;
|
||||
transition: background 0.1s, color 0.1s;
|
||||
}
|
||||
|
||||
.titlebar-btn:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.titlebar-close:hover {
|
||||
background: #e81123;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nw-editor {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nw-error {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.nw-error-detail {
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
max-width: 400px;
|
||||
text-align: center;
|
||||
word-break: break-word;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { showSettings, theme, appConfig, updateAvailable as globalUpdateAvailable, updateObj as globalUpdateObj, installType, settingsTab } from '$lib/stores/app';
|
||||
import { setTheme, setAccentColor, setFontSize, setFontFamily, setGeneralSettings, importObsidian, createBackup, listBackups, restoreBackup, deleteBackup, setBackupSettings, setAiSettings, testAiConnection } from '$lib/api';
|
||||
import { setTheme, setAccentColor, setFontSize, setFontFamily, setLineHeight, 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';
|
||||
import { check as checkUpdate } from '@tauri-apps/plugin-updater';
|
||||
@@ -293,9 +293,17 @@
|
||||
{ name: 'Mono', value: 'mono', stack: '"JetBrains Mono", "Fira Code", "Cascadia Code", monospace' },
|
||||
];
|
||||
|
||||
const lineHeightPresets = [
|
||||
{ label: 'Tight', value: 1.4 },
|
||||
{ label: 'Default', value: 1.6 },
|
||||
{ label: 'Relaxed', value: 1.8 },
|
||||
{ label: 'Loose', value: 2.0 },
|
||||
];
|
||||
|
||||
let activeAccent = $state($appConfig?.accent_color ?? 'Indigo');
|
||||
let activeFontSize = $state($appConfig?.font_size ?? 14);
|
||||
let activeFontFamily = $state($appConfig?.font_family ?? 'system');
|
||||
let activeLineHeight = $state($appConfig?.line_height ?? 1.6);
|
||||
|
||||
// General settings
|
||||
let compactNotes = $state($appConfig?.compact_notes ?? false);
|
||||
@@ -422,6 +430,17 @@
|
||||
document.documentElement.style.setProperty('--editor-font-family', stack);
|
||||
}
|
||||
|
||||
function selectLineHeight(value: number) {
|
||||
activeLineHeight = value;
|
||||
applyLineHeight(value);
|
||||
if ($appConfig) $appConfig.line_height = value;
|
||||
setLineHeight(value).catch((e) => console.error('Failed to save line height:', e));
|
||||
}
|
||||
|
||||
function applyLineHeight(value: number) {
|
||||
document.documentElement.style.setProperty('--editor-line-height', String(value));
|
||||
}
|
||||
|
||||
function close() {
|
||||
// Ensure AI settings are saved when closing (in case input wasn't blurred)
|
||||
// Only save if the key differs from what's already stored
|
||||
@@ -454,6 +473,11 @@
|
||||
applyFontFamily(preset.stack);
|
||||
}
|
||||
}
|
||||
const savedLineHeight = $appConfig?.line_height;
|
||||
if (savedLineHeight) {
|
||||
activeLineHeight = savedLineHeight;
|
||||
applyLineHeight(savedLineHeight);
|
||||
}
|
||||
// Sync general settings
|
||||
if ($appConfig) {
|
||||
compactNotes = $appConfig.compact_notes ?? false;
|
||||
@@ -748,6 +772,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h3>Line Height</h3>
|
||||
<div class="font-size-options">
|
||||
{#each lineHeightPresets as preset}
|
||||
<button
|
||||
class="font-size-btn"
|
||||
class:active={activeLineHeight === preset.value}
|
||||
onclick={() => selectLineHeight(preset.value)}
|
||||
>
|
||||
<span class="font-size-label">{preset.label}</span>
|
||||
<span class="font-size-value">{preset.value}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h3>Font</h3>
|
||||
<div class="font-family-options">
|
||||
|
||||
@@ -41,6 +41,7 @@ export interface AppConfig {
|
||||
accent_color: string | null;
|
||||
font_size: number | null;
|
||||
font_family: string | null;
|
||||
line_height: number | null;
|
||||
compact_notes: boolean;
|
||||
time_format: string;
|
||||
gpu_acceleration: boolean;
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { WebviewWindow } from '@tauri-apps/api/webviewWindow';
|
||||
|
||||
const openNoteWindows = new Map<string, string>();
|
||||
|
||||
export async function openNoteWindow(notePath: string, noteTitle: string) {
|
||||
const existingLabel = openNoteWindows.get(notePath);
|
||||
if (existingLabel) {
|
||||
const existing = await WebviewWindow.getByLabel(existingLabel);
|
||||
if (existing) {
|
||||
await existing.show();
|
||||
await existing.unminimize();
|
||||
await existing.setFocus();
|
||||
return;
|
||||
}
|
||||
openNoteWindows.delete(notePath);
|
||||
}
|
||||
|
||||
const label = `note-${Date.now()}`;
|
||||
const url = `/?note=${encodeURIComponent(notePath)}`;
|
||||
|
||||
const noteWindow = new WebviewWindow(label, {
|
||||
url,
|
||||
title: `${noteTitle} - HelixNotes`,
|
||||
width: 900,
|
||||
height: 700,
|
||||
minWidth: 500,
|
||||
minHeight: 400,
|
||||
decorations: false,
|
||||
center: true
|
||||
});
|
||||
|
||||
openNoteWindows.set(notePath, label);
|
||||
|
||||
noteWindow.once('tauri://destroyed', () => {
|
||||
openNoteWindows.delete(notePath);
|
||||
});
|
||||
|
||||
noteWindow.once('tauri://error', (e) => {
|
||||
console.error('Failed to create note window:', e);
|
||||
openNoteWindows.delete(notePath);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user