mirror of
https://github.com/zyllian/webdog.git
synced 2025-01-18 03:32:21 -08:00
Implement image display
This commit is contained in:
parent
97b9fbf46d
commit
c0ed59b2cd
19 changed files with 730 additions and 25 deletions
188
Cargo.lock
generated
188
Cargo.lock
generated
|
@ -28,6 +28,19 @@ version = "1.0.66"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6"
|
||||
|
||||
[[package]]
|
||||
name = "atom_syndication"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21fb6a0b39c6517edafe46f8137e53c51742425a4dae1c73ee12264a37ad7541"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"derive_builder",
|
||||
"diligent-date-parser",
|
||||
"never",
|
||||
"quick-xml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.1.0"
|
||||
|
@ -95,6 +108,16 @@ version = "1.0.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f"
|
||||
dependencies = [
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codemap"
|
||||
version = "0.1.3"
|
||||
|
@ -153,6 +176,72 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.12.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f2c43f534ea4b0b049015d00269734195e6d3f0f6635cb692251aca6f9f8b3c"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.12.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e91455b86830a1c21799d94524df0845183fa55bafd9aa137b01c7d1065fa36"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.12.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29b5acf0dea37a7f66f7b25d2c5e93fd46f8f6968b1a5d7a3e02e97768afc95a"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d13202debe11181040ae9063d739fa32cfcaaebe2275fe387703460ae2365b30"
|
||||
dependencies = [
|
||||
"derive_builder_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder_core"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "66e616858f6187ed828df7c64a6d71720d83767a7f19740b2d1b6fe6327b36e5"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder_macro"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "58a94ace95092c5acb1e97a7e846b310cfbd499652f72297da7493f618a98d73"
|
||||
dependencies = [
|
||||
"derive_builder_core",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "0.99.17"
|
||||
|
@ -176,6 +265,15 @@ dependencies = [
|
|||
"crypto-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diligent-date-parser"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2d0fd95c7c02e2d6c588c6c5628466fff9bdde4b8c6196465e087b08e792720"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dtoa"
|
||||
version = "0.4.8"
|
||||
|
@ -191,6 +289,12 @@ dependencies = [
|
|||
"dtoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797"
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.31"
|
||||
|
@ -595,6 +699,12 @@ dependencies = [
|
|||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.3.0"
|
||||
|
@ -653,6 +763,15 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.10.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "0.4.8"
|
||||
|
@ -857,6 +976,12 @@ dependencies = [
|
|||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "never"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c96aba5aa877601bb3f6dd6a63a969e1f82e60646e81e71b14496995e9853c91"
|
||||
|
||||
[[package]]
|
||||
name = "nodrop"
|
||||
version = "0.1.14"
|
||||
|
@ -1163,6 +1288,16 @@ version = "1.2.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8533f14c8382aaad0d592c812ac3b826162128b65662331e1127b45c3d18536b"
|
||||
dependencies = [
|
||||
"encoding_rs",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.21"
|
||||
|
@ -1288,6 +1423,21 @@ dependencies = [
|
|||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rss"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acaf1331b7fc4edc3c2920819fee1766c27e8d40da593155832db3d6dea64e92"
|
||||
dependencies = [
|
||||
"atom_syndication",
|
||||
"chrono",
|
||||
"derive_builder",
|
||||
"mime",
|
||||
"never",
|
||||
"quick-xml",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.0"
|
||||
|
@ -1484,6 +1634,12 @@ version = "1.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.105"
|
||||
|
@ -1535,6 +1691,33 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376"
|
||||
dependencies = [
|
||||
"itoa 1.0.4",
|
||||
"serde",
|
||||
"time-core",
|
||||
"time-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time-core"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd"
|
||||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d967f99f534ca7e495c575c62638eebc2898a8c84c119b89e250477bc4ba16b2"
|
||||
dependencies = [
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.6.0"
|
||||
|
@ -1743,6 +1926,7 @@ dependencies = [
|
|||
"form_urlencoded",
|
||||
"idna",
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1952,11 +2136,15 @@ dependencies = [
|
|||
"gray_matter",
|
||||
"handlebars",
|
||||
"hotwatch",
|
||||
"itertools",
|
||||
"lol_html",
|
||||
"minifier",
|
||||
"percent-encoding",
|
||||
"pulldown-cmark",
|
||||
"rss",
|
||||
"serde",
|
||||
"serde_yaml",
|
||||
"time",
|
||||
"tokio",
|
||||
"url",
|
||||
"walkdir",
|
||||
|
|
|
@ -12,16 +12,20 @@ grass = { version = "0.11", default-features = false }
|
|||
gray_matter = "0.2"
|
||||
handlebars = "4.1"
|
||||
hotwatch = { version = "0.4", optional = true }
|
||||
itertools = "0.10"
|
||||
lol_html = "0.3"
|
||||
minifier = { version = "0.2", features = ["html"] }
|
||||
percent-encoding = { version = "2", optional = true }
|
||||
pulldown-cmark = { version = "0.9", default-features = false, features = ["simd"] }
|
||||
rss = { version = "2", features = ["validation"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_yaml = "0.9"
|
||||
time = { version = "0.3", features = ["serde-human-readable"] }
|
||||
tokio = { version = "1.10", features = ["macros", "rt-multi-thread"], optional = true }
|
||||
url = "2.2"
|
||||
url = { version = "2.2", features = ["serde"] }
|
||||
walkdir = "2"
|
||||
warp = { version = "0.3", optional = true }
|
||||
|
||||
[features]
|
||||
default = ["serve"]
|
||||
serve = ["futures", "hotwatch", "tokio", "warp"]
|
||||
serve = ["futures", "hotwatch", "percent-encoding", "tokio", "warp"]
|
||||
|
|
|
@ -2,3 +2,6 @@ base_url: "https://zyl.gay"
|
|||
title: Zyllian
|
||||
description: "Zoey's website."
|
||||
sass_styles: [index.scss]
|
||||
images_per_page: 10
|
||||
cdn_url: "https://i.zyl.gay"
|
||||
s3_prefix: z/
|
||||
|
|
6
site/images/amogus.yml
Normal file
6
site/images/amogus.yml
Normal file
|
@ -0,0 +1,6 @@
|
|||
title: among us 😱
|
||||
timestamp: 2022-12-14T00:00:00.00Z
|
||||
alt: Screenshot of Pokémon White on an evolution screen. Text reads "Congratulations! Your amogus evolved into Amoonguss!"
|
||||
desc: aaahhhh
|
||||
file: amogus.png
|
||||
tags: [pokémon, sussy]
|
5
site/images/cat.yml
Normal file
5
site/images/cat.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
title: cat
|
||||
timestamp: 2022-12-14T00:00:00.00Z
|
||||
alt: Picture of my cat sleeping curled up on top of some pillows.
|
||||
file: cat.jpeg
|
||||
tags: [cat]
|
5
site/images/cat2.yml
Normal file
5
site/images/cat2.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
title: cat 2
|
||||
timestamp: 2022-12-14T00:00:00.00Z
|
||||
alt: Close up picture of my cat laying on a shelf while staring not quite at the camera.
|
||||
file: cat2.jpeg
|
||||
tags: [cat]
|
5
site/images/cat3.yml
Normal file
5
site/images/cat3.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
title: cat 3
|
||||
timestamp: 2022-12-14T00:00:00.00Z
|
||||
alt: Picture of my cat sleeping in a box barely large enough for them. Their head is reasting on one edge of the box.
|
||||
file: cat/boxtop.jpeg
|
||||
tags: [cat]
|
6
site/images/trans-comfy.yml
Normal file
6
site/images/trans-comfy.yml
Normal file
|
@ -0,0 +1,6 @@
|
|||
title: shorts to dresses
|
||||
timestamp: 2022-12-14T00:00:00.00Z
|
||||
alt: Screenshot from Pokémon Black 2 of an NPC saying "This dress is comfy and easy to wear..."
|
||||
desc: yooo they're turning the comfy shorts kid trans
|
||||
file: trans-comfy.png
|
||||
tags: [pokémon, trans]
|
|
@ -51,3 +51,44 @@ main.page {
|
|||
abbr {
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.images-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.image {
|
||||
position: relative;
|
||||
padding: 4px;
|
||||
height: auto;
|
||||
|
||||
.image-actual {
|
||||
display: block;
|
||||
width: 300px;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.title {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: 4px;
|
||||
margin: 4px;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.image-full {
|
||||
.title, .tags-title {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.image-actual {
|
||||
width: 100%;
|
||||
max-height: 80vh;
|
||||
object-fit: contain;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
<html lang="en">
|
||||
|
||||
<head>
|
||||
{{{head}}}
|
||||
<meta name="referrer" content="no-referrer">
|
||||
<link rel="stylesheet" href="/styles/index.css">
|
||||
</head>
|
||||
|
@ -14,6 +13,7 @@
|
|||
<span class="pronouns">she/they</span>
|
||||
</span>
|
||||
<span class="spacer"></span>
|
||||
<a href="/images/">Images</a> |
|
||||
<a href="https://github.com/Zyllian/zyllian.github.io" rel="noopener noreferrer">Source</a>
|
||||
</header>
|
||||
<main class="page">
|
||||
|
|
8
site/templates/basic-link-list.hbs
Normal file
8
site/templates/basic-link-list.hbs
Normal file
|
@ -0,0 +1,8 @@
|
|||
<h1>{{title}}</h1>
|
||||
<div class="link-list">
|
||||
<ul>
|
||||
{{#each links}}
|
||||
<li><a href="{{this.link}}">{{this.title}}</a></li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
15
site/templates/image.hbs
Normal file
15
site/templates/image.hbs
Normal file
|
@ -0,0 +1,15 @@
|
|||
<div class="image-full">
|
||||
<h1 class="title">{{title}}</h1>
|
||||
<span class="timestamp">{{timestamp}}</span>
|
||||
<img class="image-actual" src="{{src}}" alt="{{alt}}">
|
||||
{{#if desc}}
|
||||
<p>{{desc}}</p>
|
||||
{{/if}}
|
||||
<p><a href="{{src}}">View full size image</a></p>
|
||||
<h3 class="tags-title">Tags</h3>
|
||||
<div class="image-tags">
|
||||
{{#each tags}}
|
||||
<a class="tag" href="/i/tag/{{this}}/">{{this}}</a>{{#unless @last}},{{/unless}}
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
22
site/templates/images.hbs
Normal file
22
site/templates/images.hbs
Normal file
|
@ -0,0 +1,22 @@
|
|||
{{#if tag}}
|
||||
<h1>Images tagged {{tag}}</h1>
|
||||
<p><a href="/images/">View all images</a></p>
|
||||
{{else}}
|
||||
<h1>Images</h1>
|
||||
<p><a href="/i/tags">View image tags</a></p>
|
||||
{{/if}}
|
||||
<h3>Page {{page}}/{{page_max}}</h3>
|
||||
{{#if previous}}
|
||||
<a href="./{{previous}}">Previous page</a>
|
||||
{{/if}}
|
||||
{{#if next}}
|
||||
<a href="./{{next}}">Next page</a>
|
||||
{{/if}}
|
||||
<div class="images-list">
|
||||
{{#each images}}
|
||||
<a class="image" href="/i/{{id}}">
|
||||
<img class="image-actual" src="{{src}}" alt="{{alt}}">
|
||||
<span class="title">{{title}}</span>
|
||||
</a>
|
||||
{{/each}}
|
||||
</div>
|
1
site/templates/rss/image.hbs
Normal file
1
site/templates/rss/image.hbs
Normal file
|
@ -0,0 +1 @@
|
|||
<img src="{{src}}" alt="{{alt}}">
|
|
@ -11,7 +11,7 @@ use serde::Serialize;
|
|||
use url::Url;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
use crate::{util, PageMetadata, Site, ROOT_PATH, SASS_PATH, STATIC_PATH};
|
||||
use crate::{images::ImageMetadata, util, PageMetadata, Site, ROOT_PATH, SASS_PATH, STATIC_PATH};
|
||||
|
||||
/// Struct containing data to be sent to templates when rendering them.
|
||||
#[derive(Debug, Serialize)]
|
||||
|
@ -102,29 +102,23 @@ impl<'a> SiteBuilder<'a> {
|
|||
.context("Failed to copy static directory")?;
|
||||
}
|
||||
|
||||
let images_path = self.build_path.join(crate::images::IMAGES_OUT_PATH);
|
||||
if !images_path.exists() {
|
||||
std::fs::create_dir(images_path).context("Failed to create images path")?;
|
||||
}
|
||||
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Builds a page.
|
||||
pub fn build_page(&self, page_name: &str) -> anyhow::Result<()> {
|
||||
let page_path = self.site.page_index.get(page_name).unwrap();
|
||||
|
||||
let input = std::fs::read_to_string(page_path)
|
||||
.with_context(|| format!("Failed to read page at {}", page_path.display()))?;
|
||||
let page = self.matter.parse(&input);
|
||||
let page_metadata = if let Some(data) = page.data {
|
||||
data.deserialize()?
|
||||
} else {
|
||||
PageMetadata::default()
|
||||
};
|
||||
|
||||
let parser = Parser::new_ext(&page.content, Options::all());
|
||||
let mut page_html = String::new();
|
||||
pulldown_cmark::html::push_html(&mut page_html, parser);
|
||||
|
||||
/// Helper to build a page without writing it to disk.
|
||||
pub fn build_page_raw(
|
||||
&self,
|
||||
page_metadata: PageMetadata,
|
||||
page_html: &str,
|
||||
) -> anyhow::Result<String> {
|
||||
let out = self.reg.render(
|
||||
&page_metadata.template.unwrap_or_else(|| "base".to_string()),
|
||||
&TemplateData { page: &page_html },
|
||||
&TemplateData { page: page_html },
|
||||
)?;
|
||||
|
||||
let title = match &page_metadata.title {
|
||||
|
@ -184,6 +178,28 @@ impl<'a> SiteBuilder<'a> {
|
|||
out = minifier::html::minify(&out);
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Builds a page.
|
||||
pub fn build_page(&self, page_name: &str) -> anyhow::Result<()> {
|
||||
let page_path = self.site.page_index.get(page_name).expect("Missing page");
|
||||
|
||||
let input = std::fs::read_to_string(page_path)
|
||||
.with_context(|| format!("Failed to read page at {}", page_path.display()))?;
|
||||
let page = self.matter.parse(&input);
|
||||
let page_metadata = if let Some(data) = page.data {
|
||||
data.deserialize()?
|
||||
} else {
|
||||
PageMetadata::default()
|
||||
};
|
||||
|
||||
let parser = Parser::new_ext(&page.content, Options::all());
|
||||
let mut page_html = String::new();
|
||||
pulldown_cmark::html::push_html(&mut page_html, parser);
|
||||
|
||||
let out = self.build_page_raw(page_metadata, &page_html)?;
|
||||
|
||||
let out_path = self.build_path.join(page_name).with_extension("html");
|
||||
std::fs::create_dir_all(out_path.parent().unwrap())
|
||||
.with_context(|| format!("Failed to create directory for page {}", page_name))?;
|
||||
|
@ -239,4 +255,14 @@ impl<'a> SiteBuilder<'a> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Builds the site's various image pages.
|
||||
pub fn build_images(&self) -> anyhow::Result<()> {
|
||||
let images = ImageMetadata::load_all(&self.site.site_path)?;
|
||||
ImageMetadata::build_lists(self, &images)?;
|
||||
for (id, image) in images {
|
||||
image.build(self, id)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
292
src/images.rs
Normal file
292
src/images.rs
Normal file
|
@ -0,0 +1,292 @@
|
|||
use std::{
|
||||
collections::BTreeMap,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use itertools::Itertools;
|
||||
use rss::{validation::Validate, ChannelBuilder, ItemBuilder};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use time::{format_description::well_known::Rfc2822, OffsetDateTime};
|
||||
use url::Url;
|
||||
|
||||
use crate::{builder::SiteBuilder, link_list::Link, PageMetadata, SiteConfig};
|
||||
|
||||
pub(crate) const IMAGES_PATH: &str = "images";
|
||||
pub(crate) const IMAGES_OUT_PATH: &str = "i";
|
||||
|
||||
/// Definition for a remote image.
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct ImageMetadata {
|
||||
/// The image's title.
|
||||
pub title: String,
|
||||
/// The image's timestamp.
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
pub timestamp: OffsetDateTime,
|
||||
/// The image's alt text.
|
||||
pub alt: String,
|
||||
/// The image's extra description, if any.
|
||||
pub desc: Option<String>,
|
||||
/// The image's file path.
|
||||
pub file: String,
|
||||
/// The image's tags.
|
||||
pub tags: Vec<String>,
|
||||
}
|
||||
|
||||
impl ImageMetadata {
|
||||
/// Gets an image's ID from its path.
|
||||
pub fn get_id(path: &Path) -> String {
|
||||
path.with_extension("")
|
||||
.file_name()
|
||||
.expect("Should never fail")
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
/// Loads an image's ID and metadata.
|
||||
pub fn load(path: &Path) -> anyhow::Result<(String, Self)> {
|
||||
let id = Self::get_id(path);
|
||||
let metadata: ImageMetadata = serde_yaml::from_str(&std::fs::read_to_string(path)?)?;
|
||||
Ok((id, metadata))
|
||||
}
|
||||
|
||||
/// Loads all available images.
|
||||
pub fn load_all(site_path: &Path) -> anyhow::Result<Vec<(String, Self)>> {
|
||||
let images_path = site_path.join(IMAGES_PATH);
|
||||
let mut images = Vec::new();
|
||||
for e in images_path.read_dir()? {
|
||||
let p = e?.path();
|
||||
if let Some(ext) = p.extension() {
|
||||
if ext == "yml" {
|
||||
let (id, metadata) = Self::load(&p)?;
|
||||
images.push((id, metadata));
|
||||
}
|
||||
}
|
||||
}
|
||||
images.sort_by(|a, b| a.1.timestamp.cmp(&b.1.timestamp).reverse());
|
||||
Ok(images)
|
||||
}
|
||||
|
||||
/// Builds an image's page.
|
||||
pub fn build(self, builder: &SiteBuilder, id: String) -> anyhow::Result<()> {
|
||||
let out = {
|
||||
let data = ImageTemplateData {
|
||||
image: &self,
|
||||
src: self.cdn_url(&builder.site.config)?.to_string(),
|
||||
id: &id,
|
||||
};
|
||||
builder.reg.render("image", &data)?
|
||||
};
|
||||
let out = builder.build_page_raw(
|
||||
PageMetadata {
|
||||
title: Some(self.title),
|
||||
..Default::default()
|
||||
},
|
||||
&out,
|
||||
)?;
|
||||
let out_path = Self::build_path(&builder.build_path, &id);
|
||||
std::fs::write(out_path, out)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets an image's CDN url.
|
||||
pub fn cdn_url(&self, config: &SiteConfig) -> anyhow::Result<Url> {
|
||||
Ok(config.cdn_url.join(&config.s3_prefix)?.join(&self.file)?)
|
||||
}
|
||||
|
||||
/// Gets an image's build path.
|
||||
pub fn build_path(build_path: &Path, id: &str) -> PathBuf {
|
||||
build_path
|
||||
.join(IMAGES_OUT_PATH)
|
||||
.join(id)
|
||||
.with_extension("html")
|
||||
}
|
||||
|
||||
/// Builds the various list pages for images.
|
||||
pub fn build_lists(builder: &SiteBuilder, metadata: &[(String, Self)]) -> anyhow::Result<()> {
|
||||
let mut data = Vec::with_capacity(metadata.len());
|
||||
for (id, metadata) in metadata {
|
||||
data.push(ImageTemplateData {
|
||||
image: metadata,
|
||||
src: metadata.cdn_url(&builder.site.config)?.to_string(),
|
||||
id,
|
||||
});
|
||||
}
|
||||
|
||||
fn build_list(
|
||||
builder: &SiteBuilder,
|
||||
list: Vec<&ImageTemplateData>,
|
||||
title: &str,
|
||||
tag: Option<&str>,
|
||||
out_path: &Path,
|
||||
items_per_page: usize,
|
||||
) -> anyhow::Result<()> {
|
||||
if !out_path.exists() {
|
||||
std::fs::create_dir_all(out_path)?;
|
||||
}
|
||||
|
||||
let page_max = list.len() / items_per_page
|
||||
+ if list.len() % items_per_page == 0 {
|
||||
0
|
||||
} else {
|
||||
1
|
||||
};
|
||||
let mut previous = None;
|
||||
let mut next;
|
||||
for (page, iter) in list.iter().chunks(items_per_page).into_iter().enumerate() {
|
||||
next = (page + 1 != page_max).then_some(page + 2);
|
||||
let data = ImageListTemplateData {
|
||||
images: iter.copied().collect(),
|
||||
tag,
|
||||
page: page + 1,
|
||||
page_max,
|
||||
previous,
|
||||
next,
|
||||
};
|
||||
let out = builder.reg.render("images", &data)?;
|
||||
let out = builder.build_page_raw(
|
||||
PageMetadata {
|
||||
title: Some(title.to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
&out,
|
||||
)?;
|
||||
if page == 0 {
|
||||
std::fs::write(out_path.join("index.html"), &out)?;
|
||||
}
|
||||
std::fs::write(
|
||||
out_path.join(out_path.join((page + 1).to_string()).with_extension("html")),
|
||||
out,
|
||||
)?;
|
||||
previous = Some(page + 1);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
build_list(
|
||||
builder,
|
||||
data.iter().collect(),
|
||||
"Images",
|
||||
None,
|
||||
&builder.build_path.join("images"),
|
||||
builder.site.config.images_per_page,
|
||||
)?;
|
||||
|
||||
let mut tags: BTreeMap<String, Vec<&ImageTemplateData>> = BTreeMap::new();
|
||||
for image in &data {
|
||||
for tag in image.image.tags.iter().cloned() {
|
||||
tags.entry(tag).or_default().push(image);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let links = tags
|
||||
.iter()
|
||||
.map(|(tag, data)| {
|
||||
let count = data.len();
|
||||
(
|
||||
Link::new(
|
||||
format!("/{IMAGES_OUT_PATH}/tag/{tag}/"),
|
||||
format!("{tag} ({count})"),
|
||||
),
|
||||
count,
|
||||
)
|
||||
})
|
||||
.sorted_by(|(_, a), (_, b)| a.cmp(b).reverse())
|
||||
.map(|(l, _)| l)
|
||||
.collect();
|
||||
let out = crate::link_list::render_basic_link_list(builder, links, "Image Tags")?;
|
||||
std::fs::write(
|
||||
builder.build_path.join(IMAGES_OUT_PATH).join("tags.html"),
|
||||
out,
|
||||
)?;
|
||||
}
|
||||
|
||||
for (tag, data) in tags {
|
||||
build_list(
|
||||
builder,
|
||||
data,
|
||||
&format!("Images tagged {tag}"),
|
||||
Some(tag.as_str()),
|
||||
&builder
|
||||
.build_path
|
||||
.join(IMAGES_OUT_PATH)
|
||||
.join("tag")
|
||||
.join(&tag),
|
||||
builder.site.config.images_per_page,
|
||||
)?;
|
||||
}
|
||||
|
||||
let mut items = Vec::with_capacity(data.len());
|
||||
for image in data {
|
||||
items.push(
|
||||
ItemBuilder::default()
|
||||
.title(Some(image.image.title.to_owned()))
|
||||
.link(Some(
|
||||
builder
|
||||
.site
|
||||
.config
|
||||
.base_url
|
||||
.join(&format!("{IMAGES_OUT_PATH}/{}", image.id))?
|
||||
.to_string(),
|
||||
))
|
||||
.description(image.image.desc.to_owned())
|
||||
.pub_date(Some(image.image.timestamp.format(&Rfc2822)?))
|
||||
.content(Some(builder.reg.render("rss/image", &image)?))
|
||||
.build(),
|
||||
);
|
||||
}
|
||||
|
||||
// Build RSS feed
|
||||
let channel = ChannelBuilder::default()
|
||||
.title("Zyllian's images".to_string())
|
||||
.link(
|
||||
builder
|
||||
.site
|
||||
.config
|
||||
.base_url
|
||||
.join("images/")
|
||||
.expect("Should never fail"),
|
||||
)
|
||||
.description("Feed of newly uploaded images from Zyllian's website.".to_string())
|
||||
.last_build_date(Some(OffsetDateTime::now_utc().format(&Rfc2822)?))
|
||||
.items(items)
|
||||
.build();
|
||||
channel.validate().context("Failed to validate RSS feed")?;
|
||||
let out = channel.to_string();
|
||||
std::fs::write(builder.build_path.join("images").join("rss.xml"), out)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Template data for a specific image.
|
||||
#[derive(Debug, Serialize)]
|
||||
struct ImageTemplateData<'i> {
|
||||
/// The image's regular metadata.
|
||||
#[serde(flatten)]
|
||||
image: &'i ImageMetadata,
|
||||
/// Direct URL to the image's CDN location.
|
||||
/// TODO: link to smaller versions on list pages
|
||||
src: String,
|
||||
/// The image's ID.
|
||||
id: &'i str,
|
||||
}
|
||||
|
||||
/// Template data for image lists.
|
||||
#[derive(Debug, Serialize)]
|
||||
struct ImageListTemplateData<'i> {
|
||||
/// The list of images to display.
|
||||
images: Vec<&'i ImageTemplateData<'i>>,
|
||||
/// The current tag, if any.
|
||||
tag: Option<&'i str>,
|
||||
/// The current page.
|
||||
page: usize,
|
||||
/// The total number of pages.
|
||||
page_max: usize,
|
||||
/// The previous page, if any.
|
||||
previous: Option<usize>,
|
||||
/// The next page, if any.
|
||||
next: Option<usize>,
|
||||
}
|
12
src/lib.rs
12
src/lib.rs
|
@ -1,4 +1,6 @@
|
|||
mod builder;
|
||||
mod images;
|
||||
mod link_list;
|
||||
#[cfg(feature = "serve")]
|
||||
pub mod serving;
|
||||
mod util;
|
||||
|
@ -10,6 +12,7 @@ use std::{
|
|||
|
||||
use anyhow::Context;
|
||||
use serde::Deserialize;
|
||||
use url::Url;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
use builder::SiteBuilder;
|
||||
|
@ -24,7 +27,7 @@ const ROOT_PATH: &str = "root";
|
|||
#[derive(Debug, Deserialize)]
|
||||
pub struct SiteConfig {
|
||||
/// The location the site is at.
|
||||
pub base_url: String,
|
||||
pub base_url: Url,
|
||||
/// The site's title.
|
||||
pub title: String,
|
||||
/// The site's description? Not sure if this will actually be used or not
|
||||
|
@ -33,6 +36,12 @@ pub struct SiteConfig {
|
|||
pub build: Option<String>,
|
||||
/// A list of Sass stylesheets that will be built.
|
||||
pub sass_styles: Vec<PathBuf>,
|
||||
/// The number of images to display on a single page of an image list.
|
||||
pub images_per_page: usize,
|
||||
/// URL to the CDN used for the site's images.
|
||||
pub cdn_url: Url,
|
||||
/// Prefix applied to all files uploaded to the site's S3 space.
|
||||
pub s3_prefix: String,
|
||||
}
|
||||
|
||||
/// Struct for the front matter in templates. (nothing here yet)
|
||||
|
@ -124,6 +133,7 @@ impl Site {
|
|||
|
||||
builder.site.build_all_pages(&builder)?;
|
||||
builder.build_sass()?;
|
||||
builder.build_images()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
50
src/link_list.rs
Normal file
50
src/link_list.rs
Normal file
|
@ -0,0 +1,50 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{builder::SiteBuilder, PageMetadata};
|
||||
|
||||
/// Helper for links.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Link<'l> {
|
||||
/// The link's actual link.
|
||||
pub link: Cow<'l, str>,
|
||||
/// The link's title.
|
||||
pub title: Cow<'l, str>,
|
||||
}
|
||||
|
||||
impl<'l> Link<'l> {
|
||||
/// Creates a new link.
|
||||
pub fn new(link: impl Into<Cow<'l, str>>, title: impl Into<Cow<'l, str>>) -> Self {
|
||||
let link = link.into();
|
||||
let title = title.into();
|
||||
Self { link, title }
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders a basic list of links.
|
||||
pub fn render_basic_link_list(
|
||||
builder: &SiteBuilder,
|
||||
links: Vec<Link>,
|
||||
title: &str,
|
||||
) -> anyhow::Result<String> {
|
||||
let data = LinkTemplateData { links, title };
|
||||
let out = builder.reg.render("basic-link-list", &data)?;
|
||||
let out = builder.build_page_raw(
|
||||
PageMetadata {
|
||||
title: Some(title.to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
&out,
|
||||
)?;
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Template data for a list of links.
|
||||
#[derive(Debug, Serialize)]
|
||||
struct LinkTemplateData<'l> {
|
||||
/// The actual links.
|
||||
links: Vec<Link<'l>>,
|
||||
/// The title for the page.
|
||||
title: &'l str,
|
||||
}
|
|
@ -18,7 +18,10 @@ use warp::{
|
|||
Filter,
|
||||
};
|
||||
|
||||
use crate::{Site, SiteBuilder, PAGES_PATH, ROOT_PATH, SASS_PATH, STATIC_PATH, TEMPLATES_PATH};
|
||||
use crate::{
|
||||
images::ImageMetadata, Site, SiteBuilder, PAGES_PATH, ROOT_PATH, SASS_PATH, STATIC_PATH,
|
||||
TEMPLATES_PATH,
|
||||
};
|
||||
|
||||
fn with_build_path(
|
||||
build_path: PathBuf,
|
||||
|
@ -63,6 +66,7 @@ fn create(
|
|||
builder.refresh_template(&template_name_str, path)?;
|
||||
if build {
|
||||
builder.site.build_all_pages(builder)?;
|
||||
builder.build_images()?;
|
||||
}
|
||||
} else if let Ok(_static_path) = relative_path.strip_prefix(STATIC_PATH) {
|
||||
std::fs::copy(path, builder.build_path.join(relative_path))?;
|
||||
|
@ -76,6 +80,9 @@ fn create(
|
|||
}
|
||||
} else if let Ok(root_path) = relative_path.strip_prefix(ROOT_PATH) {
|
||||
std::fs::copy(path, builder.build_path.join(root_path))?;
|
||||
} else if let Ok(_image_path) = relative_path.strip_prefix(crate::images::IMAGES_PATH) {
|
||||
// HACK: this could get very inefficient with a larger number of images. should definitely optimize
|
||||
builder.build_images()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -108,6 +115,11 @@ fn remove(builder: &mut SiteBuilder, path: &Path, relative_path: &Path) -> anyho
|
|||
builder.build_sass().context("Failed to rebuild Sass")?;
|
||||
} else if let Ok(root_path) = relative_path.strip_prefix(ROOT_PATH) {
|
||||
std::fs::remove_file(builder.build_path.join(root_path))?;
|
||||
} else if let Ok(_image_path) = relative_path.strip_prefix(crate::images::IMAGES_PATH) {
|
||||
let p = ImageMetadata::build_path(&builder.build_path, &ImageMetadata::get_id(path));
|
||||
std::fs::remove_file(p)?;
|
||||
// HACK: same as in `create`
|
||||
builder.build_images()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -134,6 +146,9 @@ impl Site {
|
|||
}
|
||||
}
|
||||
builder.build_sass().context("Failed to build Sass")?;
|
||||
builder
|
||||
.build_images()
|
||||
.context("Failed to build image pages")?;
|
||||
|
||||
// Map of websocket connections
|
||||
let peers: Arc<Mutex<HashMap<SocketAddr, WebSocket>>> =
|
||||
|
@ -238,13 +253,16 @@ impl Site {
|
|||
.and_then(move |path: FullPath, build_path: PathBuf| async move {
|
||||
// Serve static files
|
||||
let p = &path.as_str()[1..];
|
||||
let p = percent_encoding::percent_decode_str(p)
|
||||
.decode_utf8()
|
||||
.expect("Failed to decode URL");
|
||||
|
||||
if p == "static/_dev.js" {
|
||||
let res = Response::new(include_str!("./refresh_websocket.js").into());
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
let mut p = build_path.join(p);
|
||||
let mut p = build_path.join(p.as_ref());
|
||||
|
||||
if !p.exists() {
|
||||
p = p.with_extension("html");
|
||||
|
|
Loading…
Add table
Reference in a new issue