Skip to content

Commit c98d92f

Browse files
committed
feat: Added rusqlite for clipboard management and persistence. Added image preview for copied media
1 parent 1e63910 commit c98d92f

12 files changed

Lines changed: 431 additions & 24 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ once_cell = "1.21.3"
3131
rand = "0.9.2"
3232
rayon = "1.11.0"
3333
rfd = "0.17.2"
34+
rusqlite = { version = "0.39.0", features = ["bundled"] }
3435
serde = { version = "1.0.228", features = ["derive"] }
3536
serde_json = "1.0.149"
3637
tokio = { version = "1.48.0", features = ["full"] }

src/app/pages/clipboard.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,83 @@ fn viewport_content(content: &ClipBoardContentType, theme: &Theme) -> Element<'s
127127
.width(Length::Fill)
128128
.into()
129129
}
130+
ClipBoardContentType::Files(files, img_opt) => {
131+
let is_single_image = files.len() == 1 && {
132+
let p = std::path::Path::new(&files[0]);
133+
if let Some(ext) = p.extension().and_then(|s| s.to_str()) {
134+
matches!(
135+
ext.to_lowercase().as_str(),
136+
"png" | "jpg" | "jpeg" | "gif" | "bmp" | "webp" | "ico" | "tiff"
137+
)
138+
} else {
139+
false
140+
}
141+
};
142+
143+
if is_single_image {
144+
container(
145+
Viewer::new(Handle::from_path(&files[0]))
146+
.content_fit(ContentFit::ScaleDown)
147+
.scale_step(0.)
148+
.max_scale(1.)
149+
.min_scale(1.),
150+
)
151+
.padding(10)
152+
.style(|_| container::Style {
153+
border: iced::Border {
154+
color: iced::Color::WHITE,
155+
width: 1.,
156+
radius: Radius::new(0.),
157+
},
158+
..Default::default()
159+
})
160+
.width(Length::Fill)
161+
.into()
162+
} else if let Some(data) = img_opt {
163+
let bytes = data.to_owned_img().into_owned_bytes();
164+
container(
165+
Viewer::new(
166+
Handle::from_rgba(data.width as u32, data.height as u32, bytes.to_vec())
167+
.clone(),
168+
)
169+
.content_fit(ContentFit::ScaleDown)
170+
.scale_step(0.)
171+
.max_scale(1.)
172+
.min_scale(1.),
173+
)
174+
.padding(10)
175+
.style(|_| container::Style {
176+
border: iced::Border {
177+
color: iced::Color::WHITE,
178+
width: 1.,
179+
radius: Radius::new(0.),
180+
},
181+
..Default::default()
182+
})
183+
.width(Length::Fill)
184+
.into()
185+
} else {
186+
Scrollable::with_direction(
187+
container(
188+
Text::new(files.join("\n"))
189+
.height(Length::Fill)
190+
.width(Length::Fill)
191+
.align_x(Alignment::Start)
192+
.font(theme.font())
193+
.size(16),
194+
)
195+
.width(Length::Fill)
196+
.height(Length::Fill),
197+
Direction::Both {
198+
vertical: Scrollbar::hidden(),
199+
horizontal: Scrollbar::hidden(),
200+
},
201+
)
202+
.height(Length::Fill)
203+
.width(Length::Fill)
204+
.into()
205+
}
206+
}
130207
};
131208

132209
let theme_clone = theme.clone();

src/app/tile.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ use tray_icon::TrayIcon;
3434
use std::collections::HashMap;
3535
use std::fmt::Debug;
3636
use std::str::FromStr;
37+
use std::sync::Arc;
3738
use std::time::Duration;
3839

3940
/// This is a wrapper around the sender to disable dropping
@@ -169,6 +170,7 @@ pub struct Tile {
169170
page: Page,
170171
pub height: f32,
171172
pub file_search_sender: Option<tokio::sync::watch::Sender<(String, Vec<String>)>>,
173+
pub db: Arc<crate::database::Database>,
172174
debouncer: Debouncer,
173175
}
174176

