make webui build properly now

This commit is contained in:
saji 2024-08-05 17:59:42 -05:00
parent 67e425c946
commit c50f8cb955
3 changed files with 143 additions and 59 deletions

View file

@ -1,67 +1,17 @@
use crate::display::{create_display_thread, EInkPanel};
use crate::dither::{DitherMethod, DitheredImage};
use crate::eink::Palette;
use axum::async_trait;
use axum::extract::{FromRequest, Multipart, State};
use axum::http::{header, StatusCode};
use axum::response::IntoResponse;
use axum::{response::Response, routing::post, Router};
use axum::{routing::post, Router};
use image::{imageops::resize, imageops::FilterType, ImageReader, RgbImage};
use std::io::Cursor;
use std::str;
use std::str::FromStr;
use std::sync::mpsc;
use std::sync::Arc;
use std::thread::JoinHandle;
use tracing::{error, info, instrument};
use tracing::{info, instrument};
#[derive(thiserror::Error, Debug)]
pub enum ApiError {
#[error("missing image field")]
MissingImage,
}
#[derive(Clone)]
pub struct AppState {
display_channel: mpsc::Sender<Box<DitheredImage>>,
display_task: Arc<JoinHandle<()>>,
}
impl AppState {
#[must_use]
pub fn new(disp: Box<dyn EInkPanel + Send>) -> Self {
let (handle, tx) = create_display_thread(disp);
Self {
display_channel: tx,
display_task: Arc::new(handle),
}
}
}
// Make our own error that wraps `anyhow::Error`.
struct AppError(anyhow::Error);
// Tell axum how to convert `AppError` into a response.
impl IntoResponse for AppError {
fn into_response(self) -> Response {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", self.0),
)
.into_response()
}
}
// This enables using `?` on functions that return `Result<_, anyhow::Error>` to turn them into
// `Result<_, AppError>`. That way you don't need to do that manually.
impl<E> From<E> for AppError
where
E: Into<anyhow::Error>,
{
fn from(err: E) -> Self {
Self(err.into())
}
}
use crate::app::{ApiError, AppError, AppState};
/// API routes for axum
/// Start with the basics: Send an image, crop it, dither, and upload.
@ -132,7 +82,6 @@ async fn set_image(
State(ctx): State<AppState>,
img_req: ImageRequest,
) -> Result<impl IntoResponse, AppError> {
// FIXME: resize image to 800x480 to match the eink panel.
info!("Got image");
let mut buf = DitheredImage::new(800, 480, img_req.palette.value().to_vec());
let resized = resize(&*img_req.image, 800, 480, FilterType::Lanczos3);

136
src/app.rs Normal file
View file

@ -0,0 +1,136 @@
use crate::api;
use crate::display::create_display_thread;
use crate::dither::DitheredImage;
use axum::extract::{Path, State};
use axum::http::{header, StatusCode};
use axum::response::{IntoResponse, Response};
use axum::routing::get;
use axum::Router;
use include_dir::{include_dir, Dir, DirEntry};
use minijinja::Environment;
use std::sync::mpsc;
use std::sync::Arc;
use std::thread::JoinHandle;
use tracing::{debug, warn};
use tracing::{error, info, instrument};
#[derive(thiserror::Error, Debug)]
pub enum ApiError {
#[error("missing image field")]
MissingImage,
}
#[derive(Clone)]
pub struct AppState {
pub display_channel: mpsc::Sender<Box<DitheredImage>>,
pub display_task: Arc<JoinHandle<()>>,
pub templates: Arc<Environment<'static>>,
}
impl Default for AppState {
#[must_use]
fn default() -> Self {
let (handle, tx) = create_display_thread();
Self {
display_channel: tx,
display_task: Arc::new(handle),
templates: Arc::new(make_environment().expect("valid")),
}
}
}
/// Represents an error that can be returned to the requester by implementing ``IntoResponse``.
/// This wraps ``anyhow::Error`` so that it can accept any error object.
pub struct AppError(anyhow::Error);
// Tell axum how to convert `AppError` into a response.
impl IntoResponse for AppError {
fn into_response(self) -> Response {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", self.0),
)
.into_response()
}
}
// This enables using `?` on functions that return `Result<_, anyhow::Error>` to turn them into
// `Result<_, AppError>`. That way you don't need to do that manually.
impl<E> From<E> for AppError
where
E: Into<anyhow::Error>,
{
fn from(err: E) -> Self {
Self(err.into())
}
}
static TEMPLATES_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates");
static ASSETS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/static");
#[instrument]
fn make_environment() -> Result<Environment<'static>, anyhow::Error> {
let mut env = Environment::new();
let mut entries = vec![&TEMPLATES_DIR];
while let Some(dir) = entries.pop() {
for entry in dir.entries() {
match entry {
DirEntry::Dir(d) => {
entries.push(d);
}
DirEntry::File(f) => {
debug!("adding {:?} to minijinja environment", f.path());
env.add_template(
f.path().to_str().expect("valid pathname"),
f.contents_utf8().expect("contents are valid"),
)?;
}
}
}
}
info!(
"loaded {} templates into environment",
env.templates().count()
);
Ok(env)
}
pub fn make_app_router() -> Router {
Router::new()
.route("/app/*path", get(app_handler))
.nest("/api", api::router())
.route("/assets", get(asset_handler))
.with_state(AppState::default())
}
#[instrument(skip(ctx))]
async fn app_handler(
State(ctx): State<AppState>,
Path(path): Path<String>,
) -> Result<impl IntoResponse, AppError> {
let template = ctx.templates.get_template(&path.to_string())?;
let content = template.render("")?;
let headers = [(header::CONTENT_TYPE, mime::TEXT_HTML_UTF_8.to_string())];
Ok((StatusCode::OK, headers, content))
}
async fn asset_handler() -> Result<impl IntoResponse, AppError> {
Ok(StatusCode::OK)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_environment() -> anyhow::Result<()> {
make_environment()?;
Ok(())
}
}

View file

@ -1,12 +1,12 @@
pub mod api;
pub mod app;
pub mod display;
pub mod dither;
pub mod eink;
pub mod app;
use serde::Deserialize;
use tower_http::trace::TraceLayer;
use std::path::PathBuf;
use tower_http::trace::TraceLayer;
use crate::display::get_display;
use crate::dither::{DitherMethod, DitheredImage};
@ -84,10 +84,9 @@ async fn main() -> anyhow::Result<()> {
display.display(&eink_buf)?;
}
Command::Serve => {
let display = get_display();
let ctx = api::AppState::new(display);
let app = api::router().with_state(ctx).layer(TraceLayer::new_for_http());
let app = app::make_app_router()
.layer(TraceLayer::new_for_http());
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
info!("Listening on 0.0.0.0:3000");
axum::serve(listener, app).await?;