Compare commits

..

12 Commits

Author SHA1 Message Date
p11
3342de6067 Removed partial password feature 2025-08-09 21:49:52 +02:00
p11
20b1290821 Collapsed some if-let-ifs 2025-08-09 21:48:54 +02:00
p11
dad0adeb8c Defined comment system externally 2025-08-03 16:21:24 +02:00
p11
7e0c4c80a3 Updated vn-settings: Support for layer default paths 2025-07-21 23:02:51 +02:00
p11
40b88cefff Use specified colors for textboxes 2025-07-21 11:13:57 +02:00
p11
447df9d9ea Applied character transform correctly 2025-07-21 10:54:48 +02:00
p11
1309ac06b2 Immediately refresh the current site info 2025-05-31 15:48:50 +02:00
p11
f8f656fcd0 Moved tabs to separate module 2025-05-31 15:45:06 +02:00
p11
097a5303e9 Made tab content rendering a method of the tab info 2025-05-31 15:32:05 +02:00
p11
f7327f31e5 Moved tab structs and handlers out of relative connection handler 2025-05-31 15:27:13 +02:00
p11
09815b04f4 Simplified writing of the tab styles 2025-05-31 15:18:54 +02:00
p11
8e790e2409 Use Self instead of RequestData in method on RequestData 2025-05-31 15:10:12 +02:00
5 changed files with 366 additions and 366 deletions

14
Cargo.lock generated
View File

