mirror of
https://github.com/ByteAtATime/raycast-linux.git
synced 2025-08-31 11:17:27 +00:00
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:
parent
23c222a956
commit
7156827585
7 changed files with 186 additions and 223 deletions
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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))?;
|
||||
|
||||
|
|
|
@ -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)?;
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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
33
src-tauri/src/store.rs
Normal 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()
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue