diff --git a/i18n/en/cosmic_files.ftl b/i18n/en/cosmic_files.ftl index 96132a2..fe080cd 100644 --- a/i18n/en/cosmic_files.ftl +++ b/i18n/en/cosmic_files.ftl @@ -200,6 +200,9 @@ compressed = Compressed {$items} {$items -> *[other] items } from "{$from}" to "{$to}" copy_noun = Copy +pasted-image = Pasted Image +pasted-text = Pasted Text +pasted-video = Pasted Video creating = Creating "{$name}" in "{$parent}" created = Created "{$name}" in "{$parent}" copying = Copying {$items} {$items -> diff --git a/src/app.rs b/src/app.rs index 160350d..9a48671 100644 --- a/src/app.rs +++ b/src/app.rs @@ -72,7 +72,10 @@ use wayland_client::{Proxy, protocol::wl_output::WlOutput}; use crate::{ FxOrderMap, - clipboard::{ClipboardCopy, ClipboardKind, ClipboardPaste}, + clipboard::{ + ClipboardCopy, ClipboardKind, ClipboardPaste, ClipboardPasteImage, ClipboardPasteText, + ClipboardPasteVideo, + }, config::{ AppTheme, Config, DesktopConfig, Favorite, IconSizes, State, TIME_CONFIG_ID, TabConfig, TimeConfig, TypeToSearch, @@ -87,7 +90,7 @@ use crate::{ mounter::{MOUNTERS, MounterAuth, MounterItem, MounterItems, MounterKey, MounterMessage}, operation::{ Controller, Operation, OperationError, OperationErrorType, OperationSelection, - ReplaceResult, + ReplaceResult, copy_unique_path, }, spawn_detached::spawn_detached, tab::{ @@ -385,6 +388,12 @@ pub enum Message { Overlap(window::Id, OverlapNotifyEvent), Paste(Option), PasteContents(PathBuf, ClipboardPaste), + PasteImage(PathBuf), + PasteImageContents(PathBuf, ClipboardPasteImage), + PasteText(PathBuf), + PasteTextContents(PathBuf, ClipboardPasteText), + PasteVideo(PathBuf), + PasteVideoContents(PathBuf, ClipboardPasteVideo), PendingCancel(u64), PendingCancelAll, PendingComplete(u64, OperationSelection), @@ -3488,7 +3497,8 @@ impl Application for App { Some(contents) => { cosmic::action::app(Message::PasteContents(to.clone(), contents)) } - None => cosmic::action::none(), + // No file data in clipboard, try image data + None => cosmic::action::app(Message::PasteImage(to.clone())), } }); } @@ -3509,6 +3519,86 @@ impl Application for App { }; } } + Message::PasteImage(to) => { + return clipboard::read_data::().map(move |contents_opt| { + match contents_opt { + Some(contents) => { + cosmic::action::app(Message::PasteImageContents(to.clone(), contents)) + } + // No image data in clipboard, try video data + None => cosmic::action::app(Message::PasteVideo(to.clone())), + } + }); + } + Message::PasteImageContents(to, contents) => { + // Generate unique filename for the pasted image + let base_name = format!("{}.{}", fl!("pasted-image"), contents.extension()); + let base_path = to.join(&base_name); + let final_path = copy_unique_path(&base_path, &to); + + // Write image data to file + match fs::write(&final_path, &contents.data) { + Ok(_) => { + log::info!("Pasted image saved to {:?}", final_path); + } + Err(err) => { + log::error!("Failed to save pasted image: {}", err); + } + } + } + Message::PasteVideo(to) => { + return clipboard::read_data::().map(move |contents_opt| { + match contents_opt { + Some(contents) => { + cosmic::action::app(Message::PasteVideoContents(to.clone(), contents)) + } + // No video data in clipboard, try text data + None => cosmic::action::app(Message::PasteText(to.clone())), + } + }); + } + Message::PasteVideoContents(to, contents) => { + // Generate unique filename for the pasted video + let base_name = format!("{}.{}", fl!("pasted-video"), contents.extension()); + let base_path = to.join(&base_name); + let final_path = copy_unique_path(&base_path, &to); + + // Write video data to file + match fs::write(&final_path, &contents.data) { + Ok(_) => { + log::info!("Pasted video saved to {:?}", final_path); + } + Err(err) => { + log::error!("Failed to save pasted video: {}", err); + } + } + } + Message::PasteText(to) => { + return clipboard::read_data::().map(move |contents_opt| { + match contents_opt { + Some(contents) => { + cosmic::action::app(Message::PasteTextContents(to.clone(), contents)) + } + None => cosmic::action::none(), + } + }); + } + Message::PasteTextContents(to, contents) => { + // Generate unique filename for the pasted text + let base_name = format!("{}.txt", fl!("pasted-text")); + let base_path = to.join(&base_name); + let final_path = copy_unique_path(&base_path, &to); + + // Write text data to file + match fs::write(&final_path, &contents.data) { + Ok(_) => { + log::info!("Pasted text saved to {:?}", final_path); + } + Err(err) => { + log::error!("Failed to save pasted text: {}", err); + } + } + } Message::PendingCancel(id) => { if let Some((_, controller)) = self.pending_operations.get(&id) { controller.cancel(); diff --git a/src/clipboard.rs b/src/clipboard.rs index 0c17a09..c3e0bc8 100644 --- a/src/clipboard.rs +++ b/src/clipboard.rs @@ -162,3 +162,162 @@ impl TryFrom<(Vec, String)> for ClipboardPaste { Ok(Self { kind, paths }) } } + +/// Image data from clipboard for pasting as a new file. +#[derive(Clone, Debug)] +pub struct ClipboardPasteImage { + pub data: Vec, + pub mime_type: String, +} + +impl AllowedMimeTypes for ClipboardPasteImage { + fn allowed() -> Cow<'static, [String]> { + Cow::from(vec![ + "image/png".to_string(), + "image/jpeg".to_string(), + "image/gif".to_string(), + "image/bmp".to_string(), + "image/webp".to_string(), + "image/tiff".to_string(), + "image/x-tiff".to_string(), + "image/svg+xml".to_string(), + "image/x-icon".to_string(), + "image/vnd.microsoft.icon".to_string(), + "image/x-bmp".to_string(), + "image/x-ms-bmp".to_string(), + "image/pjpeg".to_string(), + "image/x-png".to_string(), + "image/avif".to_string(), + "image/heic".to_string(), + "image/heif".to_string(), + "image/jxl".to_string(), + ]) + } +} + +impl TryFrom<(Vec, String)> for ClipboardPasteImage { + type Error = Box; + fn try_from(value: (Vec, String)) -> Result { + let (data, mime) = value; + if data.is_empty() { + return Err("Empty image data".into()); + } + Ok(Self { + data, + mime_type: mime, + }) + } +} + +impl ClipboardPasteImage { + /// Get the file extension for the image based on MIME type. + pub fn extension(&self) -> &'static str { + match self.mime_type.as_str() { + "image/png" | "image/x-png" => "png", + "image/jpeg" | "image/pjpeg" => "jpg", + "image/gif" => "gif", + "image/bmp" | "image/x-bmp" | "image/x-ms-bmp" => "bmp", + "image/webp" => "webp", + "image/tiff" | "image/x-tiff" => "tiff", + "image/svg+xml" => "svg", + "image/x-icon" | "image/vnd.microsoft.icon" => "ico", + "image/avif" => "avif", + "image/heic" => "heic", + "image/heif" => "heif", + "image/jxl" => "jxl", + _ => "png", // Default to png + } + } +} + +/// Video data from clipboard for pasting as a new file. +#[derive(Clone, Debug)] +pub struct ClipboardPasteVideo { + pub data: Vec, + pub mime_type: String, +} + +impl AllowedMimeTypes for ClipboardPasteVideo { + fn allowed() -> Cow<'static, [String]> { + Cow::from(vec![ + "video/mp4".to_string(), + "video/webm".to_string(), + "video/ogg".to_string(), + "video/mpeg".to_string(), + "video/quicktime".to_string(), + "video/x-msvideo".to_string(), + "video/x-matroska".to_string(), + "video/x-flv".to_string(), + "video/3gpp".to_string(), + "video/3gpp2".to_string(), + "video/x-ms-wmv".to_string(), + "video/avi".to_string(), + ]) + } +} + +impl TryFrom<(Vec, String)> for ClipboardPasteVideo { + type Error = Box; + fn try_from(value: (Vec, String)) -> Result { + let (data, mime) = value; + if data.is_empty() { + return Err("Empty video data".into()); + } + Ok(Self { + data, + mime_type: mime, + }) + } +} + +impl ClipboardPasteVideo { + /// Get the file extension for the video based on MIME type. + pub fn extension(&self) -> &'static str { + match self.mime_type.as_str() { + "video/mp4" => "mp4", + "video/webm" => "webm", + "video/ogg" => "ogv", + "video/mpeg" => "mpeg", + "video/quicktime" => "mov", + "video/x-msvideo" | "video/avi" => "avi", + "video/x-matroska" => "mkv", + "video/x-flv" => "flv", + "video/3gpp" => "3gp", + "video/3gpp2" => "3g2", + "video/x-ms-wmv" => "wmv", + _ => "mp4", // Default to mp4 + } + } +} + +/// Text data from clipboard for pasting as a new text file. +#[derive(Clone, Debug)] +pub struct ClipboardPasteText { + pub data: String, +} + +impl AllowedMimeTypes for ClipboardPasteText { + fn allowed() -> Cow<'static, [String]> { + Cow::from(vec![ + "text/plain".to_string(), + "text/plain;charset=utf-8".to_string(), + "UTF8_STRING".to_string(), + "STRING".to_string(), + "TEXT".to_string(), + ]) + } +} + +impl TryFrom<(Vec, String)> for ClipboardPasteText { + type Error = Box; + fn try_from(value: (Vec, String)) -> Result { + let (data, _mime) = value; + if data.is_empty() { + return Err("Empty text data".into()); + } + let text = str::from_utf8(&data)?; + Ok(Self { + data: text.to_string(), + }) + } +} diff --git a/src/operation/mod.rs b/src/operation/mod.rs index d17ef5d..0669624 100644 --- a/src/operation/mod.rs +++ b/src/operation/mod.rs @@ -221,7 +221,7 @@ pub async fn sync_to_disk( .await; } -fn copy_unique_path(from: &Path, to: &Path) -> PathBuf { +pub fn copy_unique_path(from: &Path, to: &Path) -> PathBuf { // List of compound extensions to check const COMPOUND_EXTENSIONS: &[&str] = &[ ".tar.gz",