@@ -23,6 +23,14 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2653cb9243a7d4453127141c162c1ba619d99c7230f1d8ad0a9cdab9cf0651ea" checksum = "2653cb9243a7d4453127141c162c1ba619d99c7230f1d8ad0a9cdab9cf0651ea"
[[package]]
name = "comment-system"
version = "0.1.0"
source = "git+https://gitlab.com/porky11/comment-system#bdef78dd69fe726a7fdc44f58c6d97e2939fe743"
dependencies = [
"data-stream",
]
[[package]] [[package]]
name = "data-stream" name = "data-stream"
version = "0.3.0" version = "0.3.0"
@@ -250,6 +258,7 @@ name = "pukram-server"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"chara", "chara",
"comment-system",
"data-stream", "data-stream",
"dialogi", "dialogi",
"event-simulation", "event-simulation",
@@ -260,6 +269,7 @@ dependencies = [
"multilinear-parser", "multilinear-parser",
"percent-encoding", "percent-encoding",
"pukram2html", "pukram2html",
"simple-color",
"threadpool", "threadpool",
"vn-settings", "vn-settings",
] ]
@@ -345,9 +355,9 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]] [[package]]
name = "vn-settings" name = "vn-settings"
version = "0.1.1" version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3c5bd91cb7361d8d179deacf6e9b927a45a1e31c7f9c29555a49be81506381a" checksum = "53e699531365195d0efa500f6be7cbe287497630d40eb39d065e09ba0508c109"
dependencies = [ dependencies = [
"dialogi", "dialogi",
"indexmap", "indexmap",

View File

@@ -4,12 +4,13 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
simple-color = "0.2.3"
percent-encoding = "2.3" percent-encoding = "2.3"
maud = "0.27.0" maud = "0.27.0"
pukram2html = "0.3.0" pukram2html = "0.3.0"
data-stream = "0.3.0" data-stream = "0.3.0"
dialogi = "0.3.1" dialogi = "0.3.1"
vn-settings = "0.1.1" vn-settings = "0.1.2"
threadpool = "1.8.1" threadpool = "1.8.1"
header-config = "0.1.5" header-config = "0.1.5"
indexmap = "2.9.0" indexmap = "2.9.0"
@@ -17,3 +18,4 @@ multilinear = "0.3.1"
multilinear-parser = "0.3.3" multilinear-parser = "0.3.3"
event-simulation = "0.1.2" event-simulation = "0.1.2"
chara = "0.2.0" chara = "0.2.0"
comment-system = { git = "https://gitlab.com/porky11/comment-system" }

View File

@@ -2,7 +2,7 @@ use std::{
collections::{HashMap, hash_map::Entry}, collections::{HashMap, hash_map::Entry},
env, env,
fs::File, fs::File,
io::{BufReader, Error, ErrorKind, Result, prelude::*}, io::{BufReader, prelude::*},
net::{TcpListener, TcpStream}, net::{TcpListener, TcpStream},
path::{Path, PathBuf}, path::{Path, PathBuf},
process::ExitCode, process::ExitCode,
@@ -15,31 +15,22 @@ use std::{
}; };
use ::chara::CharacterDefinition; use ::chara::CharacterDefinition;
use data_stream::{ use comment_system::{Comment, SiteInfo};
FromStream, ToStream, collections::SizeSettings, default_settings::PortableSettings, use data_stream::{default_settings::PortableSettings, from_stream, to_stream};
from_stream, to_stream,
};
use header_config::parse_config; use header_config::parse_config;
use indexmap::IndexMap;
use maud::html; use maud::html;
use percent_encoding::percent_decode_str; use percent_encoding::percent_decode_str;
use pukram2html::{Settings, convert, convert_extended, convert_subheader}; use pukram2html::convert;
use threadpool::ThreadPool; use threadpool::ThreadPool;
mod chara; mod chara;
mod dialog; mod dialog;
mod request; mod request;
mod tabs;
mod vn; mod vn;
use chara::render_character;
use request::Request; use request::Request;
use vn::render_novel; use tabs::{Tab, TabInfo, write_tab_styles};
#[derive(Clone)]
struct Comment {
name: Box<str>,
text: Box<str>,
}
#[derive(Default)] #[derive(Default)]
struct SharedSiteInfo { struct SharedSiteInfo {
@@ -49,14 +40,6 @@ struct SharedSiteInfo {
down: AtomicUsize, down: AtomicUsize,
} }
#[derive(Default)]
struct SiteInfo {
comments: Vec<Comment>,
visits: usize,
up: usize,
down: usize,
}
impl SharedSiteInfo { impl SharedSiteInfo {
fn new(info: SiteInfo) -> Self { fn new(info: SiteInfo) -> Self {
let SiteInfo { let SiteInfo {
@@ -92,14 +75,14 @@ impl SharedSiteInfo {
Vec::new() Vec::new()
} }
}, },
visits: self.visits.fetch_add(1, Ordering::Relaxed), visits: self.visits.fetch_add(1, Ordering::Relaxed) + 1,
up: if up { up: if up {
self.up.fetch_add(1, Ordering::Relaxed) self.up.fetch_add(1, Ordering::Relaxed) + 1
} else { } else {
self.up.load(Ordering::Relaxed) self.up.load(Ordering::Relaxed)
}, },
down: if down { down: if down {
self.down.fetch_add(1, Ordering::Relaxed) self.down.fetch_add(1, Ordering::Relaxed) + 1
} else { } else {
self.down.load(Ordering::Relaxed) self.down.load(Ordering::Relaxed)
}, },
@@ -132,8 +115,7 @@ fn main() -> ExitCode {
let address = args.next(); let address = args.next();
let address = address.as_deref().unwrap_or("127.0.0.1:8080"); let address = address.as_deref().unwrap_or("127.0.0.1:8080");
let password = args.next(); let password = args.next();
let partial_password = args.next(); start_server(path, address, password);
start_server(path, address, password, partial_password);
ExitCode::FAILURE ExitCode::FAILURE
} }
@@ -143,12 +125,7 @@ fn get_thread_pool_size() -> usize {
.unwrap_or(4) .unwrap_or(4)
} }
fn start_server( fn start_server(path: PathBuf, address: &str, password: Option<String>) {
path: PathBuf,
address: &str,
password: Option<String>,
partial_password: Option<String>,
) {
let Ok(listener) = TcpListener::bind(address) else { let Ok(listener) = TcpListener::bind(address) else {
eprintln!("Invalid bind address {address:?}!"); eprintln!("Invalid bind address {address:?}!");
return; return;
@@ -177,16 +154,7 @@ fn start_server(
let context = context.clone(); let context = context.clone();
let path = path.clone(); let path = path.clone();
let password = password.clone(); let password = password.clone();
let hidden_password = partial_password.clone(); pool.execute(move || handle_connection(context, path, stream, password.as_deref()));
pool.execute(move || {
handle_connection(
context,
path,
stream,
password.as_deref(),
hidden_password.as_deref(),
)
});
} }
} }
@@ -195,65 +163,11 @@ fn fail(mut stream: TcpStream) {
let _ = writeln!(stream, "Page not found!"); let _ = writeln!(stream, "Page not found!");
} }
impl<S: SizeSettings> ToStream<S> for SiteInfo {
fn to_stream<W: Write>(&self, stream: &mut W) -> Result<()> {
S::size_to_stream(self.comments.len(), stream)?;
for Comment { name, text } in &self.comments {
let name_bytes = name.as_bytes();
S::size_to_stream(name_bytes.len(), stream)?;
stream.write_all(name_bytes)?;
let text_bytes = text.as_bytes();
S::size_to_stream(text_bytes.len(), stream)?;
stream.write_all(text_bytes)?;
}
S::size_to_stream(self.visits, stream)?;
S::size_to_stream(self.up, stream)?;
S::size_to_stream(self.down, stream)?;
Ok(())
}
}
impl<S: SizeSettings> FromStream<S> for SiteInfo {
fn from_stream<R: Read>(stream: &mut R) -> Result<Self> {
let size = S::size_from_stream(stream)?;
let comments = (0..size)
.map(|_| {
let name_bytes = <Vec<_> as FromStream<S>>::from_stream(stream)?;
let name = std::str::from_utf8(&name_bytes)
.map_err(|e| Error::new(ErrorKind::InvalidData, e))?
.into();
let text_bytes = <Vec<_> as FromStream<S>>::from_stream(stream)?;
let text = std::str::from_utf8(&text_bytes)
.map_err(|e| Error::new(ErrorKind::InvalidData, e))?
.into();
Ok(Comment { name, text })
})
.collect::<Result<Vec<_>>>()?;
let visits = S::size_from_stream(stream)?;
let up = S::size_from_stream(stream)?;
let down = S::size_from_stream(stream)?;
Ok(Self {
comments,
visits,
up,
down,
})
}
}
fn handle_connection( fn handle_connection(
context: Arc<Mutex<Context>>, context: Arc<Mutex<Context>>,
path: PathBuf, path: PathBuf,
mut stream: TcpStream, mut stream: TcpStream,
password: Option<&str>, password: Option<&str>,
partial_password: Option<&str>,
) { ) {
let Some(request) = Request::from(&stream) else { let Some(request) = Request::from(&stream) else {
eprintln!("Invalid request!"); eprintln!("Invalid request!");
@@ -275,7 +189,6 @@ fn handle_connection(
#[derive(PartialEq, Eq)] #[derive(PartialEq, Eq)]
enum Access { enum Access {
None, None,
Partial,
Full, Full,
} }
@@ -293,9 +206,6 @@ fn handle_connection(
if input == password { if input == password {
access = Access::Full; access = Access::Full;
cookie = Some(password); cookie = Some(password);
} else if Some(input) == partial_password {
access = Access::Partial;
cookie = partial_password;
} }
break; break;
} }
@@ -324,9 +234,6 @@ fn handle_connection(
if state == password { if state == password {
access = Access::Full; access = Access::Full;
break; break;
} else if Some(state) == partial_password {
access = Access::Partial;
break;
} }
} }
} }
@@ -455,12 +362,12 @@ fn handle_connection(
return; return;
} }
if let Some(pki_path) = &pki_path { if let Some(pki_path) = &pki_path
if !Path::is_file(pki_path) { && !Path::is_file(pki_path)
{
fail(stream); fail(stream);
return; return;
} }
}
let info = if let Ok(mut context) = context.lock() { let info = if let Ok(mut context) = context.lock() {
use Entry::*; use Entry::*;
@@ -539,7 +446,7 @@ struct RequestData<'a> {
impl<'a> RequestData<'a> { impl<'a> RequestData<'a> {
fn parse(body: &'a str) -> Self { fn parse(body: &'a str) -> Self {
let mut data = RequestData { let mut data = Self {
name: None, name: None,
text: None, text: None,
up: false, up: false,
@@ -614,12 +521,12 @@ fn handle_relative_connection(
let info = info.update(name, text, up, down); let info = info.update(name, text, up, down);
if let Ok(mut file) = File::create(file_paths.data) { if let Ok(mut file) = File::create(file_paths.data)
if to_stream::<PortableSettings, _, _>(&info, &mut file).is_err() { && to_stream::<PortableSettings, _, _>(&info, &mut file).is_err()
{
eprintln!("Error saving data!"); eprintln!("Error saving data!");
eprintln!(); eprintln!();
} }
}
let SiteInfo { let SiteInfo {
comments, comments,
@@ -658,83 +565,10 @@ fn handle_relative_connection(
section(&mut stream); section(&mut stream);
fn entry_handler<W: Write>(
path: &Path,
relative_path: &str,
censored: bool,
) -> impl Fn(&str, &mut W, usize) {
move |mut entry, output, level| {
let level = level + 1;
let mut pki_path = path.to_path_buf();
let mut audio_path = path.to_path_buf();
if let Some((real_entry, _)) = entry.split_once(':') {
entry = real_entry
}
let pki_extension = if censored { "pkc" } else { "pki" };
if relative_path.is_empty() {
pki_path.push(format!("{entry}.{pki_extension}"));
audio_path.push(format!("{entry}.mp3"));
} else {
pki_path.push(format!("{relative_path}/{entry}.{pki_extension}"));
audio_path.push(format!("{relative_path}/{entry}.mp3"));
}
let Ok(file) = File::open(pki_path) else {
return;
};
let _ = writeln!(
output,
"<h{level}><a href=\"{relative_path}/{entry}\">{entry}</a></h{level}>"
);
if Path::is_file(&audio_path) {
let _ = writeln!(
output,
"<p><audio controls src=\"/{relative_path}/{entry}.mp3\"/></p>"
);
}
convert_subheader(
BufReader::new(file).lines().map(Result::unwrap_or_default),
output,
level,
);
}
}
fn chara_handler<W: Write>(path: &Path, relative_path: &str) -> impl Fn(&str, &mut W, usize) {
move |entry, output, _level| {
let mut chara_path = path.to_path_buf();
chara_path.push(relative_path);
chara_path.push(format!("{entry}.chara"));
let Some(def) = load_character_file(&chara_path) else {
return;
};
let html = render_character(entry, &def, relative_path);
let _ = output.write_all(html.into_string().as_bytes());
}
}
if back { if back {
let _ = writeln!(stream, "<h1>{title}</h1>"); let _ = writeln!(stream, "<h1>{title}</h1>");
} }
enum TabInfo {
Lines(Vec<String>),
Chara(Vec<String>),
Game(IndexMap<Box<str>, Box<str>>),
Description,
Comment(Vec<Comment>),
}
struct Tab {
title: Box<str>,
info: TabInfo,
}
let mut sections = Vec::new(); let mut sections = Vec::new();
let check_path: &Path = relative_path.as_ref(); let check_path: &Path = relative_path.as_ref();
@@ -820,60 +654,7 @@ fn handle_relative_connection(
let count = sections.len(); let count = sections.len();
let _ = write!(stream, "<style>"); write_tab_styles(&mut stream, count);
let general_style = r"
.tab-system {
margin: 20px;
}
.tab-radio {
display: none;
}
.tab-nav {
display: flex;
gap: 5px;
margin-bottom: -1px;
}
.tab-button {
padding: 8px 15px;
background: #f0f0f0;
border: 1px solid #ddd;
border-bottom: 1px solid transparent;
cursor: pointer;
border-radius: 4px 4px 0 0;
position: relative;
}
.tab-button:hover {
background: #e0e0e0;
}
.tab-radio:checked + .tab-button {
background: white;
border-color: #ddd;
border-bottom-color: white;
z-index: 1;
}
.tab-content {
display: none;
padding: 15px;
border: 1px solid #ddd;
}";
let _ = write!(stream, "{general_style}");
for i in 1..count {
let _ = write!(stream, "#tab-{i}:checked ~ #content-{i},");
}
let _ = write!(
stream,
"#tab-{count}:checked ~ #content-{count} {{ display: block; }}"
);
let _ = write!(stream, "</style>");
let _ = write!(stream, r#"<div class="tab-system">"#); let _ = write!(stream, r#"<div class="tab-system">"#);
@@ -901,91 +682,16 @@ fn handle_relative_connection(
for (i, Tab { info, .. }) in sections.into_iter().enumerate() { for (i, Tab { info, .. }) in sections.into_iter().enumerate() {
let index = i + 1; let index = i + 1;
let _ = write!(stream, r#"<div class="tab-content" id="content-{index}">"#); let _ = write!(stream, r#"<div class="tab-content" id="content-{index}">"#);
match info { info.render_tab_content(
TabInfo::Lines(lines) => {
convert_extended(
lines,
&mut stream, &mut stream,
Settings::default() path,
.with_handler(entry_handler(path, relative_path, censored))
.with_start_level(1)
.with_use_textboxes(true),
);
}
TabInfo::Chara(lines) => {
convert_extended(
lines,
&mut stream,
Settings::default()
.with_handler(chara_handler(path, relative_path))
.with_start_level(1)
.with_use_textboxes(true),
);
}
TabInfo::Game(config_map) => {
if render_novel(
config_map,
file_paths.pk,
mlc_path.as_deref(),
file_paths.mld,
relative_path, relative_path,
&mut stream, censored,
choice, choice,
progress, progress,
) mlc_path.as_deref(),
.is_err() &file_paths,
{
fail(stream);
return;
}
}
TabInfo::Description => {
let Ok(pki_file) = File::open(file_paths.pki.unwrap()) else {
unreachable!();
};
let _ = writeln!(stream, "<h2>Description</h2>");
if let Some(audio_path) = &file_paths.audio {
if Path::is_file(audio_path) {
let _ = writeln!(
stream,
"<p><audio controls src=\"/{relative_path}.mp3\"/></p>"
); );
}
}
let lines = BufReader::new(pki_file).lines();
convert_subheader(lines.map(Result::unwrap_or_default), &mut stream, 1);
}
TabInfo::Comment(comments) => {
let html = html! {
h2 { "Comments" }
form method="POST" {
input type="text" name="name" value="anon" placeholder="Name";
br;
textarea rows="5" cols="60" name="text" placeholder="Enter comment..." {}
br;
input type="submit" value="Send!";
}
form method="POST" {
input type="submit" value="💖️" name="up";
input type="submit" value="💔️" name="down";
}
};
let _ = stream.write_all(html.into_string().as_bytes());
for Comment { name, text } in comments {
let html = html! {
fieldset {
legend { (name) }
(maud::PreEscaped(text))
}
};
let _ = stream.write_all(html.into_string().as_bytes());
}
}
}
let _ = write!(stream, "</div>"); let _ = write!(stream, "</div>");
} }

238
src/tabs.rs Normal file
View File

@@ -0,0 +1,238 @@
use std::{
fs::File,
io::{BufRead, BufReader, Write},
net::TcpStream,
path::Path,
};
use indexmap::IndexMap;
use maud::html;
use pukram2html::{Settings, convert_extended, convert_subheader};
use crate::{
Comment, DocumentPaths, chara::render_character, load_character_file, vn::render_novel,
};
static GENERAL_TAB_STYLE: &str = r"
.tab-system {
margin: 20px;
}
.tab-radio {
display: none;
}
.tab-nav {
display: flex;
gap: 5px;
margin-bottom: -1px;
}
.tab-button {
padding: 8px 15px;
background: #f0f0f0;
border: 1px solid #ddd;
border-bottom: 1px solid transparent;
cursor: pointer;
border-radius: 4px 4px 0 0;
position: relative;
}
.tab-button:hover {
background: #e0e0e0;
}
.tab-radio:checked + .tab-button {
background: white;
border-color: #ddd;
border-bottom-color: white;
z-index: 1;
}
.tab-content {
display: none;
padding: 15px;
border: 1px solid #ddd;
}";
pub fn write_tab_styles(stream: &mut TcpStream, count: usize) {
let _ = writeln!(stream, "<style>");
let _ = write!(stream, "{GENERAL_TAB_STYLE}");
for i in 1..count {
let _ = write!(stream, "#tab-{i}:checked ~ #content-{i},");
}
let _ = writeln!(
stream,
"#tab-{count}:checked ~ #content-{count} {{ display: block; }}"
);
let _ = writeln!(stream, "</style>");
}
pub enum TabInfo {
Lines(Vec<String>),
Chara(Vec<String>),
Game(IndexMap<Box<str>, Box<str>>),
Description,
Comment(Vec<Comment>),
}
impl TabInfo {
pub fn render_tab_content(
self,
stream: &mut TcpStream,
path: &Path,
relative_path: &str,
censored: bool,
choice: usize,
progress: &str,
mlc_path: Option<&Path>,
file_paths: &DocumentPaths,
) {
match self {
Self::Lines(lines) => {
convert_extended(
lines,
stream,
Settings::default()
.with_handler(entry_handler(path, relative_path, censored))
.with_start_level(1)
.with_use_textboxes(true),
);
}
Self::Chara(lines) => {
convert_extended(
lines,
stream,
Settings::default()
.with_handler(chara_handler(path, relative_path))
.with_start_level(1)
.with_use_textboxes(true),
);
}
Self::Game(config_map) => {
let _ = render_novel(
config_map,
file_paths.pk,
mlc_path,
file_paths.mld,
relative_path,
stream,
choice,
progress,
);
}
Self::Description => {
let Ok(pki_file) = File::open(file_paths.pki.unwrap()) else {
return;
};
let _ = writeln!(stream, "<h2>Description</h2>");
if let Some(audio_path) = &file_paths.audio
&& audio_path.is_file()
{
let _ = writeln!(
stream,
"<p><audio controls src=\"/{relative_path}.mp3\"/></p>"
);
}
let lines = BufReader::new(pki_file).lines().map_while(Result::ok);
convert_subheader(lines, stream, 1);
}
Self::Comment(comments) => {
render_comments(stream, comments);
}
}
}
}
fn render_comments(stream: &mut TcpStream, comments: Vec<Comment>) {
let html = html! {
h2 { "Comments" }
form method="POST" {
input type="text" name="name" value="anon" placeholder="Name";
br;
textarea rows="5" cols="60" name="text" placeholder="Enter comment..." {}
br;
input type="submit" value="Send!";
}
form method="POST" {
input type="submit" value="💖️" name="up";
input type="submit" value="💔️" name="down";
}
@for comment in comments {
fieldset {
legend { (comment.name) }
(maud::PreEscaped(comment.text))
}
}
};
let _ = stream.write_all(html.into_string().as_bytes());
}
pub struct Tab {
pub title: Box<str>,
pub info: TabInfo,
}
fn entry_handler<W: Write>(
path: &Path,
relative_path: &str,
censored: bool,
) -> impl Fn(&str, &mut W, usize) {
move |mut entry, output, level| {
let level = level + 1;
let mut pki_path = path.to_path_buf();
let mut audio_path = path.to_path_buf();
if let Some((real_entry, _)) = entry.split_once(':') {
entry = real_entry
}
let pki_extension = if censored { "pkc" } else { "pki" };
if relative_path.is_empty() {
pki_path.push(format!("{entry}.{pki_extension}"));
audio_path.push(format!("{entry}.mp3"));
} else {
pki_path.push(format!("{relative_path}/{entry}.{pki_extension}"));
audio_path.push(format!("{relative_path}/{entry}.mp3"));
}
let Ok(file) = File::open(pki_path) else {
return;
};
let _ = writeln!(
output,
"<h{level}><a href=\"{relative_path}/{entry}\">{entry}</a></h{level}>"
);
if Path::is_file(&audio_path) {
let _ = writeln!(
output,
"<p><audio controls src=\"/{relative_path}/{entry}.mp3\"/></p>"
);
}
convert_subheader(
BufReader::new(file).lines().map(Result::unwrap_or_default),
output,
level,
);
}
}
fn chara_handler<W: Write>(path: &Path, relative_path: &str) -> impl Fn(&str, &mut W, usize) {
move |entry, output, _level| {
let mut chara_path = path.to_path_buf();
chara_path.push(relative_path);
chara_path.push(format!("{entry}.chara"));
let Some(def) = load_character_file(&chara_path) else {
return;
};
let html = render_character(entry, &def, relative_path);
let _ = output.write_all(html.into_string().as_bytes());
}
}

View File

@@ -13,6 +13,7 @@ use maud::{Markup, html};
use multilinear::{BorrowedMultilinearSimulation, Event}; use multilinear::{BorrowedMultilinearSimulation, Event};
use multilinear_parser::{MultilinearParser, NamedMultilinearInfo}; use multilinear_parser::{MultilinearParser, NamedMultilinearInfo};
use pukram2html::convert; use pukram2html::convert;
use simple_color::Color;
use vn_settings::{Change, Parameter, PlayerSettings, SettingsContext, extract_layers}; use vn_settings::{Change, Parameter, PlayerSettings, SettingsContext, extract_layers};
use crate::dialog::parse_map; use crate::dialog::parse_map;
@@ -23,9 +24,22 @@ fn render_scene(settings: &PlayerSettings, name: &str) -> Markup {
@for object in &settings.objects.objects { @for object in &settings.objects.objects {
@if let Some(image_set) = object.image.get(name) { @if let Some(image_set) = object.image.get(name) {
@for image in &settings.images.images[image_set] { @for image in &settings.images.images[image_set] {
@if let Some(image_path) = image.get_ref(name) { @if let (Some(image_path), hor, ver, scale) = (
image.get_ref(name),
object.hor.get(name),
object.ver.get(name),
object.scale.get(name)
) {
@if !image_path.is_empty() {
figure .scene-image { figure .scene-image {
img src=(image_path) alt=""; img src=(image_path) alt=""
style=(format!(
"left: {}%; top: {}%; transform: translate(-50%, -50%) scale({});",
(hor + 8.0) * 100.0 / 16.0,
(ver + 4.5) * 100.0 / 9.0,
scale
));
}
} }
} }
} }
@@ -59,18 +73,47 @@ fn render_choice(block: &DialogBlock<Parameter>, index: usize, progress: &str) -
} }
} }
fn render_dialog_block(block: &DialogBlock<Parameter>) -> Markup { fn rgba_string(color: Color) -> String {
format!(
"rgba({}, {}, {}, {})",
color.r,
color.g,
color.b,
color.a as f32 / 255.0
)
}
fn render_dialog_block(settings: &PlayerSettings, block: &DialogBlock<Parameter>) -> Markup {
if block.lines.is_empty() { if block.lines.is_empty() {
return html! {}; return html! {};
} }
let text_fill_color = rgba_string(settings.colors.dialog_box.text_fill.get(&block.name));
let text_line_color = rgba_string(settings.colors.dialog_box.text_line.get(&block.name));
let name_fill_color = rgba_string(settings.colors.dialog_box.name_fill.get(&block.name));
let name_line_color = rgba_string(settings.colors.dialog_box.name_line.get(&block.name));
let mut content = Vec::new(); let mut content = Vec::new();
convert(block.lines.iter().map(|l| l.text.as_ref()), &mut content); convert(block.lines.iter().map(|l| l.text.as_ref()), &mut content);
html! { html! {
fieldset .visual-novel-box onclick="playCurrentAudio()" { fieldset .visual-novel-box onclick="playCurrentAudio()"
style=(format!(
"background: {}; border-color: {}; color: {};",
text_fill_color,
text_line_color,
text_line_color,
)) {
@if !block.name.is_empty() { @if !block.name.is_empty() {
legend .character-name { (block.name) } legend .character-name
style=(format!(
"background: {}; border-color: {}; color: {};",
name_fill_color,
name_line_color,
name_line_color,
)) {
(block.name)
}
} }
div .dialog-content { div .dialog-content {
@match String::from_utf8(content) { @match String::from_utf8(content) {
@@ -202,9 +245,10 @@ fn global_styles() -> Markup {
width: auto; width: auto;
object-fit: contain; object-fit: contain;
position: absolute; position: absolute;
top: 0;
left: 50%; left: 50%;
transform: translateX(-50%); top: 50%;
transform-origin: center center;
transition: transform 0.3s ease;
}"; }";
const TEXTBOX_STYLES: &str = r" const TEXTBOX_STYLES: &str = r"
@@ -435,7 +479,7 @@ fn process_dialog(
sections.push(html! { sections.push(html! {
(render_scene(player_settings, &block.name)) (render_scene(player_settings, &block.name))
(render_dialog_block(block)) (render_dialog_block(player_settings, block))
audio { audio {
source src=(format!("/{base_path}.{}.mp3", start_index + i)) type="audio/mpeg"; source src=(format!("/{base_path}.{}.mp3", start_index + i)) type="audio/mpeg";
} }
@@ -463,19 +507,20 @@ fn apply_block_changes(
.flat_map(|l| &l.actions) .flat_map(|l| &l.actions)
.chain(&block.final_actions) .chain(&block.final_actions)
{ {
if let Some(state) = states.get_mut(parameter) { if let Some(state) = states.get_mut(parameter)
if let Some(change) = changes[parameter].get(*state) { && let Some(change) = changes[parameter].get(*state)
{
settings.change(change); settings.change(change);
*state += 1; *state += 1;
} }
} }
}
} }
fn load_multilinear(mlc_path: Option<&Path>, mld_path: &Path) -> Option<NamedMultilinearInfo> { fn load_multilinear(mlc_path: Option<&Path>, mld_path: &Path) -> Option<NamedMultilinearInfo> {
let mut parser = MultilinearParser::default(); let mut parser = MultilinearParser::default();
if let Some(mlc_path) = mlc_path { if let Some(mlc_path) = mlc_path
if let Ok(file) = File::open(mlc_path) { && let Ok(file) = File::open(mlc_path)
{
for line in BufReader::new(file).lines() { for line in BufReader::new(file).lines() {
let Ok(line) = line else { let Ok(line) = line else {
break; break;
@@ -494,7 +539,6 @@ fn load_multilinear(mlc_path: Option<&Path>, mld_path: &Path) -> Option<NamedMul
return None; return None;
} }
} }
}
if let Err(e) = parser.parse(File::open(mld_path).ok()?, &[]) { if let Err(e) = parser.parse(File::open(mld_path).ok()?, &[]) {
eprintln!("Error parsing multilinear definition: {e}"); eprintln!("Error parsing multilinear definition: {e}");
@@ -557,12 +601,12 @@ pub fn render_novel(
let mut choices = Vec::new(); let mut choices = Vec::new();
for (i, dialog_sequence) in dialogs.iter().enumerate() { for (i, dialog_sequence) in dialogs.iter().enumerate() {
if let Some(block) = dialog_sequence.blocks.first() { if let Some(block) = dialog_sequence.blocks.first()
if simulation.callable(Event(i)) { && simulation.callable(Event(i))
{
choices.push((i, block)) choices.push((i, block))
} }
} }
}
if choices.len() == 1 { if choices.len() == 1 {
let next_choice = choices[0].0; let next_choice = choices[0].0;