use super::*;
#[cfg(feature = "gui")]
use crate::link::*;
#[cfg(all(feature = "image_processing", feature = "gui"))]
use crate::ImageSpec;
use crossbeam_channel::Sender;
use glob::Paths;
use mkutil::finder::*;
use std::ffi::OsStr;
#[cfg(feature = "image_processing")]
const MAX_READ_IMAGES_TIMES: u8 = 3;
const SEND_ERR: &str = "Failed to send InPlaceSeqDisplay via channel";
#[derive(Debug, Clone)]
#[cfg(feature = "image_processing")]
pub(super) struct ImagesScrub {
typ: SequenceType,
#[cfg(all(feature = "image_processing", feature = "gui"))]
pub(super) img_specs: Option<Vec<Option<ImageSpec>>>,
pos: usize,
}
#[cfg(feature = "image_processing")]
impl Default for ImagesScrub {
fn default() -> Self {
Self {
typ: Default::default(),
#[cfg(all(feature = "image_processing", feature = "gui"))]
img_specs: None,
pos: 1,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct InPlaceSequence {
name: String,
location_dir: Option<PathBuf>,
pub(super) img_paths: Option<Vec<PathBuf>>,
#[cfg(feature = "image_processing")]
pub(super) scrub: ImagesScrub,
times_processed: u8,
}
impl PartialEq for InPlaceSequence {
fn eq(&self, other: &Self) -> bool {
self.img_paths == other.img_paths
}
}
impl InPlaceSequence {
fn unglobbed<P>(parent_dir: PathBuf, name: String, intermediate_dirs: Option<P>) -> Self
where
P: AsRef<Path>,
{
Self {
location_dir: match intermediate_dirs {
Some(intermediate) => Some(parent_dir.join(intermediate)),
None => Some(parent_dir),
},
name,
..Default::default()
}
}
fn with_img_paths(mut self, img_paths: Option<Vec<PathBuf>>) -> Self {
self.img_paths = img_paths;
self
}
#[cfg(feature = "image_processing")]
fn sequence_type(mut self, typ: Option<&SequenceType>) -> Self {
if let Some(typ) = typ {
self.scrub.typ = typ.clone();
};
self
}
fn read_location_dir(&mut self) {
self.times_processed += 1;
if self.img_paths.is_some() {
return;
};
if let Some(location_dir) = &self.location_dir {
self.img_paths = list_images(location_dir, true).ok();
};
}
fn drain_all_except_first_image(&mut self) {
self.read_location_dir();
if let Some(images) = self.img_paths.as_mut() {
if let Ordering::Greater = images.len().cmp(&1) {
images.drain(1..);
};
};
}
#[cfg(all(feature = "image_processing", feature = "gui"))]
pub(super) fn img_specs_mut(&mut self) {
self.scrub.img_specs = self.img_paths.as_ref().map(|paths| {
paths
.iter()
.map(|i| ImageSpec::from_retained_image(i))
.collect()
});
}
#[cfg(all(feature = "image_processing", feature = "gui"))]
fn img_specs_mut_from_location_dir(&mut self) {
self.read_location_dir();
self.img_specs_mut();
}
}
#[cfg(feature = "gui")]
impl InPlaceSequence {
#[cfg(feature = "image_processing")]
fn show_make_texture_ids_button(&mut self, ui: &mut egui::Ui) {
if ui
.button("🔳 Read Images")
.on_hover_text(format!("Load images for {}", self.name))
.clicked()
{
self.img_specs_mut_from_location_dir();
};
}
fn show_peek_button(&mut self, ui: &mut egui::Ui) {
let needs_peeking = self.img_paths.is_none();
if ui
.add_enabled(needs_peeking, egui::Button::new("👓 Peek"))
.on_hover_text("Prepare for external image player")
.clicked()
{
self.drain_all_except_first_image();
};
}
#[cfg(feature = "image_processing")]
pub(super) fn show_img_embed_and_pos_slider(
&mut self,
ui: &mut egui::Ui,
thumb_width: f32,
) -> Option<egui::Response> {
let Self {
location_dir: _,
name: _,
img_paths,
scrub,
times_processed,
} = self;
match scrub.img_specs.as_mut() {
None => None,
Some(specs) => {
let path_spec_pairs: Vec<(&PathBuf, &ImageSpec)> = img_paths
.as_ref()
.unwrap()
.iter()
.zip(specs.iter())
.filter(|(_, spec)| spec.is_some())
.map(|(path, spec)| (path, spec.as_ref().unwrap()))
.collect();
let seq_len = path_spec_pairs.len();
match seq_len.cmp(&0) {
Ordering::Greater => {
let mut response: Option<egui::Response> =
if let Ordering::Greater = seq_len.cmp(&1) {
Some(
ui.add(egui::Slider::new(&mut scrub.pos, 1..=seq_len)
.prefix(scrub.typ.as_ref())
.suffix(format!("/{}", seq_len))
.integer())
)
} else {
None
};
if let Some((path, img)) = path_spec_pairs.get(scrub.pos - 1) {
let img_response =
ui.add(ImageLink::with_retained_image_actual_height(
ui.ctx(),
img.inner(),
Some(path),
thumb_width,
));
if response.is_none() {
response = Some(img_response);
};
};
response
}
_ => Some(show_loading_hint(ui, times_processed)),
}
}
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum InPlaceSeqDisplay {
Embedded(InPlaceSequence),
NotEmbedded(InPlaceSequence),
}
impl InPlaceSeqDisplay {
pub(super) fn inner(&self) -> &InPlaceSequence {
match self {
Self::Embedded(inner) | Self::NotEmbedded(inner) => inner,
}
}
pub(super) fn inner_mut(&mut self) -> &mut InPlaceSequence {
match self {
Self::Embedded(inner) | Self::NotEmbedded(inner) => inner,
}
}
pub(super) fn with_img_paths(self, img_paths: Option<Vec<PathBuf>>) -> Self {
match self {
Self::Embedded(inner) => Self::Embedded(inner.with_img_paths(img_paths)),
Self::NotEmbedded(inner) => Self::NotEmbedded(inner.with_img_paths(img_paths)),
}
}
fn name(&self) -> &String {
&self.inner().name
}
pub fn glob_over_paths(
paths: Paths,
intermediate_dirs: Option<PathBuf>,
should_embed: bool,
_typ: Option<SequenceType>,
tx: Sender<GlobbedSequences>,
) {
std::thread::spawn(move || {
let mut sequences: Vec<Self> = vec![];
let dir_paths = paths
.into_iter()
.filter_map(|p| p.ok())
.filter(|p| p.is_dir())
.filter(|p| {
p.read_dir()
.expect(&format!("Failed to read dir: {}", p.display()))
.next()
.is_some()
});
for p in dir_paths {
let name = p
.file_name()
.unwrap_or(OsStr::new("😷 broken folder name"))
.to_string_lossy()
.to_string();
#[allow(unused_mut)]
let mut seq = InPlaceSequence::unglobbed(p, name, intermediate_dirs.as_ref());
#[cfg(feature = "image_processing")]
{
seq = seq.sequence_type(_typ.as_ref());
}
let seq = match should_embed {
true => InPlaceSeqDisplay::Embedded(seq),
false => InPlaceSeqDisplay::NotEmbedded(seq),
};
sequences.push(seq);
}
match sequences.is_empty() {
false => {
if let Err(e) = tx.send(GlobbedSequences::ok(sequences)) {
error!("{}: {}", SEND_ERR, e);
};
}
true => {
if let Err(e) = tx.send(GlobbedSequences::empty()) {
error!("{}: {}", SEND_ERR, e);
};
}
}
});
}
}
#[cfg(feature = "gui")]
impl InPlaceSeqDisplay {
pub fn show_load_image_buttons(&mut self, ui: &mut egui::Ui) -> Option<egui::Response> {
ui.horizontal(|ui| {
match self {
Self::Embedded(_seq) => {
#[cfg(feature = "image_processing")]
_seq.show_make_texture_ids_button(ui);
}
Self::NotEmbedded(seq) => {
seq.show_peek_button(ui);
}
};
let external_player_response = self.inner().img_paths.as_ref().map(|paths| {
ui.add(TurntableLink::with_player("🎥 djv", || {
if let Err(e) = open_djv(paths) {
error!("Failed to open with DJV: {}", e);
};
}))
});
if let Some(location_dir) = &self.inner().location_dir {
ui.with_layout(Layout::right_to_left(Align::Max), |ui| {
ui.add(FinderLink::with_url(Some("📁"), location_dir));
});
};
external_player_response
})
.inner
}
fn ui(&mut self, ui: &mut egui::Ui, _thumb_width: f32) -> Option<egui::Response> {
let external_player_response = self.show_load_image_buttons(ui);
match self {
InPlaceSeqDisplay::NotEmbedded(_) => {
external_player_response
}
InPlaceSeqDisplay::Embedded(_) => {
#[cfg(feature = "image_processing")]
return {
self.inner_mut()
.show_img_embed_and_pos_slider(ui, _thumb_width)
};
#[cfg(not(feature = "image_processing"))]
None
}
}
}
pub fn collapsing_header_ui(
&mut self,
ui: &mut egui::Ui,
thumb_width: f32,
) -> Option<Option<egui::Response>> {
egui::CollapsingHeader::new(self.name())
.default_open(false)
.show(ui, |ui| self.ui(ui, thumb_width))
.body_returned
}
}
#[derive(Debug)]
pub struct GlobbedSequences(pub Result<Vec<InPlaceSeqDisplay>, String>);
impl GlobbedSequences {
pub fn uninitialized() -> Self {
Self(Err("⚠ Uninitialized".to_owned()))
}
fn ok(sequences: Vec<InPlaceSeqDisplay>) -> Self {
Self(Ok(sequences))
}
fn empty() -> Self {
Self(Err(
"Folder is empty, or inaccessible, or doesn't exist".to_owned()
))
}
}
impl From<&anyhow::Error> for GlobbedSequences {
fn from(error: &anyhow::Error) -> Self {
Self(Err(format!("{}", error)))
}
}
#[allow(dead_code)]
fn open_djv(sequence: &[PathBuf]) -> AnyResult<()> {
let cfg = ClientCfgCel::path_config().as_ref()?;
if !sequence.is_empty() {
let seq_start = sequence.get(0).context("Failed to get first image path")?;
try_execute_file(&cfg.exe().djv(), seq_start).map_err(|e| anyhow!("{}", e))
} else {
Err(anyhow!("Image sequence is empty"))
}
}
#[cfg(all(feature = "image_processing", feature = "gui"))]
fn show_loading_hint(ui: &mut egui::Ui, times_processed: &u8) -> egui::Response {
ui.monospace(match times_processed.cmp(&MAX_READ_IMAGES_TIMES) {
Ordering::Less => {
"Unable to load images"
}
_ => "😷 Folder contains no images",
})
}