diff --git a/Cargo.lock b/Cargo.lock index 27f72cd..6087ce3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1258,6 +1258,8 @@ dependencies = [ "html2pango", "lemmy_api_common", "markdown", + "mime_guess", + "rand", "relm4", "relm4-components", "reqwest", @@ -1423,6 +1425,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "mio" version = "0.8.8" @@ -1861,6 +1873,7 @@ dependencies = [ "js-sys", "log", "mime", + "mime_guess", "native-tls", "once_cell", "percent-encoding", @@ -2482,6 +2495,15 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.13" diff --git a/Cargo.toml b/Cargo.toml index e98a226..f14d4de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,9 +6,11 @@ edition = "2021" [dependencies] relm4 = "0.6.0" relm4-components = { version = "0.6.0", features = ["web"] } -reqwest = { version = "0.11.16", features = ["json", "blocking"] } +reqwest = { version = "0.11.16", features = ["json", "blocking", "multipart"] } serde = { version = "1.0.160", features = ["derive"] } serde_json = "1.0.96" lemmy_api_common = { git = "https://github.com/Bnyro/lemmy.git", rev = "ff69cd151988fa4cff5a6f3789c5675a2e3257f8" } markdown = "0.3.0" html2pango = "0.5.0" +rand = "0.8.5" +mime_guess = "2.0.4" diff --git a/src/api/image.rs b/src/api/image.rs new file mode 100644 index 0000000..4cd726c --- /dev/null +++ b/src/api/image.rs @@ -0,0 +1,47 @@ +use reqwest::blocking::multipart::Part; +use std::io::Read; +use std::fs::File; +use crate::settings; +use serde::Deserialize; +use rand::distributions::{Alphanumeric, DistString}; + +use super::CLIENT; + +#[derive(Deserialize)] +pub struct UploadImageResponse { + #[allow(dead_code)] + msg: String, + files: Vec, +} + +#[derive(Deserialize)] +struct UploadImageFile { + pub file: String, + #[allow(dead_code)] + pub delete_token: String, +} + +pub fn upload_image( + image: std::path::PathBuf, +) -> Result { + let mime_type = mime_guess::from_path(image.clone()).first(); + let file_name = Alphanumeric.sample_string(&mut rand::thread_rng(), 16); + + let mut file = File::open(image).unwrap(); + let mut data = Vec::new(); + file.read_to_end(&mut data).unwrap(); + + let part = Part::bytes(data).file_name(file_name).mime_str(&mime_type.unwrap().essence_str())?; + let form = reqwest::blocking::multipart::Form::new().part("images[]", part); + let account = settings::get_current_account(); + let base_url = account.instance_url; + let path = format!("{}/pictrs/image", base_url); + let res: UploadImageResponse = CLIENT + .post(&path) + .header("cookie", format!("jwt={}", account.jwt.unwrap().to_string())) + .multipart(form) + .send()? + .json()?; + + Ok(format!("{}/pictrs/image/{}", base_url, res.files[0].file)) +} \ No newline at end of file diff --git a/src/api/mod.rs b/src/api/mod.rs index 803c84d..bb964fc 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -12,6 +12,7 @@ pub mod auth; pub mod moderation; pub mod comment; pub mod site; +pub mod image; static API_VERSION: &str = "v3"; diff --git a/src/dialogs/editor.rs b/src/dialogs/editor.rs index 0710e71..ba8521e 100644 --- a/src/dialogs/editor.rs +++ b/src/dialogs/editor.rs @@ -1,6 +1,8 @@ -use relm4::prelude::*; +use relm4::{prelude::*, gtk::{ResponseType, FileFilter}}; use gtk::prelude::*; +use crate::api; + #[derive(Debug, Clone, Default)] pub struct EditorData { pub name: String, @@ -17,7 +19,8 @@ pub struct EditorDialog { url_buffer: gtk::EntryBuffer, body_buffer: gtk::TextBuffer, // Optional field to temporarily store the post or comment id - id: Option + id: Option, + window: gtk::Window } #[derive(Debug, Clone, Copy)] @@ -32,7 +35,10 @@ pub enum DialogMsg { Hide, UpdateType(EditorType, bool), UpdateData(EditorData), - Okay + Okay, + ChooseImage, + UploadImage(std::path::PathBuf), + AppendBody(String) } #[derive(Debug)] @@ -108,7 +114,15 @@ impl SimpleComponent for EditorDialog { }, gtk::Box { set_orientation: gtk::Orientation::Horizontal, - set_halign: gtk::Align::End, + set_hexpand: true, + gtk::Button { + set_label: "Upload image", + set_margin_end: 10, + connect_clicked => DialogMsg::ChooseImage, + }, + gtk::Box { + set_hexpand: true, + }, gtk::Button { set_label: "Cancel", set_margin_end: 10, @@ -136,7 +150,8 @@ impl SimpleComponent for EditorDialog { let name_buffer = gtk::EntryBuffer::builder().build(); let url_buffer = gtk::EntryBuffer::builder().build(); let body_buffer = gtk::TextBuffer::builder().build(); - let model = EditorDialog { type_: init, visible: false, is_new: true, name_buffer, url_buffer, body_buffer, id: None }; + let window = root.toplevel_window().unwrap(); + let model = EditorDialog { type_: init, visible: false, is_new: true, name_buffer, url_buffer, body_buffer, id: None, window }; let widgets = view_output!(); ComponentParts { model, widgets } } @@ -173,6 +188,37 @@ impl SimpleComponent for EditorDialog { if let Some(url) = data.url { self.url_buffer.set_text(url.to_string()); } self.body_buffer.set_text(&data.body.clone()); } + DialogMsg::ChooseImage => { + let buttons = [("_Cancel", ResponseType::Cancel), ("_Okay", ResponseType::Accept)]; + let dialog = gtk::FileChooserDialog::new(Some("Upload image"), None::<>k::ApplicationWindow>, gtk::FileChooserAction::Open, &buttons); + dialog.set_transient_for(Some(&self.window)); + let image_filter = FileFilter::new(); + image_filter.add_pattern("image/*"); + dialog.add_filter(&image_filter); + dialog.run_async(move |dialog, result | { + match result { + ResponseType::Accept => { + let path = dialog.file().unwrap().path(); + sender.input(DialogMsg::UploadImage(path.unwrap())) + }, + _ => dialog.hide(), + } + dialog.destroy(); + }); + } + DialogMsg::UploadImage(path) => { + std::thread::spawn(move || { + if let Ok(image_path) = api::image::upload_image(path) { + let new_text = format!("![]({})", image_path); + sender.input(DialogMsg::AppendBody(new_text)); + } + }); + } + DialogMsg::AppendBody(new_text) => { + let (start, end) = &self.body_buffer.bounds(); + let body = self.body_buffer.text(start, end, true).to_string(); + self.body_buffer.set_text(&format!("{}\n{}", body, new_text)); + } } } } \ No newline at end of file