use rusqlite::{params, Connection, Result};
use serde::Serialize;
use std::path::{Path, PathBuf};
use crate::frontparse::{parse_frontmatter, Frontmatter};
#[derive(Debug, Serialize)]
pub struct Note {
pub filename: String,
pub title: String,
pub summary: Option<String>,
pub date: Option<String>,
pub time: Option<String>,
pub tags: Option<Vec<String>>,
pub content: String,
}
fn db_path(notebook_path: &Path) -> PathBuf {
notebook_path.join(".smarana").join("index.db")
}
pub fn init_db(notebook_path: &Path) -> Result<()> {
let path = db_path(notebook_path);
let conn = Connection::open(&path)?;
conn.execute(
"CREATE TABLE IF NOT EXISTS notes (
filename TEXT PRIMARY KEY,
title TEXT NOT NULL,
summary TEXT,
date TEXT,
time TEXT,
tags TEXT,
content TEXT NOT NULL DEFAULT ''
)",
[],
)?;
Ok(())
}
pub fn init_db_force(notebook_path: &Path) -> Result<()> {
let path = db_path(notebook_path);
let conn = Connection::open(&path)?;
conn.execute("DROP TABLE IF EXISTS notes", [])?;
init_db(notebook_path)
}
pub fn upsert_note(notebook_path: &Path, filename: &str, fm: &Frontmatter, content: &str) -> Result<()> {
let path = db_path(notebook_path);
let conn = Connection::open(&path)?;
let title = fm.title.as_deref().unwrap_or("null");
let tags_json = if let Some(tags) = &fm.tags {
serde_json::to_string(tags).ok()
} else {
None
};
conn.execute(
"INSERT INTO notes (filename, title, summary, date, time, tags, content)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
ON CONFLICT(filename) DO UPDATE SET
title=excluded.title,
summary=excluded.summary,
date=excluded.date,
time=excluded.time,
tags=excluded.tags,
content=excluded.content",
params![
filename,
title,
fm.summary,
fm.date,
fm.time,
tags_json,
content
],
)?;
Ok(())
}
pub fn sync_file(notebook_path: &Path, file_path: &Path) {
if let Ok(content) = std::fs::read_to_string(file_path) {
let rel_path = file_path.strip_prefix(notebook_path).unwrap_or(file_path).to_string_lossy().to_string();
let normalized_path = rel_path.replace("\\", "/");
let fm = parse_frontmatter(&content, &normalized_path);
if let Err(e) = upsert_note(notebook_path, &normalized_path, &fm, &content) {
eprintln!("Failed to register note '{}': {}", normalized_path, e);
}
}
}
pub fn sync_all(notebook_path: &Path) {
if let Err(e) = init_db_force(notebook_path) {
eprintln!("Failed to initialize database schema: {}", e);
return;
}
if let Ok(entries) = std::fs::read_dir(notebook_path) {
for entry in entries.filter_map(|e| e.ok()) {
let path = entry.path();
if path.is_file() {
if let Some(ext) = path.extension() {
if ext == "typ" {
if path.to_string_lossy().contains(".smarana") {
continue;
}
sync_file(notebook_path, &path);
}
}
}
}
}
}
pub fn list_notes(notebook_path: &Path) -> Result<Vec<Note>> {
let path = db_path(notebook_path);
let conn = Connection::open(&path)?;
let mut stmt = conn.prepare("SELECT filename, title, summary, date, time, tags, content FROM notes ORDER BY date DESC, time DESC")?;
let note_iter = stmt.query_map([], |row| {
let tags_str: Option<String> = row.get(5)?;
let tags = if let Some(ts) = tags_str {
serde_json::from_str(&ts).ok()
} else {
None
};
Ok(Note {
filename: row.get(0)?,
title: row.get(1)?,
summary: row.get(2)?,
date: row.get(3)?,
time: row.get(4)?,
tags,
content: row.get(6)?
})
})?;
let mut notes = Vec::new();
for note in note_iter {
notes.push(note?);
}
Ok(notes)
}