Feat: simple html frontend
This commit is contained in:
parent
65401700c0
commit
e496324049
9 changed files with 253 additions and 5 deletions
|
@ -13,3 +13,6 @@ tokio = { version = "1.0", features = ["full"] }
|
|||
base64 = "0.21.0"
|
||||
rand = "0.8.5"
|
||||
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
90
src/color.rs
Normal 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());
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
use axum::Router;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
mod color;
|
||||
mod polygon;
|
||||
mod routes;
|
||||
|
||||
|
@ -11,6 +12,7 @@ pub struct SharedState {}
|
|||
async fn main() {
|
||||
let state = SharedState {};
|
||||
let app = Router::new()
|
||||
.nest("/", routes::static_files::routes())
|
||||
.nest("/logo", routes::logo::routes())
|
||||
.nest("/favicon.ico", routes::favicon::routes())
|
||||
.with_state(state);
|
||||
|
|
|
@ -1,27 +1,45 @@
|
|||
use axum::{extract::Query, routing::get, Router};
|
||||
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 {
|
||||
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)]
|
||||
struct ImageProperties {
|
||||
#[serde(default = "default_as_false")]
|
||||
#[serde(deserialize_with = "deserialize_bool")]
|
||||
dark_mode: bool,
|
||||
background_color: Option<Color>,
|
||||
}
|
||||
|
||||
async fn handler(Query(properties): Query<ImageProperties>) -> impl axum::response::IntoResponse {
|
||||
let surface = ImageSurface::create(Format::ARgb32, 400, 400).unwrap();
|
||||
let context = Context::new(&surface).unwrap();
|
||||
|
||||
if properties.dark_mode {
|
||||
context.set_source_rgb(0.0, 0.0, 0.0);
|
||||
if let Some(c) = properties.background_color {
|
||||
context.set_source_rgb(c.r as f64 / 255.0, c.g as f64 / 255.0, c.b as f64 / 255.0);
|
||||
} 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();
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
pub mod favicon;
|
||||
pub mod logo;
|
||||
pub mod static_files;
|
||||
|
|
56
src/routes/static_files.rs
Normal file
56
src/routes/static_files.rs
Normal 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
72
static/index.html
Normal 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
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
1
static/pico.min.css.map
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue