From 620586e0b86a8a5bf6209cd4ba3590321fb892f8 Mon Sep 17 00:00:00 2001 From: fidoriel <49869342+fidoriel@users.noreply.github.com> Date: Fri, 25 Oct 2024 22:19:27 +0200 Subject: [PATCH] working stl rendering --- .env.example | 2 +- Cargo.lock | 91 ++++++++- Cargo.toml | 8 +- backend/main.rs | 137 +++++++++---- backend/parse_library.rs | 191 ++++++++++++++++-- backend/schema.rs | 1 + backend/types.rs | 106 +++++++++- frontend/App.tsx | 36 +++- frontend/components/custom-ui/spinner.tsx | 41 ++++ .../up.sql | 3 +- 10 files changed, 551 insertions(+), 65 deletions(-) create mode 100644 frontend/components/custom-ui/spinner.tsx diff --git a/.env.example b/.env.example index 99110ff..a374cae 100644 --- a/.env.example +++ b/.env.example @@ -6,4 +6,4 @@ PORT=3000 LOG_LEVEL=info # frontend -BACKEND_URL="localhost:3000" +VITE_BACKEND_URL="localhost:3000" diff --git a/Cargo.lock b/Cargo.lock index 349eaf0..c0219c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -203,6 +203,18 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bb8" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89aabfae550a5c44b43ab941844ffcd2e993cb6900b342debf59e9ea74acdb8" +dependencies = [ + "async-trait", + "futures-util", + "parking_lot 0.12.3", + "tokio", +] + [[package]] name = "bit_field" version = "0.10.2" @@ -644,6 +656,23 @@ dependencies = [ "syn 2.0.82", ] +[[package]] +name = "deadpool" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6541a3916932fe57768d4be0b1ffb5ec7cbf74ca8c903fdfd5c0fe8aa958f0ed" +dependencies = [ + "deadpool-runtime", + "num_cpus", + "tokio", +] + +[[package]] +name = "deadpool-runtime" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" + [[package]] name = "deranged" version = "0.3.11" @@ -653,12 +682,19 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "deunicode" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339544cc9e2c4dc3fc7149fd630c5f22263a4fdf18a98afd0075784968b5cf00" + [[package]] name = "diesel" version = "2.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "158fe8e2e68695bd615d7e4f3227c0727b151330d3e253b525086c348d055d5e" dependencies = [ + "chrono", "diesel_derives", "libsqlite3-sys", "time", @@ -671,9 +707,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcb799bb6f8ca6a794462125d7b8983b0c86e6c93a33a9c55934a4a5de4409d3" dependencies = [ "async-trait", + "bb8", + "deadpool", "diesel", "futures-util", "scoped-futures", + "tokio", ] [[package]] @@ -1490,11 +1529,13 @@ dependencies = [ "diesel_migrations", "dotenvy", "envy", + "pathdiff", "serde", "serde_derive", "serde_json", "sha256", "stl-thumb", + "str_slug", "tokio", "tower-http", "tracing", @@ -1502,6 +1543,8 @@ dependencies = [ "typeshare", "url 2.5.2", "url_serde", + "uuid", + "walkdir", ] [[package]] @@ -1693,6 +1736,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi 0.3.9", + "libc", +] + [[package]] name = "num_enum" version = "0.5.11" @@ -1807,6 +1860,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "pathdiff" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d61c5ce1153ab5b689d0c074c4e7fc613e942dfb7dd9eea5ab202d2ad91fe361" + [[package]] name = "percent-encoding" version = "1.0.1" @@ -2015,6 +2074,15 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scoped-futures" version = "0.1.4" @@ -2257,6 +2325,15 @@ dependencies = [ "float-cmp", ] +[[package]] +name = "str_slug" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84da93ea0b92d99a7257a0539f00ef07e1658d54dc82dd811a3be16838dd85fa" +dependencies = [ + "deunicode", +] + [[package]] name = "strsim" version = "0.10.0" @@ -2733,9 +2810,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" dependencies = [ "getrandom", ] @@ -2758,6 +2835,16 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index b68038b..3563687 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,16 +13,18 @@ path = "backend/main.rs" anyhow = "1.0.91" axum = "0.7.7" chrono = { version = "0.4.38", features = ["serde"] } -diesel = { version = "2.2.0", features = ["sqlite", "returning_clauses_for_sqlite_3_35"] } -diesel-async = "0.5.0" +diesel = { version = "2.2.0", features = ["sqlite", "returning_clauses_for_sqlite_3_35", "chrono"] } +diesel-async = { version = "0.5.0", features = ["sqlite", "deadpool", "tokio", "bb8", "async-connection-wrapper"] } diesel_migrations = "2.2.0" dotenvy = "0.15.7" envy = "0.4.2" +pathdiff = "0.2.2" serde = "1.0.213" serde_derive = "1.0.213" serde_json = "1.0.132" sha256 = "1.5.0" stl-thumb = "0.5.0" +str_slug = "0.1.3" tokio = { version = "1.41.0", features = ["full"] } tower-http = { version = "0.6.1", features = ["full"] } tracing = "0.1.40" @@ -30,3 +32,5 @@ tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } typeshare = "1.0.3" url = "2.5.2" url_serde = "0.2.0" +uuid = "1.11.0" +walkdir = "2.5.0" diff --git a/backend/main.rs b/backend/main.rs index 9789914..0a7a000 100644 --- a/backend/main.rs +++ b/backend/main.rs @@ -1,23 +1,37 @@ +use anyhow::Ok; +use axum::Json; use axum::{ + extract::Path, + extract::State, http::StatusCode, - response::{Html, IntoResponse, Response}, + response::{Html, IntoResponse}, routing::{get, post}, Router, }; -use diesel::prelude::*; -use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; +use diesel::sqlite::SqliteConnection; +use diesel::{connection, prelude::*}; +use diesel_async::async_connection_wrapper::AsyncConnectionWrapper; +use diesel_async::pooled_connection::bb8::{Pool, PooledConnection}; +use diesel_async::pooled_connection::AsyncDieselConnectionManager; +use diesel_async::pooled_connection::ManagerConfig; +use diesel_async::sync_connection_wrapper::SyncConnectionWrapper; +use diesel_async::{AsyncConnection, RunQueryDsl}; +use diesel_migrations::MigrationHarness; +use diesel_migrations::{embed_migrations, EmbeddedMigrations}; use serde_derive::Deserialize; -use std::path::PathBuf; +use std::{os::linux::raw::stat, path::PathBuf}; use tower_http::cors::{Any, CorsLayer}; use tower_http::services::{ServeDir, ServeFile}; -use tracing::info; -use tracing_subscriber; +use tracing::{debug, info}; use tracing_subscriber::EnvFilter; +use types::ModelResponseList; mod parse_library; pub mod schema; pub mod types; -use parse_library::find_modelpack_directories; +use crate::schema::models3d; +use crate::schema::models3d::dsl::*; +use crate::types::{File3D, Model3D, NewModel3D}; #[derive(Clone, Debug, Deserialize)] struct Config { @@ -29,6 +43,8 @@ struct Config { host: String, #[serde(default = "default_port")] port: String, + #[serde(default = "default_asset_prefix")] + asset_prefix: String, #[serde(skip)] database_url: PathBuf, #[serde(skip)] @@ -49,6 +65,10 @@ fn default_log_level() -> String { "info".to_string() } +fn default_asset_prefix() -> String { + "/3d".to_string() +} + impl Config { fn initialize(&mut self) { self.database_url = self.data_dir.join("db.sqlite3"); @@ -57,26 +77,65 @@ impl Config { } } -async fn paths() -> Html { - let start_dir = PathBuf::from("./3dassets"); - let modelpack_directories = find_modelpack_directories(start_dir).await.unwrap(); - let mut paths_string = String::new(); - for dir in modelpack_directories { - paths_string.push_str(&format!("{}\n", dir.display())); - } +fn parse_config() -> Config { + let mut init_config = match envy::from_env::() { + Result::Ok(config) => config, + Err(error) => panic!("{:#?}", error), + }; + init_config.initialize(); + init_config +} + +#[derive(Clone)] +struct AppState { + config: Config, + pool: Pool>, +} + +async fn healthz() -> impl IntoResponse { + (StatusCode::OK, format!("Done")) +} + +async fn get_model_by_slug(Path(slug): Path) -> impl IntoResponse { + (StatusCode::OK, format!("Model slug: {}", slug)) +} + +async fn handle_refresh(State(state): State) -> impl IntoResponse { + parse_library::refresh_library(state.pool, state.config.clone()) + .await + .unwrap(); - Html(paths_string) + (StatusCode::OK, format!("Done")) } -async fn hello_world() -> Html<&'static str> { - Html("

