Compare commits

..

No commits in common. "2eabf66c736712d9b6b992ca38975ec88675dcbb" and "bca15bf3ad61ade5ff674e78703cb96ebacd302f" have entirely different histories.

21 changed files with 1235 additions and 325 deletions

1
.gitignore vendored
View file

@ -1,4 +1,3 @@
/target
.DS_Store
/.cache
/node_modules

View file

@ -1,11 +0,0 @@
{
"plugins": ["prettier-plugin-jinja-template"],
"overrides": [
{
"files": ["*.html"],
"options": {
"parser": "jinja-template"
}
}
]
}

1118
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -27,6 +27,15 @@ bincode = "1.3.3"
chrono = { version = "0.4.24", features = ["serde"] }
futures = "0.3.28"
headers = "0.4.0"
leptos = { version = "0.6.14", features = [
"ssr",
"nightly",
"tracing",
"default-tls",
"experimental-islands",
] }
leptos_meta = { version = "0.6.14", features = ["ssr"] }
leptos_router = { version = "0.6.14", features = ["ssr"] }
rand = { version = "0.8.5", features = ["small_rng"] }
reqwest = { version = "0.12.7", features = ["json", "native-tls", "blocking"] }
sanitize-filename-reader-friendly = "2.2.1"
@ -38,11 +47,3 @@ tower = { version = "0.5.0", features = ["util"] }
tower-http = { version = "0.5.0", features = ["fs", "trace", "limit"] }
tracing = "0.1.37"
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
# I want to use askama's block feature, this requries unreleased 0.13
[dependencies.askama]
git = "https://github.com/djc/askama.git"
features = ["with-axum"]
[dependencies.askama_axum]
git = "https://github.com/djc/askama.git"

39
package-lock.json generated
View file

@ -1,39 +0,0 @@
{
"name": "nyazoom",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"devDependencies": {
"prettier": "^3.3.3",
"prettier-plugin-jinja-template": "^2.0.0"
}
},
"node_modules/prettier": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
"integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/prettier-plugin-jinja-template": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/prettier-plugin-jinja-template/-/prettier-plugin-jinja-template-2.0.0.tgz",
"integrity": "sha512-REZDAcZuOUvMDaPS47/GNRLKvbxh9DO9euXhWA7gJGqTLGzHPK2Z841F8I4bxsR7e2lqnHezkQ8GcWaKekKBVQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"prettier": "^3.0.0"
}
}
}
}

View file

@ -1,6 +0,0 @@
{
"devDependencies": {
"prettier": "^3.3.3",
"prettier-plugin-jinja-template": "^2.0.0"
}
}

View file

