hgame 0.26.4

CG production management structs, e.g. of assets, personnels, progress, etc.
Documentation
use super::*;
#[cfg(all(feature = "ticket", feature = "gui"))]
use crate::ticket::TicketAction;

#[cfg(feature = "html")]
use mkutil::html_scraping::PlaceImageHint;

#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "mongo", derive(Serialize, Deserialize,))]
/// LEGACY DESIGN: DO NOT change field names or `serde(rename)` values.
pub struct Reply {
    #[cfg_attr(feature = "mongo", serde(skip))]
    ext: ReplyExpand,

    #[cfg(feature = "html")]
    #[cfg_attr(feature = "mongo", serde(rename = "content",))]
    raw_html: String,

    sender: String,

    #[cfg_attr(
        feature = "mongo",
        serde(
            rename = "datetime",
            with = "bson::serde_helpers::chrono_datetime_as_bson_datetime"
        )
    )]
    pub created_at: DateTime<Utc>,
}

impl Reply {
    pub fn empty() -> Self {
        Self {
            ext: Default::default(),
            #[cfg(feature = "html")]
            raw_html: String::new(),
            sender: String::new(),
            created_at: Utc::now(),
        }
    }

    #[cfg(feature = "html")]
    /// Moves the image paths scraped from HTML into `Note::embed_layout`.
    fn notes_with_image_array(mut self, img_hint: &PlaceImageHint) -> Self {
        self.ext.note = Note::default().with_html(&self.raw_html, img_hint);
        self
    }

    #[cfg(all(feature = "html", feature = "gui"))]
    /// Moves the image paths scraped from HTML into `Note::embed_layout`.
    fn notes_with_inplace_embed(mut self, img_hint: &PlaceImageHint) -> Self {
        self.ext.note = Note::inplace_embed().with_html(&self.raw_html, img_hint);
        self
    }

    fn sent_by_self(mut self, current_username: &str) -> Self {
        self.ext.sent_by_self = current_username == self.sender;
        self
    }

    fn created_at_local(mut self, chrono_fmt: &str) -> Self {
        self.ext = self.ext.created_at_local(self.created_at, chrono_fmt);
        self
    }

    fn created_now(mut self) -> Self {
        self.created_at = Utc::now();
        self
    }

    fn sender(mut self, sender: String) -> Self {
        self.sender = sender;
        self
    }

    #[cfg(feature = "html")]
    fn note_from_composer(mut self) -> AnyResult<Self> {
        self.raw_html = self.ext.composer.inner()?.into_html_legacy()?;
        Ok(self)
    }

    fn expand(mut self, ext: ReplyExpand) -> Self {
        self.ext = ext;
        self
    }

    pub fn start_editing(&mut self) {
        self.ext.start_editing()
    }

    pub fn note_as_mut(&mut self) -> &mut Note {
        &mut self.ext.note
    }

    /// This resets the `Self::ext` to default.
    pub fn clone_with_composer(&mut self) -> Self {
        let ext = std::mem::take(&mut self.ext);
        self.clone().expand(ext)
    }

    pub fn composer_as_ref_unwrap(&self) -> &Composer {
        self.ext.composer.inner_as_ref_unwrap()
    }

    pub fn composer_as_mut_unwrap(&mut self) -> &mut Composer {
        self.ext.composer.inner_as_mut_unwrap()
    }

    pub fn upload_images(
        &mut self,
        project: &Project,
        asset: &ProductionAsset,
        upload: impl Fn(&Path, &Project, &ProductionAsset) -> AnyResult<PathBuf>,
    ) -> ImageUploaded {
        self.ext
            .composer
            .inner_as_mut_unwrap()
            .upload_images(project, asset, upload)
    }

    pub fn texture_ids_mut(&mut self) {
        self.ext.note.texture_ids_mut();
    }

    pub fn send_by_self(&self) -> bool {
        self.ext.sent_by_self
    }
}

#[cfg(feature = "gui")]
impl Reply {
    pub fn sender_ui(&self, ui: &mut egui::Ui) {
        if self.ext.sent_by_self {
            ui.label(format!("{} (me)", self.sender));
        } else {
            ui.label(&self.sender);
        };
    }

    pub fn created_at_local_ui(&self, ui: &mut egui::Ui) {
        ui.small(&self.ext.created_at_local);
    }

