Initial commit — Tauri image gallery with sqlite-vec

Sets up Tauri v2 + React frontend with SQLite metadata store,
sqlite-vec for CLIP embedding infrastructure, and r2d2 connection
pool replacing the single Arc<Mutex<Connection>>. Indexer now uses
rayon for parallel file metadata collection.
This commit is contained in:
2026-04-05 15:59:48 +01:00
commit c5e9c83ac9
51 changed files with 10540 additions and 0 deletions
+153
View File
@@ -0,0 +1,153 @@
use crate::db::{self, DbPool, Folder, ImageRecord};
use crate::indexer;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use tauri::{AppHandle, State};
pub type DbState = DbPool;
#[derive(Serialize)]
pub struct ImagesPage {
pub images: Vec<ImageRecord>,
pub total: i64,
pub offset: i64,
pub limit: i64,
}
#[derive(Deserialize)]
pub struct GetImagesParams {
pub folder_id: Option<i64>,
pub search: Option<String>,
pub media_kind: Option<String>,
pub favorites_only: Option<bool>,
pub sort: Option<String>,
pub offset: Option<i64>,
pub limit: Option<i64>,
}
#[derive(Deserialize)]
pub struct UpdateImageDetailsParams {
pub image_id: i64,
pub favorite: Option<bool>,
pub rating: Option<i64>,
}
#[tauri::command]
pub async fn add_folder(
app: AppHandle,
db: State<'_, DbState>,
path: String,
cache_dir: String,
) -> Result<Folder, String> {
let folder_path = PathBuf::from(&path);
if !folder_path.exists() || !folder_path.is_dir() {
return Err("Path is not a valid directory".into());
}
let name = folder_path
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| path.clone());
let folder_id = {
let conn = db.get().map_err(|e| e.to_string())?;
db::insert_folder(&conn, &path, &name).map_err(|e| e.to_string())?
};
let folders = {
let conn = db.get().map_err(|e| e.to_string())?;
db::get_folders(&conn).map_err(|e| e.to_string())?
};
let folder = folders
.into_iter()
.find(|f| f.id == folder_id)
.ok_or("Folder not found after insert")?;
let cache_path = PathBuf::from(cache_dir);
indexer::index_folder(app, db.inner().clone(), folder_id, folder_path, cache_path);
Ok(folder)
}
#[tauri::command]
pub async fn get_folders(db: State<'_, DbState>) -> Result<Vec<Folder>, String> {
let conn = db.get().map_err(|e| e.to_string())?;
db::get_folders(&conn).map_err(|e| e.to_string())
}
#[tauri::command]
pub async fn remove_folder(db: State<'_, DbState>, folder_id: i64) -> Result<(), String> {
let conn = db.get().map_err(|e| e.to_string())?;
db::delete_folder(&conn, folder_id).map_err(|e| e.to_string())
}
#[tauri::command]
pub async fn get_images(
db: State<'_, DbState>,
params: GetImagesParams,
) -> Result<ImagesPage, String> {
let conn = db.get().map_err(|e| e.to_string())?;
let sort = params.sort.as_deref().unwrap_or("date_desc");
let offset = params.offset.unwrap_or(0);
let limit = params.limit.unwrap_or(100);
let search = params.search.as_deref();
let media_kind = params.media_kind.as_deref();
let favorites_only = params.favorites_only.unwrap_or(false);
let total = db::count_images(&conn, params.folder_id, search, media_kind, favorites_only)
.map_err(|e| e.to_string())?;
let images = db::get_images(
&conn,
params.folder_id,
search,
media_kind,
favorites_only,
sort,
offset,
limit,
)
.map_err(|e| e.to_string())?;
Ok(ImagesPage {
images,
total,
offset,
limit,
})
}
#[tauri::command]
pub async fn update_image_details(
db: State<'_, DbState>,
params: UpdateImageDetailsParams,
) -> Result<ImageRecord, String> {
let conn = db.get().map_err(|e| e.to_string())?;
db::update_image_details(&conn, params.image_id, params.favorite, params.rating)
.map_err(|e| e.to_string())
}
#[tauri::command]
pub async fn reindex_folder(
app: AppHandle,
db: State<'_, DbState>,
folder_id: i64,
cache_dir: String,
) -> Result<(), String> {
let folder_path = {
let conn = db.get().map_err(|e| e.to_string())?;
let folders = db::get_folders(&conn).map_err(|e| e.to_string())?;
folders
.into_iter()
.find(|f| f.id == folder_id)
.map(|f| PathBuf::from(f.path))
.ok_or("Folder not found")?
};
let cache_path = PathBuf::from(cache_dir);
indexer::index_folder(app, db.inner().clone(), folder_id, folder_path, cache_path);
Ok(())
}