changes
This commit is contained in:
1274
Cargo.lock
generated
1274
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
13
Cargo.toml
13
Cargo.toml
@@ -79,11 +79,15 @@ lettre = { version = "0.11.18", default-features = false, features = [
|
|||||||
"tokio1-rustls-tls",
|
"tokio1-rustls-tls",
|
||||||
"tracing",
|
"tracing",
|
||||||
], optional = true }
|
], optional = true }
|
||||||
|
|
||||||
|
aws-config = { version = "1.1", features = [
|
||||||
|
"behavior-version-latest",
|
||||||
|
], optional = true }
|
||||||
|
aws-sdk-s3 = { version = "1.104", optional = true }
|
||||||
|
|
||||||
tera = { version = "1.20.0", default-features = false, optional = true }
|
tera = { version = "1.20.0", default-features = false, optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["ssr"]
|
|
||||||
hydrate = ["leptos/hydrate", "thaw/csr"]
|
|
||||||
ssr = [
|
ssr = [
|
||||||
"dep:axum",
|
"dep:axum",
|
||||||
"dep:tower",
|
"dep:tower",
|
||||||
@@ -102,9 +106,14 @@ ssr = [
|
|||||||
"dep:leptos_axum",
|
"dep:leptos_axum",
|
||||||
"thaw/ssr",
|
"thaw/ssr",
|
||||||
"lettre",
|
"lettre",
|
||||||
|
"aws-config",
|
||||||
|
"aws-sdk-s3",
|
||||||
"tera",
|
"tera",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
default = ["ssr"]
|
||||||
|
hydrate = ["leptos/hydrate", "thaw/csr"]
|
||||||
|
|
||||||
[package.metadata.cargo-all-features]
|
[package.metadata.cargo-all-features]
|
||||||
denylist = ["axum", "tower", "tower-http", "tokio", "sqlx", "leptos_axum"]
|
denylist = ["axum", "tower", "tower-http", "tokio", "sqlx", "leptos_axum"]
|
||||||
skip_feature_sets = [["ssr", "hydrate"]]
|
skip_feature_sets = [["ssr", "hydrate"]]
|
||||||
|
|||||||
BIN
airsoftfotos.db
BIN
airsoftfotos.db
Binary file not shown.
@@ -9,11 +9,15 @@ CREATE TABLE IF NOT EXISTS albums (
|
|||||||
FOREIGN KEY(category_id) REFERENCES categories(id)
|
FOREIGN KEY(category_id) REFERENCES categories(id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- s3://airsoftfotos/{category:1}/{event-date:3-9-2025}/{id:1}/airsoftfotos.nl_{uuid}.webp -- processed
|
||||||
|
-- s3://airsoftfotos/{category:1}/{event-date:3-9-2025}/{id:1}/{uuid}_{filename} -- original file
|
||||||
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS photos (
|
CREATE TABLE IF NOT EXISTS photos (
|
||||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, -- might just store id as the filename in S3... or at least a folder with the og image, and a processed image (lower res and watermarked)
|
||||||
album_id INTEGER NOT NULL,
|
album_id INTEGER NOT NULL,
|
||||||
user_id INTEGER NOT NULL,
|
user_id INTEGER NOT NULL,
|
||||||
filename TEXT NOT NULL,
|
filename TEXT NOT NULL, -- is this the OG filename, or the filename we need to request from S3
|
||||||
description TEXT NULL,
|
description TEXT NULL,
|
||||||
visible BOOLEAN NOT NULL DEFAULT TRUE,
|
visible BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
upvotes INTEGER NOT NULL,
|
upvotes INTEGER NOT NULL,
|
||||||
|
|||||||
13
src/app.rs
13
src/app.rs
@@ -10,7 +10,7 @@ use leptos::prelude::*;
|
|||||||
use leptos_meta::*;
|
use leptos_meta::*;
|
||||||
use leptos_router::{components::*, path};
|
use leptos_router::{components::*, path};
|
||||||
|
|
||||||
use thaw::{ConfigProvider, Layout, LayoutHeader, LoadingBarProvider, Theme, ToasterProvider};
|
use thaw::{ConfigProvider, LayoutHeader, LoadingBarProvider, Theme, ToasterProvider};
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
pub mod ssr {
|
pub mod ssr {
|
||||||
@@ -28,6 +28,17 @@ pub mod ssr {
|
|||||||
.ok_or_else(|| ServerFnError::ServerError("Lettre missing".into()))
|
.ok_or_else(|| ServerFnError::ServerError("Lettre missing".into()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn s3() -> Result<aws_sdk_s3::Client, ServerFnError> {
|
||||||
|
use aws_sdk_s3::Client;
|
||||||
|
let Some(shared_config) =
|
||||||
|
with_context::<AppState, _>(|state| state.aws_shared_config.clone())
|
||||||
|
else {
|
||||||
|
return Err(ServerFnError::ServerError("s3 init error".into()));
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Client::new(&shared_config))
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn auth() -> Result<AuthSession, ServerFnError> {
|
pub async fn auth() -> Result<AuthSession, ServerFnError> {
|
||||||
let auth = leptos_axum::extract().await?;
|
let auth = leptos_axum::extract().await?;
|
||||||
Ok(auth)
|
Ok(auth)
|
||||||
|
|||||||
@@ -15,3 +15,6 @@ pub use file_preview::*;
|
|||||||
|
|
||||||
mod upload;
|
mod upload;
|
||||||
pub use upload::*;
|
pub use upload::*;
|
||||||
|
|
||||||
|
mod progress_bar;
|
||||||
|
pub use progress_bar::*;
|
||||||
|
|||||||
29
src/app/components/progress-bar.css
Normal file
29
src/app/components/progress-bar.css
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
.thaw-progress-bar {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
background-color: var(--colorNeutralBackground6);
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: var(--borderRadiusMedium);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-progress-bar__bar {
|
||||||
|
transition-timing-function: ease;
|
||||||
|
transition-duration: 0.3s;
|
||||||
|
transition-property: width;
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--colorCompoundBrandBackground);
|
||||||
|
border-radius: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-progress-bar--error .thaw-progress-bar__bar {
|
||||||
|
background-color: var(--colorPaletteRedBackground3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-progress-bar--warning .thaw-progress-bar__bar {
|
||||||
|
background-color: var(--colorPaletteDarkOrangeBackground3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-progress-bar--success .thaw-progress-bar__bar {
|
||||||
|
background-color: var(--colorPaletteGreenBackground3);
|
||||||
|
}
|
||||||
43
src/app/components/progress_bar.rs
Normal file
43
src/app/components/progress_bar.rs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
use leptos::prelude::*;
|
||||||
|
use thaw::ProgressBarColor;
|
||||||
|
use thaw_utils::{class_list, mount_style};
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn ProgressBar1(
|
||||||
|
#[prop(optional, into)] class: MaybeProp<String>,
|
||||||
|
/// A decimal number between 0 and 1 (or between 0 and max if given),
|
||||||
|
/// which specifies how much of the task has been completed.
|
||||||
|
#[prop(into, optional)]
|
||||||
|
value: Signal<i64>,
|
||||||
|
/// The maximum value, which indicates the task is complete.
|
||||||
|
/// The ProgressBar bar will be full when value equals max.
|
||||||
|
#[prop(default = 100.into(), optional)]
|
||||||
|
max: Signal<i64>,
|
||||||
|
/// ProgressBar color.
|
||||||
|
#[prop(into, optional)]
|
||||||
|
color: Signal<ProgressBarColor>,
|
||||||
|
) -> impl IntoView {
|
||||||
|
mount_style("progress-bar", include_str!("./progress-bar.css"));
|
||||||
|
|
||||||
|
let style = move || {
|
||||||
|
let max = max.get();
|
||||||
|
let value = value.get().max(0).min(max);
|
||||||
|
format!("width: {:.02}%;", (value as f64 / max as f64 * 100.0))
|
||||||
|
};
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div
|
||||||
|
class=class_list![
|
||||||
|
"thaw-progress-bar",
|
||||||
|
move || format!("thaw-progress-bar--{}", color.get().as_str()),
|
||||||
|
class
|
||||||
|
]
|
||||||
|
role="progressbar"
|
||||||
|
aria_valuemax=move || max.get()
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuenow=move || value.get()
|
||||||
|
>
|
||||||
|
<div class="thaw-progress-bar__bar" style=style></div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
app::{
|
app::{
|
||||||
components::{FilePreview, ImageUpload, Upload1},
|
components::{FilePreview, ImageUpload, ProgressBar1, Upload1},
|
||||||
models::Category,
|
|
||||||
pages::get_categories,
|
pages::get_categories,
|
||||||
},
|
},
|
||||||
auth::{Logout, User},
|
auth::{Logout, User},
|
||||||
@@ -9,7 +8,10 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use chrono::prelude::*;
|
use chrono::prelude::*;
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use leptos::{logging::log, task::spawn_local};
|
use leptos::{
|
||||||
|
logging::log,
|
||||||
|
task::{spawn, spawn_local},
|
||||||
|
};
|
||||||
use thaw::*;
|
use thaw::*;
|
||||||
use web_sys::js_sys::Uint8Array;
|
use web_sys::js_sys::Uint8Array;
|
||||||
|
|
||||||
@@ -19,10 +21,37 @@ pub async fn go_upload(
|
|||||||
date: NaiveDate,
|
date: NaiveDate,
|
||||||
photo: ImageUpload,
|
photo: ImageUpload,
|
||||||
) -> Result<(), ServerFnError> {
|
) -> Result<(), ServerFnError> {
|
||||||
// ..
|
use crate::app::ssr::{pool, s3};
|
||||||
log!("{}", category);
|
let client = s3()?;
|
||||||
log!("{}", date);
|
let pool = pool()?;
|
||||||
log!("{}", photo.name);
|
|
||||||
|
spawn(async move {
|
||||||
|
// SHOULD ALL THIS CRAP BE MICROSERVICES?
|
||||||
|
|
||||||
|
// upload the original image
|
||||||
|
// post process the image
|
||||||
|
// upload post processed image
|
||||||
|
|
||||||
|
// store progress, probably in db or something?
|
||||||
|
// so the user can come back to it later
|
||||||
|
// this really need to be a seperate process from this point forward
|
||||||
|
// not a sub-thread that gets aborted on a reload
|
||||||
|
|
||||||
|
let object = match client.list_objects_v2().bucket("airsoftfotos").send().await {
|
||||||
|
Ok(obj) => obj,
|
||||||
|
Err(e) => {
|
||||||
|
log!("{:#?}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
dbg!(object);
|
||||||
|
log!("{}", photo.name);
|
||||||
|
log!("{}", category);
|
||||||
|
log!("{}", date);
|
||||||
|
log!("{}", photo.name);
|
||||||
|
});
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,19 +95,26 @@ pub fn Account(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let file_count = Signal::derive(move || files.get().len() as i64);
|
||||||
|
let progress = RwSignal::new(0);
|
||||||
|
let current = RwSignal::new(String::new());
|
||||||
let disabled = RwSignal::new(false);
|
let disabled = RwSignal::new(false);
|
||||||
|
|
||||||
let upload = move |_| {
|
let upload = move |_| {
|
||||||
|
// todo: rewrite this so it doesn't block frontend... idk why this happens
|
||||||
disabled.set(true);
|
disabled.set(true);
|
||||||
|
|
||||||
let category = selected_category.get().parse().unwrap();
|
let category = selected_category.get().parse().unwrap();
|
||||||
let date = date.get();
|
let date = date.get();
|
||||||
let photos = files.get();
|
let photos = files.get();
|
||||||
|
|
||||||
for file in photos {
|
spawn(async move {
|
||||||
spawn_local(async move {
|
for file in photos {
|
||||||
let _ = dbg!(go_upload(category, date, file).await);
|
current.set(file.name.clone());
|
||||||
});
|
let _ = go_upload(category, date, file).await;
|
||||||
}
|
progress.update(|f| *f += 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
@@ -140,7 +176,7 @@ pub fn Account(
|
|||||||
}>
|
}>
|
||||||
{ move || if files().is_empty() {
|
{ move || if files().is_empty() {
|
||||||
view! {
|
view! {
|
||||||
<div style="margin: auto;">"Sleep hier de fotos om te uploaden."</div>
|
<div style="margin: auto;">"Sleep naar hier de fotos om deze voor te bereiden om te uploaden."</div>
|
||||||
}.into_any()
|
}.into_any()
|
||||||
} else {
|
} else {
|
||||||
files().into_iter().map(move |file| {
|
files().into_iter().map(move |file| {
|
||||||
@@ -154,6 +190,10 @@ pub fn Account(
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Upload1>
|
</Upload1>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<p>{progress}/{file_count}" - "{current}</p>
|
||||||
|
<ProgressBar1 max=file_count value=progress />
|
||||||
|
</div>
|
||||||
|
|
||||||
<Button button_type=ButtonType::Button disabled on_click=upload>
|
<Button button_type=ButtonType::Button disabled on_click=upload>
|
||||||
"Upload"
|
"Upload"
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ pub fn Login(action: ServerAction<Login>) -> impl IntoView {
|
|||||||
name="email"
|
name="email"
|
||||||
/>
|
/>
|
||||||
<Input input_type=InputType::Password placeholder="Wachtwoord" name="password"/>
|
<Input input_type=InputType::Password placeholder="Wachtwoord" name="password"/>
|
||||||
|
<input type="checkbox" name="remember" /> Onthouden
|
||||||
<Button button_type=ButtonType::Submit class="button">
|
<Button button_type=ButtonType::Submit class="button">
|
||||||
"Inloggen"
|
"Inloggen"
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -103,11 +103,14 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
let addr = leptos_options.site_addr;
|
let addr = leptos_options.site_addr;
|
||||||
let routes = generate_route_list(App);
|
let routes = generate_route_list(App);
|
||||||
|
|
||||||
|
let shared_config = aws_config::from_env().load().await;
|
||||||
|
|
||||||
let app_state = AppState {
|
let app_state = AppState {
|
||||||
leptos_options,
|
leptos_options,
|
||||||
pool: pool.clone(),
|
pool: pool.clone(),
|
||||||
routes: routes.clone(),
|
routes: routes.clone(),
|
||||||
lettre: dangerous_lettre,
|
lettre: dangerous_lettre,
|
||||||
|
aws_shared_config: shared_config,
|
||||||
};
|
};
|
||||||
|
|
||||||
// build our application with a route
|
// build our application with a route
|
||||||
|
|||||||
@@ -13,4 +13,5 @@ pub struct AppState {
|
|||||||
pub pool: SqlitePool,
|
pub pool: SqlitePool,
|
||||||
pub routes: Vec<AxumRouteListing>,
|
pub routes: Vec<AxumRouteListing>,
|
||||||
pub lettre: DangerousLettre,
|
pub lettre: DangerousLettre,
|
||||||
|
pub aws_shared_config: aws_config::SdkConfig,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user