    #[cfg(feature = "ticket")]
    /// Button to submit the `Reply` edits.
    pub fn show_dt_submit_edit_unwrap_buttons(
        &mut self,
        ui: &mut egui::Ui,
        subticket: Option<&ObjectId>,
        tx: &Sender<TicketAction>,
    ) {
        use crate::ticket::MODIFIER_LAZY_HINT;

        ui.horizontal(|ui| {
            ui.with_layout(Layout::right_to_left(Align::Max), |ui| {
                if ui.button("❌ Cancel").clicked() {
                    self.mode_mut(MediaMode::Read);
                };

                if ui
                    .add_enabled(
                        !self.ext.composer.0.as_ref().unwrap().is_empty(),
                        egui::Button::new("✔ Submit Edits"),
                    )
                    .on_hover_text(MODIFIER_LAZY_HINT)
                    .clicked()
                {
                    let _ = tx.send(TicketAction::EditReply {
                        subticket: subticket.cloned(),
                        existing: None,
                        // must move `Composer` from `Self::ext` to the cloned `Subticket`
                        updated: self.clone_with_composer(),
                        lazy: ui.ctx().input(|i| i.modifiers.alt),
                    });
                }
            });
        });
    }
}

impl ReadWriteSuggest for Reply {
    fn write_suggest() -> Self {
        Self::empty().with_mode(MediaMode::WriteSuggest)
    }

    fn with_mode(mut self, mode: MediaMode) -> Self {
        self.mode_mut(mode);
        self
    }

    fn mode(&self) -> &MediaMode {
        &self.ext.mode
    }

    fn mode_mut(&mut self, mode: MediaMode) {
        self.ext.mode = mode;
    }

    #[cfg(feature = "gui")]
    fn read_mode_ui(&mut self, ui: &mut egui::Ui) {
        ui.colored_label(Color32::TEMPORARY_COLOR, "Raw HTML")
            .on_hover_text(format!("{:?}", self.raw_html));
    }

    #[cfg(feature = "gui")]
    /// Just an "Add" button that allows changing `MediaMode`.
    fn write_suggest_ui(&mut self, ui: &mut egui::Ui) {
        // no longer `vertical_centered` as we'll be putting it to a series of buttons
        if ui
            .button("➕ New Reply")
            .on_hover_text("Add a new reply")
            .clicked()
        {
            if self.ext.composer.is_none() {
                // inits the `Composer` once
                self.ext.start_new_composing();
            } else {
                // no need to init the `Composer` again
                self.mode_mut(MediaMode::WriteCompose);
            };
        };
    }

    // fn write_compose_ui(&mut self, _ui: &mut egui::Ui) {}
}

// ----------------------------------------------------------------------------
#[derive(Debug, Clone, Default)]
struct ReplyExpand {
    mode: MediaMode,

    sent_by_self: bool,

    /// Formatted to local time.
    created_at_local: String,

    /// Contents scraped from the HTML to be displayed in `MediaMode::Read`.
    note: Note,

    /// For creating a new `Reply`.
    composer: DiscardableComposer,
}

impl ReplyExpand {
    fn start_new_composing(&mut self) {
        // replies don't have multi-lingual support
        self.composer = DiscardableComposer::new("Write your reply:");
        self.mode = MediaMode::WriteCompose;
    }

    fn start_editing(&mut self) {
        self.composer = DiscardableComposer::from_note(&self.note, "Edit reply:");
        self.mode = MediaMode::WriteEdit;
    }

    fn created_at_local(mut self, created_at: DateTime<Utc>, chrono_fmt: &str) -> Self {
        let local_time: DateTime<Local> = DateTime::from(created_at);
        self.created_at_local = local_time.format(&chrono_fmt).to_string();
        self
    }
}

// -------------------------------------------------------------------------------
#[cfg(all(feature = "html", feature = "gui"))]
#[derive(Debug)]
pub struct ReplyReadBuilder(Reply);

#[cfg(all(feature = "html", feature = "gui"))]
impl ReplyReadBuilder {
    pub fn new(reply: Reply) -> Self {
        Self(reply)
    }

    pub fn finish(
        self,
        current_username: &str,
        chrono_fmt: &str,
        layout: &EmbedLayoutPref,
        img_hint: &PlaceImageHint,
    ) -> Reply {
        match layout {
            EmbedLayoutPref::RowOrColumn => self.0.notes_with_image_array(img_hint),
            EmbedLayoutPref::Carousel => self.0.notes_with_inplace_embed(img_hint),
        }
        .sent_by_self(current_username)
        .created_at_local(chrono_fmt)
    }
}

// -------------------------------------------------------------------------------
#[cfg(feature = "html")]
pub struct ReplyWriteBuilder(Reply);

#[cfg(feature = "html")]
impl ReplyWriteBuilder {
    pub fn new(reply: Reply) -> Self {
        Self(reply)
    }

    pub fn finish(self, sender: String, is_editing: bool) -> AnyResult<Reply> {
        let reply = self.0.sender(sender).note_from_composer()?;
        if is_editing {
            // retains the `Reply::created_at` value so that the discussion order stays the same
            Ok(reply)
        } else {
            Ok(reply.created_now())
        }
    }
}