simple_gallery/
lib.rs

1use rand::seq::SliceRandom;
2use rand::thread_rng;
3use serde::Serialize;
4use std::path::PathBuf;
5use walkdir::WalkDir;
6
7/// Local directory on-disk where images are stored.
8#[derive(Clone)]
9pub struct ImageDir {
10    /// The filepath of the image directory.
11    pub path: PathBuf,
12    /// The file extension of image files, for filtering directory contents.
13    pub file_extension: String,
14}
15
16impl ImageDir {
17    /// Walk filesystem directory and return a list of all files matching file extension.
18    pub fn find_images(&self) -> Vec<PathBuf> {
19        let mut img_files: Vec<PathBuf> = Vec::new();
20        for ent in WalkDir::new(self.path.as_os_str()).into_iter().flatten() {
21            let path = ent.path();
22            if path
23                .display()
24                .to_string()
25                .ends_with(format!(".{}", self.file_extension).as_str())
26            {
27                img_files.push(path.to_path_buf());
28            }
29        }
30        img_files
31    }
32
33    /// Return a single image filepath, as string, at random from the direcotry.
34    pub fn get_random_image(&self) -> PathBuf {
35        let mut imgs = self.find_images();
36        let mut rng = thread_rng();
37        imgs.shuffle(&mut rng);
38        imgs[0].clone()
39    }
40}
41
42/// Represents the CSS declarations for fade-in/fade-out transitions
43/// between images in the slideshow.
44#[derive(Serialize)]
45pub struct TransitionConfig {
46    pub imgs: Vec<PathBuf>,
47    pub title: String,
48    pub n_imgs: f32,
49    pub duration_per_image: usize,
50    pub transition_time: f32,
51    // Whether the images should be randomized in order.
52    pub shuffle: bool,
53    /// The subroute at which the images will be served, e.g. "static/"
54    pub static_route: String,
55}
56
57impl TransitionConfig {
58    /// From a list of images, generator all the animation values for CSS.
59    pub fn new(imgs: Vec<PathBuf>, title: String, transition_time: usize, shuffle: bool) -> TransitionConfig {
60        // Via https://www.devtwins.com/blog/css-cross-fading-images
61        // Define durations in seconds. We use floats so we can do math,
62        // and will recast as integers before injecting into CSS.
63        let n_imgs: f32 = imgs.len() as f32;
64        let duration_per_image: usize = transition_time;
65        let transition_time: f32 = 2.0;
66        // let animation_delay: f32 = duration_per_image + transition_time;
67        let static_route = String::from("static");
68        TransitionConfig {
69            imgs,
70            n_imgs,
71            title,
72            duration_per_image,
73            transition_time,
74            static_route,
75            shuffle,
76        }
77    }
78
79    /// Emit full in-line HTML for application. The img-src attributes are the only
80    /// external resources loaded.
81    pub fn generate_html(&self) -> anyhow::Result<String> {
82        // Load the Tera/Jinja template.
83        let html_template = include_str!("../files/index.html.j2");
84        let mut context = tera::Context::new();
85        let imgs: Vec<_> = self
86            .imgs
87            .iter()
88            .map(|i| {
89                format!(
90                    "{}/{}",
91                    self.static_route,
92                    i.file_name().unwrap_or_default().to_str().unwrap()
93                )
94            })
95            .collect();
96        context.insert("config", &self);
97        context.insert("imgs", &imgs);
98        context.insert("duration", &self.duration_per_image);
99        context.insert("shuffle_opt", &self.shuffle);
100        Ok(tera::Tera::one_off(html_template, &context, false)?)
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    #[test]
108    fn basic_math() {
109        let mut imgs = Vec::new();
110        imgs.push(PathBuf::from("img/foo1.png"));
111        imgs.push(PathBuf::from("img/foo2.png"));
112        imgs.push(PathBuf::from("img/foo3.png"));
113        imgs.push(PathBuf::from("img/foo4.png"));
114        let c = TransitionConfig::new(imgs, "foo".to_string(), 5);
115        assert!(c.n_imgs == 4.0);
116    }
117
118    #[test]
119    fn create_image_dir() {
120        let d = String::from("img");
121        let _i = ImageDir {
122            path: d.into(),
123            file_extension: String::from("png"),
124        };
125    }
126}