Serve new UI from axum server
This commit is contained in:
parent
5e60dd2e30
commit
38994a86ae
6 changed files with 51 additions and 69 deletions
1
showbits-thermal-printer/dist
Symbolic link
1
showbits-thermal-printer/dist
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../showbits-thermal-printer-ui/dist
|
||||
|
|
@ -12,8 +12,6 @@ use tokio::{net::TcpListener, sync::mpsc};
|
|||
|
||||
use crate::{documents, drawer::Command};
|
||||
|
||||
use self::r#static::get_static_file;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Server {
|
||||
tx: mpsc::Sender<Command>,
|
||||
|
|
@ -27,6 +25,12 @@ impl Server {
|
|||
|
||||
pub async fn run(tx: mpsc::Sender<Command>, addr: String) -> anyhow::Result<()> {
|
||||
let app = Router::new()
|
||||
// Files
|
||||
.route("/", get(r#static::get_index))
|
||||
.route("/assets/{*path}", get(r#static::get_asset))
|
||||
.route("/fonts/{*path}", get(r#static::get_font))
|
||||
.route("/photo.html", get(r#static::get_photo))
|
||||
// API
|
||||
.route("/api/calendar", post(documents::calendar::post))
|
||||
.route("/api/cells", post(documents::cells::post))
|
||||
.route("/api/chat", post(documents::chat::post))
|
||||
|
|
@ -34,7 +38,7 @@ pub async fn run(tx: mpsc::Sender<Command>, addr: String) -> anyhow::Result<()>
|
|||
.route("/api/image", post(documents::image::post))
|
||||
.route("/api/text", post(documents::text::post))
|
||||
.route("/api/tictactoe", post(documents::tictactoe::post))
|
||||
.fallback(get(get_static_file))
|
||||
// Rest
|
||||
.layer(DefaultBodyLimit::max(32 * 1024 * 1024)) // 32 MiB
|
||||
.with_state(Server { tx });
|
||||
|
||||
|
|
|
|||
|
|
@ -1,62 +1,53 @@
|
|||
use axum::{
|
||||
http::{StatusCode, Uri, header},
|
||||
extract::Path,
|
||||
http::{StatusCode, header},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use rust_embed::RustEmbed;
|
||||
use showbits_assets::{
|
||||
UNIFONT, UNIFONT_JP, UNIFONT_JP_NAME, UNIFONT_NAME, UNIFONT_UPPER, UNIFONT_UPPER_NAME,
|
||||
};
|
||||
|
||||
use super::statuscode::status_code;
|
||||
|
||||
#[derive(RustEmbed)]
|
||||
#[folder = "static"]
|
||||
struct StaticFiles;
|
||||
#[folder = "dist/assets"]
|
||||
struct Assets;
|
||||
|
||||
struct StaticFile(String);
|
||||
|
||||
fn look_up_path(path: &str) -> Option<Response> {
|
||||
let path = path.trim_start_matches('/');
|
||||
let file = StaticFiles::get(path)?;
|
||||
let mime = mime_guess::from_path(path).first_or_octet_stream();
|
||||
let response = ([(header::CONTENT_TYPE, mime.as_ref())], file.data).into_response();
|
||||
Some(response)
|
||||
}
|
||||
|
||||
impl IntoResponse for StaticFile {
|
||||
fn into_response(self) -> Response {
|
||||
let mut path = self.0;
|
||||
if path.is_empty() {
|
||||
path.push('/')
|
||||
};
|
||||
|
||||
if path.ends_with(".html") {
|
||||
// A file `/foo/bar.html` should not be accessible directly, only
|
||||
// indirectly at `/foo/bar`.
|
||||
return status_code(StatusCode::NOT_FOUND);
|
||||
pub async fn get_asset(Path(path): Path<String>) -> impl IntoResponse {
|
||||
match Assets::get(&path) {
|
||||
None => status_code(StatusCode::NOT_FOUND),
|
||||
Some(content) => {
|
||||
let mime = mime_guess::from_path(&path).first_or_octet_stream();
|
||||
([(header::CONTENT_TYPE, mime.as_ref())], content.data).into_response()
|
||||
}
|
||||
|
||||
if path.ends_with("/index") {
|
||||
// A file `/foo/index.html` should not be accessible directly, only
|
||||
// indirectly at `/foo/`.
|
||||
return status_code(StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
if path.ends_with('/') {
|
||||
path.push_str("index");
|
||||
}
|
||||
|
||||
if let Some(response) = look_up_path(&path) {
|
||||
return response;
|
||||
}
|
||||
|
||||
path.push_str(".html");
|
||||
|
||||
if let Some(response) = look_up_path(&path) {
|
||||
return response;
|
||||
}
|
||||
|
||||
status_code(StatusCode::NOT_FOUND)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_static_file(uri: Uri) -> impl IntoResponse {
|
||||
StaticFile(uri.path().to_string())
|
||||
pub async fn get_font(Path(path): Path<String>) -> Response {
|
||||
let font = if path == UNIFONT_NAME {
|
||||
UNIFONT
|
||||
} else if path == UNIFONT_JP_NAME {
|
||||
UNIFONT_JP
|
||||
} else if path == UNIFONT_UPPER_NAME {
|
||||
UNIFONT_UPPER
|
||||
} else {
|
||||
return status_code(StatusCode::NOT_FOUND);
|
||||
};
|
||||
|
||||
([(header::CONTENT_TYPE, "font/otf")], font).into_response()
|
||||
}
|
||||
|
||||
pub async fn get_index() -> impl IntoResponse {
|
||||
(
|
||||
[(header::CONTENT_TYPE, "text/html; charset=utf-8")],
|
||||
include_str!("../../dist/index.html"),
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn get_photo() -> impl IntoResponse {
|
||||
(
|
||||
[(header::CONTENT_TYPE, "text/html; charset=utf-8")],
|
||||
include_str!("../../dist/photo.html"),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>TP: Index</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Thermal Printer Control</h1>
|
||||
<ul>
|
||||
<li><a href="image">Upload an image</a></li>
|
||||
<li><a href="photo">Take a photo</a></li>
|
||||
<li><a href="egg">Osterei</a></li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,160 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, height=device-height, initial-scale=1, user-scalable=0"
|
||||
/>
|
||||
<title>Instant Photo</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
video {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
video.mirrored {
|
||||
scale: -1 1;
|
||||
}
|
||||
|
||||
#button {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
|
||||
border: 10px solid #f00;
|
||||
border-radius: 100px;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
#button:active {
|
||||
border-color: #fff;
|
||||
}
|
||||
|
||||
#button .circle {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 60px;
|
||||
margin: auto;
|
||||
|
||||
background-color: #f00;
|
||||
}
|
||||
|
||||
#button:active .circle {
|
||||
background-color: #a00;
|
||||
}
|
||||
|
||||
#flip {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
|
||||
box-sizing: border-box;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
|
||||
background-color: transparent;
|
||||
border: 5px solid #fff;
|
||||
border-radius: 100px;
|
||||
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
#flip:active {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
#flip svg {
|
||||
width: 60%;
|
||||
height: 60%;
|
||||
|
||||
position: relative;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
#flip:active path {
|
||||
fill: #000;
|
||||
}
|
||||
</style>
|
||||
<script type="module">
|
||||
const video = document.getElementById("video");
|
||||
const button = document.getElementById("button");
|
||||
const flip = document.getElementById("flip");
|
||||
|
||||
const facing = new URLSearchParams(window.location.search).get("facing");
|
||||
|
||||
function getStreamFacingMode(stream) {
|
||||
if (!stream) return null;
|
||||
const videos = stream.getVideoTracks();
|
||||
if (videos.length === 0) return null;
|
||||
const video = videos[0];
|
||||
return video.getSettings().facingMode;
|
||||
}
|
||||
|
||||
async function initStream(facingMode) {
|
||||
// Display video
|
||||
let stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: { facingMode: { ideal: facingMode } },
|
||||
});
|
||||
video.srcObject = stream;
|
||||
|
||||
// Flip video horizontally if it's facing the user
|
||||
const facing = getStreamFacingMode(stream);
|
||||
if (facing !== "environment") {
|
||||
video.classList.add("mirrored");
|
||||
}
|
||||
|
||||
// Enable or disable flip button
|
||||
const canFlip = facing !== undefined;
|
||||
const facingOpposite = facing === "user" ? "environment" : "user";
|
||||
flip.hidden = !canFlip;
|
||||
flip.setAttribute("href", `?facing=${facingOpposite}`);
|
||||
}
|
||||
|
||||
await initStream(facing);
|
||||
|
||||
button.addEventListener("click", () => {
|
||||
const canvas = document.createElement("canvas");
|
||||
const scale = 384 / video.videoWidth;
|
||||
canvas.width = video.videoWidth * scale;
|
||||
canvas.height = video.videoHeight * scale;
|
||||
canvas
|
||||
.getContext("2d")
|
||||
.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
canvas.toBlob((blob) => {
|
||||
const form = new FormData();
|
||||
form.append("image", blob);
|
||||
form.append("caption", new Date().toLocaleString());
|
||||
|
||||
fetch("image", { method: "POST", body: form }).catch((error) => {
|
||||
console.error("Error uploading image:", error);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<video id="video" autoplay playsinline></video>
|
||||
<button id="button"><div class="circle"></div></button>
|
||||
<a id="flip" hidden>
|
||||
<svg viewBox="0 0 6 6">
|
||||
<path fill="#fff" stroke="none" d="M0,2h1v4h1v-4h1l-1.5,-2"></path>
|
||||
<path fill="#fff" stroke="none" d="M3,4h1v-4h1v4h1l-1.5,2"></path>
|
||||
</svg>
|
||||
</a>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue