init
commit
7821be4ecb
@ -0,0 +1 @@
|
||||
/target
|
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…
Reference in new issue