use super::*;
#[cfg(feature = "ticket")]
use crate::{ticket, Locale};
#[cfg(feature = "html")]
use hconf::CONVERT_PATH_TO_STR_ERR;
use mkutil::tempfile::NamedTempFile;
#[cfg(all(feature = "image_processing", feature = "gui"))]
use mkutil::clipboard;
#[cfg(feature = "html")]
use html_builder::{Buffer, Html5};
#[cfg(feature = "html")]
use std::fmt::Write;
pub(crate) const NULL_COMPOSER_ERR: &str = "Composer is null";
#[derive(Debug, Clone, Default)]
pub struct DiscardableComposer(pub(crate) Option<Composer>);
impl DiscardableComposer {
pub fn empty() -> Self {
Self(None)
}
pub fn is_some(&self) -> bool {
self.0.is_some()
}
pub fn is_none(&self) -> bool {
self.0.is_none()
}
pub fn new(tagline: &str) -> Self {
Self(Some(Composer::with_tagline(tagline)))
}
pub fn from_note(note: &Note, tagline: &str) -> Self {
let composer: Composer = note.into();
Self(Some(composer.tagline(tagline)))
}
pub fn inner(&self) -> AnyResult<&Composer> {
Ok(self.0.as_ref().context(NULL_COMPOSER_ERR)?)
}
pub fn inner_as_mut(&mut self) -> AnyResult<&mut Composer> {
Ok(self.0.as_mut().context(NULL_COMPOSER_ERR)?)
}
pub fn inner_as_ref_unwrap(&self) -> &Composer {
self.0.as_ref().expect(NULL_COMPOSER_ERR)
}
pub fn inner_as_mut_unwrap(&mut self) -> &mut Composer {
self.0.as_mut().expect(NULL_COMPOSER_ERR)
}
#[cfg(feature = "gui")]
pub fn draft_unwrap_with_discard_ui(&mut self, ui: &mut egui::Ui, idx: usize) {
self.inner_as_mut_unwrap()
.draft_text_and_images_ui(ui, format!("composer{}", idx));
if ui
.button(format!("🗑 Remove #{}", idx + 1))
.on_hover_text(
RichText::new("Delete current draft for this subticket").color(Color32::RED),
)
.clicked()
{
self.inner_as_mut_unwrap().clear_all();
self.0.take();
};
}
}
#[derive(Debug, Default)]
pub struct Composer {
tagline: String,
text: String,
pasted_images: Vec<UserImage<NamedTempFile>>,
picked_images: Vec<UserImage<PathBuf>>,
}
impl std::clone::Clone for Composer {
fn clone(&self) -> Self {
Self {
tagline: self.tagline.clone(),
text: self.text.clone(),
pasted_images: vec![],
picked_images: vec![],
}
}
}
impl Composer {
pub(crate) fn with_tagline(tagline: &str) -> Self {
Self {
tagline: tagline.to_owned(),
..Default::default()
}
}
fn tagline(mut self, tagline: &str) -> Self {
self.tagline = tagline.to_owned();
self
}
fn contains_no_images(&self) -> bool {
self.picked_images.is_empty() && self.pasted_images.is_empty()
}
pub fn is_empty(&self) -> bool {
self.text.is_empty() && self.contains_no_images()
}
#[cfg(feature = "image_processing")]
fn push_pasted_image(&mut self, pasted: NamedTempFile) {
self.pasted_images.push(UserImage::selecting(pasted));
}
#[cfg(feature = "image_processing")]
fn append_picked_images(&mut self, picked: Vec<PathBuf>) {
self.picked_images.append(
&mut picked
.into_iter()
.map(|p| UserImage::selecting(p))
.collect(),
)
}
#[cfg(debug_assertions)]
fn log_uploaded(&self) {
self.pasted_images
.iter()
.filter(|img| img.is_selected())
.for_each(|img| img.log_uploaded_unwrap());
self.picked_images
.iter()
.filter(|img| img.is_selected())
.for_each(|img| img.log_uploaded_unwrap());
}
fn clear_images(&mut self) {
self.pasted_images.clear();
self.picked_images.clear();
}
pub fn clear_all(&mut self) {
self.clear_images();
self.text.clear();
}
pub fn upload_images(
&mut self,
project: &Project,
asset: &ProductionAsset,
upload: impl Fn(&Path, &Project, &ProductionAsset) -> AnyResult<PathBuf>,
) -> ImageUploaded {
if self.contains_no_images() {
info!("Found no images in Composer to upload");
return ImageUploaded::NoTask;
};
let mut count: u8 = 0;
self.pasted_images
.iter_mut()
.filter(|img| img.is_selected())
.for_each(|img| {
img.uploaded_mut(
upload(img.selecting_as_ref_unwrap().path(), &project, &asset),
&mut count,
);
});
self.picked_images
.iter_mut()
.filter(|img| img.is_selected())
.for_each(|img| {
img.uploaded_mut(
upload(img.selecting_as_ref_unwrap().as_path(), &project, &asset),
&mut count,
);
});
#[cfg(debug_assertions)]
self.log_uploaded();
ImageUploaded::Success(count)
}
#[cfg(feature = "html")]
pub fn into_html_legacy(&self) -> AnyResult<String> {
let mut buf = Buffer::new();
let mut div = buf.div();
let mut p = div.p();
writeln!(p, "{}", self.text)?;
self.pasted_images
.iter()
.filter_map(|img| img.uploaded.as_ref())
.filter_map(|uploaded| uploaded.as_ref().ok())
.map(|img| img.to_str().expect(CONVERT_PATH_TO_STR_ERR).to_string())
.for_each(|path| {
p.img().attr(&format!("src=\"{}\"", path));
});
self.picked_images
.iter()
.filter_map(|img| img.uploaded.as_ref())
.filter_map(|uploaded| uploaded.as_ref().ok())
.map(|img| img.to_str().expect(CONVERT_PATH_TO_STR_ERR).to_string())
.for_each(|path| {
p.img().attr(&format!("src=\"{}\"", path));
});
Ok(buf.finish())
}
#[cfg(feature = "ticket")]
pub(crate) fn into_subticket_multilingual_note(
&self,
composing_locale: &Locale,
) -> AnyResult<ticket::SubTicketRawNote> {
let html = self.into_html_legacy()?;
let mut subticket_note = ticket::SubTicketRawNote::empty();
match composing_locale {
Locale::EN => {
subticket_note.en = Some(html);
}
Locale::VI => {
subticket_note.vi = Some(html);
}
Locale::ZH => {
subticket_note.zh = Some(html);
}
Locale::FR => {
subticket_note.fr = Some(html);
}
};
Ok(subticket_note)
}
}
#[cfg(feature = "gui")]
impl Composer {
fn show_selected_images(&mut self, ui: &mut egui::Ui, id_source: impl std::hash::Hash) {
if self.contains_no_images() {
return;
};
egui::Grid::new(id_source).show(ui, |ui| {
self.pasted_images
.iter_mut()
.enumerate()
.for_each(|(i, img)| {
img.hint_saved_ui(ui, i);
ui.end_row();
});
self.picked_images.iter_mut().for_each(|img| {
img.hint_selected_ui(ui);
ui.end_row();
});
});
}
fn show_upload_images_buttons(&mut self, ui: &mut egui::Ui) {
if ui
.button("⊗ Clear All")
.on_hover_text("Remove all saved clipboard-images and all chosen images for this draft")
.clicked()
{
self.clear_images();
};
#[cfg(feature = "image_processing")]
if ui
.button(if cfg!(target_os = "windows") {
RichText::new("📋 Pixels").weak()
} else {
RichText::new("📋 Paste")
})
.on_hover_text(if cfg!(target_os = "windows") {
"Paste image from clipboard (when using with Windows + Shift + S)"
} else {
"Paste image from clipboard"
})
.clicked()
{
if let Ok(img) = clipboard::save_temp_image(None) {
self.push_pasted_image(img);
};
};
#[cfg(feature = "image_processing")]
if ui
.button("📁 Image")
.on_hover_text("Browse to existing images")
.clicked()
{
if let Some(picked) = mkutil::dialog::pick_images_sync() {
self.append_picked_images(picked);
};
};
}
fn menu_ui(&mut self, ui: &mut egui::Ui, _easy_mark_help: bool) {
ui.horizontal(|ui| {
ui.label(&self.tagline);
ui.with_layout(Layout::right_to_left(Align::Min), |ui| {
#[cfg(feature = "easy_mark")]
if _easy_mark_help {
crate::locale::easy_mark_syntax_help(ui);
};
self.show_upload_images_buttons(ui);
});
});
}
pub fn draft_text_and_images_ui(&mut self, ui: &mut egui::Ui, id_source: impl std::hash::Hash) {
ui.vertical(|ui| {
self.menu_ui(ui, true);
ui.add(egui::TextEdit::multiline(&mut self.text).desired_width(f32::INFINITY));
self.show_selected_images(ui, id_source);
});
}
pub fn draft_images_only_ui(&mut self, ui: &mut egui::Ui, id_source: impl std::hash::Hash) {
ui.vertical(|ui| {
self.menu_ui(ui, false);
self.show_selected_images(ui, id_source);
});
}
}
impl From<&Note> for Composer {
fn from(note: &Note) -> Self {
Self {
text: note.text.trim().to_owned(), picked_images: match note.embed_layout.img_paths_as_ref() {
Some(paths) => paths
.iter()
.map(|p| UserImage::selecting(p.clone()))
.collect(),
None => vec![],
},
..Default::default()
}
}
}