feat: paste images, videos, and text from clipboard
This commit is contained in:
parent
7de4ceac77
commit
08d442aee2
4 changed files with 256 additions and 4 deletions
|
|
@ -200,6 +200,9 @@ compressed = Compressed {$items} {$items ->
|
||||||
*[other] items
|
*[other] items
|
||||||
} from "{$from}" to "{$to}"
|
} from "{$from}" to "{$to}"
|
||||||
copy_noun = Copy
|
copy_noun = Copy
|
||||||
|
pasted-image = Pasted Image
|
||||||
|
pasted-text = Pasted Text
|
||||||
|
pasted-video = Pasted Video
|
||||||
creating = Creating "{$name}" in "{$parent}"
|
creating = Creating "{$name}" in "{$parent}"
|
||||||
created = Created "{$name}" in "{$parent}"
|
created = Created "{$name}" in "{$parent}"
|
||||||
copying = Copying {$items} {$items ->
|
copying = Copying {$items} {$items ->
|
||||||
|
|
|
||||||
96
src/app.rs
96
src/app.rs
|
|
@ -72,7 +72,10 @@ use wayland_client::{Proxy, protocol::wl_output::WlOutput};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
FxOrderMap,
|
FxOrderMap,
|
||||||
clipboard::{ClipboardCopy, ClipboardKind, ClipboardPaste},
|
clipboard::{
|
||||||
|
ClipboardCopy, ClipboardKind, ClipboardPaste, ClipboardPasteImage, ClipboardPasteText,
|
||||||
|
ClipboardPasteVideo,
|
||||||
|
},
|
||||||
config::{
|
config::{
|
||||||
AppTheme, Config, DesktopConfig, Favorite, IconSizes, State, TIME_CONFIG_ID, TabConfig,
|
AppTheme, Config, DesktopConfig, Favorite, IconSizes, State, TIME_CONFIG_ID, TabConfig,
|
||||||
TimeConfig, TypeToSearch,
|
TimeConfig, TypeToSearch,
|
||||||
|
|
@ -87,7 +90,7 @@ use crate::{
|
||||||
mounter::{MOUNTERS, MounterAuth, MounterItem, MounterItems, MounterKey, MounterMessage},
|
mounter::{MOUNTERS, MounterAuth, MounterItem, MounterItems, MounterKey, MounterMessage},
|
||||||
operation::{
|
operation::{
|
||||||
Controller, Operation, OperationError, OperationErrorType, OperationSelection,
|
Controller, Operation, OperationError, OperationErrorType, OperationSelection,
|
||||||
ReplaceResult,
|
ReplaceResult, copy_unique_path,
|
||||||
},
|
},
|
||||||
spawn_detached::spawn_detached,
|
spawn_detached::spawn_detached,
|
||||||
tab::{
|
tab::{
|
||||||
|
|
@ -385,6 +388,12 @@ pub enum Message {
|
||||||
Overlap(window::Id, OverlapNotifyEvent),
|
Overlap(window::Id, OverlapNotifyEvent),
|
||||||
Paste(Option<Entity>),
|
Paste(Option<Entity>),
|
||||||
PasteContents(PathBuf, ClipboardPaste),
|
PasteContents(PathBuf, ClipboardPaste),
|
||||||
|
PasteImage(PathBuf),
|
||||||
|
PasteImageContents(PathBuf, ClipboardPasteImage),
|
||||||
|
PasteText(PathBuf),
|
||||||
|
PasteTextContents(PathBuf, ClipboardPasteText),
|
||||||
|
PasteVideo(PathBuf),
|
||||||
|
PasteVideoContents(PathBuf, ClipboardPasteVideo),
|
||||||
PendingCancel(u64),
|
PendingCancel(u64),
|
||||||
PendingCancelAll,
|
PendingCancelAll,
|
||||||
PendingComplete(u64, OperationSelection),
|
PendingComplete(u64, OperationSelection),
|
||||||
|
|
@ -3488,7 +3497,8 @@ impl Application for App {
|
||||||
Some(contents) => {
|
Some(contents) => {
|
||||||
cosmic::action::app(Message::PasteContents(to.clone(), 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::<ClipboardPasteImage>().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::<ClipboardPasteVideo>().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::<ClipboardPasteText>().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) => {
|
Message::PendingCancel(id) => {
|
||||||
if let Some((_, controller)) = self.pending_operations.get(&id) {
|
if let Some((_, controller)) = self.pending_operations.get(&id) {
|
||||||
controller.cancel();
|
controller.cancel();
|
||||||
|
|
|
||||||
159
src/clipboard.rs
159
src/clipboard.rs
|
|
@ -162,3 +162,162 @@ impl TryFrom<(Vec<u8>, String)> for ClipboardPaste {
|
||||||
Ok(Self { kind, paths })
|
Ok(Self { kind, paths })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Image data from clipboard for pasting as a new file.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ClipboardPasteImage {
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
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<u8>, String)> for ClipboardPasteImage {
|
||||||
|
type Error = Box<dyn Error>;
|
||||||
|
fn try_from(value: (Vec<u8>, String)) -> Result<Self, Self::Error> {
|
||||||
|
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<u8>,
|
||||||
|
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<u8>, String)> for ClipboardPasteVideo {
|
||||||
|
type Error = Box<dyn Error>;
|
||||||
|
fn try_from(value: (Vec<u8>, String)) -> Result<Self, Self::Error> {
|
||||||
|
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<u8>, String)> for ClipboardPasteText {
|
||||||
|
type Error = Box<dyn Error>;
|
||||||
|
fn try_from(value: (Vec<u8>, String)) -> Result<Self, Self::Error> {
|
||||||
|
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(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -221,7 +221,7 @@ pub async fn sync_to_disk(
|
||||||
.await;
|
.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
|
// List of compound extensions to check
|
||||||
const COMPOUND_EXTENSIONS: &[&str] = &[
|
const COMPOUND_EXTENSIONS: &[&str] = &[
|
||||||
".tar.gz",
|
".tar.gz",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue