Compare commits
10 Commits
1af69ca8e5
...
dad0adeb8c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dad0adeb8c | ||
|
|
7e0c4c80a3 | ||
|
|
40b88cefff | ||
|
|
447df9d9ea | ||
|
|
1309ac06b2 | ||
|
|
f8f656fcd0 | ||
|
|
097a5303e9 | ||
|
|
f7327f31e5 | ||
|
|
09815b04f4 | ||
|
|
8e790e2409 |
14
Cargo.lock
generated
14
Cargo.lock
generated
@ -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",
|
||||||
|
|||||||
@ -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" }
|
||||||
|
|||||||
313
src/main.rs
313
src/main.rs
@ -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)
|
||||||
},
|
},
|
||||||
@ -195,59 +178,6 @@ 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,
|
||||||
@ -539,7 +469,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,
|
||||||
@ -658,83 +588,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 +677,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 +705,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) => {
|
&mut stream,
|
||||||
convert_extended(
|
path,
|
||||||
lines,
|
relative_path,
|
||||||
&mut stream,
|
censored,
|
||||||
Settings::default()
|
choice,
|
||||||
.with_handler(entry_handler(path, relative_path, censored))
|
progress,
|
||||||
.with_start_level(1)
|
mlc_path.as_deref(),
|
||||||
.with_use_textboxes(true),
|
&file_paths,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
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,
|
|
||||||
&mut stream,
|
|
||||||
choice,
|
|
||||||
progress,
|
|
||||||
)
|
|
||||||
.is_err()
|
|
||||||
{
|
|
||||||
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
238
src/tabs.rs
Normal 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 {
|
||||||
|
if 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
62
src/vn.rs
62
src/vn.rs
@ -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) = (
|
||||||
figure .scene-image {
|
image.get_ref(name),
|
||||||
img src=(image_path) alt="";
|
object.hor.get(name),
|
||||||
|
object.ver.get(name),
|
||||||
|
object.scale.get(name)
|
||||||
|
) {
|
||||||
|
@if !image_path.is_empty() {
|
||||||
|
figure .scene-image {
|
||||||
|
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";
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user