stuffsies

master
Drake 1 year ago
parent 7821be4ecb
commit 4698d6f0ae

185
Cargo.lock generated

@ -28,6 +28,15 @@ dependencies = [
"memchr",
]
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "ansi_term"
version = "0.12.1"
@ -227,6 +236,21 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b"
dependencies = [
"iana-time-zone",
"js-sys",
"num-integer",
"num-traits",
"time 0.1.45",
"wasm-bindgen",
"winapi",
]
[[package]]
name = "clap"
version = "2.34.0"
@ -248,6 +272,16 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e769b5c8c8283982a987c6e948e540254f1058d5a74b8794914d4ef5fc2a24"
[[package]]
name = "codespan-reporting"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
dependencies = [
"termcolor",
"unicode-width",
]
[[package]]
name = "cookie"
version = "0.16.2"
@ -255,7 +289,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb"
dependencies = [
"percent-encoding",
"time",
"time 0.3.20",
"version_check",
]
@ -271,7 +305,7 @@ dependencies = [
"publicsuffix",
"serde",
"serde_json",
"time",
"time 0.3.20",
"url",
]
@ -300,6 +334,50 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "cxx"
version = "1.0.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a140f260e6f3f79013b8bfc65e7ce630c9ab4388c6a89c71e07226f49487b72"
dependencies = [
"cc",
"cxxbridge-flags",
"cxxbridge-macro",
"link-cplusplus",
]
[[package]]
name = "cxx-build"
version = "1.0.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da6383f459341ea689374bf0a42979739dc421874f112ff26f829b8040b8e613"
dependencies = [
"cc",
"codespan-reporting",
"once_cell",
"proc-macro2",
"quote",
"scratch",
"syn",
]
[[package]]
name = "cxxbridge-flags"
version = "1.0.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90201c1a650e95ccff1c8c0bb5a343213bdd317c6e600a93075bca2eff54ec97"
[[package]]
name = "cxxbridge-macro"
version = "1.0.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b75aed41bb2e6367cae39e6326ef817a851db13c13e4f3263714ca3cfb8de56"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "darling"
version = "0.14.4"
@ -483,7 +561,7 @@ checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
dependencies = [
"cfg-if",
"libc",
"wasi",
"wasi 0.11.0+wasi-snapshot-preview1",
]
[[package]]
@ -652,6 +730,30 @@ dependencies = [
"tokio-native-tls",
]
[[package]]
name = "iana-time-zone"
version = "0.1.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"winapi",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca"
dependencies = [
"cxx",
"cxx-build",
]
[[package]]
name = "ident_case"
version = "1.0.1"
@ -761,6 +863,15 @@ version = "0.2.139"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
[[package]]
name = "link-cplusplus"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5"
dependencies = [
"cc",
]
[[package]]
name = "linux-raw-sys"
version = "0.1.4"
@ -849,7 +960,7 @@ checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9"
dependencies = [
"libc",
"log",
"wasi",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.45.0",
]
@ -881,6 +992,25 @@ dependencies = [
"winapi",
]
[[package]]
name = "num-integer"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
dependencies = [
"autocfg",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
dependencies = [
"autocfg",
]
[[package]]
name = "num_cpus"
version = "1.15.0"
@ -1267,6 +1397,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "scratch"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1"
[[package]]
name = "security-framework"
version = "2.8.2"
@ -1444,6 +1580,15 @@ dependencies = [
"windows-sys 0.42.0",
]
[[package]]
name = "termcolor"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6"
dependencies = [
"winapi-util",
]
[[package]]
name = "textwrap"
version = "0.11.0"
@ -1483,6 +1628,17 @@ dependencies = [
"once_cell",
]
[[package]]
name = "time"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a"
dependencies = [
"libc",
"wasi 0.10.0+wasi-snapshot-preview1",
"winapi",
]
[[package]]
name = "time"
version = "0.3.20"
@ -1761,7 +1917,9 @@ dependencies = [
"anyhow",
"axum",
"cached",
"chrono",
"grass",
"lazy_static",
"maud",
"regex",
"serde",
@ -1781,6 +1939,12 @@ dependencies = [
"try-lock",
]
[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
@ -1855,9 +2019,9 @@ checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d"
[[package]]
name = "wattpad"
version = "0.2.3"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2661f9c5a05a40a378a809fc77076644d8cdd6839b468d8ed3f3cffa887870f4"
checksum = "9826298a0db9284b994af09fef8a564840af0e9e2621440aef46a5bd066481ed"
dependencies = [
"anyhow",
"regex",
@ -1893,6 +2057,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [
"winapi",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"

@ -9,11 +9,13 @@ edition = "2021"
anyhow = "1.0.69"
axum = { version = "0.6.10", features = ["macros"] }
cached = "0.42.0"
chrono = "0.4.24"
grass = { version = "0.12.3", features = ["macro"] }
lazy_static = "1.4.0"
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"
wattpad = "0.2.4"

@ -42,15 +42,24 @@ a:hover {
display: flex;
align-items: center;
overflow-x: scroll;
gap: 0.125em;
padding-top: 0.5em;
padding-bottom: 0.5em;
gap: 0.5em;
* {
margin: 0;
padding: 0.5em;
text-align: center;
white-space: nowrap;
}
:first-child {
margin-left: 0.5em;
}
:last-child {
margin-right: 0.5em;
}
.header-separator {
padding: 0 !important;
}
@ -58,6 +67,7 @@ a:hover {
.story-page-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-top: 0.5em;
@ -75,6 +85,30 @@ a:hover {
flex-direction: column;
}
}
.story-buttons {
display: flex;
align-items: center;
justify-content: center;
column-gap: 0.25em;
margin-top: 0.75em;
}
}
.tag-container {
display: flex;
flex-wrap: wrap;
column-gap: 0.25em;
}
.index-content {
margin-top: 0.5em;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-left: 2.5vw;
margin-right: 2.5vw;
}
.story-content {
@ -82,3 +116,9 @@ a:hover {
margin-right: 2.5vw;
overflow-wrap: anywhere;
}
.not-found-page {
margin-top: 0.5em;
display: flex;
justify-content: center;
}

@ -0,0 +1,39 @@
const oldOnloadHandler = window.onload ?? (() => {});
window.onload = () => {
oldOnloadHandler();
// Scroll header to current entry
document.getElementById("selected-header")?.scrollIntoView({
block: "center",
inline: "center"
});
// Sync history
const params = new URL(document.location.href).searchParams;
if (params.get("chapter")) {
localStorage.setItem(
`${params.get("id")}_progress`,
params.get("chapter")
);
}
const readingHistory = JSON.parse(
localStorage.getItem("readingHistory") ?? "[]"
);
readingHistory.forEach((id, idx) => {
if (id === params.get("id")) {
readingHistory.splice(idx, 1);
}
});
readingHistory.unshift(params.get("id"));
//TODO: do pagination on the history page to make this un-necessary
if (readingHistory.length > 50) {
readingHistory.pop();
}
localStorage.setItem("readingHistory", JSON.stringify(readingHistory));
};

@ -0,0 +1,58 @@
const oldOnloadHandler = window.onload ?? (() => {});
window.onload = () => {
oldOnloadHandler();
// Sync to position button
const position = localStorage.getItem(
new URL(document.URL).searchParams.get("id") + "_progress"
);
if (position) {
const buttonsList = document.getElementById("detail-buttons");
const buttonEle = document.createElement("button");
buttonEle.className = "favorite-story-button";
buttonEle.textContent = "Sync to story position";
buttonEle.onclick = () => {
const currURL = new URL(document.location.href);
currURL.searchParams.set("chapter", position);
window.location.href = currURL.toString();
};
buttonsList?.appendChild(buttonEle);
}
// Favorite button
const button = document.getElementById("favorite");
if (!button) throw "how did your button not init what";
let savedStories = JSON.parse(localStorage.getItem("savedStories") ?? "[]");
function handler() {
if (button?.textContent === "Save Story") {
savedStories.push({
id: new URL(document.URL).searchParams.get("id"),
name: document.getElementById("storyname")?.textContent,
coverURL: document.getElementById("storycover").src
});
button.textContent = "Unsave Story";
} else if (button?.textContent === "Unsave Story") {
savedStories = savedStories.filter(
(story) =>
story.id !== new URL(document.URL).searchParams.get("id")
);
button.textContent = "Save Story";
}
localStorage.setItem("savedStories", JSON.stringify(savedStories));
}
if (
savedStories.filter(
(story) => story.id === new URL(document.URL).searchParams.get("id")
).length > 0
) {
button.textContent = "Unsave Story";
}
button.onclick = handler;
};

@ -0,0 +1,33 @@
use crate::WATTPAD;
use anyhow::{Context, Result};
use chrono::{DateTime, Local};
use lazy_static::lazy_static;
use std::{collections::HashMap, sync::Mutex};
use wattpad::{Part, Story};
lazy_static! {
static ref STORY_CACHE: Mutex<HashMap<String, (Story, DateTime<Local>)>> =
Mutex::new(HashMap::new());
}
pub async fn get_story(id: &str) -> Result<Story> {
let current_time = chrono::Local::now();
if let Some(cache_item) = STORY_CACHE
.lock()
.expect("Failed to lock StoryCache hashmap (somehow)")
.get(id)
{
let time_diff = current_time.signed_duration_since(cache_item.1);
if time_diff.num_minutes() <= 5 {
tracing::debug!("Cache hit for story {}", id);
return Ok(cache_item.0.clone());
}
}
tracing::debug!("Caching story {}", id);
let story = WATTPAD.get_story(id).await.context("Failed to get story")?;
STORY_CACHE
.lock()
.expect("Failed to lock StoryCache hashmap (somehow)")
.insert(id.to_string(), (story.clone(), current_time));
Ok(story)
}

@ -18,7 +18,7 @@ impl Render for Header {
div .header-separator { (prefix) }
}
@if header_part.path == self.1 {
div .selected-header { (header_part.name) }
div .selected-header #selected-header { (header_part.name) }
} @else {
a .unselected-header href=(header_part.path) { (header_part.name) }
}

@ -1,33 +1,37 @@
mod cached_wattpad;
mod components;
mod routes;
use routes::{index, story};
use routes::{index, not_found, story};
use axum::{routing::get, Router};
use std::net::SocketAddr;
use axum::{extract::Extension, routing::get, Router};
use lazy_static::lazy_static;
use maud::{html, Markup};
use std::{net::SocketAddr, sync::Arc};
use tokio::runtime::Runtime;
use wattpad::Wattpad;
#[derive(Clone)]
pub struct State {
pub wattpad: Wattpad,
lazy_static! {
pub static ref TOKIO: Runtime = Runtime::new().unwrap();
pub static ref WATTPAD: Wattpad =
TOKIO.block_on(async { Wattpad::new().await.expect("Failed to initialize Wattpad!") });
}
#[tokio::main]
async fn main() {
fn main() {
tracing_subscriber::fmt::init();
lazy_static::initialize(&WATTPAD);
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);
.fallback(not_found::render);
tracing::info!("Listening on {}", addr);
let _ = axum::Server::bind(&addr)
.serve(app.into_make_service())
.await;
TOKIO.block_on(async {
let _ = axum::Server::bind(&addr)
.serve(app.into_make_service())
.await;
})
}

@ -1,8 +1,14 @@
use crate::components::header::{Header, HeaderLink};
use maud::{html, Markup};
use cached::proc_macro::cached;
use maud::{html, Markup, PreEscaped};
#[cached]
pub async fn render() -> Markup {
html! {
meta name="viewport" content="width=device-width, initial-scale=1.0";
style {(PreEscaped(
grass::include!("./css/index.scss")
))}
(
Header(vec![
HeaderLink {
@ -10,9 +16,30 @@ pub async fn render() -> Markup {
path: "/".to_string(),
prefix: None,
postfix: None
}
},
HeaderLink {
name: "Search".to_string(),
path: "/search".to_string(),
prefix: None,
postfix: None
},
HeaderLink {
name: "History".to_string(),
path: "/history".to_string(),
prefix: None,
postfix: None
},
HeaderLink {
name: "About".to_string(),
path: "/about".to_string(),
prefix: None,
postfix: None
},
], "/".to_string())
)
h1 { "Hello, world!" }
div .index-content {
h1 { "Voltpad" }
div { "A blazingly fast (but for real this time) Wattpad frontend" }
}
}
}

@ -1,2 +1,3 @@
pub mod index;
pub mod not_found;
pub mod story;

@ -0,0 +1,48 @@
use crate::components::header::{Header, HeaderLink};
use axum::http::StatusCode;
use cached::proc_macro::cached;
use maud::{html, Markup, PreEscaped};
#[cached]
pub async fn render() -> (StatusCode, Markup) {
(
StatusCode::NOT_FOUND,
html! {
meta name="viewport" content="width=device-width, initial-scale=1.0";
style {(PreEscaped(
grass::include!("./css/index.scss")
))}
(
Header(vec![
HeaderLink {
name: "Home".to_string(),
path: "/".to_string(),
prefix: None,
postfix: None
},
HeaderLink {
name: "Search".to_string(),
path: "/search".to_string(),
prefix: None,
postfix: None
},
HeaderLink {
name: "History".to_string(),
path: "/history".to_string(),
prefix: None,
postfix: None
},
HeaderLink {
name: "About".to_string(),
path: "/about".to_string(),
prefix: None,
postfix: None
},
], "/404".to_string())
)
div .not-found-page {
img src="https://media.ruthenic.com/cat404.png";
}
},
)
}

@ -1,6 +1,9 @@
use std::sync::Arc;
use crate::cached_wattpad::*;
use crate::components::header::{Header, HeaderLink};
use crate::State;
use axum::extract::{Query, State as AxumState};
use crate::WATTPAD;
use axum::extract::{Extension, Query, State as AxumState};
use maud::{html, Markup, PreEscaped, DOCTYPE};
use regex::Regex;
use serde::Deserialize;
@ -11,16 +14,14 @@ pub struct StoryParams {
chapter: Option<usize>,
}
pub async fn render(
AxumState(state): AxumState<State>,
Query(params): Query<StoryParams>,
) -> Markup {
pub async fn render(Query(params): Query<StoryParams>) -> Markup {
if let Some(id) = params.id {
let story = state.wattpad.get_story(id.as_str()).await;
//let story = state.wattpad.get_story(id.as_str()).await;
let story = 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 part = story.get_part(part_idx).await;
if let Ok(part) = part {
let style_matching_regex = Regex::new("style=\"(.+?)\"").unwrap();
html! {
(DOCTYPE)
@ -30,11 +31,14 @@ pub async fn render(
style {(PreEscaped(
grass::include!("./css/index.scss")
))}
script {(PreEscaped(
include_str!("../../scripts/StoryContent.js")
))}
}
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()) });
let mut story_parts: Vec<HeaderLink> = story._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: None, 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)))
@ -59,7 +63,6 @@ pub async fn render(
}
}
} else {
let parts = story.get_parts().await.unwrap();
html! {
(DOCTYPE)
html {
@ -68,19 +71,22 @@ pub async fn render(
style {(PreEscaped(
grass::include!("./css/index.scss")
))}
script {(PreEscaped(
include_str!("../../scripts/StoryDetails.js")
))}
}
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()) });
let mut story_parts: Vec<HeaderLink> = story._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: None, 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);
img .story-cover #storycover src=(story.cover);
div .story-details {
h3 #story-name {
h3 #storyname {
(story.title)
}
p {
@ -98,12 +104,15 @@ pub async fn render(
div .tag-container {
@for tag in story.tags {
div .tag {
a href=(format!("/search?query={}&type=tag", tag)) { (tag) }
a href=(format!("/search?query={}&type=tag", tag)) { "#"(tag) }
}
}
}
}
}
div .story-buttons #detail-buttons {
button #favorite { "Save Story" }
}
}
}
}

Loading…
Cancel
Save