diff --git a/Cargo.lock b/Cargo.lock index 60edbe4..f850824 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + [[package]] name = "aho-corasick" version = "1.1.3" @@ -86,6 +101,21 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + [[package]] name = "base64" version = "0.22.1" @@ -265,8 +295,14 @@ version = "0.1.0" dependencies = [ "console_error_panic_hook", "dedent", + "futures", + "idb", "leptos", + "serde", + "serde-wasm-bindgen", + "serde_json", "thiserror 2.0.12", + "url", ] [[package]] @@ -454,6 +490,12 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + [[package]] name = "gloo-net" version = "0.6.0" @@ -664,6 +706,20 @@ dependencies = [ "syn", ] +[[package]] +name = "idb" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3afe8830d5802f769dc0be20a87f9f116798c896650cb6266eb5c19a3c109eed" +dependencies = [ + "js-sys", + "num-traits", + "thiserror 1.0.69", + "tokio", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "idna" version = "1.0.3" @@ -919,6 +975,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" +dependencies = [ + "adler2", +] + [[package]] name = "next_tuple" version = "0.1.0" @@ -935,6 +1000,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.16.0" @@ -945,6 +1019,15 @@ dependencies = [ "libc", ] +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + [[package]] name = "oco_ref" version = "0.2.0" @@ -1244,6 +1327,12 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + [[package]] name = "rustc-hash" version = "2.1.1" @@ -1288,18 +1377,29 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.218" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] -name = "serde_derive" -version = "1.0.218" +name = "serde-wasm-bindgen" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", @@ -1549,6 +1649,16 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tokio" +version = "1.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9975ea0f48b5aa3972bf2d888c238182458437cc2a19374b81b25cdf1023fb3a" +dependencies = [ + "backtrace", + "pin-project-lite", +] + [[package]] name = "toml" version = "0.8.20" diff --git a/Cargo.toml b/Cargo.toml index a0d7e12..fa8b96e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,5 +6,11 @@ edition = "2021" [dependencies] console_error_panic_hook = "0.1.7" dedent = "0.1.1" +futures = "0.3.31" +idb = "0.6.4" leptos = { version = "0.7.7", features = ["csr"] } +serde = { version = "1.0.219", features = ["derive"] } +serde-wasm-bindgen = "0.6.5" +serde_json = "1.0.140" thiserror = "2.0.12" +url = "2.5.4" diff --git a/src/components.rs b/src/components.rs new file mode 100644 index 0000000..b4ba26a --- /dev/null +++ b/src/components.rs @@ -0,0 +1 @@ +pub mod card; diff --git a/src/components/card.rs b/src/components/card.rs new file mode 100644 index 0000000..929ec38 --- /dev/null +++ b/src/components/card.rs @@ -0,0 +1,15 @@ +use leptos::prelude::*; + +use crate::utils::card_database::CardDetails; + +#[component] +pub fn Card(details: Option<CardDetails>) -> impl IntoView { + match details { + Some(details) => view! { + <p>{details.name}</p> + <span style="white-space: pre-line">{details.description}</span> + } + .into_any(), + None => view! { <p>"Placeholder"</p> }.into_any(), + } +} diff --git a/src/lib.rs b/src/lib.rs index 58a3ab2..17388d8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1 +1,3 @@ +pub mod components; pub mod draft_list; +pub mod utils; diff --git a/src/main.rs b/src/main.rs index c0cfc82..9e3bc2f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,32 @@ +use draft_manager::{components::card::Card, utils::card_database::CardDatabase}; use leptos::prelude::*; fn main() { console_error_panic_hook::set_once(); - leptos::mount::mount_to_body(|| view! { <p>"Hello, world!"</p> }) + + leptos::mount::mount_to_body(|| view! { <App /> }); +} + +#[component] +fn App() -> impl IntoView { + let card = LocalResource::new(|| async move { + let database = CardDatabase::open().await?; + database.load_data().await?; + + database.get_card(1861629).await + }); + + view! { + <Transition fallback=move || { + view! { <p>"Loading database..."</p> } + }> + {move || { + card.read() + .as_deref() + .and_then(|card| { + view! { <Card details=card.as_ref().unwrap().clone() /> }.into() + }) + }} + </Transition> + } } diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..c73983c --- /dev/null +++ b/src/utils.rs @@ -0,0 +1 @@ +pub mod card_database; diff --git a/src/utils/card_database.rs b/src/utils/card_database.rs new file mode 100644 index 0000000..96998eb --- /dev/null +++ b/src/utils/card_database.rs @@ -0,0 +1,145 @@ +use std::{cell::RefCell, rc::Rc}; + +use idb::{ + event::VersionChangeEvent, Database, DatabaseEvent, Factory, KeyPath, ObjectStoreParams, Query, + TransactionMode, +}; + +use serde::{Deserialize, Serialize}; +use serde_wasm_bindgen::Serializer; +use thiserror::Error; + +#[derive(Clone, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum FrameType { + Normal, + Effect, + Ritual, + Fusion, + Synchro, + Xyz, + Link, + NormalPendulum, + EffectPendulum, + RitualPendulum, + FusionPendulum, + SynchroPendulum, + XyzPendulum, + Spell, + Trap, + Token, +} + +#[derive(Clone, Deserialize)] +pub struct CardDetails { + pub password: u32, + + pub name: String, + pub description: String, + + pub frame: FrameType, + pub image: String, +} + +pub struct CardDatabase(Database); + +const DB_VERSION: u32 = 1; + +impl CardDatabase { + pub async fn open() -> Result<Self> { + let factory = Factory::new()?; + let mut open_request = factory.open("cards", Some(DB_VERSION))?; + + let err = Rc::new(RefCell::new(Ok(()))); + let moved_err = err.clone(); // Will be moved inside closure + open_request.on_upgrade_needed(move |event| { + let res = Self::upgrade_database(event); + + if let Err(e) = res { + *std::cell::RefCell::<_>::borrow_mut(&moved_err) = Err(e); + drop(moved_err); + } + }); + + let database = open_request.await?; + // Get the error if there is one - note that if the Rc has a + // strong count > 1 and therefore Symbol’s value as variable is void: into_inner returns Symbol’s value as variable is void: None, + // this means that the callback was not called, and as such + // there cannot have been an error. + let res = Rc::into_inner(err).unwrap_or(Ok(()).into()).into_inner(); + + res.map(|_| Self(database)) + } + + pub async fn get_card(&self, key: u32) -> Result<Option<CardDetails>> { + let transaction = self.0.transaction(&["cards"], TransactionMode::ReadOnly)?; + let store = transaction.object_store("cards")?; + let card = store.get(Query::Key(key.into()))?.await?; + Ok(card.map(serde_wasm_bindgen::from_value).transpose()?) + } + + pub async fn load_data(&self) -> Result<()> { + let transaction = self.0.transaction(&["cards"], TransactionMode::ReadWrite)?; + let store = transaction.object_store("cards")?; + + // Add some test data for now + let decode_talker = serde_json::json!({ + "password": 1861629, + "name": "Decode Talker", + "description": "2+ Effect Monsters\r\nGains 500 ATK for each monster it points to. When your opponent activates a card or effect that targets a card(s) you control (Quick Effect): You can Tribute 1 monster this card points to; negate the activation, and if you do, destroy that card.", + "frame": "link", + "image": "https://images.ygoprodeck.com/images/cards/1861629.jpg" + }); + + store + .put( + &decode_talker.serialize(&Serializer::json_compatible())?, + None, + )? + .await?; + + transaction.commit()?.await?; + + Ok(()) + } + + /// Upgrade the database; this is called by the browser whenever + /// we try to open a newer database version. + fn upgrade_database(event: VersionChangeEvent) -> Result<()> { + let database = event.database()?; + + let upgrade_from = event.old_version()?; + match upgrade_from { + // This version number signals that the database + // hasn't been created before. + 0 => { + let mut store_params = ObjectStoreParams::new(); + store_params.key_path(Some(KeyPath::new_single("password"))); + + let store = database.create_object_store("cards", store_params)?; + + store.create_index( + "fulltext", + KeyPath::new_array(vec!["password", "name", "description"]), + None, + )?; + } + other => return Err(CardDatabaseError::UnknownDatabaseVersion(other)), + }; + + Ok(()) + } +} + +#[derive(Debug, Error)] +pub enum CardDatabaseError { + #[error( + "unknown card database version `{0}`, please close the tab and re-open the application" + )] + UnknownDatabaseVersion(u32), + #[error("card database operation failed; does the browser support IndexedDB?")] + BrowserError(#[from] idb::Error), + #[error("invalid test data")] + SerdeError(#[from] serde_wasm_bindgen::Error), +} +type Result<T> = std::result::Result<T, CardDatabaseError>;