master
Drake 1 year ago
commit 7821be4ecb

1
.gitignore vendored

@ -0,0 +1 @@
/target

1990
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -0,0 +1,19 @@
[package]
name = "voltpad-rs"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.69"
axum = { version = "0.6.10", features = ["macros"] }
cached = "0.42.0"
grass = { version = "0.12.3", features = ["macro"] }
maud = { git = "https://github.com/lambda-fairy/maud", features = ["axum"] }
regex = "1.7.1"
serde = { version = "1.0.152", features = ["derive"] }
tokio = { version = "1.26.0", features = ["full"] }
tracing = "0.1.37"
tracing-subscriber = "0.3.16"
wattpad = "0.2.3"

@ -0,0 +1,84 @@
@import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&display=swap");
html {
color-scheme: dark;
font-family: "IBM Plex Sans", sans-serif;
background-color: #181a1a;
color: #e8e6e3;
}
body {
margin: 0;
padding: 0;
}
img {
object-fit: scale-down;
width: auto;
height: auto;
max-width: 75%;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 0;
padding: 0;
}
a:link,
a:visited,
a:hover {
color: #e8e6e3;
}
.page-header {
position: relative;
background-color: #4c4f50;
width: 100%;
display: flex;
align-items: center;
overflow-x: scroll;
gap: 0.125em;
* {
margin: 0;
padding: 0.5em;
text-align: center;
white-space: nowrap;
}
.header-separator {
padding: 0 !important;
}
}
.story-page-container {
display: flex;
align-items: center;
justify-content: center;
margin-top: 0.5em;
.story-page {
display: flex;
align-items: center;
justify-content: center;
@media (max-width: 900px) {
flex-direction: column;
}
column-gap: 0.5em;
width: 75%;
.story-details {
display: flex;
flex-direction: column;
}
}
}
.story-content {
margin-left: 2.5vw;
margin-right: 2.5vw;
overflow-wrap: anywhere;
}

@ -0,0 +1,32 @@
use maud::{html, Markup, Render};
pub struct HeaderLink {
pub path: String,
pub name: String,
pub prefix: Option<String>,
pub postfix: Option<String>,
}
pub struct Header(pub Vec<HeaderLink>, pub String);
impl Render for Header {
fn render(&self) -> Markup {
html! {
header .page-header {
@for header_part in &self.0 {
@if let Some(prefix) = header_part.prefix.clone() {
div .header-separator { (prefix) }
}
@if header_part.path == self.1 {
div .selected-header { (header_part.name) }
} @else {
a .unselected-header href=(header_part.path) { (header_part.name) }
}
@if let Some(postfix) = header_part.postfix.clone() {
div .header-separator { (postfix) }
}
}
}
}
}
}

@ -0,0 +1 @@
pub mod header;

@ -0,0 +1,33 @@
mod components;
mod routes;
use routes::{index, story};
use axum::{routing::get, Router};
use std::net::SocketAddr;
use wattpad::Wattpad;
#[derive(Clone)]
pub struct State {
pub wattpad: Wattpad,
}
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let state = State {
wattpad: Wattpad::new().await.unwrap(),
};
let addr: SocketAddr = "127.0.0.1:3000".parse().unwrap();
let app = Router::new()
.route("/", get(index::render))
.route("/story", get(story::render))
.with_state(state);
tracing::info!("Listening on {}", addr);
let _ = axum::Server::bind(&addr)
.serve(app.into_make_service())
.await;
}

@ -0,0 +1,18 @@
use crate::components::header::{Header, HeaderLink};
use maud::{html, Markup};
pub async fn render() -> Markup {
html! {
(
Header(vec![
HeaderLink {
name: "Home".to_string(),
path: "/".to_string(),
prefix: None,
postfix: None
}
], "/".to_string())
)
h1 { "Hello, world!" }
}
}

@ -0,0 +1,2 @@
pub mod index;
pub mod story;

