feat(card_db): Import card database from edopro GitHub repo
This commit is contained in:
parent
88da43e208
commit
a3ce7f8008
14 changed files with 691 additions and 108 deletions
src
14
src/bin/database_worker.rs
Normal file
14
src/bin/database_worker.rs
Normal file
|
@ -0,0 +1,14 @@
|
|||
use draft_manager::database::BrowserDatabase;
|
||||
|
||||
fn main() {
|
||||
console_error_panic_hook::set_once();
|
||||
wasm_logger::init(wasm_logger::Config::default());
|
||||
|
||||
wasm_bindgen_futures::spawn_local(async {
|
||||
let a = BrowserDatabase::init_worker().await;
|
||||
|
||||
if let Err(e) = a {
|
||||
log::error!("{:?}", e);
|
||||
}
|
||||
})
|
||||
}
|
|
@ -1,13 +1,13 @@
|
|||
use leptos::prelude::*;
|
||||
|
||||
use crate::database::models::CardDetails;
|
||||
use crate::database::models::CardData;
|
||||
|
||||
#[component]
|
||||
pub fn Card(details: Option<CardDetails>) -> impl IntoView {
|
||||
pub fn Card(details: Option<CardData>) -> impl IntoView {
|
||||
match details {
|
||||
Some(details) => view! {
|
||||
<p>{details.name}</p>
|
||||
<span style="white-space: pre-line">{details.description}</span>
|
||||
<p>{details.id}</p>
|
||||
<span style="white-space: pre-line">{details.level}</span>
|
||||
}
|
||||
.into_any(),
|
||||
None => view! { <p>"Placeholder"</p> }.into_any(),
|
||||
|
|
|
@ -1,54 +1,60 @@
|
|||
use crate::database::{self, models::FrameType, schema::cards};
|
||||
use database::models::CardDetails;
|
||||
use diesel::{prelude::*, Connection};
|
||||
use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness};
|
||||
use thiserror::Error;
|
||||
use gloo::worker::{Registrable, Spawnable, WorkerBridge};
|
||||
|
||||
pub mod models;
|
||||
pub mod schema;
|
||||
mod worker;
|
||||
|
||||
const DB_URL: &str = "cards.db";
|
||||
const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
|
||||
use sqlite_wasm_rs::export::{install_opfs_sahpool, OpfsSAHError};
|
||||
use thiserror_ext::AsReport;
|
||||
use worker::{DatabaseWorker, Query};
|
||||
|
||||
pub struct BrowserDatabase(SqliteConnection);
|
||||
use crate::utils::github_fetch_file;
|
||||
|
||||
pub struct BrowserDatabase {
|
||||
worker: WorkerBridge<DatabaseWorker>,
|
||||
}
|
||||
|
||||
impl BrowserDatabase {
|
||||
pub fn open() -> Result<Self> {
|
||||
let mut connection = SqliteConnection::establish(DB_URL)?;
|
||||
connection.run_pending_migrations(MIGRATIONS).unwrap();
|
||||
Ok(Self(connection))
|
||||
pub fn new() -> Self {
|
||||
let bridge = DatabaseWorker::spawner()
|
||||
.callback(move |o| {
|
||||
log::info!("{:?}", o);
|
||||
})
|
||||
.spawn_with_loader("/database_worker_loader.js");
|
||||
|
||||
log::debug!("Shoulda spawned a worker");
|
||||
|
||||
Self { worker: bridge }
|
||||
}
|
||||
|
||||
pub async fn get_card(&mut self, password: i32) -> Result<Option<CardDetails>> {
|
||||
match cards::dsl::cards.find(password).first(&mut self.0) {
|
||||
Ok(details) => Ok(Some(details)),
|
||||
Err(diesel::result::Error::NotFound) => Ok(None),
|
||||
Err(other) => Err(other.into()),
|
||||
}
|
||||
pub fn send_message(&self) {
|
||||
self.worker.send(Query::ByPassword(1861629));
|
||||
}
|
||||
|
||||
pub fn load_data(&mut self) -> Result<()> {
|
||||
let decode_talker = CardDetails {
|
||||
password: 1861629,
|
||||
name: "Decode Talker".to_owned(),
|
||||
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.".to_owned(),
|
||||
frame: FrameType::Link,
|
||||
image: "https://images.ygoprodeck.com/images/cards/1861629.jpg".to_owned()
|
||||
/// Initialize the database worker.
|
||||
///
|
||||
/// Must be executed in a worker script, not on the main thread.
|
||||
pub async fn init_worker() -> Result<(), OpfsSAHError> {
|
||||
log::debug!("Initializing database worker...");
|
||||
let sah_pool_util = install_opfs_sahpool(None, false).await?;
|
||||
|
||||
let babel_cdb = match github_fetch_file("ProjectIgnis", "BabelCDB", "cards.cdb").await {
|
||||
Ok(cdb) => cdb,
|
||||
Err(err) => {
|
||||
log::error!("{}", err.to_report_string());
|
||||
panic!("{:?}", err);
|
||||
}
|
||||
};
|
||||
|
||||
diesel::insert_into(cards::table)
|
||||
.values(decode_talker)
|
||||
.execute(&mut self.0)?;
|
||||
|
||||
sah_pool_util.import_db("/cards.db", &babel_cdb)?;
|
||||
DatabaseWorker::registrar().register();
|
||||
log::debug!("Database worker set up!");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum BrowserDatabaseError {
|
||||
#[error("failed to connect to database")]
|
||||
DieselConnectionError(#[from] diesel::ConnectionError),
|
||||
#[error("failed to query database")]
|
||||
DieselQueryError(#[from] diesel::result::Error),
|
||||
impl Default for BrowserDatabase {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
type Result<T> = std::result::Result<T, BrowserDatabaseError>;
|
||||
|
|
|
@ -1,38 +1,79 @@
|
|||
use diesel::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::database::schema::cards;
|
||||
use crate::database::schema::datas;
|
||||
use crate::database::schema::texts;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Insertable, Selectable, Queryable)]
|
||||
#[diesel(table_name = cards)]
|
||||
#[derive(Debug, Selectable, Queryable)]
|
||||
#[diesel(table_name = datas)]
|
||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||
pub struct CardData {
|
||||
pub id: i32,
|
||||
pub ot: i32,
|
||||
pub alias: i32,
|
||||
pub setcode: i32,
|
||||
pub card_type: i32,
|
||||
pub atk: i32,
|
||||
pub def: i32,
|
||||
pub level: i32,
|
||||
pub race: i32,
|
||||
pub attribute: i32,
|
||||
pub category: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Associations, Selectable, Queryable)]
|
||||
#[diesel(table_name = texts)]
|
||||
#[diesel(belongs_to(CardData, foreign_key = id))]
|
||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||
pub struct CardTexts {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
pub desc: String,
|
||||
pub str1: String,
|
||||
pub str2: String,
|
||||
pub str3: String,
|
||||
pub str4: String,
|
||||
pub str5: String,
|
||||
pub str6: String,
|
||||
pub str7: String,
|
||||
pub str8: String,
|
||||
pub str9: String,
|
||||
pub str10: String,
|
||||
pub str11: String,
|
||||
pub str12: String,
|
||||
pub str13: String,
|
||||
pub str14: String,
|
||||
pub str15: String,
|
||||
pub str16: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CardDetails {
|
||||
pub password: i32,
|
||||
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
|
||||
pub frame: FrameType,
|
||||
pub image: String,
|
||||
pub desc: String,
|
||||
pub card_type: i32,
|
||||
pub atk: i32,
|
||||
pub def: i32,
|
||||
pub level: i32,
|
||||
pub race: i32,
|
||||
pub attribute: i32,
|
||||
pub category: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, diesel_derive_enum::DbEnum)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum FrameType {
|
||||
Normal,
|
||||
Effect,
|
||||
Ritual,
|
||||
Fusion,
|
||||
Synchro,
|
||||
Xyz,
|
||||
Link,
|
||||
NormalPendulum,
|
||||
EffectPendulum,
|
||||
RitualPendulum,
|
||||
FusionPendulum,
|
||||
SynchroPendulum,
|
||||
XyzPendulum,
|
||||
Spell,
|
||||
Trap,
|
||||
Token,
|
||||
impl CardDetails {
|
||||
pub fn from_data(data: CardData, texts: CardTexts) -> Self {
|
||||
Self {
|
||||
password: data.id,
|
||||
name: texts.name,
|
||||
desc: texts.desc,
|
||||
card_type: data.card_type,
|
||||
atk: data.atk,
|
||||
def: data.def,
|
||||
level: data.level,
|
||||
race: data.race,
|
||||
attribute: data.attribute,
|
||||
category: data.category,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,40 @@
|
|||
diesel::table! {
|
||||
cards (password) {
|
||||
password -> Integer,
|
||||
name -> VarChar,
|
||||
description -> Text,
|
||||
frame -> crate::database::models::FrameTypeMapping,
|
||||
image -> Text,
|
||||
datas {
|
||||
id -> Integer,
|
||||
ot -> Integer,
|
||||
alias -> Integer,
|
||||
setcode -> Integer,
|
||||
#[sql_name = "type"]
|
||||
card_type -> Integer,
|
||||
atk -> Integer,
|
||||
def -> Integer,
|
||||
level -> Integer,
|
||||
race -> Integer,
|
||||
attribute -> Integer,
|
||||
category -> Integer,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
texts {
|
||||
id -> Integer,
|
||||
name -> Text,
|
||||
desc -> Text,
|
||||
str1 -> Text,
|
||||
str2 -> Text,
|
||||
str3 -> Text,
|
||||
str4 -> Text,
|
||||
str5 -> Text,
|
||||
str6 -> Text,
|
||||
str7 -> Text,
|
||||
str8 -> Text,
|
||||
str9 -> Text,
|
||||
str10 -> Text,
|
||||
str11 -> Text,
|
||||
str12 -> Text,
|
||||
str13 -> Text,
|
||||
str14 -> Text,
|
||||
str15 -> Text,
|
||||
str16 -> Text,
|
||||
}
|
||||
}
|
||||
|
|
140
src/database/worker.rs
Normal file
140
src/database/worker.rs
Normal file
|
@ -0,0 +1,140 @@
|
|||
use diesel::{prelude::*, Connection};
|
||||
use futures::TryFutureExt;
|
||||
use gloo::worker::{HandlerId, Worker, WorkerScope};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
use thiserror_ext::AsReport;
|
||||
use wasm_bindgen_futures::spawn_local;
|
||||
|
||||
use super::{
|
||||
models::CardDetails,
|
||||
schema::{datas, texts},
|
||||
};
|
||||
|
||||
const DB_URL: &str = "file:/cards.db?vfs=opfs-sahpool&mode=ro";
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum Query {
|
||||
ByPassword(i32),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum Response {
|
||||
ByPassword(Option<CardDetails>),
|
||||
Error(String),
|
||||
}
|
||||
|
||||
pub struct DatabaseWorker {}
|
||||
|
||||
impl DatabaseWorker {
|
||||
async fn run_query(query: &Query) -> Result<Response> {
|
||||
let res = match query {
|
||||
Query::ByPassword(pw) => Response::ByPassword(Self::get_card(*pw).await?),
|
||||
};
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Open a connection to the database.
|
||||
///
|
||||
/// We do not hold a connection in memory because that requires
|
||||
/// lots of awkward multi-thread data management.
|
||||
fn open_connection() -> Result<SqliteConnection> {
|
||||
let connection = SqliteConnection::establish(DB_URL)?;
|
||||
Ok(connection)
|
||||
}
|
||||
|
||||
async fn get_card(password: i32) -> Result<Option<CardDetails>> {
|
||||
let mut connection = Self::open_connection()?;
|
||||
|
||||
let res = {
|
||||
let data = datas::dsl::datas.find(password).first(&mut connection)?;
|
||||
let texts = texts::dsl::texts.find(password).first(&mut connection)?;
|
||||
|
||||
Ok((data, texts))
|
||||
};
|
||||
|
||||
let Some((data, texts)) = match res {
|
||||
Ok(details) => Ok(Some(details)),
|
||||
Err(diesel::result::Error::NotFound) => Ok(None),
|
||||
Err(other) => Err(other),
|
||||
}?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
// match datas::dsl::datas.find(password).first(&mut connection) {
|
||||
// Ok(details) => Ok(Some(details)),
|
||||
// Err(diesel::result::Error::NotFound) => Ok(None),
|
||||
// Err(other) => Err(other.into()),
|
||||
// };
|
||||
|
||||
Ok(Some(CardDetails::from_data(data, texts)))
|
||||
}
|
||||
|
||||
// pub async fn load_card_data(&mut self) -> Result<()> {
|
||||
// let last_updated =
|
||||
// utils::github_fetch_last_updated("ProjectIgnis", "BabelCDB", "cards.cdb").await?;
|
||||
// if last_updated > time::OffsetDateTime::UNIX_EPOCH {
|
||||
// let card_data =
|
||||
// utils::github_fetch_file("ProjectIgnis", "BabelCDB", "cards.cdb").await?;
|
||||
// self.opfs_util.import_db("cards.cdb", &card_data)?;
|
||||
// }
|
||||
|
||||
// let decode_talker = CardDetails {
|
||||
// password: 1861629,
|
||||
// name: "Decode Talker".to_owned(),
|
||||
// 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.".to_owned(),
|
||||
// frame: FrameType::Link,
|
||||
// image: "https://images.ygoprodeck.com/images/cards/1861629.jpg".to_owned()
|
||||
// };
|
||||
|
||||
// diesel::insert_into(cards::table)
|
||||
// .values(decode_talker)
|
||||
// .execute(&mut self.connection)?;
|
||||
|
||||
// Ok(())
|
||||
// }
|
||||
}
|
||||
|
||||
impl Worker for DatabaseWorker {
|
||||
type Input = Query;
|
||||
type Message = ();
|
||||
type Output = std::result::Result<Response, String>;
|
||||
|
||||
fn create(_scope: &WorkerScope<Self>) -> Self {
|
||||
Self {}
|
||||
}
|
||||
|
||||
fn update(&mut self, _scope: &WorkerScope<Self>, _msg: Self::Message) {
|
||||
log::debug!("Unimplemented!");
|
||||
}
|
||||
|
||||
fn received(&mut self, scope: &WorkerScope<Self>, query: Self::Input, id: HandlerId) {
|
||||
let scope = scope.clone();
|
||||
|
||||
spawn_local(async move {
|
||||
let res = DatabaseWorker::run_query(&query)
|
||||
.map_err(|err| {
|
||||
log::error!("{}", err.to_report_string_pretty());
|
||||
format!("{}", err.as_report())
|
||||
})
|
||||
.await;
|
||||
|
||||
scope.respond(id, res);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum BrowserDatabaseError {
|
||||
// #[error("failed to set up browser database storage")]
|
||||
// VFSError(#[from] sqlite_wasm_rs::export::OpfsSAHError),
|
||||
// #[error("could not fetch updated card database")]
|
||||
// CardDbFetchError(#[from] GithubError),
|
||||
#[error("failed to connect to database")]
|
||||
DieselConnectionError(#[from] diesel::ConnectionError),
|
||||
#[error("failed to query database")]
|
||||
DieselQueryError(#[from] diesel::result::Error),
|
||||
}
|
||||
type Result<T> = std::result::Result<T, BrowserDatabaseError>;
|
|
@ -1,3 +1,4 @@
|
|||
pub mod components;
|
||||
pub mod draft_list;
|
||||
pub mod database;
|
||||
pub mod draft_list;
|
||||
pub mod utils;
|
||||
|
|
39
src/main.rs
39
src/main.rs
|
@ -1,32 +1,37 @@
|
|||
use draft_manager::{components::card::Card, database::BrowserDatabase};
|
||||
use std::time::Duration;
|
||||
|
||||
use draft_manager::database::BrowserDatabase;
|
||||
use leptos::prelude::*;
|
||||
use wasm_bindgen_futures::spawn_local;
|
||||
|
||||
fn main() {
|
||||
console_error_panic_hook::set_once();
|
||||
wasm_logger::init(wasm_logger::Config::default());
|
||||
|
||||
log::debug!("Startin app database");
|
||||
let db = BrowserDatabase::new();
|
||||
log::debug!("Database initialized");
|
||||
|
||||
leptos::mount::mount_to_body(|| view! { <App /> });
|
||||
|
||||
spawn_local(async move {
|
||||
let db = db;
|
||||
|
||||
log::info!("Running loop!");
|
||||
db.send_message();
|
||||
|
||||
// We create a loop so that listeners can be held for forever.
|
||||
loop {
|
||||
gloo::timers::future::sleep(Duration::from_secs(3600)).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn App() -> impl IntoView {
|
||||
let card = LocalResource::new(|| async move {
|
||||
let mut database = BrowserDatabase::open()?;
|
||||
database.load_data()?;
|
||||
|
||||
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>
|
||||
}>{move || {}}</Transition>
|
||||
}
|
||||
}
|
||||
|
|
60
src/utils.rs
Normal file
60
src/utils.rs
Normal file
|
@ -0,0 +1,60 @@
|
|||
use gloo::net::http;
|
||||
use thiserror::Error;
|
||||
use time::{format_description::well_known::Rfc3339, OffsetDateTime};
|
||||
|
||||
const GITHUB_API: &str = "https://api.github.com";
|
||||
|
||||
pub async fn github_fetch_last_updated(
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
path: &str,
|
||||
) -> Result<OffsetDateTime, GithubError> {
|
||||
let commit: Vec<serde_json::Value> = http::Request::get(&format!(
|
||||
"{GITHUB_API}/repos/{owner}/{repo}/commits?path={path}&page=1&per_page=1"
|
||||
))
|
||||
.send()
|
||||
.await?
|
||||
.json()
|
||||
.await?;
|
||||
|
||||
commit[0]
|
||||
.get("commit")
|
||||
.and_then(|commit| commit.get("committer"))
|
||||
.and_then(|committer| committer.get("date"))
|
||||
.map(|date| {
|
||||
OffsetDateTime::parse(
|
||||
date.as_str().ok_or(GithubError::MissingCommitDateError)?,
|
||||
&Rfc3339,
|
||||
)
|
||||
.map_err(|err| err.into())
|
||||
})
|
||||
.unwrap_or(Err(GithubError::MissingCommitDateError))
|
||||
}
|
||||
|
||||
pub async fn github_fetch_file(
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
path: &str,
|
||||
) -> Result<Vec<u8>, GithubError> {
|
||||
http::Request::get(&format!(
|
||||
"{GITHUB_API}/repos/{owner}/{repo}/contents/{path}"
|
||||
))
|
||||
.header("Accept", "application/vnd.github.raw+json")
|
||||
.send()
|
||||
.await?
|
||||
.binary()
|
||||
.await
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum GithubError {
|
||||
#[error("no file returned from the given GitHub API")]
|
||||
NoSuchFile,
|
||||
#[error("failed to fetch from GitHub API")]
|
||||
RequestError(#[from] gloo::net::Error),
|
||||
#[error("file fetched from the GitHub API does not have a commit date")]
|
||||
MissingCommitDateError,
|
||||
#[error("date/time format reported for GitHub commit is invalid")]
|
||||
InvalidDateTimeError(#[from] time::error::Parse),
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue