mirror of
https://gitlab.com/ArkHost/HelixNotes.git
synced 2026-07-24 07:45:57 +02:00
v1.2.9: mermaid diagrams, math editor, external viewer, manual notebook sort, heading hotkeys
This commit is contained in:
@@ -190,7 +190,6 @@ async fn stream_anthropic(
|
||||
}
|
||||
}
|
||||
|
||||
// Stream ended — send done
|
||||
let _ = app.emit(
|
||||
"ai-stream",
|
||||
AiStreamEvent {
|
||||
|
||||
@@ -207,7 +207,7 @@ pub fn restore_backup(vault_path: &str, backup_path: &str) -> Result<(), String>
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
// Keep hidden dirs (.helixnotes) — we'll handle attachments separately
|
||||
// Keep hidden dirs (.helixnotes) - we'll handle attachments separately
|
||||
if name.starts_with('.') {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ use tauri::{AppHandle, Manager, State};
|
||||
pub fn open_vault(app: AppHandle, state: State<'_, AppState>, path: String) -> Result<(), String> {
|
||||
operations::ensure_vault_structure(&path)?;
|
||||
|
||||
// Initialize search index — rebuild in background on Android (FUSE is slow)
|
||||
// Initialize search index - rebuild in background on Android (FUSE is slow)
|
||||
let search = std::sync::Arc::new(SearchIndex::new(&path)?);
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
@@ -430,7 +430,7 @@ pub fn get_graph_data(state: State<'_, AppState>) -> Result<crate::types::GraphD
|
||||
let vault_path = config.active_vault.as_ref().ok_or("No active vault")?;
|
||||
let vault = std::path::Path::new(vault_path);
|
||||
|
||||
// Pass 1: collect ALL notes as nodes (no title deduplication — every note must appear)
|
||||
// Pass 1: collect ALL notes as nodes (no title deduplication - every note must appear)
|
||||
let mut graph_nodes = Vec::new();
|
||||
// title → list of node indices (one title can map to multiple notes in different folders)
|
||||
let mut title_to_idxs: HashMap<String, Vec<usize>> = HashMap::new();
|
||||
@@ -495,7 +495,7 @@ pub fn get_graph_data(state: State<'_, AppState>) -> Result<crate::types::GraphD
|
||||
if src == tgt { return; }
|
||||
if edge_map.contains_key(&(src, tgt)) { return; } // exact duplicate
|
||||
if let Some(&rev_idx) = edge_map.get(&(tgt, src)) {
|
||||
// Reverse direction already exists — mark it as bidirectional
|
||||
// Reverse direction already exists - mark it as bidirectional
|
||||
edges[rev_idx].bidirectional = true;
|
||||
} else {
|
||||
let idx = edges.len();
|
||||
@@ -527,7 +527,7 @@ pub fn get_graph_data(state: State<'_, AppState>) -> Result<crate::types::GraphD
|
||||
if let Some(&target_idx) = relpath_to_idx.get(&link) {
|
||||
add_edge(&mut edges, &mut edge_map, source_idx, target_idx);
|
||||
} else if let Some(targets) = title_to_idxs.get(&link) {
|
||||
// 2. Title match — connect to all notes with this title
|
||||
// 2. Title match - connect to all notes with this title
|
||||
for &target_idx in targets {
|
||||
add_edge(&mut edges, &mut edge_map, source_idx, target_idx);
|
||||
}
|
||||
@@ -728,6 +728,32 @@ pub fn copy_image_to_clipboard(_path: String) -> Result<(), String> {
|
||||
Err("Clipboard image copy not supported on Android".to_string())
|
||||
}
|
||||
|
||||
/// Copy PNG bytes directly to the system clipboard.
|
||||
#[cfg(not(target_os = "android"))]
|
||||
#[tauri::command]
|
||||
pub fn copy_png_to_clipboard(data: Vec<u8>) -> Result<(), String> {
|
||||
let img = image::load_from_memory(&data)
|
||||
.map_err(|e| format!("Failed to decode image: {}", e))?;
|
||||
let rgba = img.to_rgba8();
|
||||
let (w, h) = rgba.dimensions();
|
||||
let img_data = arboard::ImageData {
|
||||
width: w as usize,
|
||||
height: h as usize,
|
||||
bytes: std::borrow::Cow::Owned(rgba.into_raw()),
|
||||
};
|
||||
let mut clipboard = arboard::Clipboard::new()
|
||||
.map_err(|e| format!("Clipboard init failed: {}", e))?;
|
||||
clipboard.set_image(img_data)
|
||||
.map_err(|e| format!("Failed to set clipboard image: {}", e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
#[tauri::command]
|
||||
pub fn copy_png_to_clipboard(_data: Vec<u8>) -> Result<(), String> {
|
||||
Err("Clipboard image copy not supported on Android".to_string())
|
||||
}
|
||||
|
||||
// ── Attachments ──
|
||||
|
||||
#[tauri::command]
|
||||
@@ -1459,6 +1485,13 @@ pub fn copy_file_to(source: String, destination: String) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn write_bytes_to(destination: String, data: Vec<u8>) -> Result<(), String> {
|
||||
std::fs::write(&destination, &data).map_err(|e| format!("Failed to write file: {}", e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
// ── Backup ──
|
||||
|
||||
#[tauri::command]
|
||||
@@ -1717,10 +1750,10 @@ pub fn ai_ask(
|
||||
|
||||
let mut system_prompt = "You are a helpful writing assistant inside a note-taking app called HelixNotes. \
|
||||
You help users improve, rewrite, summarize, and transform their text. \
|
||||
Return ONLY the resulting text — no explanations, no markdown code fences, no preamble. \
|
||||
Return ONLY the resulting text - no explanations, no markdown code fences, no preamble. \
|
||||
Preserve the original language of the text unless specifically asked to translate. \
|
||||
Preserve any markdown formatting (bold, italic, links, images, tables, etc.) unless the user asks to change it. \
|
||||
If the text contains placeholders like __MEDIA_0__, __MEDIA_1__, etc., keep them exactly as they are in their original positions — they represent images and embedded files.".to_string();
|
||||
If the text contains placeholders like __MEDIA_0__, __MEDIA_1__, etc., keep them exactly as they are in their original positions - they represent images and embedded files.".to_string();
|
||||
|
||||
if let Some(ref style) = writing_style {
|
||||
system_prompt.push_str(&format!(
|
||||
|
||||
@@ -157,7 +157,7 @@ fn prune_versions(dir: &Path, max: u32) -> Result<(), String> {
|
||||
.filter(|p| p.extension().map_or(false, |ext| ext == "md"))
|
||||
.collect();
|
||||
|
||||
// Sort by name (timestamps sort lexicographically) — newest last
|
||||
// Sort by name (timestamps sort lexicographically) - newest last
|
||||
files.sort();
|
||||
|
||||
if files.len() as u32 > max {
|
||||
|
||||
@@ -139,6 +139,8 @@ pub fn run() {
|
||||
commands::open_file,
|
||||
commands::open_url,
|
||||
commands::copy_file_to,
|
||||
commands::write_bytes_to,
|
||||
commands::copy_png_to_clipboard,
|
||||
commands::create_backup,
|
||||
commands::list_backups,
|
||||
commands::restore_backup,
|
||||
|
||||
@@ -199,6 +199,10 @@ pub struct VaultState {
|
||||
pub sidebar_collapsed: bool,
|
||||
#[serde(default)]
|
||||
pub collapsed_notebooks: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub notebook_sort_mode: String,
|
||||
#[serde(default)]
|
||||
pub notebook_order: std::collections::HashMap<String, i32>,
|
||||
}
|
||||
|
||||
impl Default for VaultState {
|
||||
@@ -209,6 +213,8 @@ impl Default for VaultState {
|
||||
notelist_width: 280.0,
|
||||
sidebar_collapsed: false,
|
||||
collapsed_notebooks: Vec::new(),
|
||||
notebook_sort_mode: String::new(),
|
||||
notebook_order: std::collections::HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ pub fn parse_note(raw: &str, filename: &str) -> (NoteMeta, String) {
|
||||
.and_then(|s| parse_date_flexible(&s))
|
||||
.unwrap_or_else(Utc::now);
|
||||
|
||||
// Don't generate UUID on read — empty string signals "no ID yet"
|
||||
// Don't generate UUID on read - empty string signals "no ID yet"
|
||||
let id = fm.id.unwrap_or_default();
|
||||
|
||||
let meta = NoteMeta {
|
||||
@@ -309,7 +309,7 @@ fn strip_html_and_markdown(input: &str) -> String {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Markdown links: [text](url) — keep the text, skip the URL
|
||||
// Markdown links: [text](url) - keep the text, skip the URL
|
||||
if ch == '[' {
|
||||
let mut link_text = String::new();
|
||||
let mut depth = 1;
|
||||
|
||||
@@ -281,7 +281,7 @@ fn read_note_entry(path: &Path, vault_root: &Path) -> Result<NoteEntry, String>
|
||||
/// uses filesystem timestamps for dates. No preview text.
|
||||
#[cfg(target_os = "android")]
|
||||
fn read_note_entry_metadata_only(path: &Path, vault_root: &Path) -> Result<NoteEntry, String> {
|
||||
// Read first 2KB — enough for frontmatter with tags, title, pinned
|
||||
// Read first 2KB - enough for frontmatter with tags, title, pinned
|
||||
let mut file = fs::File::open(path).map_err(|e| e.to_string())?;
|
||||
let mut buf = vec![0u8; 2048];
|
||||
let bytes_read = file.read(&mut buf).map_err(|e| e.to_string())?;
|
||||
@@ -917,7 +917,6 @@ pub fn get_trash_contents(vault_path: &str) -> Result<TrashContents, String> {
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user