Hello, World!

") +async fn list_models(State(state): State) -> impl IntoResponse { + let mut connection = state.pool.get().await.unwrap(); + + let all_models = models3d.load::(&mut connection).await.unwrap(); + let response = ModelResponseList::from_model_3d(all_models, &state.config).unwrap(); + + (StatusCode::OK, Json(response)) } pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("./migrations"); -async fn db_setup(database_url: PathBuf) -> SqliteConnection { - let mut connection = SqliteConnection::establish(database_url.to_str().expect("Invalid Path")) - .unwrap_or_else(|_| panic!("Error connecting to {}", database_url.display())); +async fn get_connection_pool(config: &Config) -> Pool> { + let mut db_config = ManagerConfig::default(); + db_config.custom_setup = + Box::new(|url| SyncConnectionWrapper::::establish(url)); + let mgr = + AsyncDieselConnectionManager::>::new_with_config( + config.database_url.to_str().unwrap(), + db_config, + ); + + Pool::builder().max_size(10).build(mgr).await.unwrap() +} + +fn migrate(config: &Config) { + let mut connection = + SqliteConnection::establish(config.database_url.to_str().expect("Invalid Path")) + .unwrap_or_else(|_| panic!("Error connecting to {}", config.database_url.display())); info!("DB connection established successfully"); info!("Please wait while DB is migrating"); @@ -86,8 +145,6 @@ async fn db_setup(database_url: PathBuf) -> SqliteConnection { .unwrap_or_else(|e| panic!("Error running migrations: {}", e)); info!("Migrations completed successfully"); - - connection } async fn fallback_404() -> impl IntoResponse { @@ -97,17 +154,22 @@ async fn fallback_404() -> impl IntoResponse { #[tokio::main] async fn main() { dotenvy::dotenv().ok(); - let mut init_config = match envy::from_env::() { - Ok(config) => config, - Err(error) => panic!("{:#?}", error), - }; - init_config.initialize(); - let config = init_config; + let config = parse_config(); + tracing_subscriber::fmt() - .with_env_filter(EnvFilter::new(config.log_level)) + .with_env_filter(EnvFilter::new(config.log_level.clone())) .init(); - let connection: SqliteConnection = db_setup(config.database_url).await; + debug!("Debug logs enabled"); + + migrate(&config); + + let pool = get_connection_pool(&config).await; + + let app_state = AppState { + config: config.clone(), + pool: pool, + }; let cors = CorsLayer::new() .allow_origin(Any) @@ -115,21 +177,26 @@ async fn main() { .allow_headers(Any); let api = Router::new() - .route("/lib/", get(paths)) - .route("/", get(hello_world)) - .route("/refresh", post(hello_world)); + .route("/refresh", post(handle_refresh)) + .route("/models/list", get(list_models)) + .route("/models/:slug", get(get_model_by_slug)) + .with_state(app_state); let serve_dir = ServeDir::new("dist"); let app = Router::new() + .route("/healthz", get(healthz)) .nest("/api/", api) - .nest_service("/3d", ServeDir::new("3dassets")) + .nest_service( + &config.asset_prefix.to_string(), + ServeDir::new(config.libraries_path), + ) .route_service("/", ServeFile::new("dist/index.html")) .fallback_service(serve_dir) .fallback(fallback_404) .layer(cors); - let listener = tokio::net::TcpListener::bind(config.address.to_string()) + let listener = tokio::net::TcpListener::bind(&config.address.to_string()) .await .unwrap(); diff --git a/backend/parse_library.rs b/backend/parse_library.rs index 072c5e2..e49c736 100644 --- a/backend/parse_library.rs +++ b/backend/parse_library.rs @@ -1,11 +1,22 @@ -use anyhow::Result; +use crate::schema::models3d; +use crate::schema::models3d::dsl::*; +use crate::types::ModelPackV0_1; +use crate::types::{File3D, Model3D, NewModel3D}; +use crate::Config; +use chrono::Local; +use diesel::prelude::*; use diesel::SqliteConnection; -use std::path::PathBuf; +use diesel_async::pooled_connection::bb8::Pool; +use diesel_async::sync_connection_wrapper::SyncConnectionWrapper; +use diesel_async::RunQueryDsl; +use std::collections::HashSet; +use std::path::{Path, PathBuf}; +use std::str::FromStr; use tokio::fs; +use tracing::{debug, info}; +use uuid::Uuid; -use crate::types::ModelPackV0_1; - -pub async fn find_modelpack_directories(start_path: PathBuf) -> Result> { +pub async fn find_modelpack_directories(start_path: PathBuf) -> anyhow::Result> { let mut modelpack_dirs = Vec::new(); let mut dirs_to_check = vec![start_path]; @@ -19,15 +30,16 @@ pub async fn find_modelpack_directories(start_path: PathBuf) -> Result Result Result> { - path.push("modelpack.json"); +pub async fn get_modelpack_meta(pth: &PathBuf) -> anyhow::Result { + let mut json_pth = pth.clone(); + json_pth.push("modelpack.json"); - let data = fs::read_to_string(path).await?; + let data = fs::read_to_string(json_pth).await?; let model_pack: ModelPackV0_1 = serde_json::from_str(&data)?; Ok(model_pack) } -pub async fn refresh_library(connection: SqliteConnection) { - let data_dirs = find_modelpack_directories(PathBuf::from("3dassets")) +async fn get_all_image_files(image_dir: &PathBuf) -> anyhow::Result> { + let supported_extensions = vec!["jpg", "jpeg", "png", "gif", "bmp", "tiff", "webp"]; + let mut image_files = Vec::new(); + + let mut dir_entries = fs::read_dir(&image_dir).await.unwrap(); + + while let Some(entry) = dir_entries.next_entry().await? { + let pth = entry.path(); + + if pth.is_file() { + if let Some(extension) = pth.extension() { + if supported_extensions.contains( + &extension + .to_str() + .unwrap_or("") + .to_ascii_lowercase() + .as_str(), + ) { + image_files.push(pth); + } + } + } + } + + Ok(image_files) +} + +pub async fn refresh_library( + pool: Pool>, + config: Config, +) -> anyhow::Result<()> { + info!( + "Started lib scan at {}", + Local::now().format("%Y-%m-%d %H:%M:%S") + ); + + let data_dirs = find_modelpack_directories(config.libraries_path.clone()) .await .unwrap(); + let mut connection = pool.get().await.unwrap(); + + // delete models from db which do not exist anymore in the fs + let dirs_set: HashSet = data_dirs + .iter() + .filter_map(|dir| pathdiff::diff_paths(dir, &config.libraries_path)) + .collect(); + + let possibly_old_models = models3d.load::(&mut connection).await.unwrap(); + + for model in possibly_old_models { + if !dirs_set.contains(&PathBuf::from(&model.path)) { + diesel::delete(models3d.filter(id.eq(model.id))) + .execute(&mut connection) + .await + .unwrap(); + + debug!( + "Deleted model from database: {:?} (id: {})", + model.path, model.id + ); + } + } + + // add new or update models + + for dir in data_dirs { + let model_pack_meta = get_modelpack_meta(&dir).await.unwrap(); + + let relative_dir = pathdiff::diff_paths(&dir, &config.libraries_path).unwrap(); + + let result: Option = models3d + .filter(path.eq(relative_dir.to_str().unwrap())) + .first::(&mut connection) + .await + .ok(); + + let mut image_dir = dir.clone(); + image_dir.push("images"); + + let new_object = NewModel3D::from_model_pack_v0_1( + &model_pack_meta, + &relative_dir, + get_all_image_files(&image_dir).await.unwrap(), + ) + .unwrap(); + + if let Some(existing_model) = result { + diesel::update(models3d.find(existing_model.id)) + .set(( + title.eq(&new_object.title), + name.eq(&new_object.name), + license.eq(&new_object.license), + author.eq(&new_object.author), + origin.eq(&new_object.origin), + images.eq(new_object.images), + )) + .execute(&mut connection) + .await + .unwrap(); + debug!("Updated {:?}", new_object.path) + } else { + diesel::insert_into(models3d::table) + .values(&new_object) + .execute(&mut connection) + .await + .unwrap(); + debug!("Created {:?}", new_object.path) + } + } + + // generate previews + fs::create_dir_all(config.preview_cache_dir.clone()).await?; + let models = models3d.load::(&mut connection).await.unwrap(); + + for model in models { + let mut pth = config.libraries_path.clone(); + pth.push(model.path); + pth.push("files"); + + debug!("starting search for {:?}", pth); + for entry in walkdir::WalkDir::new(pth) { + let mesh_files = ["stl", "3mf", "obj"]; + let entry = entry.unwrap(); + let file_path = entry.path(); + + if file_path.is_dir() { + continue; + } + + if let Some(extension) = file_path.extension() { + if !mesh_files.contains(&extension.to_str().unwrap()) { + continue; + } + } + + let mut img_path = config.preview_cache_dir.clone(); + img_path.push(format!("{}.png", Uuid::new_v4().to_string())); + + let mut render_config = stl_thumb::config::Config::default(); + render_config.stl_filename = file_path.to_str().unwrap().to_string(); + render_config.img_filename = img_path.to_str().unwrap().to_string(); + + stl_thumb::render_to_file(&render_config).unwrap(); + } + } + + info!( + "Finished lib scan at {}", + Local::now().format("%Y-%m-%d %H:%M:%S") + ); - for dir in data_dirs {} + Ok(()) } diff --git a/backend/schema.rs b/backend/schema.rs index c55f91b..58dbc57 100644 --- a/backend/schema.rs +++ b/backend/schema.rs @@ -21,6 +21,7 @@ diesel::table! { path -> Text, origin -> Nullable, date_added -> Nullable, + images -> Text, } } diff --git a/backend/types.rs b/backend/types.rs index b7bfc49..c27baf7 100644 --- a/backend/types.rs +++ b/backend/types.rs @@ -1,13 +1,31 @@ +use std::path::PathBuf; + use crate::schema::{files3d, models3d}; +use crate::Config; +use anyhow::{Error, Result}; use chrono::NaiveDateTime; use diesel::prelude::*; use serde::{Deserialize, Serialize}; +use stl_thumb::config::{self}; +use str_slug::StrSlug; use typeshare::typeshare; +fn comma_separated_to_pathbuf_vec(input: &str) -> Vec { + input.split(',').map(|s| PathBuf::from(s.trim())).collect() +} + +fn pathbuf_vec_to_comma_separated(paths: Vec) -> String { + paths + .iter() + .map(|path| path.to_string_lossy().into_owned()) + .collect::>() + .join(",") +} + #[typeshare] #[derive(Serialize, Deserialize, Debug)] pub struct ModelPackV0_1 { - schema_version: u32, + version: String, title: String, author: String, origin: String, @@ -15,7 +33,7 @@ pub struct ModelPackV0_1 { } #[typeshare] -#[derive(Debug, Serialize, Deserialize, Queryable, Identifiable)] +#[derive(Debug, Serialize, Deserialize, Queryable, Identifiable, Selectable)] #[diesel(table_name = models3d)] pub struct Model3D { pub id: i32, @@ -26,6 +44,65 @@ pub struct Model3D { pub path: String, pub origin: Option, pub date_added: Option, + pub images: String, +} + +impl Model3D { + pub fn relative_image_paths(&self) -> Vec { + return comma_separated_to_pathbuf_vec(&self.images); + } +} + +#[typeshare] +#[derive(Debug, Serialize, Deserialize)] +pub struct ModelResponse { + pub id: i32, + pub title: String, + pub name: String, + pub license: Option, + pub author: Option, + pub origin: Option, + pub images: Vec, +} + +impl ModelResponse { + pub fn from_model_3d(model: &Model3D, config: &Config) -> Result { + let prefixed_images: Vec = model + .relative_image_paths() + .iter() + .map(|image| format!("{}{}", config.asset_prefix, image.display())) + .collect(); + + Ok(ModelResponse { + id: model.id, + title: model.title.clone(), + name: model.name.clone(), + license: model.license.clone(), + author: model.author.clone(), + origin: model.origin.clone(), + images: prefixed_images, + }) + } +} + +#[typeshare] +#[derive(Debug, Serialize, Deserialize)] +pub struct ModelResponseList { + pub models: Vec, +} + +impl ModelResponseList { + pub fn from_model_3d(model: Vec, config: &Config) -> Result { + let mut models: Vec = Vec::new(); + + for m in model { + let model_response = ModelResponse::from_model_3d(&m, &config).unwrap(); + models.push(model_response); + } + + let response = ModelResponseList { models }; + Ok(response) + } } #[typeshare] @@ -38,10 +115,33 @@ pub struct NewModel3D { pub author: Option, pub path: String, pub origin: Option, + pub images: String, +} + +impl NewModel3D { + pub fn from_model_pack_v0_1( + pack: &ModelPackV0_1, + path: &PathBuf, + image_paths: Vec, + ) -> Result { + Ok(NewModel3D { + title: pack.title.clone(), + name: str_slug::slug(pack.title.clone()), + license: Some(pack.license.clone()), + author: Some(pack.author.clone()), + path: path.clone().into_os_string().into_string().unwrap(), + origin: Some(pack.origin.clone()), + images: pathbuf_vec_to_comma_separated(image_paths), + }) + } + + pub fn relative_image_paths(&self) -> Vec { + return comma_separated_to_pathbuf_vec(&self.images); + } } #[typeshare] -#[derive(Debug, Serialize, Deserialize, Queryable, Identifiable, Associations)] +#[derive(Debug, Serialize, Deserialize, Queryable, Identifiable, Associations, Selectable)] #[diesel(belongs_to(Model3D, foreign_key = model_id))] #[diesel(table_name = files3d)] pub struct File3D { diff --git a/frontend/App.tsx b/frontend/App.tsx index 80d8749..9a78ebf 100644 --- a/frontend/App.tsx +++ b/frontend/App.tsx @@ -5,14 +5,44 @@ import { ThemeProvider } from "./components/theme-provider"; import Models from "./Models"; import { BrowserRouter, Routes, Route } from "react-router-dom"; import { RefreshCcw, Upload } from "lucide-react"; +import React, { useState } from 'react'; import { NavLink } from "react-router-dom"; import { Button } from "./components/ui/button"; +import { LoadingSpinner } from "./components/custom-ui/spinner"; const ACTIVE_NAV = "text-sm font-medium text-primary"; const NON_ACTIVE_NAV = "text-sm font-medium text-muted-foreground transition-colors hover:text-primary"; +const BACKEND_BASE_URL = import.meta.env.VITE_BACKEND_URL || ""; + +function Refresh() { + const [loading, setLoading] = useState(false); + + async function handleRefresh() { + console.log('handleRefresh called'); + setLoading(true); + try { + const response = await fetch( BACKEND_BASE_URL + '/api/refresh', { method: 'POST' }); + if (!response.ok) { + throw new Error('Network response was not ok'); + } + } catch (error) { + console.error('Fetch error:', error); + } finally { + setLoading(false); + } + }; + + return ( + + ); + }; + + function Navbar() { return (
@@ -62,12 +92,10 @@ function Navbar() {
- {" "} + +
diff --git a/frontend/components/custom-ui/spinner.tsx b/frontend/components/custom-ui/spinner.tsx new file mode 100644 index 0000000..fb0dcba --- /dev/null +++ b/frontend/components/custom-ui/spinner.tsx @@ -0,0 +1,41 @@ +import * as React from 'react' +import { cn } from '@/lib/utils' + +const spinnerStyles = 'border-4 border-t-4 rounded-full animate-spin' + +const defaultSpinnerColors = { + border: 'gray-200', + borderTop: 'gray-900', + size: 'w-16 h-16', +} + +interface LoadingSpinnerProps extends React.HTMLAttributes { + className?: string + borderColor?: string + borderTopColor?: string + size?: number | string +} + +const LoadingSpinner = React.forwardRef( + (props, ref) => { + const { className, borderColor, borderTopColor, size, ...rest } = props + + const borderStyle = cn( + `${spinnerStyles} border-${borderColor || defaultSpinnerColors.border} border-t-${ + borderTopColor || defaultSpinnerColors.borderTop + }`, + ) + + const sizeStyle = size ? `h-${size} w-${size}` : defaultSpinnerColors.size + + return ( +
+
+
+ ) + }, +) + +LoadingSpinner.displayName = 'LoadingSpinner' + +export { LoadingSpinner } \ No newline at end of file diff --git a/migrations/2024-10-22-185038_create_models_and_filles/up.sql b/migrations/2024-10-22-185038_create_models_and_filles/up.sql index 7268234..a9d9600 100644 --- a/migrations/2024-10-22-185038_create_models_and_filles/up.sql +++ b/migrations/2024-10-22-185038_create_models_and_filles/up.sql @@ -6,7 +6,8 @@ CREATE TABLE models3d ( author VARCHAR(256), path VARCHAR(4096) NOT NULL UNIQUE, origin VARCHAR(2048), - date_added TIMESTAMP DEFAULT CURRENT_TIMESTAMP + date_added TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + images VARCHAR ); CREATE TABLE files3d (