Feat: simple html frontend

This commit is contained in:
Dorian Zedler 2023-03-11 22:45:14 +01:00
parent 65401700c0
commit e496324049
Signed by: dozedler
GPG key ID: 989DE36109AFA354
9 changed files with 253 additions and 5 deletions

View file

@ -13,3 +13,6 @@ tokio = { version = "1.0", features = ["full"] }
base64 = "0.21.0" base64 = "0.21.0"
rand = "0.8.5" rand = "0.8.5"
serde = { version = "1.0.152", features = ["derive"] } serde = { version = "1.0.152", features = ["derive"] }
rust-embed = "6.6.0"
mime_guess = "2.0.4"
serde_json = "1.0.94"

90
src/color.rs Normal file
View file

@ -0,0 +1,90 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug)]
pub struct Color {
pub r: u8,
pub g: u8,
pub b: u8,
}
impl<'de> Deserialize<'de> for Color {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let buf = String::deserialize(deserializer)?;
if buf.len() != 7 {
return Err(serde::de::Error::custom("color must be 7 chars long"));
}
let mut chars = buf.chars().collect::<Vec<char>>();
if chars.remove(0) != '#' {
return Err(serde::de::Error::custom("color must start with #"));
}
let colors = chars
.chunks(2)
.map(|c| c.iter().collect::<String>())
.map(|s| parse_color(&s))
.collect::<Result<Vec<u8>, D::Error>>()?;
Ok(Color {
r: colors[0],
g: colors[1],
b: colors[2],
})
}
}
impl Serialize for Color {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let string = format!("#{:02X}{:02X}{:02X}", self.r, self.g, self.b);
serializer.serialize_str(&string)
}
}
fn parse_color<E>(hex_code: &str) -> Result<u8, E>
where
E: serde::de::Error,
{
let res = u8::from_str_radix(hex_code, 16);
if res.is_err() {
return Err(E::custom("could not deserialize color"));
}
Ok(res.unwrap())
}
#[cfg(test)]
#[test]
fn test_serialize_color() {
let color = Color {
r: 10,
g: 20,
b: 30,
};
let serialized_color = serde_json::to_string(&color).unwrap();
assert_eq!(serialized_color, "\"#0A141E\"");
}
#[cfg(test)]
#[test]
fn test_deserialize_color() {
let color = "\"#0A141E\"";
let deserialized_color: Color = serde_json::from_str(color).unwrap();
assert_eq!(deserialized_color.r, 10);
assert_eq!(deserialized_color.g, 20);
assert_eq!(deserialized_color.b, 30);
assert!(serde_json::from_str::<Color>("\"000000\"").is_err());
assert!(serde_json::from_str::<Color>("\"#00000\"").is_err());
assert!(serde_json::from_str::<Color>("\"#0000000\"").is_err());
assert!(serde_json::from_str::<Color>("\"#G00000\"").is_err());
}

View file

