Refine note info path and history

This commit is contained in:
Yuri Karamian
2026-08-03 02:03:58 +02:00
parent 213797a38e
commit 6b165059cc
5 changed files with 136 additions and 237 deletions
+4
View File
@@ -217,6 +217,10 @@ export async function saveVaultState(vaultState: VaultState): Promise<void> {
return invoke("save_vault_state", { vaultState });
}
export async function copyTextToClipboard(text: string): Promise<void> {
return invoke("copy_text_to_clipboard", { text });
}
export async function readClipboardImage(): Promise<number[]> {
return invoke("read_clipboard_image");
}
+113 -113
View File
@@ -37,7 +37,7 @@
import { convertFileSrc } from '@tauri-apps/api/core';
import { getCurrentWindow } from '@tauri-apps/api/window';
import { readFile } from '@tauri-apps/plugin-fs';
import { openFile, openUrl, copyFileTo, copyImageToClipboard as copyImageToClipboardCmd, writeBytesTo, copyPngToClipboard } from '$lib/api';
import { openFile, openUrl, copyFileTo, copyImageToClipboard as copyImageToClipboardCmd, writeBytesTo, copyPngToClipboard, copyTextToClipboard } from '$lib/api';
import { save as saveDialog } from '@tauri-apps/plugin-dialog';
import { activeNote, activeNotePath, appConfig, editorDirty, sourceMode, focusMode, readOnly, quickAccessPaths, notes, navHistory, canGoBack, canGoForward, viewerNote, notebooks, outlineWidth } from '$lib/stores/app';
import { saveNote, saveImage, saveAttachment, readClipboardImage, addQuickAccess, removeQuickAccess, getQuickAccess, getNoteVersions, getNoteVersionContent, createVersion, aiAsk, getAllNoteTitles, readNote, renameNote } from '$lib/api';
@@ -331,6 +331,15 @@
let infoToggleBtnEl = $state<HTMLElement | null>(null);
let wordCount = $state(0);
let charCount = $state(0);
let infoPathCopyState = $state<'idle' | 'copied' | 'error'>('idle');
let infoPathCopyTimer: ReturnType<typeof setTimeout> | null = null;
$effect(() => {
$activeNotePath;
infoPathCopyState = 'idle';
if (infoPathCopyTimer) clearTimeout(infoPathCopyTimer);
infoPathCopyTimer = null;
});
// In-note search
let noteSearchOpen = $state(false);
@@ -3066,19 +3075,27 @@
const scheduleCounts = debounce(updateCounts, 250);
async function toggleInfo() {
if (!showInfo && $activeNote) {
historyLoading = true;
try {
historyVersions = await getNoteVersions($activeNote.meta.id);
} catch {
historyVersions = [];
}
historyLoading = false;
}
function toggleInfo() {
showInfo = !showInfo;
}
async function copyActiveNotePath() {
const path = $activeNotePath;
if (!path) return;
if (infoPathCopyTimer) clearTimeout(infoPathCopyTimer);
try {
await copyTextToClipboard(path);
infoPathCopyState = 'copied';
} catch (e) {
console.error('Failed to copy note path:', e);
infoPathCopyState = 'error';
}
infoPathCopyTimer = setTimeout(() => {
infoPathCopyState = 'idle';
infoPathCopyTimer = null;
}, 1600);
}
async function toggleHistory() {
showHistory = !showHistory;
historyPreview = null;
@@ -5806,6 +5823,7 @@
clearTaskReveal();
destroyEditor();
unlistenFileChange?.();
if (infoPathCopyTimer) clearTimeout(infoPathCopyTimer);
});
</script>
@@ -6401,37 +6419,35 @@
</span>
</div>
{/if}
</div>
<div class="info-section info-section-versions">
<div class="info-section-header">
<div class="info-section-label">Snapshots</div>
<button class="info-snapshot-btn" onclick={handleCreateVersion} title="Save snapshot">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
</svg>
Save now
</button>
</div>
{#if historyLoading}
<div class="info-empty">Loading...</div>
{:else if historyVersions.length === 0}
<div class="info-empty">No snapshots yet. They're created automatically as you edit.</div>
{:else}
<div class="info-versions-list">
{#each historyVersions as v}
<button
class="info-version-item"
class:active={historySelected?.timestamp === v.timestamp}
onclick={() => { previewVersion(v); showHistory = true; showInfo = false; }}
>
<span class="info-version-date">{formatVersionDate(v.timestamp)}</span>
<span class="info-version-size">{formatVersionSize(v.size)}</span>
</button>
{/each}
{#if !isMobile && $activeNotePath}
<div class="info-note-path">
<div class="info-note-path-header">
<span class="info-key">Path on disk</span>
<button
type="button"
class="info-copy-path-btn"
class:copied={infoPathCopyState === 'copied'}
class:error={infoPathCopyState === 'error'}
onclick={copyActiveNotePath}
aria-live="polite"
title="Copy note path"
>
{#if infoPathCopyState === 'copied'}
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="m5 12 4 4L19 6" /></svg>
Copied
{:else if infoPathCopyState === 'error'}
Copy failed
{:else}
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="11" height="11" rx="2" /><path d="M15 9V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h3" /></svg>
Copy
{/if}
</button>
</div>
<code class="info-note-path-value" title={$activeNotePath}>{$activeNotePath}</code>
</div>
{/if}
</div>
</div>
{/if}
</div>
@@ -11402,13 +11418,6 @@
flex: 1;
}
.info-section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.info-section-label {
font-size: 11px;
font-weight: 600;
@@ -11418,28 +11427,6 @@
margin-bottom: 8px;
}
.info-section-header .info-section-label {
margin-bottom: 0;
}
.info-snapshot-btn {
display: flex;
align-items: center;
gap: 4px;
background: none;
border: 1px solid var(--border-color);
border-radius: 4px;
color: var(--text-secondary);
cursor: pointer;
padding: 3px 7px;
font-size: 11px;
}
.info-snapshot-btn:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.info-row {
display: flex;
align-items: baseline;
@@ -11472,6 +11459,64 @@
max-width: 140px;
}
.info-note-path {
min-width: 0;
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid var(--border-light);
}
.info-note-path-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.info-copy-path-btn {
display: inline-flex;
align-items: center;
gap: 4px;
margin: -3px -4px -3px 0;
padding: 3px 5px;
border: none;
border-radius: 4px;
background: transparent;
color: var(--accent);
font-size: 11px;
font-weight: 600;
cursor: pointer;
}
.info-copy-path-btn:hover {
background: var(--accent-light);
}
.info-copy-path-btn:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 1px;
}
.info-copy-path-btn.copied {
color: var(--success);
}
.info-copy-path-btn.error {
color: var(--danger);
}
.info-note-path-value {
display: block;
margin-top: 4px;
overflow: hidden;
color: var(--text-secondary);
font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Consolas, monospace;
font-size: 10px;
line-height: 1.4;
text-overflow: ellipsis;
white-space: nowrap;
}
.info-tags {
display: flex;
flex-wrap: wrap;
@@ -11487,51 +11532,6 @@
padding: 1px 5px;
}
.info-empty {
font-size: 12px;
color: var(--text-tertiary);
line-height: 1.5;
padding: 4px 0;
}
.info-versions-list {
display: flex;
flex-direction: column;
gap: 1px;
margin-top: 4px;
}
.info-version-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 8px;
border-radius: 5px;
background: none;
border: none;
cursor: pointer;
text-align: left;
width: 100%;
}
.info-version-item:hover {
background: var(--bg-hover);
}
.info-version-item.active {
background: var(--bg-active);
}
.info-version-date {
font-size: 12px;
color: var(--text-primary);
}
.info-version-size {
font-size: 11px;
color: var(--text-tertiary);
}
.editor-container.mobile .editor-body-row:has(.info-panel) > .editor-body {
display: none;
}
+1 -124
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import { showInfo, appConfig, activeNotePath } from '$lib/stores/app';
import { showInfo } from '$lib/stores/app';
import { getVaultStats, findOrphanedAttachments, trashOrphanedAttachments } from '$lib/api';
import type { OrphanAttachment } from '$lib/api';
import { openUrl } from '$lib/api';
@@ -90,17 +90,11 @@
let orphans = $state<OrphanAttachment[] | null>(null);
let scanning = $state(false);
let trashing = $state(false);
let locationCopyState = $state<'idle' | 'copied' | 'error'>('idle');
let locationCopyTimer: ReturnType<typeof setTimeout> | null = null;
const orphanTotal = $derived((orphans ?? []).reduce((a, o) => a + o.size, 0));
const diskLocation = $derived($activeNotePath ?? $appConfig?.active_vault ?? null);
getVersion().then(v => appVersion = v).catch(() => appVersion = '0.0.0');
function close() {
if (locationCopyTimer) clearTimeout(locationCopyTimer);
locationCopyTimer = null;
locationCopyState = 'idle';
$showInfo = false;
activeTab = isMobile ? 'about' : 'shortcuts';
}
@@ -109,24 +103,6 @@
openUrl(url).catch(console.error);
}
async function copyDiskLocation() {
if (!diskLocation) return;
if (locationCopyTimer) clearTimeout(locationCopyTimer);
try {
if (!navigator.clipboard?.writeText) throw new Error('Clipboard API unavailable');
await navigator.clipboard.writeText(diskLocation);
locationCopyState = 'copied';
} catch (e) {
console.error('Failed to copy disk location:', e);
locationCopyState = 'error';
}
locationCopyTimer = setTimeout(() => {
locationCopyState = 'idle';
locationCopyTimer = null;
}, 1600);
}
function formatSize(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
@@ -200,34 +176,6 @@
<p class="app-version">v{appVersion}</p>
<p class="app-description">A local markdown note-taking app.</p>
{#if diskLocation}
<div class="disk-location">
<div class="disk-location-header">
<span class="disk-location-label">Disk location</span>
<button
class="copy-location-btn"
class:copied={locationCopyState === 'copied'}
class:error={locationCopyState === 'error'}
onclick={copyDiskLocation}
aria-live="polite"
title="Copy disk location"
>
{#if locationCopyState === 'copied'}
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="m5 12 4 4L19 6" /></svg>
Copied
{:else if locationCopyState === 'error'}
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12" /></svg>
Copy failed
{:else}
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="11" height="11" rx="2" /><path d="M15 9V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h3" /></svg>
Copy
{/if}
</button>
</div>
<code class="disk-location-path" title={diskLocation}>{diskLocation}</code>
</div>
{/if}
{#if stats}
<div class="info-stats">
<div class="stat-row">
@@ -468,71 +416,6 @@
margin-top: 4px;
}
.disk-location {
width: 100%;
margin-top: 18px;
padding: 11px 14px 12px;
border: 1px solid var(--border-light);
border-radius: 10px;
background: var(--bg-secondary);
text-align: left;
}
.disk-location-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.disk-location-label {
color: var(--text-tertiary);
font-size: 11px;
font-weight: 600;
letter-spacing: 0.02em;
}
.disk-location-path {
display: block;
margin-top: 7px;
color: var(--text-primary);
font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Consolas, monospace;
font-size: 11px;
line-height: 1.45;
overflow-wrap: anywhere;
}
.copy-location-btn {
display: inline-flex;
align-items: center;
gap: 5px;
margin: -4px -6px -4px 0;
padding: 4px 6px;
border: none;
border-radius: 6px;
background: transparent;
color: var(--accent);
font-size: 11px;
font-weight: 600;
cursor: pointer;
}
.copy-location-btn:hover {
background: var(--accent-light);
}
.copy-location-btn.copied {
color: var(--success);
}
.copy-location-btn.error {
color: var(--danger);
}
.disk-location + .info-stats {
margin-top: 12px;
}
.info-stats {
width: 100%;
margin-top: 20px;
@@ -784,11 +667,5 @@
.info-header {
padding-top: calc(env(safe-area-inset-top, 12px) + 12px);
}
.copy-location-btn {
min-height: 44px;
margin-block: -10px;
padding-inline: 10px;
}
}
</style>