dkdc-draw-webapp 0.1.0

Webapp for draw (axum)
Documentation
use axum::Router;
use axum::extract::State;
use axum::http::{HeaderMap, HeaderValue, header};
use axum::response::{Html, IntoResponse};
use axum::routing::{delete, get, post};
use std::net::SocketAddr;
use std::sync::Arc;

mod api;

const INDEX_HTML: &str = include_str!("../frontend/index.html");
const STYLE_CSS: &str = include_str!("../frontend/style.css");
const APP_JS: &str = include_str!("../frontend/app.js");
const CANVAS_JS: &str = include_str!("../frontend/canvas.js");
const INTERACTIONS_JS: &str = include_str!("../frontend/interactions.js");
const API_JS: &str = include_str!("../frontend/api.js");
const THEME_JS: &str = include_str!("../frontend/theme.js");
const FAVICON: &[u8] = include_bytes!("../frontend/favicon.png");
const WASM_GLUE_JS: &str = include_str!("../frontend/wasm_glue.js");
const WASM_BG: &[u8] = include_bytes!("../frontend/draw_wasm_bg.wasm");

const PORT: u16 = 1213;

struct AppState {
    open_id: Option<String>,
}

pub fn create_router(open_id: Option<String>) -> Router {
    let state = Arc::new(AppState { open_id });

    Router::new()
        .route("/", get(index))
        .route("/style.css", get(serve_css))
        .route("/favicon.png", get(serve_favicon))
        .route("/theme.js", get(serve_theme_js))
        .route("/app.js", get(serve_app_js))
        .route("/canvas.js", get(serve_canvas_js))
        .route("/interactions.js", get(serve_interactions_js))
        .route("/api.js", get(serve_api_js))
        .route("/wasm_glue.js", get(serve_wasm_glue_js))
        .route("/draw_wasm_bg.wasm", get(serve_wasm_bg))
        .route("/api/drawings", get(api::list_drawings))
        .route("/api/drawings", post(api::save_drawing))
        .route("/api/drawings/{id}", get(api::load_drawing))
        .route("/api/drawings/{id}", delete(api::delete_drawing))
        .route("/api/export/svg", post(api::export_svg))
        .route("/api/export/png", post(api::export_png))
        .with_state(state)
}

async fn index(State(state): State<Arc<AppState>>) -> Html<String> {
    let open_script = if let Some(id) = &state.open_id {
        format!(r#"<script>window.__OPEN_DRAWING_ID = "{id}";</script>"#)
    } else {
        String::new()
    };
    Html(INDEX_HTML.replace("<!-- OPEN_SCRIPT -->", &open_script))
}

async fn serve_css() -> impl IntoResponse {
    let mut headers = HeaderMap::new();
    headers.insert(header::CONTENT_TYPE, HeaderValue::from_static("text/css"));
    (headers, STYLE_CSS)
}

async fn serve_favicon() -> impl IntoResponse {
    let mut headers = HeaderMap::new();
    headers.insert(header::CONTENT_TYPE, HeaderValue::from_static("image/png"));
    (headers, FAVICON)
}
async fn serve_theme_js() -> impl IntoResponse {
    js_response(THEME_JS)
}
async fn serve_app_js() -> impl IntoResponse {
    js_response(APP_JS)
}
async fn serve_canvas_js() -> impl IntoResponse {
    js_response(CANVAS_JS)
}
async fn serve_interactions_js() -> impl IntoResponse {
    js_response(INTERACTIONS_JS)
}
async fn serve_api_js() -> impl IntoResponse {
    js_response(API_JS)
}
async fn serve_wasm_glue_js() -> impl IntoResponse {
    js_response(WASM_GLUE_JS)
}
async fn serve_wasm_bg() -> impl IntoResponse {
    let mut headers = HeaderMap::new();
    headers.insert(
        header::CONTENT_TYPE,
        HeaderValue::from_static("application/wasm"),
    );
    (headers, WASM_BG)
}

fn js_response(content: &'static str) -> impl IntoResponse {
    let mut headers = HeaderMap::new();
    headers.insert(
        header::CONTENT_TYPE,
        HeaderValue::from_static("application/javascript"),
    );
    (headers, content)
}

pub fn run_webapp(open_id: Option<String>) -> anyhow::Result<()> {
    let rt = tokio::runtime::Runtime::new()?;
    rt.block_on(async {
        let port = PORT;
        let app = create_router(open_id);
        let addr = SocketAddr::from(([127, 0, 0, 1], port));

        let listener = tokio::net::TcpListener::bind(addr).await?;
        let actual_addr = listener.local_addr()?;

        println!("draw webapp: http://localhost:{}", actual_addr.port());
        let _ = open::that(format!("http://localhost:{}", actual_addr.port()));

        axum::serve(listener, app)
            .with_graceful_shutdown(async {
                tokio::signal::ctrl_c().await.ok();
                println!("\nshutting down...");
            })
            .await?;
        Ok(())
    })
}