feat(card_db): Implement basic indexed-db based card storage

This commit is contained in:
Tristan Daniël Maat 2025-03-12 00:39:59 +08:00
parent aca5be2a9e
commit 7e9f0ff128
Signed by: tlater
GPG key ID: 49670FD774E43268
8 changed files with 312 additions and 6 deletions

120
Cargo.lock generated
View file

@ -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"

View file

@ -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"

1
src/components.rs Normal file
View file

@ -0,0 +1 @@
pub mod card;

15
src/components/card.rs Normal file
View file

@ -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(),
}
}

View file

@ -1 +1,3 @@
pub mod components;
pub mod draft_list;
pub mod utils;

View file

@ -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>
}
}

1
src/utils.rs Normal file
View file

@ -0,0 +1 @@
pub mod card_database;

145
src/utils/card_database.rs Normal file
View file

@ -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 Symbols value as variable is void: into_inner returns Symbols 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>;