Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ homepage = "https://rustcast.app"
repository = "https://github.com/RustCastLabs/rustcast"

[dependencies]
arboard = "3.6.1"
block2 = "0.6.2"
emojis = "0.8.0"
global-hotkey = "0.7.0"
Expand All @@ -31,8 +30,10 @@ once_cell = "1.21.3"
rand = "0.9.2"
rayon = "1.11.0"
rfd = "0.17.2"
rusqlite = { version = "0.39.0", features = ["bundled"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
sha2 = "0.11.0"
tokio = { version = "1.48.0", features = ["full"] }
toml = "0.9.8"
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
Expand Down
174 changes: 155 additions & 19 deletions src/app/pages/clipboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use iced::{
use crate::{
app::{Editable, ToApp, pages::prelude::*},
clipboard::ClipBoardContentType,
styles::{delete_button_style, settings_text_input_item_style},
styles::{delete_button_style, settings_text_input_item_style, clipboard_image_border_style},
};

/// The clipboard view
Expand All @@ -30,6 +30,8 @@ pub fn clipboard_view(
clipboard_content: Vec<ClipBoardContentType>,
focussed_id: u32,
theme: Theme,
rankings: &std::collections::HashMap<String, i32>,
search_query: &str,
) -> Element<'static, Message> {
let theme_clone = theme.clone();
let theme_clone_2 = theme.clone();
Expand All @@ -49,19 +51,77 @@ pub fn clipboard_view(
.into();
}

let mut apps: Vec<(crate::app::apps::App, ClipBoardContentType)> = clipboard_content
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to just switch to a ClipboardContent struct that will also store the ranking?

.into_iter()
.filter_map(|c| {
let mut app = c.to_app();
if !search_query.is_empty()
&& !app.search_name.to_lowercase().contains(search_query)
&& !app.display_name.to_lowercase().contains(search_query) {
return None;
}
if let Some(r) = rankings.get(&app.search_name) {
app.ranking = *r;
}
Some((app, c))
})
.collect();

apps.sort_by(|a, b| {
let rank_a = if a.0.ranking == -1 { 0 } else { 1 };
let rank_b = if b.0.ranking == -1 { 0 } else { 1 };
rank_a.cmp(&rank_b)
});

let mut elements: Vec<Element<'static, Message>> = Vec::new();
let mut has_pinned = false;
let mut has_copied = false;

let apps_len = apps.len();
for (i, (app, _)) in apps.iter().enumerate() {
if app.ranking == -1 && !has_pinned {
elements.push(
container(
Text::new("Pinned")
.font(iced::Font { weight: iced::font::Weight::Bold, ..theme.font() })
.size(12)
.style(|_theme| iced::widget::text::Style { color: Some(iced::Color::from_rgb8(150, 150, 150)) })
)
.padding([5, 10])
.into()
);
has_pinned = true;
} else if app.ranking != -1 && !has_copied {
if has_pinned {
elements.push(Text::new("").size(10).into());
}
elements.push(
container(
Text::new("Copied")
.font(iced::Font { weight: iced::font::Weight::Bold, ..theme.font() })
.size(12)
.style(|_theme| iced::widget::text::Style { color: Some(iced::Color::from_rgb8(150, 150, 150)) })
)
.padding([5, 10])
.into()
);
has_copied = true;
}
elements.push(app.clone().render(theme.clone(), i as u32, focussed_id, None));
}

let viewport_content: Element<'static, Message> =
match clipboard_content.get(focussed_id as usize) {
Some(content) => viewport_content(content, &theme),
None => Text::new("").into(),
if focussed_id < apps_len as u32 {
let (_, content) = &apps[focussed_id as usize];
viewport_content(content, &theme)
} else {
Text::new("").into()
};

container(Row::from_iter([
container(
Scrollable::with_direction(
Column::from_iter(clipboard_content.iter().enumerate().map(|(i, content)| {
content
.to_app()
.render(theme.clone(), i as u32, focussed_id, None)
}))
Column::from_iter(elements)
.width(WINDOW_WIDTH / 3.),
Direction::Vertical(Scrollbar::hidden()),
)
Expand Down Expand Up @@ -104,10 +164,10 @@ fn viewport_content(content: &ClipBoardContentType, theme: &Theme) -> Element<'s
.into(),

ClipBoardContentType::Image(data) => {
let bytes = data.to_owned_img().into_owned_bytes();
let bytes = data.bytes.to_vec();
container(
Viewer::new(
Handle::from_rgba(data.width as u32, data.height as u32, bytes.to_vec())
Handle::from_rgba(data.width as u32, data.height as u32, bytes)
.clone(),
)
.content_fit(ContentFit::ScaleDown)
Expand All @@ -116,17 +176,93 @@ fn viewport_content(content: &ClipBoardContentType, theme: &Theme) -> Element<'s
.min_scale(1.),
)
.padding(10)
.style(|_| container::Style {
border: iced::Border {
color: iced::Color::WHITE,
width: 1.,
radius: Radius::new(0.),
},
..Default::default()
})
.style(|_| clipboard_image_border_style())
.width(Length::Fill)
.into()
}
ClipBoardContentType::Files(files, img_opt) => {
let is_single_image = files.len() == 1 && {
let p = std::path::Path::new(&files[0]);
if let Some(ext) = p.extension().and_then(|s| s.to_str()) {
matches!(
ext.to_lowercase().as_str(),
"png" | "jpg" | "jpeg" | "gif" | "bmp" | "webp" | "ico" | "tiff"
)
} else {
false
}
};

if is_single_image {
container(
Viewer::new(Handle::from_path(&files[0]))
.content_fit(ContentFit::ScaleDown)
.scale_step(0.)
.max_scale(1.)
.min_scale(1.),
)
.padding(10)
.style(|_| clipboard_image_border_style())
.width(Length::Fill)
.into()
} else {
let display_text = if files.len() > 1 {
let mut s = format!("{} Files Copied", files.len());
for f in files.iter().take(3) {
let fname = std::path::Path::new(f).file_name().unwrap_or_default().to_string_lossy();
s.push_str(&format!("\n• {}", fname));
}
if files.len() > 3 {
s.push_str(&format!("\n...and {} more", files.len() - 3));
}
s
} else {
let fname = std::path::Path::new(&files[0]).file_name().unwrap_or_default().to_string_lossy();
format!("File: {}", fname)
};

let text_elem = Scrollable::with_direction(
container(
Text::new(display_text)
.height(Length::Fill)
.width(Length::Fill)
.align_x(Alignment::Start)
.font(theme.font())
.size(16),
)
.width(Length::Fill)
.height(Length::Fill),
Direction::Both {
vertical: Scrollbar::hidden(),
horizontal: Scrollbar::hidden(),
},
)
.height(Length::Fill)
.width(Length::Fill);

if let Some(data) = img_opt {
let bytes = data.bytes.to_vec();
let image_elem = container(
Viewer::new(
Handle::from_rgba(data.width as u32, data.height as u32, bytes)
.clone(),
)
.content_fit(ContentFit::ScaleDown)
.scale_step(0.)
.max_scale(1.)
.min_scale(1.),
)
.padding(10)
.style(|_| clipboard_image_border_style())
.width(Length::Fill)
.height(Length::Fixed(220.0));

Column::new().push(image_elem).push(text_elem).into()
} else {
text_elem.into()
}
}
}
};

let theme_clone = theme.clone();
Expand Down
51 changes: 39 additions & 12 deletions src/app/tile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ use crate::debounce::Debouncer;
use crate::platform::default_app_paths;
use crate::platform::macos::launching::Shortcut;

use arboard::Clipboard;

use iced::futures::SinkExt;
use iced::futures::channel::mpsc::{Sender, channel};
Expand All @@ -33,6 +32,7 @@ use tray_icon::TrayIcon;
use std::collections::HashMap;
use std::fmt::Debug;
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;

/// This is a wrapper around the sender to disable dropping
Expand Down Expand Up @@ -82,7 +82,7 @@ impl AppIndex {

fn get_rankings(&self) -> HashMap<String, i32> {
HashMap::from_iter(self.by_name.iter().filter_map(|(name, app)| {
if app.ranking > 0 {
if app.ranking != 0 {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will match the favourited apps as well

Some((name.to_owned(), app.ranking.to_owned()))
} else {
None
Expand Down Expand Up @@ -181,6 +181,7 @@ pub struct Tile {
page: Page,
pub height: f32,
pub file_search_sender: Option<tokio::sync::watch::Sender<(String, Vec<String>)>>,
pub db: Arc<crate::database::Database>,
debouncer: Debouncer,
}

Expand Down Expand Up @@ -335,27 +336,53 @@ impl Tile {
/// This is the subscription function that handles the change in clipboard history
fn handle_clipboard_history() -> impl futures::Stream<Item = Message> {
stream::channel(100, async |mut output| {
let mut clipboard = Clipboard::new().unwrap();
let mut prev_byte_rep: Option<ClipBoardContentType> = None;
let initial_files = crate::platform::get_copied_files();
let initial_img = crate::platform::get_copied_image();

let mut prev_byte_rep = if let Some(files) = initial_files {
Some(ClipBoardContentType::Files(files, initial_img))
} else if let Some(img) = initial_img {
Some(ClipBoardContentType::Image(img))
} else if let Some(a) = crate::platform::get_copied_text() {
if !a.trim().is_empty() { Some(ClipBoardContentType::Text(a)) } else { None }
} else {
None
};

loop {
let byte_rep = if let Ok(a) = clipboard.get_image() {
Some(ClipBoardContentType::Image(a))
} else if let Ok(a) = clipboard.get_text()
&& !a.trim().is_empty()
{
Some(ClipBoardContentType::Text(a))
let files_opt = crate::platform::get_copied_files();
let img_opt = crate::platform::get_copied_image();

let byte_rep = if let Some(files) = files_opt {
Some(ClipBoardContentType::Files(files, img_opt))
} else if let Some(img) = img_opt {
Some(ClipBoardContentType::Image(img))
} else if let Some(a) = crate::platform::get_copied_text() {
if !a.trim().is_empty() {
Some(ClipBoardContentType::Text(a))
} else {
None
}
} else {
None
};

if byte_rep != prev_byte_rep
&& let Some(content) = &byte_rep
&& let Some(content_ref) = &byte_rep
{
let mut content = content_ref.clone();
if let ClipBoardContentType::Files(ref files, ref mut img_opt) = content {
if files.len() > 1 {
if let Some(multi) = crate::clipboard::generate_multi_file_thumbnail(files) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than doing a multi file thumbnail as the main "preview", is it possible to make it show each individual thumbnail as a carousell element? I think that would be more useful that showing a thumbnail that will might look extremely cluttered if there are too many images / files / folders copied

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually had this at a max of 3 or 4, like most previews do

*img_opt = Some(multi);
}
}
}

info!("Adding item to cbhist");
output
.send(Message::EditClipboardHistory(crate::app::Editable::Create(
content.to_owned(),
content,
)))
.await
.ok();
Expand Down
17 changes: 9 additions & 8 deletions src/app/tile/elm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
//! architecture. If the subscription function becomes too large, it should be moved to this file

use std::collections::HashMap;
use std::fs;

use iced::border::Radius;
use iced::widget::scrollable::{Anchor, Direction, Scrollbar};
Expand Down Expand Up @@ -32,6 +31,7 @@ use crate::{
config::Config,
platform::transform_process_to_ui_element,
};
use std::sync::Arc;

/// Initialise the base window
pub fn new(hotkeys: Hotkeys, config: &Config) -> (Tile, Task<Message>) {
Expand Down Expand Up @@ -59,12 +59,10 @@ pub fn new(hotkeys: Hotkeys, config: &Config) -> (Tile, Task<Message>) {
options.par_sort_by_key(|x| x.display_name.len());
let options = AppIndex::from_apps(options);

let home = std::env::var("HOME").unwrap_or("/".to_string());

let ranking = toml::from_str(
&fs::read_to_string(home + "/.config/rustcast/ranking.toml").unwrap_or("".to_string()),
)
.unwrap_or(HashMap::new());
let db =
Arc::new(crate::database::Database::new().expect("Failed to initialize SQLite database"));
let ranking = db.get_rankings().unwrap_or_default();
let clipboard_content = vec![];

(
Tile {
Expand All @@ -83,12 +81,13 @@ pub fn new(hotkeys: Hotkeys, config: &Config) -> (Tile, Task<Message>) {
config: config.clone(),
ranking,
theme: config.theme.to_owned().clone().into(),
clipboard_content: vec![],
clipboard_content,
tray_icon: None,
sender: None,
page: Page::Main,
height: DEFAULT_WINDOW_HEIGHT,
file_search_sender: None,
db,
debouncer: Debouncer::new(config.debounce_delay),
},
Task::batch([open.map(|_| Message::OpenWindow)]),
Expand Down Expand Up @@ -126,6 +125,8 @@ pub fn view(tile: &Tile, wid: window::Id) -> Element<'_, Message> {
tile.clipboard_content.clone(),
tile.focus_id,
tile.config.theme.clone(),
&tile.ranking,
&tile.query_lc,
),
Page::EmojiSearch => emoji_page(
tile.config.theme.clone(),
Expand Down
Loading
Loading