@@ -347,8 +349,13 @@ fn handle_clipboard_history() -> impl futures::Stream<Item = Message> {
347349
let mut prev_byte_rep: Option<ClipBoardContentType> = None;
348350

349351
loop {
350-
let byte_rep = if let Ok(a) = clipboard.get_image() {
351-
Some(ClipBoardContentType::Image(a))
352+
let files_opt = crate::platform::get_copied_files();
353+
let img_opt = clipboard.get_image().ok();
354+
355+
let byte_rep = if let Some(files) = files_opt {
356+
Some(ClipBoardContentType::Files(files, img_opt))
357+
} else if let Some(img) = img_opt {
358+
Some(ClipBoardContentType::Image(img))
352359
} else if let Ok(a) = clipboard.get_text()
353360
&& !a.trim().is_empty()
354361
{

src/app/tile/elm.rs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
//! architecture. If the subscription function becomes too large, it should be moved to this file
33
44
use std::collections::HashMap;
5-
use std::fs;
65

76
use global_hotkey::hotkey::HotKey;
87
use iced::border::Radius;
@@ -33,6 +32,7 @@ use crate::{
3332
config::Config,
3433
platform::transform_process_to_ui_element,
3534
};
35+
use std::sync::Arc;
3636

3737
/// Initialise the base window
3838
pub fn new(hotkey: HotKey, config: &Config) -> (Tile, Task<Message>) {
@@ -78,12 +78,10 @@ pub fn new(hotkey: HotKey, config: &Config) -> (Tile, Task<Message>) {
7878
shells: shells_map,
7979
};
8080

81-
let home = std::env::var("HOME").unwrap_or("/".to_string());
82-
83-
let ranking = toml::from_str(
84-
&fs::read_to_string(home + "/.config/rustcast/ranking.toml").unwrap_or("".to_string()),
85-
)
86-
.unwrap_or(HashMap::new());
81+
let db =
82+
Arc::new(crate::database::Database::new().expect("Failed to initialize SQLite database"));
83+
let ranking = db.get_rankings().unwrap_or_default();
84+
let clipboard_content = db.get_clipboard_history(100).unwrap_or_default();
8785

8886
(
8987
Tile {
@@ -102,12 +100,13 @@ pub fn new(hotkey: HotKey, config: &Config) -> (Tile, Task<Message>) {
102100
config: config.clone(),
103101
ranking,
104102
theme: config.theme.to_owned().clone().into(),
105-
clipboard_content: vec![],
103+
clipboard_content,
106104
tray_icon: None,
107105
sender: None,
108106
page: Page::Main,
109107
height: DEFAULT_WINDOW_HEIGHT,
110108
file_search_sender: None,
109+
db,
111110
debouncer: Debouncer::new(config.debounce_delay),
112111
},
113112
Task::batch([open.map(|_| Message::OpenWindow)]),

src/app/tile/update.rs

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use iced::widget::operation::AbsoluteOffset;
1111
use iced::window;
1212
use iced::window::Id;
1313
use log::info;
14-
use rayon::iter::IntoParallelRefIterator;
14+
use crate::clipboard::ClipBoardContentType;
1515
use rayon::iter::ParallelIterator;
1616
use rayon::slice::ParallelSliceMut;
1717

@@ -238,10 +238,9 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task<Message> {
238238

239239
Message::SaveRanking => {
240240
tile.ranking = tile.options.get_rankings();
241-
let string_rep = toml::to_string(&tile.ranking).unwrap_or("".to_string());
242-
let ranking_file_path =
243-
std::env::var("HOME").unwrap_or("/".to_string()) + "/.config/rustcast/ranking.toml";
244-
fs::write(ranking_file_path, string_rep).ok();
241+
for (name, rank) in &tile.ranking {
242+
let _ = tile.db.save_ranking(name, *rank);
243+
}
245244
Task::none()
246245
}
247246

@@ -451,16 +450,31 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task<Message> {
451450
Message::EditClipboardHistory(action) => {
452451
match action {
453452
Editable::Create(content) => {
454-
if !tile.clipboard_content.contains(&content) {
455-
tile.clipboard_content.insert(0, content);
453+
let old_item = tile.clipboard_content.iter().find(|x| {
454+
if let (ClipBoardContentType::Files(f1, _), ClipBoardContentType::Files(f2, _)) = (x, &content) {
455+
f1 == f2
456+
} else {
457+
*x == &content
458+
}
459+
}).cloned();
460+
461+
if old_item.is_none() {
462+
tile.clipboard_content.insert(0, content.clone());
463+
let _ = tile.db.save_clipboard_item(&content);
456464
return Task::none();
457465
}
458466

459467
let new_content_vec = tile
460468
.clipboard_content
461-
.par_iter()
469+
.iter()
462470
.filter_map(|x| {
463-
if *x == content {
471+
let is_match = if let (ClipBoardContentType::Files(f1, _), ClipBoardContentType::Files(f2, _)) = (x, &content) {
472+
f1 == f2
473+
} else {
474+
x == &content
475+
};
476+
477+
if is_match {
464478
None
465479
} else {
466480
Some(x.to_owned())
@@ -469,7 +483,11 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task<Message> {
469483
.collect();
470484

471485
tile.clipboard_content = new_content_vec;
472-
tile.clipboard_content.insert(0, content);
486+
tile.clipboard_content.insert(0, content.clone());
487+
if let Some(old) = old_item {
488+
let _ = tile.db.delete_clipboard_item(&old);
489+
}
490+
let _ = tile.db.save_clipboard_item(&content);
473491
}
474492
Editable::Delete(content) => {
475493
tile.clipboard_content = tile
@@ -483,13 +501,16 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task<Message> {
483501
}
484502
})
485503
.collect();
504+
let _ = tile.db.delete_clipboard_item(&content);
486505
}
487506
Editable::Update { old, new } => {
488507
tile.clipboard_content = tile
489508
.clipboard_content
490509
.iter()
491510
.map(|x| if x == &old { new.clone() } else { x.to_owned() })
492511
.collect();
512+
let _ = tile.db.delete_clipboard_item(&old);
513+
let _ = tile.db.save_clipboard_item(&new);
493514
}
494515
}
495516
Task::none()
@@ -693,6 +714,7 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task<Message> {
693714

694715
Message::ClearClipboardHistory => {
695716
tile.clipboard_content.clear();
717+
let _ = tile.db.clear_clipboard();
696718
Task::none()
697719
}
698720

src/clipboard.rs

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,40 @@ use crate::{
1111
pub enum ClipBoardContentType {
1212
Text(String),
1313
Image(ImageData<'static>),
14+
Files(Vec<String>, Option<ImageData<'static>>),
1415
}
1516

1617
impl ToApp for ClipBoardContentType {
1718
/// Returns the iced element for rendering the clipboard item, and the entire content since the
1819
/// display name is only the first line
1920
fn to_app(&self) -> App {
20-
let mut display_name = match self {
21-
ClipBoardContentType::Image(_) => "Image".to_string(),
22-
ClipBoardContentType::Text(a) => a.get(0..25).unwrap_or(a).to_string(),
21+
let (mut display_name, desc) = match self {
22+
ClipBoardContentType::Image(_) => ("Image".to_string(), "Clipboard Item".to_string()),
23+
ClipBoardContentType::Text(a) => (
24+
a.get(0..25).unwrap_or(a).to_string(),
25+
"Clipboard Item".to_string(),
26+
),
27+
ClipBoardContentType::Files(f, _) => {
28+
if f.len() == 1 {
29+
let path = std::path::Path::new(&f[0]);
30+
let name = path
31+
.file_name()
32+
.unwrap_or_default()
33+
.to_string_lossy()
34+
.to_string();
35+
// Fall back to the raw path string if the file name was entirely empty
36+
let mut final_name = name;
37+
if final_name.is_empty() {
38+
final_name = f[0].clone();
39+
}
40+
(final_name, f[0].clone())
41+
} else {
42+
(
43+
format!("{} Files", f.len()),
44+
"Multiple files copied".to_string(),
45+
)
46+
}
47+
}
2348
};
2449

2550
let self_clone = self.clone();
@@ -33,7 +58,7 @@ impl ToApp for ClipBoardContentType {
3358
open_command: crate::app::apps::AppCommand::Function(Function::CopyToClipboard(
3459
self_clone.to_owned(),
3560
)),
36-
desc: "Clipboard Item".to_string(),
61+
desc,
3762
icons: None,
3863
display_name,
3964
search_name,
@@ -52,6 +77,17 @@ impl PartialEq for ClipBoardContentType {
5277
&& let Self::Image(other_image_data) = other
5378
{
5479
return image_data.bytes == other_image_data.bytes;
80+
} else if let Self::Files(f1, img1) = self
81+
&& let Self::Files(f2, img2) = other
82+
{
83+
if f1 != f2 {
84+
return false;
85+
}
86+
return match (img1, img2) {
87+
(Some(a), Some(b)) => a.bytes == b.bytes,
88+
(None, None) => true,
89+
_ => false,
90+
};
5591
}
5692
false
5793
}

src/commands.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,9 @@ impl Function {
105105
ClipBoardContentType::Image(img) => {
106106
Clipboard::new().unwrap().set_image(img.to_owned_img()).ok();
107107
}
108+
ClipBoardContentType::Files(files, _) => {
109+
crate::platform::put_copied_files(files);
110+
}
108111
},
109112

110113
Function::Quit => std::process::exit(0),

0 commit comments

Comments
 (0)