This commit is contained in:
2026-05-20 19:31:16 +02:00
parent 20f33f5694
commit 8b07e305f0
17 changed files with 1794 additions and 573 deletions

View File

@ -1,6 +1,5 @@
use crate::models::{ClipboardData, ClipboardEntry, Image};
use image::codecs::jpeg::JpegEncoder;
use image::codecs::png::PngEncoder;
use image::{ExtendedColorType, ImageEncoder};
use rusqlite::Connection;
use std::error::Error;
@ -17,23 +16,32 @@ pub struct Database {
impl Database {
pub fn init(dir_path: &str) -> Result<Self, Box<dyn Error>> {
let base_path = Path::new(dir_path);
let images_path = base_path.join("images");
std::fs::create_dir_all(&images_path)?;
fs::create_dir_all(base_path.join("images"))?;
let db_path = base_path.join("clipboard.db");
let conn = Connection::open(base_path.join("clipboard.db"))?;
let conn = Connection::open(&db_path)?;
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS schema_version (version INTEGER NOT NULL);
INSERT OR IGNORE INTO schema_version (version) SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM schema_version);",
)?;
conn.execute(
"CREATE TABLE IF NOT EXISTS history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
type TEXT NOT NULL,
content TEXT NOT NULL,
timestamp INTEGER NOT NULL
)",
id INTEGER PRIMARY KEY AUTOINCREMENT,
type TEXT NOT NULL,
content TEXT NOT NULL,
timestamp INTEGER NOT NULL
)",
[],
)?;
conn.execute_batch(
"DELETE FROM history WHERE id NOT IN (
SELECT MAX(id) FROM history GROUP BY content
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_history_content ON history(content);",
)?;
Ok(Self {
conn,
dir_path: dir_path.to_string(),
@ -41,61 +49,62 @@ impl Database {
}
pub fn append(&self, entry: ClipboardEntry) -> Result<(), Box<dyn Error>> {
let timestamp_millis = entry.timestamp.duration_since(UNIX_EPOCH)?.as_millis() as i64;
let ts = entry.timestamp.duration_since(UNIX_EPOCH)?.as_millis() as i64;
let (entry_type, content) = match &entry.content {
ClipboardData::Text(text) => ("text", text.clone()),
let (kind, content) = match &entry.content {
ClipboardData::Text(t) => {
if t.trim().is_empty() {
return Ok(());
}
("text", t.clone())
}
ClipboardData::Image(img) => {
if let Some(raw_pixels) = &img.raw_pixels {
let img_path = img.file_path(&self.dir_path);
let file = fs::File::create(&img_path)?;
let rgb_pixels: Vec<u8> = raw_pixels
if let Some(px) = &img.raw_pixels {
let path = img.file_path(&self.dir_path);
let file = fs::File::create(&path)?;
let rgb: Vec<u8> = px
.chunks_exact(4)
.flat_map(|rgba| [rgba[0], rgba[1], rgba[2]])
.collect();
let encoder = JpegEncoder::new_with_quality(file, 70);
encoder.write_image(
&rgb_pixels,
JpegEncoder::new_with_quality(file, 70).write_image(
&rgb,
img.width,
img.height,
ExtendedColorType::Rgb8,
)?;
}
("image", img.id.to_string())
("image", format!("{}.jpg", img.id))
}
};
self.conn.execute(
"INSERT INTO history (type, content, timestamp) VALUES (?1, ?2, ?3)",
(entry_type, content, timestamp_millis),
"INSERT OR REPLACE INTO history (type, content, timestamp) VALUES (?1, ?2, ?3)",
(kind, &content, ts),
)?;
Ok(())
}
pub fn read_history(&self) -> Result<Vec<ClipboardEntry>, Box<dyn Error>> {
let mut stmt = self
.conn
.prepare("SELECT type, content, timestamp FROM history ORDER BY timestamp ASC")?;
pub fn read_history(&self, limit: usize) -> Result<Vec<ClipboardEntry>, Box<dyn Error>> {
let mut stmt = self.conn.prepare(
"SELECT type, content, timestamp FROM history ORDER BY timestamp DESC LIMIT ?1",
)?;
let rows = stmt.query_map([], |row| {
let ty: String = row.get(0)?;
let content: String = row.get(1)?;
let timestamp: i64 = row.get(2)?;
Ok((ty, content, timestamp))
let rows = stmt.query_map([limit as i64], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, i64>(2)?,
))
})?;
let mut entries = Vec::new();
for row in rows {
let (ty, content, timestamp) = row?;
let timestamp = UNIX_EPOCH + Duration::from_millis(timestamp as u64);
let (ty, content, ts_ms) = row?;
let timestamp = UNIX_EPOCH + Duration::from_millis(ts_ms as u64);
let data = if ty == "text" {
ClipboardData::Text(content)
} else {
let id = Uuid::parse_str(&content)?;
let id = Uuid::parse_str(content.trim_end_matches(".jpg"))?;
ClipboardData::Image(Image {
id,
raw_pixels: None,
@ -103,13 +112,11 @@ impl Database {
height: 0,
})
};
entries.push(ClipboardEntry {
content: data,
timestamp,
});
}
Ok(entries)
}
@ -118,4 +125,27 @@ impl Database {
.execute("DELETE FROM history WHERE content = ?1", [content])?;
Ok(())
}
pub fn update_entry_content(&self, old: &str, new: &str) -> Result<(), Box<dyn Error>> {
let rows_affected = self.conn.execute(
"UPDATE history SET content = ?1 WHERE content = ?2",
[new, old],
)?;
if rows_affected == 0 {
return Err(format!("Entrée introuvable : {old}").into());
}
Ok(())
}
pub fn clear_history(&self) -> Result<(), Box<dyn Error>> {
let images_dir = Path::new(&self.dir_path).join("images");
if images_dir.exists() {
fs::remove_dir_all(&images_dir)?;
}
fs::create_dir_all(&images_dir)?;
self.conn.execute("DELETE FROM history", [])?;
Ok(())
}
}