v1.2.5 - calendar view for daily notes, trash notebook restore, attachment/link fixes

This commit is contained in:
Yuri Karamian
2026-03-12 00:12:45 +01:00
parent 0b17a7c458
commit 7be1b56b7c
15 changed files with 572 additions and 59 deletions
+1 -1
View File
@@ -1850,7 +1850,7 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "helixnotes"
version = "1.2.4"
version = "1.2.5"
dependencies = [
"arboard",
"chrono",
+1 -1
View File
@@ -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"
+29 -12
View File
@@ -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<NoteEntry, String> {
pub fn create_daily_note(state: State<'_, AppState>, date: Option<String>) -> Result<NoteEntry, 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")?;
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<Vec<NoteEntry>, String> {
pub fn get_trash(state: State<'_, AppState>) -> Result<TrashContents, 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::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<String>,
) -> Result<String, 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::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<String, 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::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]
+1
View File
@@ -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,
+14
View File
@@ -19,6 +19,20 @@ pub struct NoteEntry {
pub preview: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrashContents {
pub notes: Vec<NoteEntry>,
pub notebooks: Vec<TrashNotebookEntry>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrashNotebookEntry {
pub name: String,
pub path: String,
pub note_count: usize,
pub modified: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NotebookEntry {
pub name: String,
+91 -11
View File
@@ -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<NotebookEntry> {
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<NoteEntry, String> {
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<NoteEntry, String> {
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<String, S
Ok(dest.to_string_lossy().to_string())
}
pub fn get_trash_notes(vault_path: &str) -> Result<Vec<NoteEntry>, 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<TrashContents, String> {
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<Vec<NoteEntry>, 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::<Utc>::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<String, String> {
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(())
}
+1 -1
View File
@@ -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",