mirror of
https://gitlab.com/ArkHost/HelixNotes.git
synced 2026-07-23 23:35:57 +02:00
v1.2.0 - Image toolbar, graph view fixes, mobile header improvements
This commit is contained in:
Generated
+50
-3
@@ -647,6 +647,12 @@ dependencies = [
|
||||
"error-code",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "color_quant"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||
|
||||
[[package]]
|
||||
name = "combine"
|
||||
version = "4.6.7"
|
||||
@@ -1603,6 +1609,16 @@ dependencies = [
|
||||
"wasip3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gif"
|
||||
version = "0.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e"
|
||||
dependencies = [
|
||||
"color_quant",
|
||||
"weezl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gio"
|
||||
version = "0.18.4"
|
||||
@@ -1833,13 +1849,14 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "helixnotes"
|
||||
version = "1.1.9"
|
||||
version = "1.2.0"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"chrono",
|
||||
"dirs",
|
||||
"futures",
|
||||
"gray_matter",
|
||||
"image",
|
||||
"log",
|
||||
"notify",
|
||||
"png 0.17.16",
|
||||
@@ -2157,10 +2174,25 @@ checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder-lite",
|
||||
"color_quant",
|
||||
"gif",
|
||||
"image-webp",
|
||||
"moxcms",
|
||||
"num-traits",
|
||||
"png 0.18.1",
|
||||
"tiff",
|
||||
"zune-core 0.5.1",
|
||||
"zune-jpeg 0.5.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image-webp"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3"
|
||||
dependencies = [
|
||||
"byteorder-lite",
|
||||
"quick-error",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5406,7 +5438,7 @@ dependencies = [
|
||||
"half",
|
||||
"quick-error",
|
||||
"weezl",
|
||||
"zune-jpeg",
|
||||
"zune-jpeg 0.4.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7225,13 +7257,28 @@ version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a"
|
||||
|
||||
[[package]]
|
||||
name = "zune-core"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9"
|
||||
|
||||
[[package]]
|
||||
name = "zune-jpeg"
|
||||
version = "0.4.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713"
|
||||
dependencies = [
|
||||
"zune-core",
|
||||
"zune-core 0.4.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zune-jpeg"
|
||||
version = "0.5.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "410e9ecef634c709e3831c2cfdb8d9c32164fae1c67496d5b68fff728eec37fe"
|
||||
dependencies = [
|
||||
"zune-core 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "helixnotes"
|
||||
version = "1.1.9"
|
||||
version = "1.2.0"
|
||||
description = "Local markdown note-taking app"
|
||||
authors = ["HelixNotes"]
|
||||
license = "AGPL-3.0-or-later"
|
||||
@@ -43,3 +43,4 @@ tauri-plugin-updater = "2"
|
||||
tauri-plugin-single-instance = "2"
|
||||
arboard = { version = "3", features = ["image-data", "wayland-data-control"] }
|
||||
png = "0.17"
|
||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif"] }
|
||||
|
||||
@@ -400,6 +400,128 @@ pub fn get_all_note_titles(state: State<'_, AppState>) -> Result<Vec<NoteTitleEn
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
// ── Graph ──
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_graph_data(state: State<'_, AppState>) -> Result<crate::types::GraphData, String> {
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
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 vault = std::path::Path::new(vault_path);
|
||||
|
||||
// Pass 1: collect all notes with titles (fast title extraction, no YAML parsing)
|
||||
let mut graph_nodes = Vec::new();
|
||||
let mut title_to_idx: HashMap<String, usize> = HashMap::new();
|
||||
let mut seen_paths: HashSet<String> = HashSet::new();
|
||||
let mut contents: Vec<String> = Vec::new();
|
||||
|
||||
for entry in walkdir::WalkDir::new(vault)
|
||||
.into_iter()
|
||||
.filter_map(|e| e.ok())
|
||||
{
|
||||
let path = entry.path();
|
||||
if !path.is_file() { continue; }
|
||||
let path_str = path.to_string_lossy().to_string();
|
||||
if path_str.contains("/.helixnotes/") || path_str.contains("/.trash/")
|
||||
|| path_str.contains("/.stversions/") || path_str.contains("/.stfolder") { continue; }
|
||||
if path.extension().and_then(|e| e.to_str()) != Some("md") { continue; }
|
||||
// Skip Syncthing conflict files
|
||||
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
|
||||
if name.contains(".sync-conflict-") { continue; }
|
||||
}
|
||||
// Deduplicate by canonical path (handles symlinks)
|
||||
let canonical = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
|
||||
let canonical_str = canonical.to_string_lossy().to_string();
|
||||
if !seen_paths.insert(canonical_str) { continue; }
|
||||
|
||||
let raw = std::fs::read_to_string(path).unwrap_or_default();
|
||||
|
||||
// Fast title extraction: scan for "title: " line in frontmatter without full YAML parse
|
||||
let title = extract_title_fast(&raw).unwrap_or_else(|| {
|
||||
path.file_stem().unwrap_or_default().to_string_lossy().to_string()
|
||||
});
|
||||
|
||||
// Deduplicate by title — skip if we already have a node with this title
|
||||
let title_lower = title.to_lowercase();
|
||||
if title_to_idx.contains_key(&title_lower) { continue; }
|
||||
|
||||
let idx = graph_nodes.len();
|
||||
title_to_idx.insert(title_lower, idx);
|
||||
graph_nodes.push(crate::types::GraphNode {
|
||||
title,
|
||||
path: path_str,
|
||||
});
|
||||
contents.push(raw);
|
||||
}
|
||||
|
||||
// Pass 2: extract edges from wiki-links (inline scan, no regex)
|
||||
let mut edges = Vec::new();
|
||||
let mut edge_set: HashSet<(usize, usize)> = HashSet::new();
|
||||
|
||||
for (source_idx, body) in contents.iter().enumerate() {
|
||||
let bytes = body.as_bytes();
|
||||
let len = bytes.len();
|
||||
let mut i = 0;
|
||||
while i + 1 < len {
|
||||
if bytes[i] == b'[' && bytes[i + 1] == b'[' {
|
||||
i += 2;
|
||||
let start = i;
|
||||
while i + 1 < len && !(bytes[i] == b']' && bytes[i + 1] == b']') {
|
||||
i += 1;
|
||||
}
|
||||
if i + 1 < len {
|
||||
let link_raw = &body[start..i];
|
||||
// Strip |alias, #heading, ^block
|
||||
let link = link_raw.split('|').next().unwrap_or(link_raw);
|
||||
let link = link.split('#').next().unwrap_or(link);
|
||||
let link = link.split('^').next().unwrap_or(link);
|
||||
let link = link.trim().to_lowercase();
|
||||
|
||||
if let Some(&target_idx) = title_to_idx.get(&link) {
|
||||
if target_idx != source_idx {
|
||||
let key = if source_idx < target_idx { (source_idx, target_idx) } else { (target_idx, source_idx) };
|
||||
if edge_set.insert(key) {
|
||||
edges.push(crate::types::GraphEdge { source: source_idx, target: target_idx });
|
||||
}
|
||||
}
|
||||
}
|
||||
i += 2;
|
||||
}
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(crate::types::GraphData { nodes: graph_nodes, edges })
|
||||
}
|
||||
|
||||
/// Fast title extraction from frontmatter without full YAML parsing.
|
||||
/// Scans for `title: ...` line within `---` fences.
|
||||
fn extract_title_fast(raw: &str) -> Option<String> {
|
||||
let trimmed = raw.trim_start();
|
||||
if !trimmed.starts_with("---") { return None; }
|
||||
// Find the closing ---
|
||||
let after_open = &trimmed[3..];
|
||||
let end = after_open.find("\n---")?;
|
||||
let frontmatter = &after_open[..end];
|
||||
for line in frontmatter.lines() {
|
||||
let line = line.trim();
|
||||
if line.starts_with("title:") {
|
||||
let val = line[6..].trim();
|
||||
// Strip surrounding quotes
|
||||
if (val.starts_with('"') && val.ends_with('"')) || (val.starts_with('\'') && val.ends_with('\'')) {
|
||||
return Some(val[1..val.len()-1].to_string());
|
||||
}
|
||||
if !val.is_empty() {
|
||||
return Some(val.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
// ── Search ──
|
||||
|
||||
#[tauri::command]
|
||||
@@ -509,6 +631,33 @@ pub fn read_clipboard_image() -> Result<Vec<u8>, String> {
|
||||
Err("Clipboard image reading not supported on Android".to_string())
|
||||
}
|
||||
|
||||
/// Copy an image file to the system clipboard.
|
||||
#[cfg(not(target_os = "android"))]
|
||||
#[tauri::command]
|
||||
pub fn copy_image_to_clipboard(path: String) -> Result<(), String> {
|
||||
let data = std::fs::read(&path).map_err(|e| format!("Failed to read image: {}", e))?;
|
||||
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_image_to_clipboard(_path: String) -> Result<(), String> {
|
||||
Err("Clipboard image copy not supported on Android".to_string())
|
||||
}
|
||||
|
||||
// ── Attachments ──
|
||||
|
||||
#[tauri::command]
|
||||
|
||||
@@ -112,6 +112,7 @@ pub fn run() {
|
||||
commands::move_note,
|
||||
commands::get_all_tags,
|
||||
commands::get_all_note_titles,
|
||||
commands::get_graph_data,
|
||||
commands::search_notes,
|
||||
commands::reindex,
|
||||
commands::get_trash,
|
||||
@@ -121,6 +122,7 @@ pub fn run() {
|
||||
commands::load_vault_state,
|
||||
commands::save_vault_state,
|
||||
commands::read_clipboard_image,
|
||||
commands::copy_image_to_clipboard,
|
||||
commands::save_image,
|
||||
commands::save_attachment,
|
||||
commands::get_notebook_icons,
|
||||
|
||||
@@ -254,3 +254,21 @@ pub struct NoteTitleEntry {
|
||||
pub title: String,
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GraphData {
|
||||
pub nodes: Vec<GraphNode>,
|
||||
pub edges: Vec<GraphEdge>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GraphNode {
|
||||
pub title: String,
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GraphEdge {
|
||||
pub source: usize,
|
||||
pub target: usize,
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"productName": "HelixNotes",
|
||||
"version": "1.1.9",
|
||||
"version": "1.2.0",
|
||||
"identifier": "com.helixnotes.app",
|
||||
"build": {
|
||||
"frontendDist": "../build",
|
||||
|
||||
Reference in New Issue
Block a user