Browse Source

big refactor

Ondřej Hruška 3 months ago
parent
commit
8fb340395f
Signed by: Ondřej Hruška <ondra@ondrovo.com> GPG key ID: 2C5FD5035250423D

+ 1 - 0
Cargo.lock View File

@@ -82,6 +82,7 @@ dependencies = [
82 82
  "base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)",
83 83
  "blake2 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
84 84
  "chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
85
+ "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
85 86
  "image 0.21.0 (registry+https://github.com/rust-lang/crates.io-index)",
86 87
  "image-utils 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
87 88
  "markdown 0.2.0 (git+https://github.com/johannhof/markdown.rs)",

+ 2 - 0
Cargo.toml View File

@@ -13,3 +13,5 @@ image-utils = "0.2.0"
13 13
 image = "0.21.0"
14 14
 blake2 = "0.8.0"
15 15
 base64 = "0.10.1"
16
+failure = "0.1.5"
17
+

+ 6 - 5
src/hash_dict.rs View File

@@ -1,8 +1,8 @@
1 1
 use std::collections::HashMap;
2 2
 use std::fs::File;
3
+use std::io;
3 4
 use std::io::Read;
4 5
 use std::io::Write;
5
-use std::io;
6 6
 use std::path::PathBuf;
7 7
 
8 8
 // file-stored hash map used to prevent needless image regenerating
@@ -14,7 +14,7 @@ pub struct HashDict {
14 14
 }
15 15
 
16 16
 impl HashDict {
17
-    pub fn load(path: PathBuf) -> Result<HashDict, io::Error>  {
17
+    pub fn load(path: PathBuf) -> Result<HashDict, io::Error> {
18 18
         let mut hd = HashDict {
19 19
             hashes: HashMap::new(),
20 20
             path,
@@ -33,18 +33,19 @@ impl HashDict {
33 33
         for l in lines {
34 34
             let halves: Vec<&str> = l.split("\t").collect();
35 35
             if halves.len() == 2 {
36
-                hd.hashes.insert(halves[0].to_string(), halves[1].to_string());
36
+                hd.hashes
37
+                    .insert(halves[0].to_string(), halves[1].to_string());
37 38
             }
38 39
         }
39 40
 
40 41
         Ok(hd)
41 42
     }
42 43
 
43
-    pub fn get(&self, key : &str) -> Option<&String> {
44
+    pub fn get(&self, key: &str) -> Option<&String> {
44 45
         self.hashes.get(key)
45 46
     }
46 47
 
47
-    pub fn put(&mut self, key : String, value: String) {
48
+    pub fn put(&mut self, key: String, value: String) {
48 49
         self.hashes.insert(key, value);
49 50
         self.any_change = true;
50 51
     }

+ 304 - 172
src/main.rs View File

@@ -1,27 +1,39 @@
1
+use blake2::{Blake2b, Digest};
2
+use chrono;
3
+use chrono::offset::TimeZone;
4
+use chrono::Date;
5
+use chrono::NaiveDate;
6
+use chrono::Utc;
7
+use failure::Fallible;
8
+use image_utils;
9
+use markdown;
10
+use percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET};
11
+use rss::{Channel, ChannelBuilder, Guid, ItemBuilder};
1 12
 use std::env;
2
-use std::io;
3 13
 use std::fs;
4 14
 use std::fs::DirEntry;
5 15
 use std::fs::File;
16
+use std::fs::OpenOptions;
17
+use std::io;
6 18
 use std::io::prelude::*;
7 19
 use std::path::{Path, PathBuf};
8
-use chrono;
9
-use chrono::NaiveDate;
10
-use markdown;
11
-use std::fs::OpenOptions;
12
-use rss::{Channel, ChannelBuilder, Item, ItemBuilder, Guid};
13
-use percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET};
14
-use image_utils;
15
-use chrono::offset::TimeZone;
16
-use chrono::Date;
17
-use chrono::Utc;
18
-use blake2::{Digest,Blake2b};
19 20
 
20 21
 use base64;
22
+use std::collections::HashMap;
21 23
 
22 24
 mod hash_dict;
23 25
 
24 26
 #[derive(Debug)]
27
+struct BreadRendered {
28
+    thumb: String,
29
+    detail: String,
30
+    rss_item: Option<rss::Item>,
31
+    title: String,
32
+    url: String,
33
+    detail_fname: String,
34
+}
35
+
36
+#[derive(Debug)]
25 37
 struct Bread {
26 38
     path: PathBuf,
27 39
     rel_path: PathBuf,
@@ -29,243 +41,363 @@ struct Bread {
29 41
     note: String,
30 42
     rss_note: String,
31 43
     images: Vec<PathBuf>,
44
+    rendered: BreadRendered,
32 45
 }
33 46
 
34
-impl Bread {
35
-    fn thumb_photo(&self) -> (&str, &str) {
36
-        let mut first_img : &PathBuf = self.images.get(0).unwrap();
37
-
38
-        for im in &self.images {
39
-            if im.file_name().unwrap().to_str().unwrap().contains("cover") {
40
-                first_img = im;
41
-                break;
42
-            }
43
-        }
44
-
45
-        let img_path = first_img.to_str().unwrap();
46
-        let img_alt = first_img.file_name().unwrap().to_str().unwrap();
47
-
48
-        (img_path, img_alt)
49
-    }
50
-
51
-    fn parse(base_dir : &PathBuf, bread_dir : &DirEntry) -> Result<Bread, std::io::Error> {
52
-        let bpath = bread_dir.path();
53
-        let mut note = String::new();
54
-        let mut rss_note = String::new();
47
+//impl From<std::option::NoneError> for failure::Error {
48
+//    fn from(x: std::option::NoneError) -> Self {
49
+//        failure::err_msg("Expected something, found nothing.")
50
+//    }
51
+//}
55 52
 
56
-        let mut note_path = bpath.join("note.txt");
57
-        let mut rss_note_path = bpath.join("rss.txt");
58
-
59
-        // try a md one as a fallback
60
-        if !note_path.exists() {
61
-            note_path = bpath.join("note.md");
62
-        }
63
-        
64
-        if !rss_note_path.exists() {
65
-            rss_note_path = bpath.join("rss.md");
66
-        }
67
-
68
-        if note_path.exists() {
69
-            let mut note_file = File::open(note_path)?;
70
-            note_file.read_to_string(&mut note)?;
71
-            note = markdown::to_html(&note);
72
-        }
73
-
74
-        if rss_note_path.exists() {
75
-            let mut note_file = File::open(rss_note_path)?;
76
-            note_file.read_to_string(&mut rss_note)?;
77
-            rss_note = markdown::to_html(&rss_note);
78
-        }
79
-
80
-        let mut bread_files: Vec<DirEntry> = fs::read_dir(&bpath)?.map(|e| e.unwrap()).collect();
81
-        bread_files.sort_by(|x, y| x.file_name().cmp(&y.file_name()));
82
-
83
-        let images = bread_files.iter().filter(|&f| {
84
-            let fname = f.file_name();
85
-            let name = fname.to_str().unwrap();
86
-            return
87
-                name.ends_with(".png") ||
88
-                    name.ends_with(".jpg") ||
89
-                    name.ends_with(".jpeg") ||
90
-                    name.ends_with(".gif");
91
-        }).map(|x| x.path().strip_prefix(base_dir).unwrap().to_path_buf()).collect();
92
-
93
-        return Ok(Bread {
94
-            date: NaiveDate::parse_from_str(bpath.file_name().unwrap().to_str().unwrap(), "%Y-%m-%d").unwrap(),
95
-            rel_path: bpath.strip_prefix(base_dir).unwrap().to_path_buf(),
96
-            path: bpath,
97
-            note,
98
-            rss_note,
99
-            images
100
-        });
101
-    }
53
+#[derive(Debug)]
54
+struct BreadLink {
55
+    label: String,
56
+    url: String,
102 57
 }
103 58
 
104
-fn main() {
105
-    let cwd = env::current_dir().unwrap();
106
-    let web_path = Path::new(&cwd).join("web");
107
-    let data_path = web_path.join("data");
108
-    let tpl_path = web_path.join("templates");
109
-    let thumbs_path = web_path.join("thumbs");
110
-
111
-    let mut bread_dirs: Vec<DirEntry> = fs::read_dir(&data_path).unwrap().map(|e| e.unwrap()).collect();
112
-    bread_dirs.sort_by(|x, y| x.file_name().cmp(&y.file_name()));
113
-
114
-    let mut breads : Vec<Bread> = Vec::new();
115
-
116
-    for bread_dir in bread_dirs {
117
-        if let Ok(b) = Bread::parse(&web_path, &bread_dir) {
118
-            breads.push(b);
119
-        }
120
-    }
121
-
122
-    let mut main_tpl = String::new();
123
-    let mut thumb_tpl = String::new();
124
-    let mut detail_tpl = String::new();
125
-    let mut head_tpl = String::new();
126
-    File::open(tpl_path.join("index.html")).unwrap().read_to_string(&mut main_tpl).unwrap();
127
-    File::open(tpl_path.join("_thumb.html")).unwrap().read_to_string(&mut thumb_tpl).unwrap();
128
-    File::open(tpl_path.join("_head.html")).unwrap().read_to_string(&mut head_tpl).unwrap();
129
-    File::open(tpl_path.join("detail.html")).unwrap().read_to_string(&mut detail_tpl).unwrap();
130
-
131
-    let mut thumbs = Vec::<String>::new();
132
-
133
-    let mut channel : Channel = ChannelBuilder::default()
134
-        .title("Piggo's Bread Gallery")
135
-        .link("https://www.ondrovo.com/bread")
136
-        .description("Sourdough feed")
137
-        .build()
138
-        .unwrap();
139
-
140
-    let mut channel_items = Vec::<Item>::new();
141
-
142
-    let mut hashes = hash_dict::HashDict::load(cwd.join(".hashes.txt")).unwrap();
143
-    
144
-    // TODO separate thumbs by year and generate per-year pages
145
-    // TODO limit RSS to last N breads
146
-
147
-    for bread in &breads {
148
-        let date = bread.date.format("%Y/%m/%d").to_string();
149
-        let date_slug = bread.date.format("%Y-%m-%d").to_string();
59
+impl Bread {
60
+    fn compile(&mut self, config: &mut GalleryConfig, prev : Option<BreadLink>, next : Option<BreadLink>) -> Fallible<()> {
61
+        let date = self.date.format("%Y/%m/%d").to_string();
62
+        let date_slug = self.date.format("%Y-%m-%d").to_string();
150 63
         let detail_file = date_slug.clone() + ".html";
151
-
152 64
         println!("+ {}", date_slug);
153 65
 
154
-        let (img_path, img_alt) = bread.thumb_photo();
155
-        let note = if bread.note.is_empty() { "<i>There's no note about this bread.</i>" } else { &bread.note };
156
-        let rss_note = &bread.rss_note;
66
+        self.rendered.title = date.clone();
67
+        self.rendered.detail_fname = detail_file.clone();
68
+
69
+        // figure out the thumbnail pic
70
+        let (img_path, img_alt) = {
71
+            let mut first_img: &PathBuf = self.images.get(0).expect(&format!("No images for bread {}", date_slug));
72
+            for im in &self.images {
73
+                if im.file_name().unwrap().to_str().unwrap().contains("cover") {
74
+                    first_img = im;
75
+                    break;
76
+                }
77
+            }
78
+            (
79
+                first_img.to_str().unwrap().to_owned(),
80
+                first_img.file_name().unwrap().to_str().unwrap().to_owned(),
81
+            )
82
+        };
83
+
84
+        let note = if self.note.is_empty() {
85
+            "<i>There's no note about this bread.</i>"
86
+        } else {
87
+            &self.note
88
+        };
157 89
 
158 90
         let thumb_fname = date_slug.clone() + "." + Path::new(&img_path).extension().unwrap().to_str().unwrap();
159
-        let thumb_path = thumbs_path.join(&thumb_fname);
160
-        let thumb_relpath = thumb_path.strip_prefix(&web_path).unwrap();
91
+        let thumb_path = config.thumbs_path.join(&thumb_fname);
92
+        let thumb_relpath = thumb_path.strip_prefix(&config.web_path)?;
161 93
 
162
-        let image_path_encoded = utf8_percent_encode(thumb_relpath.to_str().unwrap(), DEFAULT_ENCODE_SET).to_string();
94
+        let image_path_encoded =
95
+            utf8_percent_encode(thumb_relpath.to_str().unwrap(), DEFAULT_ENCODE_SET).to_string();
163 96
 
164
-        // TODO keep the original path in bread so we dont have to reconstruct it here
165
-        let image_real_path = web_path.join(img_path);
97
+        let image_real_path = config.web_path.join(img_path);
166 98
 
167 99
         // Create the thumb
168 100
         {
169
-            let mut img_file = fs::File::open(&image_real_path).unwrap();
101
+            let mut img_file = fs::File::open(&image_real_path)?;
170 102
             let mut hasher = Blake2b::new();
171
-            io::copy(&mut img_file, &mut hasher).unwrap();
103
+            io::copy(&mut img_file, &mut hasher)?;
172 104
             let hash = base64::encode(&hasher.result());
173 105
 
174 106
             let hash_key = thumb_path.to_str().unwrap();
175
-            let old_hash = hashes.get(hash_key);
107
+            let old_hash = config.image_hashes.get(hash_key);
176 108
             if old_hash.is_none() || !old_hash.unwrap().eq(&hash) {
177 109
                 println!("building thumb...");
178 110
 
179
-                let im = image::open(&image_real_path).unwrap();
111
+                let im = image::open(&image_real_path)?;
180 112
                 let im = im.thumbnail(500, 500);
181
-                im.save(&thumb_path).unwrap();
113
+                im.save(&thumb_path)?;
182 114
 
183
-                hashes.put(hash_key.to_string(), hash);
115
+                config.image_hashes.put(hash_key.to_string(), hash);
184 116
             }
185 117
         }
186 118
 
187 119
         // Prepare the thumb card for the gallery page
188 120
         {
189
-            let thumb = thumb_tpl
121
+            self.rendered.thumb = config
122
+                .template("_thumb.html")?
190 123
                 .replace("{detail_url}", &detail_file)
191 124
                 .replace("{img_src}", &image_path_encoded)
192 125
                 .replace("{img_alt}", &img_alt)
193 126
                 .replace("{title}", &date);
194
-
195
-            thumbs.push(thumb);
196 127
         }
197 128
 
198 129
         // Add to RSS
199 130
         {
200
-            let image_url : String = channel.link().to_string() + "/" + &image_path_encoded;
131
+            let image_url: String = config.base_url.to_owned() + "/" + &image_path_encoded;
201 132
 
202
-            let link : String = channel.link().to_string() + "/" + &detail_file;
133
+            let link: String = config.base_url.to_owned() + "/" + &detail_file;
203 134
             let mut guid = Guid::default();
204 135
             guid.set_value(link.clone());
205 136
             guid.set_permalink(true);
206 137
 
207
-            let date_formatted : Date<Utc> = chrono::Utc.from_local_date(&bread.date).unwrap();
208
-            let dt = date_formatted.and_hms(12,0,0);
209
-            
138
+            let date_formatted: Date<Utc> = chrono::Utc.from_local_date(&self.date).unwrap();
139
+            let dt = date_formatted.and_hms(12, 0, 0);
140
+
210 141
             let mut descr = String::new();
211
-            if !rss_note.is_empty() {
212
-                descr.push_str(&(rss_note.to_string() + "<hr>"));
142
+            if !self.rss_note.is_empty() {
143
+                descr.push_str(&self.rss_note);
144
+                descr.push_str("<hr>");
213 145
             }
214 146
             descr.push_str(note);
215
-            descr.push_str(&format!("<img src=\"{}\" alt=\"{}\"><p><i>Open the link for full-res photos ({} total)</i>", image_url, img_alt, bread.images.len()));
147
+            descr.push_str(&format!(
148
+                "<img src=\"{}\" alt=\"{}\"><p><i>Open the link for full-res photos ({} total)</i>",
149
+                image_url,
150
+                img_alt,
151
+                self.images.len()
152
+            ));
216 153
 
217
-            channel_items.push(ItemBuilder::default()
154
+            self.rendered.url = link.clone();
155
+
156
+            let item: rss::Item = ItemBuilder::default()
218 157
                 .title(date.clone())
219
-                .link(link.clone())
158
+                .link(link)
220 159
                 .description(descr)
221 160
                 .guid(guid)
222 161
                 .pub_date(dt.to_rfc2822())
223
-                .build().unwrap());
162
+                .build().unwrap();
163
+
164
+            self.rendered.rss_item = Some(item);
224 165
         }
225 166
 
167
+        let head_tpl = config.template("_head.html")?;
168
+
226 169
         // Generate the detail page
227 170
         {
228 171
             let win_title = format!("Bread from {}", date);
229
-        
230
-            let detail = detail_tpl
172
+
173
+            let detail = config
174
+                .template("detail.html")?
231 175
                 .replace("{head}", &head_tpl.replace("{title}", &win_title))
232 176
                 .replace("{title}", &win_title)
233 177
                 .replace("{date}", &date_slug)
234
-                .replace("{url}", &format!("https://www.ondrovo.com/bread/{}", detail_file))
235
-                .replace("{thumb_url}", &format!("https://www.ondrovo.com/bread/thumbs/{}", thumb_fname))
178
+                .replace("{url}", &format!("{}/{}", config.base_url, detail_file))
179
+                .replace(
180
+                    "{thumb_url}",
181
+                    &format!("{}/thumbs/{}", config.base_url, thumb_fname),
182
+                )
236 183
                 .replace("{heading}", &date)
184
+                .replace("{prev}", &(match prev {
185
+                    Some(b) => format!("<a class=\"prev\" href=\"{}\" title=\"{}\">&lt;</a>", b.url, b.label),
186
+                    None => "".to_string()
187
+                }))
188
+                .replace("{next}", &(match next {
189
+                    Some(b) => format!("<a class=\"next\" href=\"{}\" title=\"{}\">&gt;</a>", b.url, b.label),
190
+                    None => "".to_string()
191
+                }))
237 192
                 .replace("{note}", note.trim());
238 193
 
239 194
             let mut pics = String::new();
240
-            for img in &bread.images {
241
-                pics.push_str(&format!("      <a href=\"{src}\"><img src=\"{src}\"></a>\n", src=&utf8_percent_encode(img.to_str().unwrap(), DEFAULT_ENCODE_SET).to_string()))
195
+            for img in &self.images {
196
+                pics.push_str(&format!(
197
+                    "      <a href=\"{src}\"><img src=\"{src}\"></a>\n",
198
+                    src = &utf8_percent_encode(img.to_str().unwrap(), DEFAULT_ENCODE_SET).to_string()
199
+                ))
242 200
             }
243 201
 
244 202
             let detail = detail.replace("{images}", &pics.trim());
245 203
 
246
-            let mut f = OpenOptions::new().write(true).truncate(true).create(true).open(web_path.join(detail_file)).unwrap();
204
+            let mut f = OpenOptions::new().write(true).truncate(true).create(true).open(config.web_path.join(detail_file)).unwrap();
247 205
             f.write(detail.as_bytes()).unwrap();
206
+
207
+            self.rendered.detail = detail;
248 208
         }
209
+
210
+        Ok(())
249 211
     }
250 212
 
251
-    hashes.save();
213
+    fn to_link(&self) -> BreadLink {
214
+        BreadLink {
215
+            label: self.date.format("%Y/%m/%d").to_string(),
216
+            url: self.date.format("%Y-%m-%d.html").to_string(),
217
+        }
218
+    }
252 219
 
253
-    {
254
-        // make thumbs go from the newest to the oldest
255
-        thumbs.reverse();
256
-    
257
-        println!("Building the gallery page");
258
-        let main = main_tpl.replace("{breads}", &thumbs.join("").trim())
259
-            .replace("{head}", &head_tpl.replace("{title}", "Piggo's breads").trim());
220
+    fn parse(base_dir: &PathBuf, bread_dir: &DirEntry) -> Result<Bread, std::io::Error> {
221
+        let bpath = bread_dir.path();
222
+        let mut note = String::new();
223
+        let mut rss_note = String::new();
260 224
 
261
-        let mut f = OpenOptions::new().write(true).truncate(true).create(true).open(web_path.join("index.html")).unwrap();
262
-        f.write(main.as_bytes()).unwrap();
225
+        let mut note_path = bpath.join("note.txt");
226
+        let mut rss_note_path = bpath.join("rss.txt");
227
+
228
+        // try a md one as a fallback
229
+        if !note_path.exists() {
230
+            note_path = bpath.join("note.md");
231
+        }
232
+
233
+        if !rss_note_path.exists() {
234
+            rss_note_path = bpath.join("rss.md");
235
+        }
236
+
237
+        if note_path.exists() {
238
+            let mut note_file = File::open(note_path)?;
239
+            note_file.read_to_string(&mut note)?;
240
+            note = markdown::to_html(&note);
241
+        }
242
+
243
+        if rss_note_path.exists() {
244
+            let mut note_file = File::open(rss_note_path)?;
245
+            note_file.read_to_string(&mut rss_note)?;
246
+            rss_note = markdown::to_html(&rss_note);
247
+        }
248
+
249
+        let mut bread_files: Vec<DirEntry> = fs::read_dir(&bpath)?.map(|e| e.unwrap()).collect();
250
+        bread_files.sort_by(|x, y| x.file_name().cmp(&y.file_name()));
251
+
252
+        let images = bread_files
253
+            .iter()
254
+            .filter(|&f| {
255
+                let fname = f.file_name();
256
+                let name = fname.to_str().unwrap();
257
+                return name.ends_with(".png")
258
+                    || name.ends_with(".jpg")
259
+                    || name.ends_with(".jpeg")
260
+                    || name.ends_with(".gif");
261
+            })
262
+            .map(|x| x.path().strip_prefix(base_dir).unwrap().to_path_buf())
263
+            .collect();
264
+
265
+        return Ok(Bread {
266
+            date: NaiveDate::parse_from_str(
267
+                bpath.file_name().unwrap().to_str().unwrap(),
268
+                "%Y-%m-%d",
269
+            )
270
+            .unwrap(),
271
+            rel_path: bpath.strip_prefix(base_dir).unwrap().to_path_buf(),
272
+            path: bpath,
273
+            note,
274
+            rss_note,
275
+            images,
276
+            rendered: BreadRendered {
277
+                thumb: "".to_string(),
278
+                detail: "".to_string(),
279
+                rss_item: None,
280
+                title: "".to_string(),
281
+                url: "".to_string(),
282
+                detail_fname: "".to_string()
283
+            },
284
+        });
285
+    }
286
+}
287
+
288
+struct GalleryConfig {
289
+    web_path: PathBuf,
290
+    data_path: PathBuf,
291
+    tpl_path: PathBuf,
292
+    thumbs_path: PathBuf,
293
+    base_url: String,
294
+    templates: HashMap<String, String>,
295
+    image_hashes: hash_dict::HashDict,
296
+}
297
+
298
+impl GalleryConfig {
299
+    fn template(&mut self, name: &str) -> Fallible<String> {
300
+        if let Some(text) = self.templates.get(name) {
301
+            return Ok(text.clone());
302
+        }
303
+
304
+        let mut tpl = String::new();
305
+        File::open(self.tpl_path.join(name))?.read_to_string(&mut tpl)?;
306
+        self.templates.insert(name.to_string(), tpl.clone());
307
+        Ok(tpl)
308
+    }
309
+}
310
+
311
+fn main() -> Fallible<()> {
312
+
313
+    let mut config = {
314
+        let cwd = env::current_dir().unwrap();
315
+        let web_path = cwd.join("web");
316
+        let data_path = web_path.join("data");
317
+        let tpl_path = web_path.join("templates");
318
+        let thumbs_path = web_path.join("thumbs");
319
+
320
+        GalleryConfig {
321
+            web_path,
322
+            data_path,
323
+            tpl_path,
324
+            thumbs_path,
325
+            base_url: "https://www.ondrovo.com/bread".into(),
326
+            templates: HashMap::new(),
327
+            image_hashes: hash_dict::HashDict::load(cwd.join(".hashes.txt"))?,
328
+        }
329
+    };
330
+
331
+    let mut bread_dirs: Vec<DirEntry> = fs::read_dir(&config.data_path)?.filter_map(|e| e.ok()).collect();
332
+    bread_dirs.sort_by(|x, y| x.file_name().cmp(&y.file_name()));
333
+
334
+    let mut breads: Vec<Bread> = bread_dirs
335
+        .iter()
336
+        .filter_map(|p| Bread::parse(&config.web_path, &p).ok())
337
+        .collect();
338
+
339
+    for i in 0..breads.len() {
340
+        let preceding = if i <= 0 { None } else {
341
+            match breads.get(i - 1) {
342
+                Some(b) => Some(b.to_link()),
343
+                None => None
344
+            }
345
+        };
346
+
347
+        let following = match breads.get(i + 1) {
348
+            Some(b) => Some(b.to_link()),
349
+            None => None
350
+        };
351
+
352
+        let cur = breads.get_mut(i).unwrap();
353
+
354
+        cur.compile(&mut config, preceding, following)?;
263 355
     }
264 356
 
357
+    let mut channel: Channel = ChannelBuilder::default()
358
+        .title("Piggo's Bread Gallery")
359
+        .link("https://www.ondrovo.com/bread")
360
+        .description("Sourdough feed")
361
+        .build()
362
+        .unwrap();
363
+
364
+    config.image_hashes.save();
365
+
366
+    let mut start = breads.len() as i32 - 10;
367
+    if start < 0 {
368
+        start = 0;
369
+    }
370
+
371
+    // rss
265 372
     {
373
+        let mut channel_items = vec![];
374
+        for b in &breads[start as usize..] {
375
+            channel_items.push(b.rendered.rss_item.clone().unwrap());
376
+        }
377
+
266 378
         println!("Generating feed...");
267
-        let f = OpenOptions::new().write(true).truncate(true).create(true).open(web_path.join("feed.xml")).unwrap();
379
+        let f = OpenOptions::new().write(true).truncate(true).create(true).open(config.web_path.join("feed.xml")).unwrap();
268 380
         channel.set_items(channel_items);
269 381
         channel.pretty_write_to(f, b' ', 2).unwrap();
270 382
     }
383
+
384
+    // main page
385
+    {
386
+        // make thumbs go from the newest to the oldest
387
+        breads.reverse();
388
+
389
+        let mut thumbs_buf = String::new();
390
+        for b in &breads {
391
+            thumbs_buf.push_str(&b.rendered.thumb);
392
+        }
393
+
394
+        println!("Building the gallery page");
395
+        let main = config.template("index.html")?.replace("{breads}", &thumbs_buf.trim())
396
+            .replace("{head}", config.template("_head.html")?.replace("{title}", "Piggo's breads").trim());
397
+
398
+        let mut f = OpenOptions::new().write(true).truncate(true).create(true).open(config.web_path.join("index.html")).unwrap();
399
+        f.write(main.as_bytes()).unwrap();
400
+    }
401
+
402
+    Ok(())
271 403
 }

+ 7 - 1
web/assets/style.css View File

@@ -93,4 +93,10 @@ a {
93 93
 
94 94
 #crumb a:hover {
95 95
     color: #ccc;
96
-}
96
+}
97
+
98
+.prev, .next {
99
+  font-size: 90%;
100
+  padding: 0 1em;
101
+  
102
+}

BIN
web/data/2019-02-13/2019-02-13 21.56.35.jpg View File


+ 1 - 0
web/data/2019-02-13/note.txt View File

@@ -0,0 +1 @@
1
+a little flat but A+ taste and cronch

BIN
web/data/2019-02-18/bread.jpg View File


+ 3 - 0
web/data/2019-02-18/note.txt View File

@@ -0,0 +1,3 @@
1
+The previous bread came out a bit tough. I run out of bread flour then, so it was made from fine flour. May be why.
2
+
3
+This one is almost perfect, but it's still turning tough quickly. I'm investigating ways to keep it fresh longer... If you have a tip, please let me know!

+ 1 - 1
web/templates/detail.html View File

@@ -25,7 +25,7 @@
25 25
     <meta itemprop="image" content="{thumb_url}">
26 26
 </head>
27 27
 <body>
28
-  <h1>{heading}</h1>
28
+<h1>{prev}<a href="index.html">{heading}</a>{next}</h1>
29 29
 
30 30
   <section class="BreadDetail">
31 31
     <div class="note">