diff --git a/package.json b/package.json index d19d62e..a40e5cd 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "helixnotes", "private": true, "license": "AGPL-3.0-or-later", - "version": "1.2.4", + "version": "1.2.5", "type": "module", "scripts": { "dev": "vite dev", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 9074872..3b7f629 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1850,7 +1850,7 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "helixnotes" -version = "1.2.4" +version = "1.2.5" dependencies = [ "arboard", "chrono", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 02fbd54..f986ace 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "helixnotes" -version = "1.2.4" +version = "1.2.5" description = "Local markdown note-taking app" authors = ["HelixNotes"] license = "AGPL-3.0-or-later" diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 4270e9c..987dcc5 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -201,9 +201,11 @@ pub fn move_notebook( #[tauri::command] pub fn delete_notebook(state: State<'_, AppState>, path: String) -> Result<(), String> { - let config = state.config.lock().map_err(|e| e.to_string())?; - let vault_path = config.active_vault.as_ref().ok_or("No active vault")?; - operations::delete_notebook(vault_path, &path) + let vault_path = { + let config = state.config.lock().map_err(|e| e.to_string())?; + config.active_vault.as_ref().ok_or("No active vault")?.clone() + }; + operations::delete_notebook(&vault_path, &path) } // ── Notes ── @@ -272,10 +274,10 @@ pub fn create_note( } #[tauri::command] -pub fn create_daily_note(state: State<'_, AppState>) -> Result { +pub fn create_daily_note(state: State<'_, AppState>, date: Option) -> Result { let config = state.config.lock().map_err(|e| e.to_string())?; let vault_path = config.active_vault.as_ref().ok_or("No active vault")?; - let entry = operations::create_daily_note(vault_path)?; + let entry = operations::create_daily_note(vault_path, date.as_deref())?; if let Ok(search_guard) = state.search_index.lock() { if let Some(ref search) = *search_guard { @@ -554,10 +556,10 @@ pub fn reindex(state: State<'_, AppState>) -> Result<(), String> { // ── Trash ── #[tauri::command] -pub fn get_trash(state: State<'_, AppState>) -> Result, String> { +pub fn get_trash(state: State<'_, AppState>) -> Result { let config = state.config.lock().map_err(|e| e.to_string())?; let vault_path = config.active_vault.as_ref().ok_or("No active vault")?; - operations::get_trash_notes(vault_path) + operations::get_trash_contents(vault_path) } #[tauri::command] @@ -566,14 +568,29 @@ pub fn restore_note( trash_path: String, dest_notebook: Option, ) -> Result { - let config = state.config.lock().map_err(|e| e.to_string())?; - let vault_path = config.active_vault.as_ref().ok_or("No active vault")?; - operations::restore_note(vault_path, &trash_path, dest_notebook.as_deref()) + let vault_path = { + let config = state.config.lock().map_err(|e| e.to_string())?; + config.active_vault.as_ref().ok_or("No active vault")?.clone() + }; + operations::restore_note(&vault_path, &trash_path, dest_notebook.as_deref()) } #[tauri::command] -pub fn permanent_delete(path: String) -> Result<(), String> { - operations::permanent_delete(&path) +pub fn restore_notebook(state: State<'_, AppState>, trash_path: String) -> Result { + let vault_path = { + let config = state.config.lock().map_err(|e| e.to_string())?; + config.active_vault.as_ref().ok_or("No active vault")?.clone() + }; + operations::restore_notebook(&vault_path, &trash_path) +} + +#[tauri::command] +pub fn permanent_delete(state: State<'_, AppState>, path: String) -> Result<(), String> { + let vault_path = { + let config = state.config.lock().map_err(|e| e.to_string())?; + config.active_vault.as_ref().ok_or("No active vault")?.clone() + }; + operations::permanent_delete(&vault_path, &path) } #[tauri::command] diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 9ee530b..427dcd8 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -117,6 +117,7 @@ pub fn run() { commands::reindex, commands::get_trash, commands::restore_note, + commands::restore_notebook, commands::permanent_delete, commands::empty_trash, commands::load_vault_state, diff --git a/src-tauri/src/types.rs b/src-tauri/src/types.rs index 64daa5c..df7b322 100644 --- a/src-tauri/src/types.rs +++ b/src-tauri/src/types.rs @@ -19,6 +19,20 @@ pub struct NoteEntry { pub preview: String, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TrashContents { + pub notes: Vec, + pub notebooks: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TrashNotebookEntry { + pub name: String, + pub path: String, + pub note_count: usize, + pub modified: DateTime, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NotebookEntry { pub name: String, diff --git a/src-tauri/src/vault/operations.rs b/src-tauri/src/vault/operations.rs index b322573..30d84cc 100644 --- a/src-tauri/src/vault/operations.rs +++ b/src-tauri/src/vault/operations.rs @@ -1,6 +1,6 @@ -use crate::types::{NoteContent, NoteEntry, NoteMeta, NotebookEntry, VaultState}; +use crate::types::{NoteContent, NoteEntry, NoteMeta, NotebookEntry, TrashContents, TrashNotebookEntry, VaultState}; use crate::vault::frontmatter; -use chrono::{Local, Locale, Utc}; +use chrono::{DateTime, Local, Locale, Utc}; use rayon::prelude::*; use std::fs; use std::io::Read; @@ -63,7 +63,9 @@ fn scan_dir_recursive(dir: &Path, vault_root: &str) -> Vec { let mut dirs: Vec<_> = read_dir .filter_map(|e| e.ok()) .filter(|e| { - e.file_type().map(|ft| ft.is_dir()).unwrap_or(false) && !is_hidden(&e.path()) + e.file_type().map(|ft| ft.is_dir()).unwrap_or(false) + && !is_hidden(&e.path()) + && e.file_name().to_string_lossy() != "Daily" }) .collect(); @@ -502,11 +504,15 @@ fn get_system_locale() -> Locale { } } -pub fn create_daily_note(vault_path: &str) -> Result { - let today = Local::now(); - let date_str = today.format("%Y-%m-%d").to_string(); +pub fn create_daily_note(vault_path: &str, date: Option<&str>) -> Result { + let target_date = match date { + Some(d) => chrono::NaiveDate::parse_from_str(d, "%Y-%m-%d") + .map_err(|e| format!("Invalid date: {}", e))?, + None => Local::now().date_naive(), + }; + let date_str = target_date.format("%Y-%m-%d").to_string(); let locale = get_system_locale(); - let title = today.format_localized("%B %d, %Y", locale).to_string(); + let title = target_date.format_localized("%B %d, %Y", locale).to_string(); let dir = Path::new(vault_path).join("Daily"); fs::create_dir_all(&dir).map_err(|e| e.to_string())?; @@ -854,14 +860,29 @@ pub fn move_notebook(notebook_path: &str, dest_parent: &str) -> Result Result, String> { +/// Remove a directory inside trash if it's empty (after restoring/deleting its last note). +fn cleanup_empty_trash_dir(vault_path: &str, dir: Option<&Path>) { + let trash_dir = helixnotes_dir(vault_path).join("trash"); + if let Some(d) = dir { + if d != trash_dir && d.starts_with(&trash_dir) { + if let Ok(mut entries) = fs::read_dir(d) { + if entries.next().is_none() { + let _ = fs::remove_dir(d); + } + } + } + } +} + +pub fn get_trash_contents(vault_path: &str) -> Result { let trash_dir = helixnotes_dir(vault_path).join("trash"); if !trash_dir.exists() { - return Ok(Vec::new()); + return Ok(TrashContents { notes: Vec::new(), notebooks: Vec::new() }); } let vault_root = Path::new(vault_path); let mut notes = Vec::new(); + let mut notebooks = Vec::new(); for entry in fs::read_dir(&trash_dir).map_err(|e| e.to_string())? { let entry = entry.map_err(|e| e.to_string())?; @@ -870,11 +891,39 @@ pub fn get_trash_notes(vault_path: &str) -> Result, String> { if let Ok(note) = read_note_entry(&path, vault_root) { notes.push(note); } + } else if path.is_dir() { + // Deleted notebook — count .md files inside + let note_count = WalkDir::new(&path) + .min_depth(1) + .into_iter() + .filter_map(|e| e.ok()) + .filter(|e| e.path().is_file() && e.path().extension().and_then(|x| x.to_str()) == Some("md")) + .count(); + let dirname = path.file_name().unwrap_or_default().to_string_lossy(); + // Strip timestamp prefix to get original notebook name + let name = if dirname.len() > 18 && dirname.chars().nth(17) == Some('_') { + dirname[18..].to_string() + } else if dirname.len() > 15 && dirname.chars().nth(14) == Some('_') { + dirname[15..].to_string() + } else { + dirname.to_string() + }; + let modified = fs::metadata(&path) + .and_then(|m| m.modified()) + .map(|t| DateTime::::from(t)) + .unwrap_or_else(|_| Utc::now()); + notebooks.push(TrashNotebookEntry { + name, + path: path.to_string_lossy().to_string(), + note_count, + modified, + }); } } notes.sort_by(|a, b| b.meta.modified.cmp(&a.meta.modified)); - Ok(notes) + notebooks.sort_by(|a, b| b.modified.cmp(&a.modified)); + Ok(TrashContents { notes, notebooks }) } pub fn restore_note( @@ -902,18 +951,49 @@ pub fn restore_note( &filename }; + let parent = src.parent().map(|p| p.to_path_buf()); let dest = dest_dir.join(original_name); fs::rename(src, &dest).map_err(|e| e.to_string())?; + + // Clean up empty parent directory (deleted notebook folder) in trash + cleanup_empty_trash_dir(vault_path, parent.as_deref()); + + Ok(dest.to_string_lossy().to_string()) +} + +pub fn restore_notebook(vault_path: &str, trash_path: &str) -> Result { + let src = Path::new(trash_path); + if !src.exists() || !src.is_dir() { + return Err("Trashed notebook does not exist".to_string()); + } + + // Strip timestamp prefix to get original notebook name + let dirname = src.file_name().unwrap_or_default().to_string_lossy(); + let original_name = if dirname.len() > 18 && dirname.chars().nth(17) == Some('_') { + &dirname[18..] + } else if dirname.len() > 15 && dirname.chars().nth(14) == Some('_') { + &dirname[15..] + } else { + &dirname + }; + + let dest = Path::new(vault_path).join(original_name); + fs::rename(src, &dest).map_err(|e| e.to_string())?; Ok(dest.to_string_lossy().to_string()) } -pub fn permanent_delete(path: &str) -> Result<(), String> { +pub fn permanent_delete(vault_path: &str, path: &str) -> Result<(), String> { let p = Path::new(path); + let parent = p.parent().map(|pp| pp.to_path_buf()); if p.is_dir() { fs::remove_dir_all(p).map_err(|e| e.to_string())?; } else { fs::remove_file(p).map_err(|e| e.to_string())?; } + + // Clean up empty parent directory (deleted notebook folder) in trash + cleanup_empty_trash_dir(vault_path, parent.as_deref()); + Ok(()) } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 38c466c..58744eb 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "productName": "HelixNotes", - "version": "1.2.4", + "version": "1.2.5", "identifier": "com.helixnotes.app", "build": { "frontendDist": "../build", diff --git a/src/lib/api.ts b/src/lib/api.ts index b76f328..30be7b7 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -7,6 +7,7 @@ import type { NotebookEntry, NoteTitleEntry, SearchResult, + TrashContents, VaultState, VaultStats, ImportResult, @@ -96,8 +97,8 @@ export async function createNote( return invoke("create_note", { notebookRelative, title }); } -export async function createDailyNote(): Promise { - return invoke("create_daily_note"); +export async function createDailyNote(date?: string): Promise { + return invoke("create_daily_note", { date: date ?? null }); } export async function renameNote( @@ -141,7 +142,7 @@ export async function reindex(): Promise { return invoke("reindex"); } -export async function getTrash(): Promise { +export async function getTrash(): Promise { return invoke("get_trash"); } @@ -152,6 +153,10 @@ export async function restoreNote( return invoke("restore_note", { trashPath, destNotebook }); } +export async function restoreNotebook(trashPath: string): Promise { + return invoke("restore_notebook", { trashPath }); +} + export async function permanentDelete(path: string): Promise { return invoke("permanent_delete", { path }); } diff --git a/src/lib/components/AppLayout.svelte b/src/lib/components/AppLayout.svelte index 3ec64c6..a2580fe 100644 --- a/src/lib/components/AppLayout.svelte +++ b/src/lib/components/AppLayout.svelte @@ -33,6 +33,7 @@ quickAccessPaths, tags, notes, + viewMode, updateAvailable as globalUpdateAvailable, settingsTab } from '$lib/stores/app'; @@ -183,7 +184,8 @@ $activeNotePath = entry.path; $editorDirty = false; editor?.loadNote(entry.path, content.content); - noteList?.refresh(); + $viewMode = 'daily'; + noteList?.refresh(true); sidebar?.refresh(); if (isMobile) $mobileView = 'editor'; } catch (e) { @@ -402,9 +404,11 @@ debouncedRefresh(); }); } else { - unlistenFileChange = await listen('file-changed', async () => { - await sidebar?.refresh(); - await noteList?.refresh(true); + const debouncedDesktopRefresh = debounce(async () => { + await Promise.all([sidebar?.refresh(), noteList?.refresh(true)]); + }, 300); + unlistenFileChange = await listen('file-changed', () => { + debouncedDesktopRefresh(); }); } diff --git a/src/lib/components/Editor.svelte b/src/lib/components/Editor.svelte index 16cd2c4..7a6322f 100644 --- a/src/lib/components/Editor.svelte +++ b/src/lib/components/Editor.svelte @@ -199,6 +199,7 @@ let linkModalInput = $state(null!); let linkSelectionFrom = 0; let linkSelectionTo = 0; + let linkModalDisplayText = ''; let linkSuggestIndex = $state(0); let linkSuggestTitles = $state([]); let linkSuggestFiltered = $derived.by(() => { @@ -2416,10 +2417,21 @@ } // Store raw URL — encoding is handled during markdown serialization/parsing const href = url.replace(/[()]/g, (c) => encodeURIComponent(c)); - editor.chain().focus().setTextSelection({ from: linkSelectionFrom, to: linkSelectionTo }).setMark('link', { href }).run(); + if (linkSelectionFrom === linkSelectionTo) { + // No text selected — insert link text with the mark + const text = linkModalDisplayText || url.replace(/\.md$/, '').split('/').pop() || url; + editor.chain().focus().setTextSelection(linkSelectionFrom).insertContent({ + type: 'text', + text, + marks: [{ type: 'link', attrs: { href } }], + }).run(); + } else { + editor.chain().focus().setTextSelection({ from: linkSelectionFrom, to: linkSelectionTo }).setMark('link', { href }).run(); + } } linkModal = false; linkModalUrl = ''; + linkModalDisplayText = ''; } function linkModalSelectNote(entry: NoteTitleEntry) { @@ -2439,12 +2451,14 @@ } else { linkModalUrl = entry.title + '.md'; } + linkModalDisplayText = entry.title; linkModalConfirm(); } function linkModalCancel() { linkModal = false; linkModalUrl = ''; + linkModalDisplayText = ''; editor?.chain().focus().run(); } @@ -3014,19 +3028,7 @@ if (href.startsWith('http://') || href.startsWith('https://') || href.startsWith('mailto:')) { openUrl(href).catch(console.error); } else { - const decoded = decodeURIComponent(href); - let absPath = decoded; - if (!decoded.startsWith('/')) { - const np = $activeNotePath; - if (np) { - const noteDir = np.substring(0, np.lastIndexOf('/')); - absPath = normalizePath(`${noteDir}/${decoded}`); - } else { - const vaultRoot = $appConfig?.active_vault; - if (vaultRoot) absPath = normalizePath(`${vaultRoot}/${decoded}`); - } - } - openFile(absPath).catch(console.error); + openFile(resolveHrefToAbsPath(href)).catch(console.error); } } @@ -3046,8 +3048,13 @@ if (pos >= 0) { editor.chain().focus().setTextSelection(pos).extendMarkRange('link').run(); } + const { from, to } = editor.state.selection; + linkSelectionFrom = from; + linkSelectionTo = to; linkModalUrl = decodeURIComponent(href); + linkModalDisplayText = ''; linkModal = true; + getAllNoteTitles().then(t => { linkSuggestTitles = t; }).catch(() => {}); tick().then(() => linkModalInput?.focus()); } @@ -3071,12 +3078,16 @@ function resolveHrefToAbsPath(href: string): string { const decoded = decodeURIComponent(href); if (decoded.startsWith('/')) return decoded; + // .helixnotes/ paths are always relative to vault root, not the note's directory + const vaultRoot = $appConfig?.active_vault; + if (decoded.startsWith('.helixnotes/') && vaultRoot) { + return normalizePath(`${vaultRoot}/${decoded}`); + } const notePath = $activeNotePath; if (notePath) { const noteDir = notePath.substring(0, notePath.lastIndexOf('/')); return normalizePath(`${noteDir}/${decoded}`); } - const vaultRoot = $appConfig?.active_vault; if (vaultRoot) return normalizePath(`${vaultRoot}/${decoded}`); return decoded; } diff --git a/src/lib/components/NoteList.svelte b/src/lib/components/NoteList.svelte index 41bd1d1..2743630 100644 --- a/src/lib/components/NoteList.svelte +++ b/src/lib/components/NoteList.svelte @@ -24,17 +24,19 @@ saveNote, getTrash, restoreNote, + restoreNotebook, permanentDelete, getQuickAccess, addQuickAccess, removeQuickAccess, reorderQuickAccess, moveNote, - getAllTags + getAllTags, + createDailyNote } from '$lib/api'; import { formatRelativeTime } from '$lib/utils/time'; import { openNoteWindow } from '$lib/utils/window'; - import type { NoteEntry, SortMode } from '$lib/types'; + import type { NoteEntry, TrashNotebookEntry, SortMode } from '$lib/types'; let { onNoteSelected = (_path: string, _content: string) => {}, onNoteMoved = () => {}, onBeforeNoteSwitch = () => {} }: { onNoteSelected?: (path: string, content: string) => void; @@ -46,6 +48,67 @@ const isMobile = /android|ios/i.test(navigator.userAgent); const isAndroid = /android/i.test(navigator.userAgent); let multiSelectMode = $state(false); + let trashNotebooks = $state([]); + let trashBusy = $state(null); + + // Calendar state for daily notes view + let calMonth = $state(new Date().getMonth()); + let calYear = $state(new Date().getFullYear()); + let dailyDates = $state>(new Set()); + + const calMonthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; + const calDayNames = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su']; + + function calDays(): Array<{ day: number; date: string; current: boolean }> { + const first = new Date(calYear, calMonth, 1); + const lastDay = new Date(calYear, calMonth + 1, 0).getDate(); + // Monday=0 offset + let startDow = first.getDay() - 1; + if (startDow < 0) startDow = 6; + const cells: Array<{ day: number; date: string; current: boolean }> = []; + for (let i = 0; i < startDow; i++) cells.push({ day: 0, date: '', current: false }); + for (let d = 1; d <= lastDay; d++) { + const mm = String(calMonth + 1).padStart(2, '0'); + const dd = String(d).padStart(2, '0'); + const date = `${calYear}-${mm}-${dd}`; + const now = new Date(); + const current = d === now.getDate() && calMonth === now.getMonth() && calYear === now.getFullYear(); + cells.push({ day: d, date, current }); + } + return cells; + } + + function calPrev() { + if (calMonth === 0) { calMonth = 11; calYear--; } + else calMonth--; + } + function calNext() { + if (calMonth === 11) { calMonth = 0; calYear++; } + else calMonth++; + } + function calToday() { + const now = new Date(); + calMonth = now.getMonth(); + calYear = now.getFullYear(); + } + + async function handleDayClick(date: string) { + try { + const entry = await createDailyNote(date); + const content = await readNote(entry.path); + onBeforeNoteSwitch(); + $activeNote = content; + $activeNotePath = entry.path; + onNoteSelected(entry.path, content.content); + if (!dailyDates.has(date)) { + dailyDates = new Set([...dailyDates, date]); + } + noteCache.clear(); + await refresh(); + } catch (e) { + console.error('Failed to open daily note:', e); + } + } /** Derive tags from current $notes store (avoids re-scanning files on mobile) */ function deriveTagsFromNotes() { @@ -205,6 +268,7 @@ if ($viewMode === 'notebook') return $activeNotebook?.name ?? 'Notebook'; if ($viewMode === 'tag') return `#${$activeTag}`; if ($viewMode === 'quickaccess') return 'Quick Access'; + if ($viewMode === 'daily') return 'Daily Notes'; if ($viewMode === 'trash') return 'Trash'; return 'Notes'; }); @@ -214,6 +278,7 @@ function cacheKey(): string { if ($viewMode === 'trash') return 'trash'; + if ($viewMode === 'daily') return 'daily'; if ($viewMode === 'quickaccess') return 'quickaccess'; if ($viewMode === 'tag' && $activeTag) return `tag:${$activeTag}`; if ($viewMode === 'notebook' && $activeNotebook) return `nb:${$activeNotebook.path}`; @@ -234,7 +299,24 @@ try { if ($viewMode === 'trash') { - $notes = await getTrash(); + const trash = await getTrash(); + $notes = trash.notes; + trashNotebooks = trash.notebooks; + } else if ($viewMode === 'daily') { + const vault = $appConfig?.active_vault; + if (vault) { + $notes = await getNotes(`${vault}/Daily`); + // Build set of dates from filenames (YYYY-MM-DD.md) + const dates = new Set(); + for (const n of $notes) { + const fname = n.path.split('/').pop() ?? ''; + const m = fname.match(/^(\d{4}-\d{2}-\d{2})\.md$/); + if (m) dates.add(m[1]); + } + dailyDates = dates; + } else { + $notes = []; + } } else if ($viewMode === 'quickaccess') { $notes = await getQuickAccess(); } else if ($viewMode === 'tag' && $activeTag) { @@ -273,6 +355,14 @@ } export async function handleCreateNote() { + if ($viewMode === 'daily') { + const today = new Date(); + const date = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`; + await handleDayClick(date); + calMonth = today.getMonth(); + calYear = today.getFullYear(); + return; + } const nbRelative = $viewMode === 'notebook' ? $activeNotebook?.relative_path ?? null : null; try { const entry = await createNote(nbRelative, 'Untitled'); @@ -351,6 +441,32 @@ } } + async function handleRestoreNotebook(nb: TrashNotebookEntry) { + trashBusy = nb.path; + try { + await restoreNotebook(nb.path); + noteCache.clear(); + await refresh(); + } catch (e) { + console.error('Failed to restore notebook:', e); + } finally { + trashBusy = null; + } + } + + async function handleDeleteNotebook(nb: TrashNotebookEntry) { + trashBusy = nb.path; + try { + await permanentDelete(nb.path); + noteCache.clear(); + trashNotebooks = trashNotebooks.filter(n => n.path !== nb.path); + } catch (e) { + console.error('Failed to delete notebook permanently:', e); + } finally { + trashBusy = null; + } + } + async function handleTogglePin(note: NoteEntry) { contextMenu = null; try { @@ -811,7 +927,41 @@ {/if}
- {#if $sortedNotes.length === 0} + {#if $viewMode === 'daily'} +
+
+ + + +
+
+ {#each calDayNames as name} +
{name}
+ {/each} + {#each calDays() as cell} + {#if cell.day === 0} +
+ {:else} + + {/if} + {/each} +
+
+ {/if} + + {#if $sortedNotes.length === 0 && $viewMode !== 'daily' && (!($viewMode === 'trash') || trashNotebooks.length === 0)}
{#if $viewMode === 'trash'}

Trash is empty

@@ -822,6 +972,37 @@
{/if} + {#if $viewMode === 'trash' && trashNotebooks.length > 0} + {#each trashNotebooks as nb (nb.path)} +
+
+ + + + {nb.name} +
+ {#if trashBusy === nb.path} +
+ + Working... +
+ {:else} +
{nb.note_count} {nb.note_count === 1 ? 'note' : 'notes'}
+
+ + +
+ {/if} +
+ {/each} + {/if} +
{#each visibleNotes as note, i (note.path)} {@const noteIndex = startIndex + i} @@ -1871,4 +2052,165 @@ .mobile-create-btn:active { opacity: 0.8; } + + .trash-notebook .note-title { + display: flex; + align-items: center; + gap: 6px; + } + + .trash-notebook-actions { + display: flex; + gap: 6px; + margin-top: 6px; + } + + .trash-nb-btn { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 3px 8px; + border: none; + border-radius: 4px; + font-size: 0.75rem; + cursor: pointer; + background: var(--bg-hover); + color: var(--text-secondary); + } + + .trash-nb-btn:hover { + background: var(--bg-active); + } + + .trash-nb-btn.delete:hover { + background: color-mix(in srgb, #ef4444 15%, transparent); + color: #ef4444; + } + + .trash-notebook.busy { + opacity: 0.6; + pointer-events: none; + } + + .trash-busy { + display: flex; + align-items: center; + gap: 6px; + } + + .spinner { + animation: spin 0.8s linear infinite; + } + + @keyframes spin { + to { transform: rotate(360deg); } + } + + /* Calendar */ + .cal { + padding: 8px 10px; + border-bottom: 1px solid var(--border); + } + + .cal-nav { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 6px; + } + + .cal-nav-btn { + background: none; + border: none; + cursor: pointer; + color: var(--text-secondary); + padding: 4px; + border-radius: 4px; + display: flex; + align-items: center; + } + + .cal-nav-btn:hover { + background: var(--bg-hover); + color: var(--text-primary); + } + + .cal-title { + font-size: 0.8rem; + font-weight: 600; + color: var(--text-primary); + background: none; + border: none; + cursor: pointer; + padding: 2px 8px; + border-radius: 4px; + } + + .cal-title:hover { + background: var(--bg-hover); + } + + .cal-grid { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 4px 2px; + padding: 0 2px; + } + + .cal-head { + font-size: 0.65rem; + color: var(--text-secondary); + text-align: center; + padding: 2px 0; + font-weight: 500; + } + + .cal-cell { + display: flex; + align-items: center; + justify-content: center; + font-size: 0.72rem; + width: 28px; + height: 28px; + border-radius: 50%; + border: none; + background: none; + color: var(--text-secondary); + cursor: pointer; + padding: 0; + margin: 0 auto; + } + + .cal-cell.empty { + cursor: default; + } + + .cal-cell:not(.empty):hover { + background: var(--bg-hover); + } + + .cal-cell.today { + color: var(--accent); + font-weight: 700; + } + + .cal-cell.has-note { + background: color-mix(in srgb, var(--accent) 15%, transparent); + color: var(--text-primary); + font-weight: 600; + } + + .cal-cell.has-note:hover { + background: color-mix(in srgb, var(--accent) 25%, transparent); + } + + .cal-cell.active { + background: var(--accent); + color: white; + font-weight: 600; + } + + .cal-cell.active:hover { + background: var(--accent); + } diff --git a/src/lib/components/Sidebar.svelte b/src/lib/components/Sidebar.svelte index 9f6f433..1fe2c55 100644 --- a/src/lib/components/Sidebar.svelte +++ b/src/lib/components/Sidebar.svelte @@ -106,6 +106,13 @@ onViewChanged(); } + function selectDaily() { + $viewMode = 'daily'; + $activeNotebook = null; + $activeTag = null; + onViewChanged(); + } + function selectTrash() { $viewMode = 'trash'; $activeNotebook = null; @@ -379,6 +386,20 @@ Quick Access + +