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"
|
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
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 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);
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
pub mod favicon;
|
pub mod favicon;
|
||||||
pub mod logo;
|
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