@ -0,0 +1,122 @@
use crate::components::header::{Header, HeaderLink};
use crate::State;
use axum::extract::{Query, State as AxumState};
use maud::{html, Markup, PreEscaped, DOCTYPE};
use regex::Regex;
use serde::Deserialize;
#[derive(Deserialize)]
pub struct StoryParams {
id: Option<String>,
chapter: Option<usize>,
}
pub async fn render(
AxumState(state): AxumState<State>,
Query(params): Query<StoryParams>,
) -> Markup {
if let Some(id) = params.id {
let story = state.wattpad.get_story(id.as_str()).await;
if let Ok(story) = story {
if let Some(part_idx) = params.chapter {
let parts = story.get_parts().await.unwrap();
if let Some(part) = parts.get(part_idx) {
let style_matching_regex = Regex::new("style=\"(.+?)\"").unwrap();
html! {
(DOCTYPE)
html {
head {
meta name="viewport" content="width=device-width, initial-scale=1.0";
style {(PreEscaped(
grass::include!("./css/index.scss")
))}
}
body {
(Header({
let mut story_parts: Vec<HeaderLink> = parts.iter().enumerate().map(|(idx, part)| HeaderLink { path: format!("/story?id={}&chapter={}", story.id, idx), name: part.title.clone(), prefix: None, postfix: None }).collect();
story_parts.insert(0, HeaderLink { path: format!("/story?id={}", story.id), name: "Details".to_string(), prefix: Some("|".to_string()), postfix: Some("|".to_string()) });
story_parts.insert(0, HeaderLink { path: "/".to_string(), name: "Homepage".to_string(), prefix: None, postfix: None });
story_parts
}, format!("/story?id={}&chapter={}", story.id, part_idx)))
div .story-content {
@for paragraph in part.get_paragraphs().expect("Failed to get paragraphs of part") {
p style=({
let attributes = paragraph.attributes.unwrap_or_default();
let captures = style_matching_regex.captures(attributes.as_str());
match captures {
Some(e) => e.get(1).unwrap().as_str().to_string(),
None => String::default()
}
}) { (PreEscaped(paragraph.html)) }
}
}
}
}
}
} else {
html! {
h1 { "Err: Chapter not found" }
}
}
} else {
let parts = story.get_parts().await.unwrap();
html! {
(DOCTYPE)
html {
head {
meta name="viewport" content="width=device-width, initial-scale=1.0";
style {(PreEscaped(
grass::include!("./css/index.scss")
))}
}
body {
(Header({
let mut story_parts: Vec<HeaderLink> = parts.iter().enumerate().map(|(idx, part)| HeaderLink { path: format!("/story?id={}&chapter={}", story.id, idx), name: part.title.clone(), prefix: None, postfix: None }).collect();
story_parts.insert(0, HeaderLink { path: format!("/story?id={}", story.id), name: "Details".to_string(), prefix: Some("|".to_string()), postfix: Some("|".to_string()) });
story_parts.insert(0, HeaderLink { path: "/".to_string(), name: "Homepage".to_string(), prefix: None, postfix: None });
story_parts
}, format!("/story?id={}", story.id)))
div .story-page-container {
div .story-page {
img .story-cover src=(story.cover);
div .story-details {
h3 #story-name {
(story.title)
}
p {
(PreEscaped(story.description.replace("\n", "<br/>")))
}
p {
"Original URL: "
a href=(story.url) { (story.url) }
br;
"Author: "
a href=(format!("/user?name={}", story._user.fullname)) {(story._user.name)}
br;
"© " (story.copyright)
}
div .tag-container {
@for tag in story.tags {
div .tag {
a href=(format!("/search?query={}&type=tag", tag)) { (tag) }
}
}
}
}
}
}
}
}
}
}
} else {
html! {
h1 { "Err: Story not found" }
}
}
} else {
html! {
h1 { "Err: Story not specified" }
}
}
}
Loading…
Cancel
Save