@ -1,7 +1,7 @@
use axum::{
extract::{ConnectInfo, Request},
middleware::{self, Next},
response::IntoResponse,
response::{Html, IntoResponse},
routing::get,
Router,
};
@ -12,7 +12,7 @@ use std::{io, net::SocketAddr};
use nyazoom::*;
use util::{headers::ForwardedFor, logging, sweeper};
use util::{headers::ForwardedFor, logging, ssr, sweeper};
#[tokio::main]
async fn main() -> io::Result<()> {
@ -44,8 +44,9 @@ async fn main() -> io::Result<()> {
async fn welcome() -> impl IntoResponse {
let fact = views::get_cat_fact().await;
views::askama::WelcomeTemplate { fact }
Html(ssr::render(move || {
leptos::view! { <WelcomePage fact /> }
}))
}
async fn log_source(

View file

@ -6,7 +6,7 @@ use axum::{
};
use reqwest::StatusCode;
use crate::{askama::DownloadLinkTemplate, AppState, AsyncRemoveRecord};
use crate::{util::ssr, AppState, AsyncRemoveRecord, DownloadLinkPage};
pub fn get_link_router() -> Router<AppState> {
// Link pages
@ -18,7 +18,7 @@ pub fn get_link_router() -> Router<AppState> {
async fn link(
axum::extract::Path(id): axum::extract::Path<String>,
State(mut state): State<AppState>,
) -> Result<impl IntoResponse, Redirect> {
) -> Result<Html<String>, Redirect> {
{
let mut records = state.records.lock().await;
@ -26,10 +26,10 @@ async fn link(
.get_mut(&id)
.filter(|record| record.can_be_downloaded())
{
return Ok(DownloadLinkTemplate {
id,
record: record.clone(),
});
return Ok(Html(ssr::render({
let record = record.clone();
|| leptos::view! { <DownloadLinkPage id record /> }
})));
}
}

View file

@ -1,6 +1,12 @@
use axum::{extract::State, response::IntoResponse, routing::get, Json, Router};
use axum::{
extract::State,
response::{Html, IntoResponse},
routing::get,
Json, Router,
};
use leptos::CollectView;
use crate::{askama::LinkListTemplate, AppState};
use crate::{util::ssr, AppState, HtmxPage};
pub fn get_records_router() -> Router<AppState> {
// Records views
@ -18,6 +24,34 @@ pub(crate) async fn records(State(state): State<AppState>) -> impl IntoResponse
pub async fn records_links(State(state): State<AppState>) -> impl IntoResponse {
let records = state.records.lock().await.clone();
let record_keys: Vec<String> = records.keys().cloned().collect();
LinkListTemplate { record_keys }
let records_list_view = records
.keys()
.map(|key| {
leptos::view! {
<li class="link-wrapper">
<a href="/link/{key}">{key}</a>
<button
style="margin-left: 1em;"
hx-target="closest .link-wrapper"
hx-swap="outerHTML"
hx-delete="/link/{key}"
>
"X"
</button>
</li>
}
})
.collect_view();
Html(ssr::render(move || {
leptos::view! {
<HtmxPage>
<div class="form-wrapper">
<div class="column-container">
<ul>{records_list_view}</ul>
</div>
</div>
</HtmxPage>
}
}))
}

View file

@ -1,6 +1,5 @@
use std::path::Path;
use askama::Template;
use async_zip::{base::write::ZipFileWriter, Compression, ZipEntryBuilder};
use axum::{
extract::{DefaultBodyLimit, Multipart, State},
@ -15,7 +14,11 @@ use tokio::io;
use tokio_util::{compat::FuturesAsyncWriteCompatExt, io::StreamReader};
use tower_http::limit::RequestBodyLimitLayer;
use crate::{askama::DownloadLinkFragment, cache, util, AppState, UploadRecord};
use crate::{
cache,
util::{self, ssr},
AppState, LinkView, UploadRecord,
};
pub fn get_upload_router() -> Router<AppState> {
// Upload needs a subrouter to increase the body limit
@ -89,7 +92,7 @@ async fn upload_to_zip(
.status(200)
.header("Content-Type", "text/html")
.header("HX-Push-Url", format!("/link/{}", &id))
.body(DownloadLinkFragment { id, record }.render().unwrap())
.body(ssr::render(|| leptos::view! { <LinkView id record /> }))
.unwrap();
Ok(response)

View file

@ -1,5 +1,6 @@
pub mod headers;
pub mod logging;
pub mod ssr;
pub mod sweeper;
use rand::{

7
src/util/ssr.rs Normal file
View file

@ -0,0 +1,7 @@
pub fn render<F, N>(f: F) -> String
where
F: FnOnce() -> N + 'static,
N: leptos::IntoView,
{
leptos::ssr::render_to_string(f).to_string()
}

View file

@ -1,50 +0,0 @@
use askama_axum::Template;
use crate::{link::get_remaining_text, UploadRecord};
#[derive(Template)]
#[template(path = "welcome.html")]
pub struct WelcomeTemplate {
pub fact: String,
}
impl WelcomeTemplate {
pub fn new(fact: String) -> WelcomeTemplate {
WelcomeTemplate { fact }
}
}
#[derive(Template)]
#[template(path = "link.html")]
pub struct DownloadLinkTemplate {
pub id: String,
pub record: UploadRecord,
}
impl DownloadLinkTemplate {
fn get_downloads_remaining_text(record: &UploadRecord) -> String {
let downloads_remaining = record.max_downloads - record.downloads;
get_remaining_text(downloads_remaining)
}
}
#[derive(Template)]
#[template(path = "link.html", block = "content")]
pub struct DownloadLinkFragment {
pub id: String,
pub record: UploadRecord,
}
impl DownloadLinkFragment {
fn get_downloads_remaining_text(record: &UploadRecord) -> String {
let downloads_remaining = record.max_downloads - record.downloads;
get_remaining_text(downloads_remaining)
}
}
#[derive(Template)]
#[template(path = "linklist.html")]
pub struct LinkListTemplate {
pub record_keys: Vec<String>,
}

26
src/views/base_page.rs Normal file
View file

@ -0,0 +1,26 @@
use leptos::{component, view, Children, IntoView};
#[component]
pub fn HtmxPage(children: Children) -> impl IntoView {
view! {
<head>
<title>"Nyazoom"</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link href="/css/main.css" rel="stylesheet" />
<link href="/css/link.css" rel="stylesheet" />
<script src="/scripts/file_label.js" />
<script src="/scripts/link.js" />
<script
src="https://unpkg.com/htmx.org@2.0.2"
integrity="sha384-Y7hw+L/jvKeWIRRkqWYfPcvVxHzVzn5REgzbawhxAuQGwX1XWe70vji+VSeHOThJ"
crossorigin="anonymous"
></script>
</head>
<body>
<h1>"NyaZoom"<sup>"2"</sup></h1>
{children()}
</body>
}
}

47
src/views/links.rs Normal file
View file

@ -0,0 +1,47 @@
use leptos::{component, view, IntoView};
use crate::link::get_remaining_text;
use crate::state::UploadRecord;
use crate::HtmxPage;
// <link href="../dist/css/link.css" rel="stylesheet" />
// #TODO: Handle pushing cleaner
#[component]
pub fn DownloadLinkPage(id: String, record: UploadRecord) -> impl IntoView {
view! {
<HtmxPage>
<div class="form-wrapper">
<LinkView id record />
</div>
</HtmxPage>
}
}
#[component]
pub fn LinkView(id: String, record: UploadRecord) -> impl IntoView {
let downloads_remaining = record.max_downloads - record.downloads;
view! {
<div class="column-container">
<div class="link-wrapper">
<a id="link" href="/download/{id}">
"Download Now!"
</a>
</div>
<div
class="link-wrapper"
hx-get="/link/{id}/remaining"
hx-trigger="click from:#link delay:0.2s, every 10s"
>
{get_remaining_text(downloads_remaining)}
</div>
<button class="return-button" onclick="clipboard()">
Copy to Clipboard
</button>
<a href="/" class="return-button">
"Return to home"
</a>
</div>
}
}

View file

@ -1,7 +1,13 @@
use futures::TryFutureExt;
use serde::Deserialize;
pub mod askama;
pub mod base_page;
pub mod links;
pub mod welcome;
pub use base_page::*;
pub use links::*;
pub use welcome::*;
#[derive(Debug, Deserialize)]
pub struct CatFact {

49
src/views/welcome.rs Normal file
View file

@ -0,0 +1,49 @@
use leptos::{component, view, IntoView};
use crate::HtmxPage;
// {https://api.thecatapi.com/v1/images/search?size=small&format=src}
// {https://cataas.com/cat?width=250&height=250}
#[component]
pub fn WelcomePage(fact: String) -> impl IntoView {
view! {
<HtmxPage>
<div class="form-wrapper">
<WelcomeView fact />
</div>
</HtmxPage>
}
}
#[component]
pub fn WelcomeView(fact: String) -> impl IntoView {
view! {
<form
id="form"
hx-swap="outerHTML"
hx-post="/upload"
hx-encoding="multipart/form-data"
class="column-container"
>
<div class="cat-img-wrapper">
<img
class="cat-img"
src="https://api.thecatapi.com/v1/images/search?size=small&format=src"
/>
</div>
<input
type="file"
id="file"
name="file"
data-multiple-caption="{{count}} files selected"
multiple
/>
<label for="file">"Select Files"</label>
<input type="submit" value="Get Link~" />
<p id="cat-fact">{fact}</p>
<progress id="progress" class="htmx-indicator" value="0" max="100"></progress>
</form>
<script src="/scripts/loading_progress.js" />
}
}

View file

@ -1,21 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<title>Nyazoom</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link href="/css/main.css" rel="stylesheet" />
<link href="/css/link.css" rel="stylesheet" />
<script src="/scripts/file_label.js"></script>
<script src="/scripts/link.js"></script>
<script
src="https://unpkg.com/htmx.org@2.0.2"
integrity="sha384-Y7hw+L/jvKeWIRRkqWYfPcvVxHzVzn5REgzbawhxAuQGwX1XWe70vji+VSeHOThJ"
crossorigin="anonymous"
></script>
</head>
<body>
<h1>NyaZoom<sup>2</sup></h1>
{% block content %}{% endblock %}
</body>
</html>

View file

@ -1,24 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="form-wrapper">
<div class="column-container">
<div class="link-wrapper">
<a id="link" href="/download/{{id}}"> "Download Now!" </a>
</div>
<div
class="link-wrapper"
hx-get="/link/{{id}}/remaining"
hx-trigger="click from:#link delay:0.2s, every 10s"
>
{{Self::get_downloads_remaining_text(record)}}
</div>
<button class="return-button" onclick="clipboard()">
Copy to Clipboard
</button>
<a href="/" class="return-button"> "Return to home" </a>
</div>
</div>
{% endblock content %}

View file

@ -1,25 +0,0 @@
{% extends "base.html" %}
{% block content %}
<HtmxPage>
<div class="form-wrapper">
<div class="column-container">
<ul>
{% for key in record_keys %}
<li class="link-wrapper">
<a href="/link/{{ key }}">{{ key }}</a>
<button
style="margin-left: 1em;"
hx-target="closest .link-wrapper"
hx-swap="outerHTML"
hx-delete="/link/{{ key }}"
>
"X"
</button>
</li>
{% endfor %}
</ul>
</div>
</div>
</HtmxPage>
{% endblock content %}

View file

@ -1,38 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="form-wrapper">
<form
id="form"
hx-swap="outerHTML"
hx-post="/upload"
hx-encoding="multipart/form-data"
class="column-container"
>
<div class="cat-img-wrapper">
<img
class="cat-img"
src="https://api.thecatapi.com/v1/images/search?size=small&format=src"
/>
</div>
<input
type="file"
id="file"
name="file"
data-multiple-caption="{count} files selected"
multiple
/>
<label for="file">Select Files</label>
<input type="submit" value="Get Link~" />
<p id="cat-fact">{{ fact }}</p>
<progress
id="progress"
class="htmx-indicator"
value="0"
max="100"
></progress>
</form>
<script src="/scripts/loading_progress.js"></script>
</div>
{% endblock content %}