From 04e0a661221f3cbafeb106996376c0069878f22f Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Fri, 12 Apr 2024 17:04:51 -0400 Subject: [PATCH 01/49] Add server_fn crate --- Cargo.lock | 6 ++++-- Cargo.toml | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 90dc998..fb9df85 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1513,6 +1513,7 @@ dependencies = [ "openssl", "pbkdf2", "serde", + "server_fn", "thiserror", "time", "tokio", @@ -2298,9 +2299,9 @@ dependencies = [ [[package]] name = "server_fn" -version = "0.6.10" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15a46a2ffdecb81430ecfb995989218a18b6e94c1ead50cb806b5927c986a8ce" +checksum = "536a5b959673643ee01e59ae41bf01425482c8070dee95d7061ee2d45296b59c" dependencies = [ "axum", "bytes", @@ -2314,6 +2315,7 @@ dependencies = [ "hyper", "inventory", "js-sys", + "multer", "once_cell", "send_wrapper", "serde", diff --git a/Cargo.toml b/Cargo.toml index 4677ca9..d4f43a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ tower-sessions = { version = "0.11", default-features = false } tower-sessions-redis-store = { version = "0.11", optional = true } async-trait = "0.1.79" axum-login = { version = "0.14.0", optional = true } +server_fn = { version = "0.6.11", features = ["multipart"] } [patch.crates-io] gloo-net = { git = "https://github.com/rustwasm/gloo.git", rev = "a823fab7ecc4068e9a28bd669da5eaf3f0a56380" } From 6bf08b7507f25632214b79f48c5a88041f61a91e Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Fri, 19 Apr 2024 09:23:10 -0400 Subject: [PATCH 02/49] Add symphonia crate --- Cargo.lock | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 ++ 2 files changed, 64 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index fb9df85..b47c5f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -56,6 +56,12 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + [[package]] name = "async-recursion" version = "1.1.0" @@ -244,6 +250,12 @@ version = "3.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" +[[package]] +name = "bytemuck" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6d68c57235a3a081186990eca2867354726650f42f7516ca50c28d6281fd15" + [[package]] name = "byteorder" version = "1.5.0" @@ -1514,6 +1526,7 @@ dependencies = [ "pbkdf2", "serde", "server_fn", + "symphonia", "thiserror", "time", "tokio", @@ -2421,6 +2434,55 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +[[package]] +name = "symphonia" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "815c942ae7ee74737bb00f965fa5b5a2ac2ce7b6c01c0cc169bbeaf7abd5f5a9" +dependencies = [ + "lazy_static", + "symphonia-bundle-mp3", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-bundle-mp3" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c01c2aae70f0f1fb096b6f0ff112a930b1fb3626178fba3ae68b09dce71706d4" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-core" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "798306779e3dc7d5231bd5691f5a813496dc79d3f56bf82e25789f2094e022c3" +dependencies = [ + "arrayvec", + "bitflags 1.3.2", + "bytemuck", + "lazy_static", + "log", +] + +[[package]] +name = "symphonia-metadata" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc622b9841a10089c5b18e99eb904f4341615d5aa55bbf4eedde1be721a4023c" +dependencies = [ + "encoding_rs", + "lazy_static", + "log", + "symphonia-core", +] + [[package]] name = "syn" version = "1.0.109" diff --git a/Cargo.toml b/Cargo.toml index d4f43a8..aab7d94 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ tower-sessions-redis-store = { version = "0.11", optional = true } async-trait = "0.1.79" axum-login = { version = "0.14.0", optional = true } server_fn = { version = "0.6.11", features = ["multipart"] } +symphonia = { version = "0.5.4", default-features = false, features = ["mp3"], optional = true } [patch.crates-io] gloo-net = { git = "https://github.com/rustwasm/gloo.git", rev = "a823fab7ecc4068e9a28bd669da5eaf3f0a56380" } @@ -62,6 +63,7 @@ ssr = [ "tower-http", "tower-sessions-redis-store", "axum-login", + "symphonia", ] # Defines a size-optimized profile for the WASM bundle in release mode From 490772654d0fe85e2cfae4e989017442e22d9853 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Fri, 10 May 2024 10:55:26 -0400 Subject: [PATCH 03/49] Add multer crate --- Cargo.lock | 1 + Cargo.toml | 2 ++ 2 files changed, 3 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index b47c5f2..9939a7f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1522,6 +1522,7 @@ dependencies = [ "leptos_icons", "leptos_meta", "leptos_router", + "multer", "openssl", "pbkdf2", "serde", diff --git a/Cargo.toml b/Cargo.toml index aab7d94..78b142c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ async-trait = "0.1.79" axum-login = { version = "0.14.0", optional = true } server_fn = { version = "0.6.11", features = ["multipart"] } symphonia = { version = "0.5.4", default-features = false, features = ["mp3"], optional = true } +multer = { version = "3.0.0", optional = true } [patch.crates-io] gloo-net = { git = "https://github.com/rustwasm/gloo.git", rev = "a823fab7ecc4068e9a28bd669da5eaf3f0a56380" } @@ -64,6 +65,7 @@ ssr = [ "tower-sessions-redis-store", "axum-login", "symphonia", + "multer", ] # Defines a size-optimized profile for the WASM bundle in release mode From 3338cc26624ff0294d01fbd7c8e9311da368e687 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Fri, 10 May 2024 11:28:11 -0400 Subject: [PATCH 04/49] Add util module --- src/lib.rs | 2 ++ src/util/mod.rs | 0 2 files changed, 2 insertions(+) create mode 100644 src/util/mod.rs diff --git a/src/lib.rs b/src/lib.rs index 89cd04e..e26e2c5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,6 +13,8 @@ pub mod users; pub mod search; pub mod fileserv; pub mod error_template; +pub mod util; + use cfg_if::cfg_if; cfg_if! { diff --git a/src/util/mod.rs b/src/util/mod.rs new file mode 100644 index 0000000..e69de29 From 79ba1914152f5d07cd484e777484aaa058f55fbd Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Fri, 10 May 2024 11:28:43 -0400 Subject: [PATCH 05/49] Add function to get audio file duration --- src/util/audio.rs | 33 +++++++++++++++++++++++++++++++++ src/util/mod.rs | 7 +++++++ 2 files changed, 40 insertions(+) create mode 100644 src/util/audio.rs diff --git a/src/util/audio.rs b/src/util/audio.rs new file mode 100644 index 0000000..719052f --- /dev/null +++ b/src/util/audio.rs @@ -0,0 +1,33 @@ +use symphonia::core::formats::FormatOptions; +use symphonia::core::io::MediaSourceStream; +use symphonia::core::meta::MetadataOptions; +use symphonia::core::probe::Hint; +use std::fs::File; + +/// Measure the duration (in seconds) of an audio file +pub fn measure_duration(file: File) -> Result> { + let source_stream = MediaSourceStream::new(Box::new(file), Default::default()); + + let hint = Hint::new(); + let format_opts = FormatOptions::default(); + let metadata_opts = MetadataOptions::default(); + + let probe = symphonia::default::get_probe().format(&hint, source_stream, &format_opts, &metadata_opts)?; + let reader = probe.format; + + if reader.tracks().len() != 1 { + return Err(format!("Expected 1 track, found {}", reader.tracks().len()).into()) + } + + let track = &reader.tracks()[0]; + + let time_base = track.codec_params.time_base.ok_or("Missing time base")?; + let duration = track.codec_params.n_frames + .map(|frames| track.codec_params.start_ts + frames) + .ok_or("Missing number of frames")?; + + duration + .checked_mul(time_base.numer as u64) + .and_then(|v| v.checked_div(time_base.denom as u64)) + .ok_or("Overflow while computing duration".into()) +} diff --git a/src/util/mod.rs b/src/util/mod.rs index e69de29..cf7196e 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -0,0 +1,7 @@ +use cfg_if::cfg_if; + +cfg_if! { + if #[cfg(feature = "ssr")] { + pub mod audio; + } +} From 5d7ac2bb805c27dce8491458c32796cfffa5c7c9 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Fri, 10 May 2024 14:09:24 -0400 Subject: [PATCH 06/49] Add returning codec type to audio util function --- src/util/audio.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/util/audio.rs b/src/util/audio.rs index 719052f..ff7291f 100644 --- a/src/util/audio.rs +++ b/src/util/audio.rs @@ -1,11 +1,13 @@ +use symphonia::core::codecs::CodecType; use symphonia::core::formats::FormatOptions; use symphonia::core::io::MediaSourceStream; use symphonia::core::meta::MetadataOptions; use symphonia::core::probe::Hint; use std::fs::File; -/// Measure the duration (in seconds) of an audio file -pub fn measure_duration(file: File) -> Result> { +/// Extract the codec and duration of an audio file +/// This is combined into one function because the file object will be consumed +pub fn extract_metadata(file: File) -> Result<(CodecType, u64), Box> { let source_stream = MediaSourceStream::new(Box::new(file), Default::default()); let hint = Hint::new(); @@ -26,8 +28,10 @@ pub fn measure_duration(file: File) -> Result> { .map(|frames| track.codec_params.start_ts + frames) .ok_or("Missing number of frames")?; - duration + let duration = duration .checked_mul(time_base.numer as u64) .and_then(|v| v.checked_div(time_base.denom as u64)) - .ok_or("Overflow while computing duration".into()) + .ok_or("Overflow while computing duration")?; + + Ok((track.codec_params.codec, duration)) } From 353a8e3a9ce3303b867ca93c13cf851cafc6afae Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Fri, 10 May 2024 14:18:48 -0400 Subject: [PATCH 07/49] Allow finding Artist and Album by primary key --- src/models.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/models.rs b/src/models.rs index 15a6d12..73dbc62 100644 --- a/src/models.rs +++ b/src/models.rs @@ -44,7 +44,7 @@ pub struct User { } /// Model for an artist -#[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Insertable))] +#[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Insertable, Identifiable))] #[cfg_attr(feature = "ssr", diesel(table_name = crate::schema::artists))] #[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))] #[derive(Serialize, Deserialize)] @@ -165,7 +165,7 @@ impl Artist { } /// Model for an album -#[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Insertable))] +#[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Insertable, Identifiable))] #[cfg_attr(feature = "ssr", diesel(table_name = crate::schema::albums))] #[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))] #[derive(Serialize, Deserialize)] From 161ea5f9c20daf38b079dfa0b3fc0fb8dd568655 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Fri, 10 May 2024 15:20:57 -0400 Subject: [PATCH 08/49] Implement file upload backend --- src/lib.rs | 1 + src/upload.rs | 274 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 275 insertions(+) create mode 100644 src/upload.rs diff --git a/src/lib.rs b/src/lib.rs index e26e2c5..e7949e3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,6 +13,7 @@ pub mod users; pub mod search; pub mod fileserv; pub mod error_template; +pub mod upload; pub mod util; use cfg_if::cfg_if; diff --git a/src/upload.rs b/src/upload.rs new file mode 100644 index 0000000..db3a3e5 --- /dev/null +++ b/src/upload.rs @@ -0,0 +1,274 @@ +use leptos::*; +use server_fn::{codec::{MultipartData, MultipartFormData}, error::NoCustomError}; +use time::Date; + +use cfg_if::cfg_if; + +cfg_if! { + if #[cfg(feature = "ssr")] { + use multer::Field; + use crate::database::get_db_conn; + use diesel::prelude::*; + use log::*; + } +} + +/// Extract the text from a multipart field +#[cfg(feature = "ssr")] +async fn extract_field(field: Field<'static>) -> Result { + let field = match field.text().await { + Ok(field) => field, + Err(e) => Err(ServerFnError::::ServerError(format!("Error reading field: {}", e)))?, + }; + + Ok(field) +} + +/// Validate the artist ids in a multipart field +/// Expects a field with a comma-separated list of artist ids, and ensures each is a valid artist id in the database +#[cfg(feature = "ssr")] +async fn validate_artist_ids(artist_ids: Field<'static>) -> Result, ServerFnError> { + use crate::models::Artist; + use diesel::result::Error::NotFound; + + // Extract the artist id from the field + match artist_ids.text().await { + Ok(artist_ids) => { + let artist_ids = artist_ids.split(','); + + artist_ids.map(|artist_id| { + // Parse the artist id as an integer + if let Ok(artist_id) = artist_id.parse::() { + // Check if the artist exists + let db_con = &mut get_db_conn(); + let artist = crate::schema::artists::dsl::artists.find(artist_id).first::(db_con); + + match artist { + Ok(_) => Ok(artist_id), + Err(NotFound) => Err(ServerFnError:::: + ServerError("Artist does not exist".to_string())), + Err(e) => Err(ServerFnError:::: + ServerError(format!("Error finding artist id: {}", e))), + } + } else { + Err(ServerFnError::::ServerError("Error parsing artist id".to_string())) + } + }).collect() + }, + + Err(e) => Err(ServerFnError::::ServerError(format!("Error reading artist id: {}", e))), + } +} + +/// Validate the album id in a multipart field +/// Expects a field with an album id, and ensures it is a valid album id in the database +#[cfg(feature = "ssr")] +async fn validate_album_id(album_id: Field<'static>) -> Result { + use crate::models::Album; + use diesel::result::Error::NotFound; + + // Extract the album id from the field + match album_id.text().await { + Ok(album_id) => { + // Parse the album id as an integer + if let Ok(album_id) = album_id.parse::() { + // Check if the album exists + let db_con = &mut get_db_conn(); + let album = crate::schema::albums::dsl::albums.find(album_id).first::(db_con); + + match album { + Ok(_) => Ok(album_id), + Err(NotFound) => Err(ServerFnError:::: + ServerError("Album does not exist".to_string())), + Err(e) => Err(ServerFnError:::: + ServerError(format!("Error finding album id: {}", e))), + } + } else { + Err(ServerFnError::::ServerError("Error parsing album id".to_string())) + } + }, + Err(e) => Err(ServerFnError::::ServerError(format!("Error reading album id: {}", e))), + } +} + +/// Validate the track number in a multipart field +/// Expects a field with a track number, and ensures it is a valid track number (non-negative integer) +#[cfg(feature = "ssr")] +async fn validate_track_number(track_number: Field<'static>) -> Result { + match track_number.text().await { + Ok(track_number) => { + if let Ok(track_number) = track_number.parse::() { + if track_number < 0 { + return Err(ServerFnError:::: + ServerError("Track number must be positive or 0".to_string())); + } else { + Ok(track_number) + } + } else { + return Err(ServerFnError::::ServerError("Error parsing track number".to_string())); + } + }, + Err(e) => Err(ServerFnError::::ServerError(format!("Error reading track number: {}", e)))?, + } +} + +/// Validate the release date in a multipart field +/// Expects a field with a release date, and ensures it is a valid date in the format [year]-[month]-[day] +#[cfg(feature = "ssr")] +async fn validate_release_date(release_date: Field<'static>) -> Result { + match release_date.text().await { + Ok(release_date) => { + let date_format = time::macros::format_description!("[year]-[month]-[day]"); + let release_date = Date::parse(&release_date.trim(), date_format); + + match release_date { + Ok(release_date) => Ok(release_date), + Err(_) => Err(ServerFnError::::ServerError("Invalid release date".to_string())), + } + }, + Err(e) => Err(ServerFnError::::ServerError(format!("Error reading release date: {}", e))), + } +} + +/// Handle the file upload form +#[server(input = MultipartFormData, endpoint = "/upload")] +pub async fn upload(data: MultipartData) -> Result<(), ServerFnError> { + // Safe to unwrap - "On the server side, this always returns Some(_). On the client side, always returns None." + let mut data = data.into_inner().unwrap(); + + let mut title = None; + let mut artist_ids = None; + let mut album_id = None; + let mut track = None; + let mut release_date = None; + let mut file_name = None; + let mut duration = None; + + // Fetch the fields from the form data + while let Ok(Some(mut field)) = data.next_field().await { + let name = field.name().unwrap_or_default().to_string(); + + println!("Field name: {}", name); + + match name.as_str() { + "title" => { title = Some(extract_field(field).await?); }, + "artist_ids" => { artist_ids = Some(validate_artist_ids(field).await?); }, + "album_id" => { album_id = Some(validate_album_id(field).await?); }, + "track_number" => { track = Some(validate_track_number(field).await?); }, + "release_date" => { release_date = Some(validate_release_date(field).await?); }, + "file" => { + use symphonia::core::codecs::CODEC_TYPE_MP3; + use crate::util::audio::extract_metadata; + use std::fs::OpenOptions; + use std::io::{Seek, Write}; + + // Some logging is done here where there is high potential for bugs / failures, + // or behavior that we may wish to change in the future + + // Create file name + let title = title.clone().ok_or(ServerFnError:::: + ServerError("Title field required and must precede file field".to_string()))?; + + let clean_title = title.replace(" ", "_").replace("/", "_"); + let date_format = time::macros::format_description!("[year]-[month]-[day]_[hour]:[minute]:[second]"); + let date_str = time::OffsetDateTime::now_utc().format(date_format).unwrap_or_default(); + let upload_path = format!("assets/audio/upload-{}_{}.mp3", date_str, clean_title); + file_name = Some(format!("upload-{}_{}.mp3", date_str, clean_title)); + + debug!("Saving uploaded file {}", upload_path); + + // Save file to disk + // Use these open options to create the file, write to it, then read from it + let mut file = OpenOptions::new() + .read(true) + .write(true) + .create(true) + .open(upload_path.clone())?; + + while let Some(chunk) = field.chunk().await? { + file.write(&chunk)?; + } + + file.flush()?; + + // Rewind the file so the duration can be measured + file.rewind()?; + + // Get the codec and duration of the file + let (file_codec, file_duration) = extract_metadata(file) + .map_err(|e| { + let msg = format!("Error measuring duration of audio file {}: {}", upload_path, e); + warn!("{}", msg); + ServerFnError::::ServerError(msg) + })?; + + if file_codec != CODEC_TYPE_MP3 { + let msg = format!("Invalid uploaded audio file codec: {}", file_codec); + warn!("{}", msg); + return Err(ServerFnError::::ServerError(msg)); + } + + duration = Some(file_duration); + }, + _ => { + warn!("Unknown file upload field: {}", name); + } + } + } + + // Unwrap mandatory fields + let title = title.ok_or(ServerFnError::::ServerError("Missing title".to_string()))?; + let artist_ids = artist_ids.unwrap_or(vec![]); + let file_name = file_name.ok_or(ServerFnError::::ServerError("Missing file".to_string()))?; + let duration = duration.ok_or(ServerFnError::::ServerError("Missing duration".to_string()))?; + let duration = i32::try_from(duration).map_err(|e| ServerFnError:::: + ServerError(format!("Error converting duration to i32: {}", e)))?; + + // Create the song + use crate::models::Song; + let song = Song { + id: None, + title, + album_id, + track, + duration, + release_date, + storage_path: file_name, + image_path: None, + }; + + // Save the song to the database + let db_con = &mut get_db_conn(); + let song = song.insert_into(crate::schema::songs::table) + .get_result::(db_con) + .map_err(|e| { + let msg = format!("Error saving song to database: {}", e); + warn!("{}", msg); + ServerFnError::::ServerError(msg) + })?; + + // Save the song's artists to the database + let song_id = song.id.ok_or_else(|| { + let msg = "Error saving song to database: song id not found after insertion".to_string(); + warn!("{}", msg); + ServerFnError::::ServerError(msg) + })?; + + use crate::schema::song_artists; + use diesel::ExpressionMethods; + + let artist_ids = artist_ids.into_iter().map(|artist_id| { + (song_artists::song_id.eq(song_id), song_artists::artist_id.eq(artist_id)) + }).collect::>(); + + diesel::insert_into(crate::schema::song_artists::table) + .values(&artist_ids) + .execute(db_con) + .map_err(|e| { + let msg = format!("Error saving song artists to database: {}", e); + warn!("{}", msg); + ServerFnError::::ServerError(msg) + })?; + + Ok(()) +} From 7abfbaf60033c9092d29a4e741491d48a5e8f73f Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Fri, 10 May 2024 15:25:57 -0400 Subject: [PATCH 09/49] Fix unused imports --- src/upload.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/upload.rs b/src/upload.rs index db3a3e5..b3c48b7 100644 --- a/src/upload.rs +++ b/src/upload.rs @@ -1,6 +1,5 @@ use leptos::*; -use server_fn::{codec::{MultipartData, MultipartFormData}, error::NoCustomError}; -use time::Date; +use server_fn::codec::{MultipartData, MultipartFormData}; use cfg_if::cfg_if; @@ -10,6 +9,8 @@ cfg_if! { use crate::database::get_db_conn; use diesel::prelude::*; use log::*; + use server_fn::error::NoCustomError; + use time::Date; } } From 8a959d530d85be3024a888a18f7522fbacff100a Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Fri, 10 May 2024 15:51:26 -0400 Subject: [PATCH 10/49] Implement basic upload dialog --- src/app.rs | 6 ++- src/components.rs | 3 +- src/components/sidebar.rs | 8 +++- src/components/upload.rs | 86 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 98 insertions(+), 5 deletions(-) create mode 100644 src/components/upload.rs diff --git a/src/app.rs b/src/app.rs index 946b4c2..9fbbf2d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -46,18 +46,20 @@ use crate::components::sidebar::*; use crate::components::dashboard::*; use crate::components::search::*; use crate::components::personal::*; +use crate::components::upload::*; /// Renders the home page of your application. #[component] fn HomePage() -> impl IntoView { let play_status = PlayStatus::default(); let play_status = create_rw_signal(play_status); - + let upload_open = create_rw_signal(false); let (dashboard_open, set_dashboard_open) = create_signal(true); view! {
- + + } diff --git a/src/components.rs b/src/components.rs index 4d0c8a5..ede9b31 100644 --- a/src/components.rs +++ b/src/components.rs @@ -1,4 +1,5 @@ pub mod sidebar; pub mod dashboard; pub mod search; -pub mod personal; \ No newline at end of file +pub mod personal; +pub mod upload; diff --git a/src/components/sidebar.rs b/src/components/sidebar.rs index 805fa4e..9f8fff3 100644 --- a/src/components/sidebar.rs +++ b/src/components/sidebar.rs @@ -1,9 +1,10 @@ use leptos::leptos_dom::*; use leptos::*; use leptos_icons::*; +use crate::components::upload::*; #[component] -pub fn Sidebar(setter: WriteSignal, active: ReadSignal) -> impl IntoView { +pub fn Sidebar(setter: WriteSignal, active: ReadSignal, upload_open: RwSignal) -> impl IntoView { let open_dashboard = move |_| { setter.update(|value| *value = true); log!("open dashboard"); @@ -16,7 +17,10 @@ pub fn Sidebar(setter: WriteSignal, active: ReadSignal) -> impl Into view! {