use super::*;
#[cfg(all(feature = "image_processing", feature = "gui"))]
use crate::pixel;
use mkutil::finder;
use std::io::{Error, ErrorKind};
const MAX_THUMB_SIZE_BYTES: u64 = 307200;
#[async_trait]
pub trait ThumbStore: DynClone + fmt::Debug + Send + Sync {
async fn thumbnail(&self, project: &Project) -> Result<ProjThumb, DatabaseError>;
async fn upload(&self, project: &Project, img: &ProjThumb) -> Result<(), ModificationError>;
async fn delete(&self, project: &Project, by: Option<Staff>) -> Result<(), ModificationError>;
}
dyn_clone::clone_trait_object!(ThumbStore);
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct ProjThumb {
#[serde(skip)]
ext: ThumbExt,
#[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
id: Option<ObjectId>,
#[serde(with = "serde_bytes", rename = "thumbnail")]
img_bytes: Option<Vec<u8>>,
#[serde(skip_serializing_if = "Option::is_none")]
last_modified_by: Option<String>,
}
impl BsonId for ProjThumb {
fn bson_id_as_ref(&self) -> Option<&ObjectId> {
self.id.as_ref()
}
fn bson_id(&self) -> AnyResult<&ObjectId> {
self.id.as_ref().context("ProjThumb without BSON ObjectId")
}
}
impl ProjThumb {
pub fn empty() -> Self {
Self {
ext: ThumbExt::empty(),
..Default::default()
}
}
pub fn modified_by(mut self, user: Staff) -> Self {
self.last_modified_by = Some(user.name_owned());
self
}
pub fn modified_mut(&mut self, by: Option<Staff>) {
self.last_modified_by = by.map(|s| s.name_owned());
}
pub fn error(err: anyhow::Error) -> Self {
let mut t = Self::empty();
t.ext.error_mut(err);
t
}
pub fn has_replacement(&self) -> bool {
self.ext.replacement.is_some()
}
pub fn replacement_as_ref_unwrap(&self) -> &PathBuf {
self.ext.replacement.as_ref().unwrap()
}
pub fn has_parsed_image(&self) -> bool {
self.ext.parsed.is_ok()
}
pub fn parsed_image_as_ref(&self) -> Result<&RetainedImage, &anyhow::Error> {
self.ext.parsed.as_ref()
}
pub fn bytes_as_ref_unwrap(&self) -> &[u8] {
self.img_bytes.as_ref().unwrap()
}
pub fn with_bytes(mut self, bytes: Vec<u8>) -> Self {
self.img_bytes = Some(bytes);
self
}
#[cfg(all(feature = "image_processing", feature = "gui"))]
pub fn retained_img_mut(mut self) -> Self {
if let Some(bytes) = &self.img_bytes {
self.ext.retained_image_mut(bytes);
};
self
}
pub fn img_bytes_mut(&mut self, bytes: Option<Vec<u8>>) {
self.img_bytes = bytes;
}
pub fn check_replacement_size(&self) -> std::io::Result<()> {
self.ext.check_replacement_size()
}
pub fn img_bytes_mut_from_draft(&mut self) -> std::io::Result<()> {
self.img_bytes_mut(Some(self.ext.img_bytes_from_draft()?));
Ok(())
}
#[cfg(feature = "gui")]
pub fn read_mode_ui_with_size(&mut self, ui: &mut egui::Ui, size: impl Into<egui::Vec2>) {
self.ext.read_mode_ui(ui, size);
}
#[cfg(feature = "gui")]
pub fn write_suggest_unlocked_mut_ui(&mut self, ui: &mut egui::Ui, unlocked: &mut bool) {
self.ext.write_suggest_ui(ui, unlocked);
}
}
impl ReadWriteSuggest for ProjThumb {
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 write_compose_ui(&mut self, ui: &mut egui::Ui) {
self.ext.write_compose_ui(ui);
}
}
struct ThumbExt {
mode: MediaMode,
parsed: AnyResult<RetainedImage>,
replacement: Option<PathBuf>,
}
impl ThumbExt {
fn empty() -> Self {
Self {
mode: MediaMode::default(),
parsed: Err(DatabaseError::Uninitialized.into()),
replacement: None,
}
}
fn error_mut(&mut self, err: anyhow::Error) {
self.parsed = Err(err);
}
#[cfg(all(feature = "image_processing", feature = "gui"))]
fn retained_image_mut(&mut self, bytes: &[u8]) {
self.parsed = pixel::retained_image_from_bytes(bytes);
}
fn check_replacement_size(&self) -> std::io::Result<()> {
if self.replacement.is_none() {
return Err(Error::new(
ErrorKind::NotFound,
"No replacement image was chosen",
));
};
let img = self.replacement.as_ref().unwrap();
match finder::file_size(img)?.cmp(&MAX_THUMB_SIZE_BYTES) {
Ordering::Greater => {
return Err(Error::new(
ErrorKind::Other,
format!(
"File size too large. Only images under {}KB are allowed.",
MAX_THUMB_SIZE_BYTES / 1024,
),
));
}
_ => Ok(()),
}
}
fn img_bytes_from_draft(&self) -> std::io::Result<Vec<u8>> {
Ok(finder::read_file_bytes(self.replacement.as_ref().unwrap())?)
}
#[cfg(all(feature = "image_processing", feature = "gui"))]
fn read_mode_ui(&self, ui: &mut egui::Ui, size: impl Into<egui::Vec2>) {
match &self.parsed {
Ok(thumb) => {
ui.image(thumb.texture_id(ui.ctx()), size);
}
Err(e) => {
ui.image(
IconCel::icon_book()
.get_included(embedded_icons::NO_PROJ_THUMB)
.texture_id(ui.ctx()),
size,
);
ui.colored_label(Color32::LIGHT_RED, "No project thumbnail")
.on_hover_text(e.to_string());
}
}
}
#[cfg(feature = "gui")]
fn write_suggest_ui(&mut self, ui: &mut egui::Ui, unlocked: &mut bool) {
ui.horizontal(|ui| {
if ui.button("❌ Cancel").clicked() {
*unlocked = false;
};
if ui.button("📁 Browse").clicked() {
use mkutil::dialog;
self.replacement = dialog::pick_one_image();
};
});
}
#[cfg(feature = "gui")]
fn write_compose_ui(&mut self, ui: &mut egui::Ui) {
match &self.replacement {
Some(path) => {
ui.label(
RichText::new(format!(
"Will replace with: {:?}",
path.file_name().unwrap_or_default()
))
.color(Color32::YELLOW),
)
.on_hover_text(format!("{}", path.display()));
}
None => {
ui.label(RichText::new("No replacement chosen").strong());
}
};
}
}
impl fmt::Debug for ThumbExt {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ThumbExt")
.field("mode", &self.mode)
.field("replacement", &self.replacement)
.finish()
}
}
impl std::clone::Clone for ThumbExt {
fn clone(&self) -> Self {
Self {
mode: self.mode.clone(),
parsed: Err(anyhow!("Unable to clone RetainedImage")),
replacement: self.replacement.clone(),
}
}
}
impl Default for ThumbExt {
fn default() -> Self {
Self::empty()
}
}