mirror of
https://gitlab.com/ArkHost/HelixNotes.git
synced 2026-09-19 09:27:29 +02:00
Refine note info path and history
This commit is contained in:
@@ -1231,6 +1231,23 @@ pub fn save_vault_state(state: State<'_, AppState>, vault_state: VaultState) ->
|
||||
|
||||
// ── Clipboard ──
|
||||
|
||||
/// Copy text to the system clipboard through the native backend.
|
||||
#[cfg(desktop)]
|
||||
#[tauri::command]
|
||||
pub fn copy_text_to_clipboard(text: String) -> Result<(), String> {
|
||||
let mut clipboard =
|
||||
arboard::Clipboard::new().map_err(|e| format!("Clipboard init failed: {}", e))?;
|
||||
clipboard
|
||||
.set_text(text)
|
||||
.map_err(|e| format!("Failed to copy text: {}", e))
|
||||
}
|
||||
|
||||
#[cfg(mobile)]
|
||||
#[tauri::command]
|
||||
pub fn copy_text_to_clipboard(_text: String) -> Result<(), String> {
|
||||
Err("Text clipboard copy is only supported on desktop".to_string())
|
||||
}
|
||||
|
||||
/// Read image from system clipboard (bypasses WebKitGTK clipboard bug).
|
||||
/// Returns PNG bytes as Vec<u8>, or error if no image on clipboard.
|
||||
#[cfg(desktop)]
|
||||
|
||||
@@ -170,6 +170,7 @@ pub fn run() {
|
||||
commands::empty_trash,
|
||||
commands::load_vault_state,
|
||||
commands::save_vault_state,
|
||||
commands::copy_text_to_clipboard,
|
||||
commands::read_clipboard_image,
|
||||
commands::copy_image_to_clipboard,
|
||||
commands::save_image,
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
+110
-110
@@ -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}
|
||||
{#if !isMobile && $activeNotePath}
|
||||
<div class="info-note-path">
|
||||
<div class="info-note-path-header">
|
||||
<span class="info-key">Path on disk</span>
|
||||
<button
|
||||
class="info-version-item"
|
||||
class:active={historySelected?.timestamp === v.timestamp}
|
||||
onclick={() => { previewVersion(v); showHistory = true; showInfo = false; }}
|
||||
type="button"
|
||||
class="info-copy-path-btn"
|
||||
class:copied={infoPathCopyState === 'copied'}
|
||||
class:error={infoPathCopyState === 'error'}
|
||||
onclick={copyActiveNotePath}
|
||||
aria-live="polite"
|
||||
title="Copy note path"
|
||||
>
|
||||
<span class="info-version-date">{formatVersionDate(v.timestamp)}</span>
|
||||
<span class="info-version-size">{formatVersionSize(v.size)}</span>
|
||||
{#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>
|
||||
{/each}
|
||||
</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,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>
|
||||
|
||||
Reference in New Issue
Block a user