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"
|
||||
checksum = "2653cb9243a7d4453127141c162c1ba619d99c7230f1d8ad0a9cdab9cf0651ea"
|
||||
|
||||
[[package]]
|
||||
name = "comment-system"
|
||||
version = "0.1.0"
|
||||
source = "git+https://gitlab.com/porky11/comment-system#bdef78dd69fe726a7fdc44f58c6d97e2939fe743"
|
||||
dependencies = [
|
||||
"data-stream",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-stream"
|
||||
version = "0.3.0"
|
||||
@ -250,6 +258,7 @@ name = "pukram-server"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chara",
|
||||
"comment-system",
|
||||
"data-stream",
|
||||
"dialogi",
|
||||
"event-simulation",
|
||||
@ -260,6 +269,7 @@ dependencies = [
|
||||
"multilinear-parser",
|
||||
"percent-encoding",
|
||||
"pukram2html",
|
||||
"simple-color",
|
||||
"threadpool",
|
||||
"vn-settings",
|
||||
]
|
||||
@ -345,9 +355,9 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "vn-settings"
|
||||
version = "0.1.1"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3c5bd91cb7361d8d179deacf6e9b927a45a1e31c7f9c29555a49be81506381a"
|
||||
checksum = "53e699531365195d0efa500f6be7cbe287497630d40eb39d065e09ba0508c109"
|
||||
dependencies = [
|
||||
"dialogi",
|
||||
"indexmap",
|
||||
|
||||
@ -4,12 +4,13 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
simple-color = "0.2.3"
|
||||
percent-encoding = "2.3"
|
||||
maud = "0.27.0"
|
||||
pukram2html = "0.3.0"
|
||||
data-stream = "0.3.0"
|
||||
dialogi = "0.3.1"
|
||||
vn-settings = "0.1.1"
|
||||
vn-settings = "0.1.2"
|
||||
threadpool = "1.8.1"
|
||||
header-config = "0.1.5"
|
||||
indexmap = "2.9.0"
|
||||
@ -17,3 +18,4 @@ multilinear = "0.3.1"
|
||||
multilinear-parser = "0.3.3"
|
||||
event-simulation = "0.1.2"
|
||||
chara = "0.2.0"
|
||||
comment-system = { git = "https://gitlab.com/porky11/comment-system" }
|
||||
|
||||
303
src/main.rs
303
src/main.rs
@ -2,7 +2,7 @@ use std::{
|
||||
collections::{HashMap, hash_map::Entry},
|
||||
env,
|
||||
fs::File,
|
||||
io::{BufReader, Error, ErrorKind, Result, prelude::*},
|
||||
io::{BufReader, prelude::*},
|
||||
net::{TcpListener, TcpStream},
|
||||
path::{Path, PathBuf},
|
||||
process::ExitCode,
|
||||
@ -15,31 +15,22 @@ use std::{
|
||||
};
|
||||
|
||||
use ::chara::CharacterDefinition;
|
||||
use data_stream::{
|
||||
FromStream, ToStream, collections::SizeSettings, default_settings::PortableSettings,
|
||||
from_stream, to_stream,
|
||||
};
|
||||
use comment_system::{Comment, SiteInfo};
|
||||
use data_stream::{default_settings::PortableSettings, from_stream, to_stream};
|
||||
use header_config::parse_config;
|
||||
use indexmap::IndexMap;
|
||||
use maud::html;
|
||||
use percent_encoding::percent_decode_str;
|
||||
use pukram2html::{Settings, convert, convert_extended, convert_subheader};
|
||||
use pukram2html::convert;
|
||||
use threadpool::ThreadPool;
|
||||
|
||||
mod chara;
|
||||
mod dialog;
|
||||
mod request;
|
||||
mod tabs;
|
||||
mod vn;
|
||||
|
||||
use chara::render_character;
|
||||
use request::Request;
|
||||
use vn::render_novel;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Comment {
|
||||
name: Box<str>,
|
||||
text: Box<str>,
|
||||
}
|
||||
use tabs::{Tab, TabInfo, write_tab_styles};
|
||||
|
||||
#[derive(Default)]
|
||||
struct SharedSiteInfo {
|
||||
@ -49,14 +40,6 @@ struct SharedSiteInfo {
|
||||
down: AtomicUsize,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct SiteInfo {
|
||||
comments: Vec<Comment>,
|
||||
visits: usize,
|
||||
up: usize,
|
||||
down: usize,
|
||||
}
|
||||
|
||||
impl SharedSiteInfo {
|
||||
fn new(info: SiteInfo) -> Self {
|
||||
let SiteInfo {
|
||||
@ -92,14 +75,14 @@ impl SharedSiteInfo {
|
||||
Vec::new()
|
||||
}
|
||||
},
|
||||
visits: self.visits.fetch_add(1, Ordering::Relaxed),
|
||||
visits: self.visits.fetch_add(1, Ordering::Relaxed) + 1,
|
||||
up: if up {
|
||||
self.up.fetch_add(1, Ordering::Relaxed)
|
||||
self.up.fetch_add(1, Ordering::Relaxed) + 1
|
||||
} else {
|
||||
self.up.load(Ordering::Relaxed)
|
||||
},
|
||||
down: if down {
|
||||
self.down.fetch_add(1, Ordering::Relaxed)
|
||||
self.down.fetch_add(1, Ordering::Relaxed) + 1
|
||||
} else {
|
||||
self.down.load(Ordering::Relaxed)
|
||||
},
|
||||
@ -195,59 +178,6 @@ fn fail(mut stream: TcpStream) {
|
||||
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(
|
||||
context: Arc<Mutex<Context>>,
|
||||
path: PathBuf,
|
||||
@ -539,7 +469,7 @@ struct RequestData<'a> {
|
||||
|
||||
impl<'a> RequestData<'a> {
|
||||
fn parse(body: &'a str) -> Self {
|
||||
let mut data = RequestData {
|
||||
let mut data = Self {
|
||||
name: None,
|
||||
text: None,
|
||||
up: false,
|
||||
@ -658,83 +588,10 @@ fn handle_relative_connection(
|
||||
|
||||
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 {
|
||||
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 check_path: &Path = relative_path.as_ref();
|
||||
@ -820,60 +677,7 @@ fn handle_relative_connection(
|
||||
|
||||
let count = sections.len();
|
||||
|
||||
let _ = write!(stream, "<style>");
|
||||
|
||||
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>");
|
||||
write_tab_styles(&mut stream, count);
|
||||
|
||||
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() {
|
||||
let index = i + 1;
|
||||
let _ = write!(stream, r#"<div class="tab-content" id="content-{index}">"#);
|
||||
match info {
|
||||
TabInfo::Lines(lines) => {
|
||||
convert_extended(
|
||||
lines,
|
||||
info.render_tab_content(
|
||||
&mut stream,
|
||||
Settings::default()
|
||||
.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,
|
||||
path,
|
||||
relative_path,
|
||||
&mut stream,
|
||||
censored,
|
||||
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>"
|
||||
mlc_path.as_deref(),
|
||||
&file_paths,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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>");
|
||||
}
|
||||
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
60
src/vn.rs
60
src/vn.rs
@ -13,6 +13,7 @@ use maud::{Markup, html};
|
||||
use multilinear::{BorrowedMultilinearSimulation, Event};
|
||||
use multilinear_parser::{MultilinearParser, NamedMultilinearInfo};
|
||||
use pukram2html::convert;
|
||||
use simple_color::Color;
|
||||
use vn_settings::{Change, Parameter, PlayerSettings, SettingsContext, extract_layers};
|
||||
|
||||
use crate::dialog::parse_map;
|
||||
@ -23,9 +24,22 @@ fn render_scene(settings: &PlayerSettings, name: &str) -> Markup {
|
||||
@for object in &settings.objects.objects {
|
||||
@if let Some(image_set) = object.image.get(name) {
|
||||
@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 {
|
||||
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() {
|
||||
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();
|
||||
convert(block.lines.iter().map(|l| l.text.as_ref()), &mut content);
|
||||
|
||||
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() {
|
||||
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 {
|
||||
@match String::from_utf8(content) {
|
||||
@ -202,9 +245,10 @@ fn global_styles() -> Markup {
|
||||
width: auto;
|
||||
object-fit: contain;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
top: 50%;
|
||||
transform-origin: center center;
|
||||
transition: transform 0.3s ease;
|
||||
}";
|
||||
|
||||
const TEXTBOX_STYLES: &str = r"
|
||||
@ -435,7 +479,7 @@ fn process_dialog(
|
||||
|
||||
sections.push(html! {
|
||||
(render_scene(player_settings, &block.name))
|
||||
(render_dialog_block(block))
|
||||
(render_dialog_block(player_settings, block))
|
||||
audio {
|
||||
source src=(format!("/{base_path}.{}.mp3", start_index + i)) type="audio/mpeg";
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user