mirror of
https://gitlab.com/ArkHost/HelixNotes.git
synced 2026-07-23 23:35:57 +02:00
Parallelize vault-stats and tasks scanning for faster Info/Tasks loads on Android
This commit is contained in:
+74
-76
@@ -664,6 +664,7 @@ fn extract_title_fast(raw: &str) -> Option<String> {
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_tasks(state: State<'_, AppState>) -> Result<Vec<crate::types::TaskItem>, String> {
|
pub fn get_tasks(state: State<'_, AppState>) -> Result<Vec<crate::types::TaskItem>, String> {
|
||||||
|
use rayon::prelude::*;
|
||||||
let config = state.config.lock().map_err(|e| e.to_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 vault_path = config.active_vault.as_ref().ok_or("No active vault")?;
|
||||||
let vault = std::path::Path::new(vault_path);
|
let vault = std::path::Path::new(vault_path);
|
||||||
@@ -673,57 +674,57 @@ pub fn get_tasks(state: State<'_, AppState>) -> Result<Vec<crate::types::TaskIte
|
|||||||
let due_re = regex::Regex::new(r"\bdue:(\d{4}-\d{2}-\d{2})\b").unwrap();
|
let due_re = regex::Regex::new(r"\bdue:(\d{4}-\d{2}-\d{2})\b").unwrap();
|
||||||
let prio_re = regex::Regex::new(r"(?i)(?:^|\s)!(high|medium|med|low)\b").unwrap();
|
let prio_re = regex::Regex::new(r"(?i)(?:^|\s)!(high|medium|med|low)\b").unwrap();
|
||||||
|
|
||||||
let mut tasks = Vec::new();
|
let paths: Vec<std::path::PathBuf> = walkdir::WalkDir::new(vault)
|
||||||
for entry in walkdir::WalkDir::new(vault)
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_entry(|e| !operations::is_hidden(e.path()) && !e.path().starts_with(&hn_dir))
|
.filter_entry(|e| !operations::is_hidden(e.path()) && !e.path().starts_with(&hn_dir))
|
||||||
.filter_map(|e| e.ok())
|
.filter_map(|e| e.ok())
|
||||||
{
|
.filter(|e| {
|
||||||
let path = entry.path();
|
e.file_type().is_file() && e.path().extension().and_then(|x| x.to_str()) == Some("md")
|
||||||
if !path.is_file() {
|
})
|
||||||
continue;
|
.map(|e| e.into_path())
|
||||||
}
|
.collect();
|
||||||
if path.extension().and_then(|e| e.to_str()) != Some("md") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let raw = match std::fs::read_to_string(path) {
|
|
||||||
Ok(r) => r,
|
|
||||||
Err(_) => continue,
|
|
||||||
};
|
|
||||||
let filename = path.file_name().unwrap_or_default().to_string_lossy().to_string();
|
|
||||||
let (meta, body) = crate::vault::frontmatter::parse_note(&raw, &filename);
|
|
||||||
let note_path = path.to_string_lossy().to_string();
|
|
||||||
|
|
||||||
for (i, line) in body.lines().enumerate() {
|
// Read and parse each note in parallel; on Android every read_to_string is a FUSE round-trip.
|
||||||
let caps = match task_re.captures(line) {
|
let tasks: Vec<crate::types::TaskItem> = paths
|
||||||
Some(c) => c,
|
.par_iter()
|
||||||
None => continue,
|
.flat_map(|path| {
|
||||||
|
let mut out: Vec<crate::types::TaskItem> = Vec::new();
|
||||||
|
let raw = match std::fs::read_to_string(path) {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(_) => return out,
|
||||||
};
|
};
|
||||||
let completed = !caps[1].trim().is_empty(); // 'x'/'X' completed, ' ' open
|
let filename = path.file_name().unwrap_or_default().to_string_lossy().to_string();
|
||||||
let content = caps[2].to_string();
|
let (meta, body) = crate::vault::frontmatter::parse_note(&raw, &filename);
|
||||||
|
let note_path = path.to_string_lossy().to_string();
|
||||||
let due = due_re.captures(&content).map(|c| c[1].to_string());
|
for (i, line) in body.lines().enumerate() {
|
||||||
let priority = prio_re.captures(&content).map(|c| {
|
let caps = match task_re.captures(line) {
|
||||||
let p = c[1].to_lowercase();
|
Some(c) => c,
|
||||||
if p == "medium" { "med".to_string() } else { p }
|
None => continue,
|
||||||
});
|
};
|
||||||
// Strip metadata tokens for the display text, then collapse whitespace.
|
let completed = !caps[1].trim().is_empty();
|
||||||
let mut text = due_re.replace_all(&content, "").to_string();
|
let content = caps[2].to_string();
|
||||||
text = prio_re.replace_all(&text, " ").to_string();
|
let due = due_re.captures(&content).map(|c| c[1].to_string());
|
||||||
let text = text.split_whitespace().collect::<Vec<_>>().join(" ");
|
let priority = prio_re.captures(&content).map(|c| {
|
||||||
|
let p = c[1].to_lowercase();
|
||||||
tasks.push(crate::types::TaskItem {
|
if p == "medium" { "med".to_string() } else { p }
|
||||||
note_path: note_path.clone(),
|
});
|
||||||
note_title: meta.title.clone(),
|
let mut text = due_re.replace_all(&content, "").to_string();
|
||||||
line: i,
|
text = prio_re.replace_all(&text, " ").to_string();
|
||||||
raw_line: line.to_string(),
|
let text = text.split_whitespace().collect::<Vec<_>>().join(" ");
|
||||||
text,
|
out.push(crate::types::TaskItem {
|
||||||
completed,
|
note_path: note_path.clone(),
|
||||||
due,
|
note_title: meta.title.clone(),
|
||||||
priority,
|
line: i,
|
||||||
});
|
raw_line: line.to_string(),
|
||||||
}
|
text,
|
||||||
}
|
completed,
|
||||||
|
due,
|
||||||
|
priority,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
out
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
Ok(tasks)
|
Ok(tasks)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1223,41 +1224,38 @@ pub fn reorder_quick_access(state: State<'_, AppState>, paths: Vec<String>) -> R
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_vault_stats(state: State<'_, AppState>) -> Result<VaultStats, String> {
|
pub fn get_vault_stats(state: State<'_, AppState>) -> Result<VaultStats, String> {
|
||||||
|
use rayon::prelude::*;
|
||||||
let config = state.config.lock().map_err(|e| e.to_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 vault_path = config.active_vault.as_ref().ok_or("No active vault")?;
|
||||||
|
|
||||||
let mut total_notes: u64 = 0;
|
let paths: Vec<std::path::PathBuf> = walkdir::WalkDir::new(vault_path)
|
||||||
let mut total_attachments: u64 = 0;
|
|
||||||
let mut notes_size: u64 = 0;
|
|
||||||
let mut attachments_size: u64 = 0;
|
|
||||||
|
|
||||||
for entry in walkdir::WalkDir::new(vault_path)
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|e| e.ok())
|
.filter_map(|e| e.ok())
|
||||||
{
|
.filter(|e| e.file_type().is_file())
|
||||||
let path = entry.path();
|
.map(|e| e.into_path())
|
||||||
if !path.is_file() {
|
.filter(|p| {
|
||||||
continue;
|
// attachments under .helixnotes count; nothing else in .helixnotes or .trash does
|
||||||
}
|
let s = p.to_string_lossy();
|
||||||
let path_str = path.to_string_lossy();
|
!(s.contains("/.helixnotes/") && !s.contains("/.helixnotes/attachments/"))
|
||||||
// Include .helixnotes/attachments, skip everything else in .helixnotes
|
&& !s.contains("/.trash/")
|
||||||
if path_str.contains("/.helixnotes/") && !path_str.contains("/.helixnotes/attachments/") {
|
})
|
||||||
continue;
|
.collect();
|
||||||
}
|
|
||||||
// Skip trash
|
// Stat files in parallel; each metadata() is a FUSE round-trip on Android.
|
||||||
if path_str.contains("/.trash/") {
|
let (total_notes, total_attachments, notes_size, attachments_size) = paths
|
||||||
continue;
|
.par_iter()
|
||||||
}
|
.map(|path| {
|
||||||
let size = entry.metadata().map(|m| m.len()).unwrap_or(0);
|
let size = std::fs::metadata(path).map(|m| m.len()).unwrap_or(0);
|
||||||
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
|
if path.extension().and_then(|e| e.to_str()) == Some("md") {
|
||||||
if ext == "md" {
|
(1u64, 0u64, size, 0u64)
|
||||||
total_notes += 1;
|
} else {
|
||||||
notes_size += size;
|
(0u64, 1u64, 0u64, size)
|
||||||
} else {
|
}
|
||||||
total_attachments += 1;
|
})
|
||||||
attachments_size += size;
|
.reduce(
|
||||||
}
|
|| (0u64, 0u64, 0u64, 0u64),
|
||||||
}
|
|a, b| (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3),
|
||||||
|
);
|
||||||
|
|
||||||
Ok(VaultStats {
|
Ok(VaultStats {
|
||||||
total_notes,
|
total_notes,
|
||||||
|
|||||||
Reference in New Issue
Block a user