refactor(backend): abstract database logic into generic Store

This commit introduces a generic `Store` struct to handle all common database operations, such as connection management and schema initialization. All existing manager structs (`QuicklinkManager`, `SnippetManager`, `FrecencyManager`, `ClipboardHistoryManager`, `AiUsageManager`) have been refactored to use this new abstraction.
This commit is contained in:
ByteAtATime 2025-06-29 15:48:03 -07:00
parent 23c222a956
commit 7156827585
No known key found for this signature in database
7 changed files with 186 additions and 223 deletions

View file

@ -1,17 +1,27 @@
use crate::error::AppError;
use crate::store::Store;
use futures_util::StreamExt;
use once_cell::sync::Lazy;
use rusqlite::{params, Connection, Result as RusqliteResult};
use rusqlite::{params, Result as RusqliteResult};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use tauri::{AppHandle, Emitter, Manager, State};
const AI_KEYRING_SERVICE: &str = "dev.byteatatime.raycast.ai";
const AI_KEYRING_USERNAME: &str = "openrouter_api_key";
const AI_USAGE_SCHEMA: &str = "CREATE TABLE IF NOT EXISTS ai_generations (
id TEXT PRIMARY KEY,
created INTEGER NOT NULL,
model TEXT NOT NULL,
tokens_prompt INTEGER NOT NULL,
tokens_completion INTEGER NOT NULL,
native_tokens_prompt INTEGER NOT NULL,
native_tokens_completion INTEGER NOT NULL,
total_cost REAL NOT NULL
)";
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
@ -223,40 +233,18 @@ pub fn ai_can_access(app: tauri::AppHandle) -> Result<bool, String> {
}
pub struct AiUsageManager {
db: Mutex<Connection>,
store: Store,
}
impl AiUsageManager {
pub fn new(app_handle: &AppHandle) -> Result<Self, AppError> {
let data_dir = app_handle
.path()
.app_local_data_dir()
.map_err(|_| AppError::DirectoryNotFound)?;
let db_path = data_dir.join("ai_usage.sqlite");
let db = Connection::open(db_path)?;
Ok(Self { db: Mutex::new(db) })
}
pub fn init_db(&self) -> RusqliteResult<()> {
let db = self.db.lock().unwrap();
db.execute(
"CREATE TABLE IF NOT EXISTS ai_generations (
id TEXT PRIMARY KEY,
created INTEGER NOT NULL,
model TEXT NOT NULL,
tokens_prompt INTEGER NOT NULL,
tokens_completion INTEGER NOT NULL,
native_tokens_prompt INTEGER NOT NULL,
native_tokens_completion INTEGER NOT NULL,
total_cost REAL NOT NULL
)",
[],
)?;
Ok(())
let store = Store::new(app_handle, "ai_usage.sqlite")?;
store.init_table(AI_USAGE_SCHEMA)?;
Ok(Self { store })
}
pub fn log_generation(&self, data: &GenerationData) -> Result<(), AppError> {
let db = self.db.lock().unwrap();
let db = self.store.conn();
db.execute(
"INSERT OR REPLACE INTO ai_generations (id, created, model, tokens_prompt, tokens_completion, native_tokens_prompt, native_tokens_completion, total_cost)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
@ -275,7 +263,7 @@ impl AiUsageManager {
}
pub fn get_history(&self, limit: u32, offset: u32) -> Result<Vec<GenerationData>, AppError> {
let db = self.db.lock().unwrap();
let db = self.store.conn();
let mut stmt = db.prepare(
"SELECT id, created, model, tokens_prompt, tokens_completion, native_tokens_prompt, native_tokens_completion, total_cost FROM ai_generations ORDER BY created DESC LIMIT ?1 OFFSET ?2",
)?;
@ -311,8 +299,9 @@ pub fn get_ai_usage_history(
async fn fetch_and_log_usage(
open_router_request_id: String,
api_key: String,
manager: &AiUsageManager,
app_handle: AppHandle,
) -> Result<(), AppError> {
let manager = app_handle.state::<AiUsageManager>();
let client = reqwest::Client::new();
let response = client
.get(format!(
@ -346,7 +335,6 @@ async fn fetch_and_log_usage(
#[tauri::command]
pub async fn ai_ask_stream(
app_handle: AppHandle,
manager: State<'_, AiUsageManager>,
request_id: String,
prompt: String,
options: AskOptions,
@ -454,11 +442,9 @@ pub async fn ai_ask_stream(
.map_err(|e| e.to_string())?;
if let Some(or_req_id) = open_router_request_id {
let manager_clone = AiUsageManager {
db: Mutex::new(Connection::open(manager.db.lock().unwrap().path().unwrap()).unwrap()),
};
let handle_clone = app_handle.clone();
tokio::spawn(async move {
if let Err(e) = fetch_and_log_usage(or_req_id, api_key, &manager_clone).await {
if let Err(e) = fetch_and_log_usage(or_req_id, api_key, handle_clone).await {
eprintln!("[AI Usage Tracking] Error: {}", e);
}
});

View file

@ -4,16 +4,31 @@ use super::{
types::{ClipboardItem, ContentType, INLINE_CONTENT_THRESHOLD_BYTES, PREVIEW_LENGTH_CHARS},
};
use crate::error::AppError;
use crate::store::Store;
use chrono::Utc;
use once_cell::sync::Lazy;
use rusqlite::{params, Connection, OptionalExtension, Result as RusqliteResult};
use rusqlite::{params, Result as RusqliteResult};
use std::path::PathBuf;
use std::sync::atomic::AtomicBool;
use std::sync::Mutex;
use tauri::{AppHandle, Manager};
const CLIPBOARD_SCHEMA: &str = "CREATE TABLE IF NOT EXISTS clipboard_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
hash TEXT UNIQUE NOT NULL,
content_type TEXT NOT NULL,
encrypted_content TEXT NOT NULL,
encrypted_preview TEXT,
content_size_bytes INTEGER,
source_app_name TEXT,
first_copied_at INTEGER NOT NULL,
last_copied_at INTEGER NOT NULL,
times_copied INTEGER NOT NULL DEFAULT 1,
is_pinned INTEGER NOT NULL DEFAULT 0
)";
pub struct ClipboardHistoryManager {
db: Mutex<Connection>,
store: Store,
key: [u8; 32],
pub image_dir: PathBuf,
}
@ -44,7 +59,7 @@ fn row_to_clipboard_item(row: &rusqlite::Row, key: &[u8; 32]) -> RusqliteResult<
}
impl ClipboardHistoryManager {
fn new(app_handle: AppHandle) -> Result<Self, AppError> {
fn new(app_handle: &AppHandle) -> Result<Self, AppError> {
let data_dir = app_handle
.path()
.app_local_data_dir()
@ -52,39 +67,18 @@ impl ClipboardHistoryManager {
let image_dir = data_dir.join("clipboard_images");
std::fs::create_dir_all(&image_dir)?;
let db_path = data_dir.join("clipboard_history.sqlite");
let db = Connection::open(db_path)?;
let store = Store::new(app_handle, "clipboard_history.sqlite")?;
store.init_table(CLIPBOARD_SCHEMA)?;
let key = get_encryption_key()?;
Ok(Self {
db: Mutex::new(db),
store,
key,
image_dir,
})
}
fn init_db(&self) -> RusqliteResult<()> {
let db = self.db.lock().unwrap();
db.execute(
"CREATE TABLE IF NOT EXISTS clipboard_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
hash TEXT UNIQUE NOT NULL,
content_type TEXT NOT NULL,
encrypted_content TEXT NOT NULL,
encrypted_preview TEXT,
content_size_bytes INTEGER,
source_app_name TEXT,
first_copied_at INTEGER NOT NULL,
last_copied_at INTEGER NOT NULL,
times_copied INTEGER NOT NULL DEFAULT 1,
is_pinned INTEGER NOT NULL DEFAULT 0
)",
[],
)?;
Ok(())
}
pub fn add_item(
&self,
hash: String,
@ -92,7 +86,7 @@ impl ClipboardHistoryManager {
content_value: String,
source_app_name: Option<String>,
) -> Result<(), AppError> {
let db = self.db.lock().unwrap();
let db = self.store.conn();
let now = Utc::now();
let existing_item: RusqliteResult<i64> = db.query_row(
@ -134,7 +128,7 @@ impl ClipboardHistoryManager {
limit: u32,
offset: u32,
) -> Result<Vec<ClipboardItem>, AppError> {
let db = self.db.lock().unwrap();
let db = self.store.conn();
let mut query = "SELECT id, hash, content_type, source_app_name, first_copied_at, last_copied_at, times_copied, is_pinned, content_size_bytes, encrypted_preview, CASE WHEN content_size_bytes <= ? THEN encrypted_content ELSE NULL END as conditional_encrypted_content FROM clipboard_history".to_string();
let mut where_clauses: Vec<String> = Vec::new();
let mut params_vec: Vec<Box<dyn rusqlite::ToSql>> =
@ -185,7 +179,7 @@ impl ClipboardHistoryManager {
}
pub fn get_content_by_offset(&self, offset: u32) -> Result<Option<String>, AppError> {
let db = self.db.lock().unwrap();
let db = self.store.conn();
let res: rusqlite::Result<String> = db.query_row(
"SELECT encrypted_content FROM clipboard_history ORDER BY last_copied_at DESC LIMIT 1 OFFSET ?",
params![offset],
@ -200,7 +194,7 @@ impl ClipboardHistoryManager {
}
pub fn get_item_content(&self, id: i64) -> Result<String, AppError> {
let db = self.db.lock().unwrap();
let db = self.store.conn();
let encrypted_content: String = db.query_row(
"SELECT encrypted_content FROM clipboard_history WHERE id = ?",
params![id],
@ -210,30 +204,28 @@ impl ClipboardHistoryManager {
}
pub fn item_was_copied(&self, id: i64) -> RusqliteResult<usize> {
self.db.lock().unwrap().execute(
self.store.conn().execute(
"UPDATE clipboard_history SET last_copied_at = ?, times_copied = times_copied + 1 WHERE id = ?",
params![Utc::now().timestamp(), id],
)
}
pub fn delete_item(&self, id: i64) -> RusqliteResult<usize> {
self.db
.lock()
.unwrap()
self.store
.conn()
.execute("DELETE FROM clipboard_history WHERE id = ?", params![id])
}
pub fn toggle_pin(&self, id: i64) -> RusqliteResult<usize> {
self.db.lock().unwrap().execute(
self.store.conn().execute(
"UPDATE clipboard_history SET is_pinned = 1 - is_pinned WHERE id = ?",
params![id],
)
}
pub fn clear_all(&self) -> RusqliteResult<usize> {
self.db
.lock()
.unwrap()
self.store
.conn()
.execute("DELETE FROM clipboard_history WHERE is_pinned = 0", [])
}
}
@ -244,12 +236,8 @@ pub static INTERNAL_CLIPBOARD_CHANGE: AtomicBool = AtomicBool::new(false);
pub fn init(app_handle: AppHandle) {
let mut manager_guard = MANAGER.lock().unwrap();
if manager_guard.is_none() {
match ClipboardHistoryManager::new(app_handle.clone()) {
match ClipboardHistoryManager::new(&app_handle) {
Ok(manager) => {
if let Err(e) = manager.init_db() {
eprintln!("Failed to initialize clipboard history database: {:?}", e);
return;
}
*manager_guard = Some(manager);
drop(manager_guard);
start_monitoring(app_handle);

View file

@ -1,9 +1,17 @@
use crate::error::AppError;
use crate::store::Store;
use chrono::Utc;
use rusqlite::{params, Connection, Result as RusqliteResult};
use rusqlite::{params, Result as RusqliteResult};
use serde::Serialize;
use std::sync::Mutex;
use tauri::{AppHandle, Manager};
use tauri::AppHandle;
const FRECENCY_SCHEMA: &str = "CREATE TABLE IF NOT EXISTS frecency (
item_id TEXT PRIMARY KEY,
use_count INTEGER NOT NULL DEFAULT 0,
last_used_at INTEGER NOT NULL
)";
const HIDDEN_ITEMS_SCHEMA: &str =
"CREATE TABLE IF NOT EXISTS hidden_items (item_id TEXT PRIMARY KEY)";
#[derive(Serialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
@ -14,41 +22,19 @@ pub struct FrecencyData {
}
pub struct FrecencyManager {
db: Mutex<Connection>,
store: Store,
}
impl FrecencyManager {
pub fn new(app_handle: AppHandle) -> Result<Self, AppError> {
let data_dir = app_handle
.path()
.app_local_data_dir()
.map_err(|_| AppError::DirectoryNotFound)?;
let db_path = data_dir.join("frecency.sqlite");
let db = Connection::open(db_path)?;
let manager = Self { db: Mutex::new(db) };
manager.init_db()?;
Ok(manager)
}
fn init_db(&self) -> RusqliteResult<()> {
let db = self.db.lock().unwrap();
db.execute(
"CREATE TABLE IF NOT EXISTS frecency (
item_id TEXT PRIMARY KEY,
use_count INTEGER NOT NULL DEFAULT 0,
last_used_at INTEGER NOT NULL
)",
[],
)?;
db.execute(
"CREATE TABLE IF NOT EXISTS hidden_items (item_id TEXT PRIMARY KEY)",
[],
)?;
Ok(())
pub fn new(app_handle: &AppHandle) -> Result<Self, AppError> {
let store = Store::new(app_handle, "frecency.sqlite")?;
store.init_table(FRECENCY_SCHEMA)?;
store.init_table(HIDDEN_ITEMS_SCHEMA)?;
Ok(Self { store })
}
pub fn record_usage(&self, item_id: String) -> Result<(), AppError> {
let db = self.db.lock().unwrap();
let db = self.store.conn();
let now = Utc::now().timestamp();
db.execute(
"INSERT INTO frecency (item_id, use_count, last_used_at) VALUES (?, 1, ?)
@ -61,7 +47,7 @@ impl FrecencyManager {
}
pub fn get_frecency_data(&self) -> Result<Vec<FrecencyData>, AppError> {
let db = self.db.lock().unwrap();
let db = self.store.conn();
let mut stmt = db.prepare("SELECT item_id, use_count, last_used_at FROM frecency")?;
let data_iter = stmt.query_map([], |row| {
Ok(FrecencyData {
@ -77,13 +63,13 @@ impl FrecencyManager {
}
pub fn delete_frecency_entry(&self, item_id: String) -> Result<(), AppError> {
let db = self.db.lock().unwrap();
let db = self.store.conn();
db.execute("DELETE FROM frecency WHERE item_id = ?", params![item_id])?;
Ok(())
}
pub fn hide_item(&self, item_id: String) -> Result<(), AppError> {
let db = self.db.lock().unwrap();
let db = self.store.conn();
db.execute(
"INSERT OR IGNORE INTO hidden_items (item_id) VALUES (?)",
params![item_id],
@ -92,7 +78,7 @@ impl FrecencyManager {
}
pub fn get_hidden_item_ids(&self) -> Result<Vec<String>, AppError> {
let db = self.db.lock().unwrap();
let db = self.store.conn();
let mut stmt = db.prepare("SELECT item_id FROM hidden_items")?;
let ids_iter = stmt.query_map([], |row| row.get(0))?;

View file

@ -14,6 +14,7 @@ mod oauth;
mod quicklinks;
mod snippets;
mod soulver;
mod store;
mod system;
use crate::snippets::input_manager::{EvdevInputManager, InputManager};
@ -290,26 +291,13 @@ pub fn run() {
let app_handle = app.handle().clone();
tauri::async_runtime::spawn(browser_extension::run_server(app_handle));
let app_handle_for_history = app.handle().clone();
clipboard_history::init(app_handle_for_history);
clipboard_history::init(app.handle().clone());
file_search::init(app.handle().clone());
let quicklink_manager = QuicklinkManager::new(app.handle().clone())?;
quicklink_manager.init_db()?;
app.manage(quicklink_manager);
let frecency_manager = FrecencyManager::new(app.handle().clone())?;
app.manage(frecency_manager);
let snippet_manager = SnippetManager::new(app.handle().clone())?;
snippet_manager.init_db()?;
app.manage(snippet_manager);
let app_handle_for_file_search = app.handle().clone();
file_search::init(app_handle_for_file_search);
let ai_usage_manager = AiUsageManager::new(app.handle())?;
ai_usage_manager.init_db()?;
app.manage(ai_usage_manager);
app.manage(QuicklinkManager::new(app.handle())?);
app.manage(FrecencyManager::new(app.handle())?);
app.manage(SnippetManager::new(app.handle())?);
app.manage(AiUsageManager::new(app.handle())?);
setup_background_refresh();
setup_global_shortcut(app)?;

View file

@ -1,11 +1,21 @@
use crate::error::AppError;
use crate::store::Store;
use chrono::{DateTime, Utc};
use rusqlite::{params, Connection, Result as RusqliteResult};
use rusqlite::{params, Result as RusqliteResult};
use serde::Serialize;
use std::sync::Mutex;
use tauri::{AppHandle, Manager};
use tauri_plugin_opener::{open_path, open_url};
const QUICKLINKS_SCHEMA: &str = "CREATE TABLE IF NOT EXISTS quicklinks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
link TEXT NOT NULL,
application TEXT,
icon TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)";
#[derive(Serialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Quicklink {
@ -19,36 +29,14 @@ pub struct Quicklink {
}
pub struct QuicklinkManager {
db: Mutex<Connection>,
store: Store,
}
impl QuicklinkManager {
pub fn new(app_handle: AppHandle) -> Result<Self, AppError> {
let data_dir = app_handle
.path()
.app_local_data_dir()
.map_err(|_| AppError::DirectoryNotFound)?;
let db_path = data_dir.join("quicklinks.sqlite");
let db = Connection::open(db_path)?;
Ok(Self { db: Mutex::new(db) })
}
pub fn init_db(&self) -> RusqliteResult<()> {
let db = self.db.lock().unwrap();
db.execute(
"CREATE TABLE IF NOT EXISTS quicklinks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
link TEXT NOT NULL,
application TEXT,
icon TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)",
[],
)?;
Ok(())
pub fn new(app_handle: &AppHandle) -> Result<Self, AppError> {
let store = Store::new(app_handle, "quicklinks.sqlite")?;
store.init_table(QUICKLINKS_SCHEMA)?;
Ok(Self { store })
}
fn create_quicklink(
@ -58,7 +46,7 @@ impl QuicklinkManager {
application: Option<String>,
icon: Option<String>,
) -> Result<i64, AppError> {
let db = self.db.lock().unwrap();
let db = self.store.conn();
let now = Utc::now().timestamp();
db.execute(
"INSERT INTO quicklinks (name, link, application, icon, created_at, updated_at)
@ -69,7 +57,7 @@ impl QuicklinkManager {
}
fn list_quicklinks(&self) -> Result<Vec<Quicklink>, AppError> {
let db = self.db.lock().unwrap();
let db = self.store.conn();
let mut stmt = db.prepare("SELECT id, name, link, application, icon, created_at, updated_at FROM quicklinks ORDER BY name ASC")?;
let quicklinks_iter = stmt.query_map([], |row| {
let created_at_ts: i64 = row.get(5)?;
@ -86,7 +74,7 @@ impl QuicklinkManager {
})?;
quicklinks_iter
.collect::<Result<Vec<_>, _>>()
.collect::<RusqliteResult<Vec<_>>>()
.map_err(|e| e.into())
}
@ -98,7 +86,7 @@ impl QuicklinkManager {
application: Option<String>,
icon: Option<String>,
) -> Result<(), AppError> {
let db = self.db.lock().unwrap();
let db = self.store.conn();
let now = Utc::now().timestamp();
db.execute(
"UPDATE quicklinks SET name = ?, link = ?, application = ?, icon = ?, updated_at = ?
@ -109,7 +97,7 @@ impl QuicklinkManager {
}
fn delete_quicklink(&self, id: i64) -> Result<(), AppError> {
let db = self.db.lock().unwrap();
let db = self.store.conn();
db.execute("DELETE FROM quicklinks WHERE id = ?", params![id])?;
Ok(())
}

View file

@ -1,70 +1,64 @@
use crate::error::AppError;
use crate::snippets::types::Snippet;
use crate::store::Store;
use chrono::{DateTime, Utc};
use rusqlite::{params, Connection, Result as RusqliteResult};
use std::sync::{Arc, Mutex};
use tauri::{AppHandle, Manager};
use rusqlite::params;
use std::sync::Arc;
use tauri::AppHandle;
const SNIPPETS_SCHEMA: &str = "CREATE TABLE IF NOT EXISTS snippets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
keyword TEXT NOT NULL UNIQUE,
content TEXT NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)";
#[derive(Clone)]
pub struct SnippetManager {
db: Arc<Mutex<Connection>>,
store: Arc<Store>,
}
impl SnippetManager {
pub fn new(app_handle: AppHandle) -> Result<Self, AppError> {
let data_dir = app_handle
.path()
.app_local_data_dir()
.map_err(|_| AppError::DirectoryNotFound)?;
let db_path = data_dir.join("snippets.sqlite");
let db = Connection::open(db_path)?;
pub fn new(app_handle: &AppHandle) -> Result<Self, AppError> {
let store = Store::new(app_handle, "snippets.sqlite")?;
store.init_table(SNIPPETS_SCHEMA)?;
// Handle simple schema migrations in a block to drop the lock
{
let db = store.conn();
let mut stmt = db.prepare("PRAGMA table_info(snippets)")?;
let columns: Vec<String> = stmt
.query_map([], |row| row.get(1))?
.collect::<Result<Vec<_>, _>>()?;
if !columns.contains(&"times_used".to_string()) {
db.execute(
"ALTER TABLE snippets ADD COLUMN times_used INTEGER NOT NULL DEFAULT 0",
[],
)?;
}
if !columns.contains(&"last_used_at".to_string()) {
db.execute(
"ALTER TABLE snippets ADD COLUMN last_used_at INTEGER NOT NULL DEFAULT 0",
[],
)?;
}
}
Ok(Self {
db: Arc::new(Mutex::new(db)),
store: Arc::new(store),
})
}
pub fn init_db(&self) -> RusqliteResult<()> {
let db = self.db.lock().unwrap();
db.execute(
"CREATE TABLE IF NOT EXISTS snippets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
keyword TEXT NOT NULL UNIQUE,
content TEXT NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)",
[],
)?;
let mut stmt = db.prepare("PRAGMA table_info(snippets)")?;
let columns: Vec<String> = stmt
.query_map([], |row| row.get(1))?
.collect::<Result<Vec<_>, _>>()?;
if !columns.contains(&"times_used".to_string()) {
db.execute(
"ALTER TABLE snippets ADD COLUMN times_used INTEGER NOT NULL DEFAULT 0",
[],
)?;
}
if !columns.contains(&"last_used_at".to_string()) {
db.execute(
"ALTER TABLE snippets ADD COLUMN last_used_at INTEGER NOT NULL DEFAULT 0",
[],
)?;
}
Ok(())
}
pub fn create_snippet(
&self,
name: String,
keyword: String,
content: String,
) -> Result<i64, AppError> {
let db = self.db.lock().unwrap();
let db = self.store.conn();
let now = Utc::now().timestamp();
db.execute(
"INSERT INTO snippets (name, keyword, content, created_at, updated_at, times_used, last_used_at)
@ -75,7 +69,7 @@ impl SnippetManager {
}
pub fn list_snippets(&self, search_term: Option<String>) -> Result<Vec<Snippet>, AppError> {
let db = self.db.lock().unwrap();
let db = self.store.conn();
let mut query = "SELECT id, name, keyword, content, created_at, updated_at, times_used, last_used_at FROM snippets".to_string();
let mut params_vec: Vec<Box<dyn rusqlite::ToSql>> = vec![];
@ -119,7 +113,7 @@ impl SnippetManager {
keyword: String,
content: String,
) -> Result<(), AppError> {
let db = self.db.lock().unwrap();
let db = self.store.conn();
let now = Utc::now().timestamp();
db.execute(
"UPDATE snippets SET name = ?1, keyword = ?2, content = ?3, updated_at = ?4 WHERE id = ?5",
@ -129,13 +123,13 @@ impl SnippetManager {
}
pub fn delete_snippet(&self, id: i64) -> Result<(), AppError> {
let db = self.db.lock().unwrap();
let db = self.store.conn();
db.execute("DELETE FROM snippets WHERE id = ?1", params![id])?;
Ok(())
}
pub fn snippet_was_used(&self, id: i64) -> Result<(), AppError> {
let db = self.db.lock().unwrap();
let db = self.store.conn();
let now = Utc::now().timestamp();
db.execute(
"UPDATE snippets SET times_used = times_used + 1, last_used_at = ?1 WHERE id = ?2",
@ -145,7 +139,7 @@ impl SnippetManager {
}
pub fn find_snippet_by_keyword(&self, keyword: &str) -> Result<Option<Snippet>, AppError> {
let db = self.db.lock().unwrap();
let db = self.store.conn();
let mut stmt = db.prepare("SELECT id, name, keyword, content, created_at, updated_at, times_used, last_used_at FROM snippets WHERE keyword = ?1")?;
let mut rows = stmt.query_map(params![keyword], |row| {
let created_at_ts: i64 = row.get(4)?;
@ -171,7 +165,7 @@ impl SnippetManager {
}
pub fn find_snippet_by_name(&self, name: &str) -> Result<Option<Snippet>, AppError> {
let db = self.db.lock().unwrap();
let db = self.store.conn();
let mut stmt = db.prepare("SELECT id, name, keyword, content, created_at, updated_at, times_used, last_used_at FROM snippets WHERE name = ?1 ORDER BY updated_at DESC LIMIT 1")?;
let mut rows = stmt.query_map(params![name], |row| {
let created_at_ts: i64 = row.get(4)?;

33
src-tauri/src/store.rs Normal file
View file

@ -0,0 +1,33 @@
use crate::error::AppError;
use rusqlite::Connection;
use std::sync::{Mutex, MutexGuard};
use tauri::{AppHandle, Manager};
pub struct Store {
db: Mutex<Connection>,
}
impl Store {
pub fn new(app_handle: &AppHandle, db_filename: &str) -> Result<Self, AppError> {
let data_dir = app_handle
.path()
.app_local_data_dir()
.map_err(|_| AppError::DirectoryNotFound)?;
if !data_dir.exists() {
std::fs::create_dir_all(&data_dir)?;
}
let db_path = data_dir.join(db_filename);
let db = Connection::open(db_path)?;
Ok(Self { db: Mutex::new(db) })
}
pub fn init_table(&self, schema: &str) -> Result<(), AppError> {
self.db.lock().unwrap().execute(schema, [])?;
Ok(())
}
pub fn conn(&self) -> MutexGuard<Connection> {
self.db.lock().unwrap()
}
}