diff --git a/Cargo.lock b/Cargo.lock index f281d82..45d9adb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "chara" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2653cb9243a7d4453127141c162c1ba619d99c7230f1d8ad0a9cdab9cf0651ea" + [[package]] name = "data-stream" version = "0.3.0" @@ -243,6 +249,7 @@ dependencies = [ name = "pukram-server" version = "0.1.0" dependencies = [ + "chara", "data-stream", "dialogi", "event-simulation", diff --git a/Cargo.toml b/Cargo.toml index 41ab646..6696be0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,3 +16,4 @@ indexmap = "2.9.0" multilinear = "0.3.1" multilinear-parser = "0.3.3" event-simulation = "0.1.2" +chara = "0.2.0" diff --git a/src/chara.rs b/src/chara.rs new file mode 100644 index 0000000..2fa5da6 --- /dev/null +++ b/src/chara.rs @@ -0,0 +1,112 @@ +use chara::CharacterDefinition; +use maud::{Markup, html}; + +pub fn render_character(def: &CharacterDefinition, relative_path: &str) -> Markup { + let relative_path = relative_path.rsplit_once('/'); + let relative_path = relative_path.map_or("", |(l, _r)| l); + + html! { + style { (CHARACTER_CSS) } + + div.customizer-container { + div.character-container { + @for layer in &def.layers { + img.character-layer + id=(format!("{}-layer", layer.internal_name)) + src=(layer.entries.first().map(|e| format!("/{relative_path}/{}", e.path)).unwrap_or_default()) + style=(format!("display: {};", + if !layer.entries.is_empty() && !layer.entries[0].path.is_empty() { + "block" + } else { + "none" + } + )); + } + } + + div.controls-column { + @for layer in &def.layers { + @if let Some(display_name) = &layer.display_name { + div.layer-group { + div.layer-title { (display_name) } + div.layer-options { + @for (i, entry) in layer.entries.iter().enumerate() { + label.option { + input type="radio" + name=(layer.internal_name) + value=(entry.path) + checked[i==0] + onchange=(format!( + "var img=document.getElementById('{}-layer');img.src=this.value;img.style.display=this.value==''?'none':'block';", + layer.internal_name + )); + (entry.name) + } + } + } + } + } + } + } + } + } +} + +const CHARACTER_CSS: &str = r" + .customizer-container { display: flex; gap: 20px; margin-top: 20px; } + .character-container { + position: relative; + width: 300px; + height: 400px; + border: 2px solid #ddd; + border-radius: 10px; + background-color: white; + flex-shrink: 0; + } + .character-layer { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + object-fit: contain; + } + .controls-column { + flex-grow: 1; + display: flex; + flex-direction: column; + gap: 15px; + } + .layer-group { + background-color: white; + padding: 15px; + border-radius: 5px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + } + .layer-title { + font-weight: bold; + margin-bottom: 10px; + color: #333; + } + .layer-options { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); + gap: 8px; + } + .option { + display: flex; + align-items: center; + } + .option input { + margin-right: 8px; + } + .back-link { + display: inline-block; + margin-bottom: 15px; + color: #333; + text-decoration: none; + } + .back-link:hover { + text-decoration: underline; + } +"; diff --git a/src/main.rs b/src/main.rs index 85d8094..d0c16d5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,6 +14,7 @@ use std::{ time::Duration, }; +use ::chara::CharacterDefinition; use data_stream::{ FromStream, ToStream, collections::SizeSettings, default_settings::PortableSettings, from_stream, to_stream, @@ -24,10 +25,12 @@ use percent_encoding::percent_decode_str; use pukram2html::{Settings, convert, convert_extended, convert_subheader}; use threadpool::ThreadPool; +mod chara; mod dialog; mod request; mod vn; +use chara::render_character; use request::Request; use vn::render_novel; @@ -374,6 +377,7 @@ fn handle_connection( reply_binary(stream, &relative_path, "image", &ending, path) } "mp3" | "wav" => reply_binary(stream, &relative_path, "audio", &ending, path), + "chara" => reply_chara(stream, &relative_path, path), _ => fail(stream), } return; @@ -449,6 +453,28 @@ fn reply_binary( } } +fn load_character_file(path: &Path) -> Option { + std::fs::read_to_string(path) + .ok() + .map(|content| CharacterDefinition::parse(&content)) +} + +fn reply_chara(mut stream: TcpStream, relative_path: &str, mut path: PathBuf) { + path.push(relative_path); + + let Some(def) = load_character_file(&path) else { + fail(stream); + return; + }; + + let _ = write!(stream, "HTTP/1.1 200 OK\r\n"); + let _ = write!(stream, "Content-Type: text/html\r\n"); + let _ = write!(stream, "\r\n"); + + let html = render_character(&def, relative_path); + let _ = stream.write_all(html.into_string().as_bytes()); +} + fn handle_relative_connection( info: Arc, mut stream: TcpStream,