@ -1,6 +1,7 @@
use axum::Router; use axum::Router;
use std::net::SocketAddr; use std::net::SocketAddr;
mod color;
mod polygon; mod polygon;
mod routes; mod routes;
@ -11,6 +12,7 @@ pub struct SharedState {}
async fn main() { async fn main() {
let state = SharedState {}; let state = SharedState {};
let app = Router::new() let app = Router::new()
.nest("/", routes::static_files::routes())
.nest("/logo", routes::logo::routes()) .nest("/logo", routes::logo::routes())
.nest("/favicon.ico", routes::favicon::routes()) .nest("/favicon.ico", routes::favicon::routes())
.with_state(state); .with_state(state);

View file

@ -1,27 +1,45 @@
use axum::{extract::Query, routing::get, Router}; use axum::{extract::Query, routing::get, Router};
use cairo::{Context, Format, ImageSurface}; use cairo::{Context, Format, ImageSurface};
use serde::Deserialize; use serde::{de, Deserialize};
use crate::{polygon, SharedState}; use crate::{color::Color, polygon, SharedState};
fn default_as_false() -> bool { fn default_as_false() -> bool {
false false
} }
fn deserialize_bool<'de, D>(deserializer: D) -> Result<bool, D::Error>
where
D: de::Deserializer<'de>,
{
let s: &str = de::Deserialize::deserialize(deserializer)?;
match s {
"true" | "on" => Ok(true),
"false" | "off" => Ok(false),
_ => Err(de::Error::unknown_variant(
s,
&["true", "on", "false", "off"],
)),
}
}
#[derive(Deserialize)] #[derive(Deserialize)]
struct ImageProperties { struct ImageProperties {
#[serde(default = "default_as_false")] #[serde(default = "default_as_false")]
#[serde(deserialize_with = "deserialize_bool")]
dark_mode: bool, dark_mode: bool,
background_color: Option<Color>,
} }
async fn handler(Query(properties): Query<ImageProperties>) -> impl axum::response::IntoResponse { async fn handler(Query(properties): Query<ImageProperties>) -> impl axum::response::IntoResponse {
let surface = ImageSurface::create(Format::ARgb32, 400, 400).unwrap(); let surface = ImageSurface::create(Format::ARgb32, 400, 400).unwrap();
let context = Context::new(&surface).unwrap(); let context = Context::new(&surface).unwrap();
if properties.dark_mode { if let Some(c) = properties.background_color {
context.set_source_rgb(0.0, 0.0, 0.0); context.set_source_rgb(c.r as f64 / 255.0, c.g as f64 / 255.0, c.b as f64 / 255.0);
} else { } else {
context.set_source_rgb(1.0, 1.0, 1.0); context.set_source_rgba(0.0, 0.0, 0.0, 0.0);
} }
context.paint().unwrap(); context.paint().unwrap();

View file

@ -1,2 +1,3 @@
pub mod favicon; pub mod favicon;
pub mod logo; pub mod logo;
pub mod static_files;

View file

@ -0,0 +1,56 @@
use axum::{
body::{boxed, Full},
http::{header, StatusCode, Uri},
response::{IntoResponse, Response},
Router,
};
use rust_embed::{EmbeddedFile, RustEmbed};
use crate::SharedState;
#[derive(RustEmbed)]
#[folder = "static"]
struct StaticFiles;
async fn static_files(uri: Uri) -> impl IntoResponse {
let path = uri.path().trim_start_matches('/');
match StaticFiles::get(path) {
Some(file) => response(file, path),
None => {
if uri.path() == "/" {
index()
} else {
not_found()
}
}
}
}
fn index() -> Response {
match StaticFiles::get("index.html") {
Some(file) => response(file, "index.html"),
None => not_found(),
}
}
fn response(file: EmbeddedFile, path: &str) -> Response {
let body = boxed(Full::from(file.data));
let mime = mime_guess::from_path(path).first_or_octet_stream();
Response::builder()
.header(header::CONTENT_TYPE, mime.as_ref())
.body(body)
.unwrap()
}
fn not_found() -> Response {
Response::builder()
.status(StatusCode::NOT_FOUND)
.body(boxed(Full::from("404")))
.unwrap()
}
pub fn routes() -> Router<SharedState> {
Router::new().fallback(static_files)
}

72
static/index.html Normal file
View file

@ -0,0 +1,72 @@
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<meta viewport="width=device-width, initial-scale=1.0">
<title>MakerLab Murnau Logo generator</title>
<link rel="stylesheet" href="pico.min.css">
<style>
@media (prefers-color-scheme: light) {
.dark-only {
display: none;
}
}
@media (prefers-color-scheme: dark) {
.light-only {
display: none;
}
}
</style>
</head>
<body>
<main class="container">
<nav>
<ul>
<li><img src="logo" style="height: 50px" class="light-only" /> <img src="logo?dark_mode=true"
style="height: 50px" class="dark-only" /> <strong>MakerLab Murnau Logo generator</strong></li>
</ul>
<ul>
<li><a href="https://makerlab-murnau.de">MakerLab Website</a></li>
<li><a href="https://git.makerlab-murnau.de/Projekte/logo-generator">Source code</a></li>
</ul>
</nav>
<p>You can use this website to generate diffrent variation of the logo of the MakerLab Murnau e.V.</p>
<details>
<summary>Full-size Logo</summary>
<form action="logo" method="get">
<fieldset>
<div class="grid">
<label for="logo_background_color">
Background color
</label>
<input type="color" id="logo_background_color" name="background_color" value="">
</div>
<label for="logo_dark_mode">
<input type="checkbox" id="logo_dark_mode" name="dark_mode" role="switch">
Dark mode
</label>
</fieldset>
<div class="grid">
<button type="submit">Generate</button>
<button type="reset">Reset</button>
</div>
</form>
</details>
<details>
<summary>Favicon</summary>
<form action="favicon.ico" method="get">
<button type="submit">Generate</button>
</form>
</details>
</main>
</body>

5
static/pico.min.css vendored Normal file

File diff suppressed because one or more lines are too long

1
static/pico.min.css.map Normal file

File diff suppressed because one or more lines are too long