1use axum::Router;
2use axum::extract::State;
3use axum::http::{HeaderMap, HeaderValue, header};
4use axum::response::{Html, IntoResponse};
5use axum::routing::{delete, get, post};
6use std::net::SocketAddr;
7use std::sync::Arc;
8
9mod api;
10
11const INDEX_HTML: &str = include_str!("../frontend/index.html");
12const STYLE_CSS: &str = include_str!("../frontend/style.css");
13const APP_JS: &str = include_str!("../frontend/app.js");
14const CANVAS_JS: &str = include_str!("../frontend/canvas.js");
15const INTERACTIONS_JS: &str = include_str!("../frontend/interactions.js");
16const API_JS: &str = include_str!("../frontend/api.js");
17const THEME_JS: &str = include_str!("../frontend/theme.js");
18const FAVICON: &[u8] = include_bytes!("../frontend/favicon.png");
19const WASM_GLUE_JS: &str = include_str!("../frontend/wasm_glue.js");
20const WASM_BG: &[u8] = include_bytes!("../frontend/draw_wasm_bg.wasm");
21
22const PORT: u16 = 1213;
23
24struct AppState {
25 open_id: Option<String>,
26}
27
28pub fn create_router(open_id: Option<String>) -> Router {
29 let state = Arc::new(AppState { open_id });
30
31 Router::new()
32 .route("/", get(index))
33 .route("/style.css", get(serve_css))
34 .route("/favicon.png", get(serve_favicon))
35 .route("/theme.js", get(serve_theme_js))
36 .route("/app.js", get(serve_app_js))
37 .route("/canvas.js", get(serve_canvas_js))
38 .route("/interactions.js", get(serve_interactions_js))
39 .route("/api.js", get(serve_api_js))
40 .route("/wasm_glue.js", get(serve_wasm_glue_js))
41 .route("/draw_wasm_bg.wasm", get(serve_wasm_bg))
42 .route("/api/drawings", get(api::list_drawings))
43 .route("/api/drawings", post(api::save_drawing))
44 .route("/api/drawings/{id}", get(api::load_drawing))
45 .route("/api/drawings/{id}", delete(api::delete_drawing))
46 .route("/api/export/svg", post(api::export_svg))
47 .route("/api/export/png", post(api::export_png))
48 .with_state(state)
49}
50
51async fn index(State(state): State<Arc<AppState>>) -> Html<String> {
52 let open_script = if let Some(id) = &state.open_id {
53 format!(r#"<script>window.__OPEN_DRAWING_ID = "{id}";</script>"#)
54 } else {
55 String::new()
56 };
57 Html(INDEX_HTML.replace("<!-- OPEN_SCRIPT -->", &open_script))
58}
59
60async fn serve_css() -> impl IntoResponse {
61 let mut headers = HeaderMap::new();
62 headers.insert(header::CONTENT_TYPE, HeaderValue::from_static("text/css"));
63 (headers, STYLE_CSS)
64}
65
66async fn serve_favicon() -> impl IntoResponse {
67 let mut headers = HeaderMap::new();
68 headers.insert(header::CONTENT_TYPE, HeaderValue::from_static("image/png"));
69 (headers, FAVICON)
70}
71async fn serve_theme_js() -> impl IntoResponse {
72 js_response(THEME_JS)
73}
74async fn serve_app_js() -> impl IntoResponse {
75 js_response(APP_JS)
76}
77async fn serve_canvas_js() -> impl IntoResponse {
78 js_response(CANVAS_JS)
79}
80async fn serve_interactions_js() -> impl IntoResponse {
81 js_response(INTERACTIONS_JS)
82}
83async fn serve_api_js() -> impl IntoResponse {
84 js_response(API_JS)
85}
86async fn serve_wasm_glue_js() -> impl IntoResponse {
87 js_response(WASM_GLUE_JS)
88}
89async fn serve_wasm_bg() -> impl IntoResponse {
90 let mut headers = HeaderMap::new();
91 headers.insert(
92 header::CONTENT_TYPE,
93 HeaderValue::from_static("application/wasm"),
94 );
95 (headers, WASM_BG)
96}
97
98fn js_response(content: &'static str) -> impl IntoResponse {
99 let mut headers = HeaderMap::new();
100 headers.insert(
101 header::CONTENT_TYPE,
102 HeaderValue::from_static("application/javascript"),
103 );
104 (headers, content)
105}
106
107pub fn run_webapp(open_id: Option<String>) -> anyhow::Result<()> {
108 let rt = tokio::runtime::Runtime::new()?;
109 rt.block_on(async {
110 let port = PORT;
111 let app = create_router(open_id);
112 let addr = SocketAddr::from(([127, 0, 0, 1], port));
113
114 let listener = tokio::net::TcpListener::bind(addr).await?;
115 let actual_addr = listener.local_addr()?;
116
117 println!("draw webapp: http://localhost:{}", actual_addr.port());
118 let _ = open::that(format!("http://localhost:{}", actual_addr.port()));
119
120 axum::serve(listener, app)
121 .with_graceful_shutdown(async {
122 tokio::signal::ctrl_c().await.ok();
123 println!("\nshutting down...");
124 })
125 .await?;
126 Ok(())
127 })
128}