commit 399c628461734a9bace8bf1163c989cd05bcdc04 Author: garrettmills Date: Tue Jul 7 19:37:56 2026 -0500 Initial commit: MTG personal library catalog (Tauri v2 + React/TS) Full working app with: - Rust core (mtgc-core): Scryfall ingester, nucleo fuzzy search, SQLite two-DB architecture (reference.db + library.db via ATTACH), criteria filters, deck ops - Tauri v2 shell exposing the core as typed IPC commands - React/TS frontend: card entry, library browser, deck editor - Features: fuzzy card search, color/CMC/type/commander-identity filters, tile/list view toggle, hover card previews, deck format + commander support, per-card location field, grouping by type/subtype/CMC/color, localStorage persistence for active tab/deck/grouping, Discord-inspired dark UI (#C8553D accent) Co-Authored-By: Claude Sonnet 4.6 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2b11039 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# Rust / Cargo +/target +**/*.rs.bk + +# Node / frontend +node_modules/ +/dist/ + +# Tauri generated +/src-tauri/gen/ + +# Local databases (reference.db is rebuildable; library.db is your synced data) +*.db +*.sqlite +*.db-shm +*.db-wal + +# OS / editor +.DS_Store +Thumbs.db +.idea/ +*.swp +*.swo diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..6a1b84a --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,4876 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "brotli" +version = "8.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.13.0", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2d30e4173c4026932d51d31d6b0613b1fd3014bf3f9f8943d4ba139c437ba0" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.2.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link 0.2.1", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.13.0", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.13.0", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.118", +] + +[[package]] +name = "ctor" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.118", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "dbus" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.118", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.0", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser", + "foldhash", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "embed-resource" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbfdaacccebec3b28e4866b8973543c7647797db5ada1bdab552e48fe665fbbd" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.2+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.13.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png 0.17.16", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.118", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.13.0", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" +dependencies = [ + "libc", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "mtgc-app" +version = "0.1.0" +dependencies = [ + "mtgc-core", + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-dialog", +] + +[[package]] +name = "mtgc-core" +version = "0.1.0" +dependencies = [ + "anyhow", + "nucleo", + "rusqlite", + "serde", + "serde_json", + "tempfile", + "thiserror 1.0.69", + "ureq", +] + +[[package]] +name = "muda" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.0", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nucleo" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5262af4c94921c2646c5ac6ff7900c2af9cbb08dc26a797e18130a7019c039d4" +dependencies = [ + "nucleo-matcher", + "parking_lot", + "rayon", +] + +[[package]] +name = "nucleo-matcher" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf33f538733d1a5a3494b836ba913207f14d9d4a1d3cd67030c5061bdd2cac85" +dependencies = [ + "memchr", + "unicode-segmentation", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[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_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.0", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.0", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.0", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plist" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da1d65da6dd5d1e44199ac0f58712d241c0f439f80adea8924d832384087f85" +dependencies = [ + "base64 0.22.1", + "indexmap 2.14.0", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.12+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.0", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "regex" +version = "1.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "rfd" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" +dependencies = [ + "block2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.60.2", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rusqlite" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" +dependencies = [ + "bitflags 2.13.0", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[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 = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.118", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.13.0", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_with" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" +dependencies = [ + "base64 0.22.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.35.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" +dependencies = [ + "bitflags 2.13.0", + "block2", + "core-foundation", + "core-graphics", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "libc", + "log", + "ndk", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "once_cell", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "667b20e2726d572dea2de7370da16e188eb06008faf9a92fab7cdc46791190b5" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.18", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png 0.17.16", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.118", + "tauri-utils", + "thiserror 2.0.18", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b394794f399a421811d06966343e7933fcae92d59f5180b9388d1174497a45" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74be5dd4bed9afbd145e5716b5fa2ec28cbc29c34ffa61c258c9273d896c8020" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "walkdir", +] + +[[package]] +name = "tauri-plugin-dialog" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65981abb771e74e571a38196c3baa11c459379164791eba0e67abc1a5fac9884" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.18", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371" +dependencies = [ + "anyhow", + "dunce", + "glob", + "log", + "objc2-foundation", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "url", +] + +[[package]] +name = "tauri-runtime" +version = "2.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e6fac707727b7a2f48e4ded90976324267371073edbb415ffb73bb0458d203f" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dom_query", + "dunce", + "glob", + "http", + "infer", + "json-patch", + "log", + "memchr", + "phf", + "plist", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.1.2+spec-1.1.0", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" +dependencies = [ + "new_debug_unreachable", + "utf-8", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "time" +version = "0.3.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.3", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.14.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.3", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.3", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.0", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65ba1e5f6b9ef9fd87e21b9c6f351554dbd717960089168fcfdef854686961dc" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" +dependencies = [ + "base64 0.22.1", + "flate2", + "log", + "once_cell", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "url", + "webpki-roots 0.26.11", +] + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[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 = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.118", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "075474b12bcb3d2e3d4546580e9de478eeeead668a1761e2a8860c836b7ef297" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.8", +] + +[[package]] +name = "webpki-roots" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.18", + "windows", + "windows-core 0.61.2", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "wry" +version = "0.55.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..efd22f2 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,25 @@ +[workspace] +resolver = "2" +members = ["crates/mtgc-core", "src-tauri"] + +[workspace.package] +version = "0.1.0" +edition = "2021" +license = "MIT" + +[workspace.dependencies] +rusqlite = { version = "0.31", features = ["bundled"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +nucleo = "0.5" +ureq = { version = "2", features = ["json"] } +anyhow = "1" +thiserror = "1" + +# Lean, self-contained release binary (used by `cargo tauri build`). +# Trades a few minutes of extra build time for a smaller executable. +[profile.release] +strip = true # drop debug symbols +lto = true # link-time optimization +codegen-units = 1 # better optimization at the cost of parallelism +panic = "abort" # no unwinding machinery diff --git a/README.md b/README.md new file mode 100644 index 0000000..586184f --- /dev/null +++ b/README.md @@ -0,0 +1,114 @@ +# mtgc — Personal Magic: The Gathering Library Catalog + +A desktop app (Tauri v2 + React) for cataloging your MTG collection: rapid +fuzzy card entry, rich criteria search (colors, commander color-identity +compatibility, mana value, type/subtype/keyword), and deck organization with +per-card deck membership. Built so the eventual mobile port is cheap — all logic +lives in a platform-agnostic Rust core. + +## Architecture + +``` +src/ React + TypeScript UI (swappable presentation) +src-tauri/ Tauri v2 shell — exposes the core as typed commands +crates/mtgc-core/ Pure Rust: db, fuzzy search, filters, decks, ingester +``` + +Two SQLite databases, joined at runtime via `ATTACH`: + +- **`reference.db`** — read-only card data built from Scryfall bulk. Bulky, + re-downloadable per device, **not synced**. +- **`library.db`** — your owned cards + decks. Small, portable, **the file you + sync** (drop it in Syncthing/Dropbox/Nextcloud). Point the app at it via + *Choose library file…*. + +Both default to the OS app-data dir (e.g. `~/.local/share/com.garrettmills.mtgc/`). + +## First-time setup + +```bash +./scripts/devel-setup.sh # Tauri system libs (webkit2gtk/gtk3) + Tauri CLI +npm install # frontend deps +``` + +## Run + +```bash +cargo tauri dev # launches the desktop app (dev mode) +``` + +On first launch, click **Download card data** — it fetches the Scryfall +*Oracle Cards* + *Default Cards* bulk (~a few hundred MB) and builds +`reference.db`. After that the app is fully offline. + +You can also build the card database from the command line: + +```bash +cargo run -p mtgc-core --bin mtgc-ingest -- /path/to/reference.db +``` + +## Production build + +Build an optimized app with the frontend embedded into the executable: + +```bash +./scripts/devel-setup.sh # once, if not already done +cargo tauri build +``` + +Outputs (under the workspace `target/release/`): + +- **`target/release/mtgc-app`** — the standalone executable. The web UI and all + app logic are baked into this one file. It dynamically links the system WebKit/GTK + libraries (`webkit2gtk4.1`, `gtk3`), so those must be present on the machine that + runs it — they are on any modern desktop Linux and are installed by + `devel-setup.sh`. +- **`target/release/bundle/appimage/*.AppImage`** — a single portable file that + bundles the app together with its runtime. This is the closest thing to a + download-and-run **single self-contained binary**: `chmod +x` it and run. + **Recommended** for copying to another machine. +- **`target/release/bundle/rpm/*.rpm`** and **`.../deb/*.deb`** — native installers + (Fedora / Debian-based) that add a menu entry and icon. + +Useful variants: + +```bash +cargo tauri build --bundles appimage # only the portable AppImage, skip installers +cargo tauri build --no-bundle # only the bare target/release/mtgc-app binary +``` + +Notes: + +- On Linux the truly portable single file is the **AppImage**; the bare `mtgc-app` + binary is also a single file but relies on the system WebKit/GTK libs. +- The binary does **not** embed card data — first run still downloads the Scryfall + bulk into the OS app-data dir, and your synced `library.db` lives there by default + (override with *Choose library file…*). +- The `[profile.release]` settings in `Cargo.toml` (strip + LTO) make the release + build take a few minutes but keep the binary small. + +## Using it + +- **Add Cards** — type a name (typos OK); ↑/↓ then Enter adds a copy. +- **Library** — filter owned cards by name, colors, commander identity (color + identity ⊆ chosen), mana value range, type/subtype/keyword, or deck. Hover a + row's thumbnail for a full-size preview that tracks the cursor. Rows show which + decks a card is in and a warning (⚠) when it's assigned to more decks than you + own. Toggle **List / Tiles** (top-right) to switch between the table and a grid + of full card images; the choice is remembered. +- Deck contents offer the same List / Tiles toggle and hover previews. +- **Decks** — create decks and assign owned cards; set per-deck quantities. + +## Develop / test + +```bash +cargo test -p mtgc-core # core unit tests (db, search, filters, decks, ingest) +npm run build # typecheck + build frontend +cargo build # build everything incl. the desktop app +``` + +## Mobile (future) + +The core and frontend are platform-agnostic; `cargo tauri icon` already emitted +iOS/Android icon sets. Porting is adding `tauri ios` / `tauri android` targets — +only reference-data provisioning differs (download on-device). diff --git a/crates/mtgc-core/Cargo.toml b/crates/mtgc-core/Cargo.toml new file mode 100644 index 0000000..88f1fc0 --- /dev/null +++ b/crates/mtgc-core/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "mtgc-core" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "Platform-agnostic core for the MTG library catalog: DB, fuzzy search, filters, decks, and Scryfall ingestion." + +[lib] +name = "mtgc_core" +path = "src/lib.rs" + +# Binary that builds reference.db from Scryfall bulk data. +[[bin]] +name = "mtgc-ingest" +path = "src/bin/ingest.rs" + +[dependencies] +rusqlite = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +nucleo = { workspace = true } +ureq = { workspace = true } +anyhow = { workspace = true } +thiserror = { workspace = true } + +[dev-dependencies] +tempfile = "3" diff --git a/crates/mtgc-core/src/bin/ingest.rs b/crates/mtgc-core/src/bin/ingest.rs new file mode 100644 index 0000000..7ba9d6c --- /dev/null +++ b/crates/mtgc-core/src/bin/ingest.rs @@ -0,0 +1,28 @@ +//! CLI: build reference.db from Scryfall bulk data. +//! +//! Usage: `mtgc-ingest [path/to/reference.db]` (defaults to ./reference.db) + +use std::path::PathBuf; +use std::process::ExitCode; + +fn main() -> ExitCode { + let dest = std::env::args() + .nth(1) + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from("reference.db")); + + eprintln!("Building reference db at {}", dest.display()); + match mtgc_core::ingest::build_reference_db(&dest, |msg| eprintln!(" {msg}")) { + Ok(stats) => { + eprintln!( + "Done: {} oracle cards, {} printings.", + stats.oracle_cards, stats.printings + ); + ExitCode::SUCCESS + } + Err(e) => { + eprintln!("Ingest failed: {e}"); + ExitCode::FAILURE + } + } +} diff --git a/crates/mtgc-core/src/db.rs b/crates/mtgc-core/src/db.rs new file mode 100644 index 0000000..18475a2 --- /dev/null +++ b/crates/mtgc-core/src/db.rs @@ -0,0 +1,118 @@ +//! Database wiring: open the library, run migrations, ATTACH the reference db. + +use std::path::Path; + +use rusqlite::Connection; + +use crate::error::Result; + +/// The reference schema, applied to a freshly-created reference.db by the ingester +/// (and by tests that build a fixture reference db). +pub const REFERENCE_SCHEMA: &str = include_str!("sql/reference_schema.sql"); + +/// Ordered library migrations. Index + 1 is the schema version recorded in +/// `PRAGMA user_version`. Append new migrations; never edit an applied one. +const LIBRARY_MIGRATIONS: &[&str] = &[ + include_str!("sql/library/0001_init.sql"), + include_str!("sql/library/0002_location.sql"), + include_str!("sql/library/0003_deck_format.sql"), +]; + +/// The attached alias under which the reference db is available at runtime. +pub const REF_DB: &str = "ref"; + +/// Correlated subquery selecting one image URL for the oracle card aliased `oc`. +/// Returns NULL when no printing has an image. Read it as `Option`. +pub fn image_subquery() -> String { + format!( + "(SELECT p.image_uri FROM {REF_DB}.printings p + WHERE p.oracle_id = oc.oracle_id AND p.image_uri <> '' LIMIT 1)" + ) +} + +/// Create (if needed) and open a reference.db, applying the reference schema. +/// Used by the ingester and by test fixtures. +pub fn open_reference_for_build(path: &Path) -> Result { + let conn = Connection::open(path)?; + conn.execute_batch(REFERENCE_SCHEMA)?; + Ok(conn) +} + +/// Open the library db at `library_path`, run migrations, and ATTACH the +/// reference db at `reference_path` under the [`REF_DB`] alias. +pub fn open(library_path: &Path, reference_path: &Path) -> Result { + let conn = Connection::open(library_path)?; + conn.pragma_update(None, "foreign_keys", true)?; + migrate_library(&conn)?; + attach_reference(&conn, reference_path)?; + Ok(conn) +} + +/// ATTACH a reference db to an already-open library connection. +pub fn attach_reference(conn: &Connection, reference_path: &Path) -> Result<()> { + conn.execute( + &format!("ATTACH DATABASE ?1 AS {REF_DB}"), + [reference_path.to_string_lossy()], + )?; + Ok(()) +} + +/// Apply any library migrations newer than the recorded `user_version`. +pub fn migrate_library(conn: &Connection) -> Result<()> { + let current: i64 = conn.pragma_query_value(None, "user_version", |r| r.get(0))?; + for (idx, sql) in LIBRARY_MIGRATIONS.iter().enumerate() { + let version = (idx + 1) as i64; + if version > current { + conn.execute_batch(&format!("BEGIN;\n{sql}\nPRAGMA user_version = {version};\nCOMMIT;"))?; + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn migrations_are_idempotent_and_bump_version() { + let conn = Connection::open_in_memory().unwrap(); + migrate_library(&conn).unwrap(); + let v: i64 = conn + .pragma_query_value(None, "user_version", |r| r.get(0)) + .unwrap(); + assert_eq!(v, LIBRARY_MIGRATIONS.len() as i64); + + // Running again is a no-op (no "table already exists" error). + migrate_library(&conn).unwrap(); + + // Core tables exist. + let n: i64 = conn + .query_row( + "SELECT count(*) FROM sqlite_master WHERE type='table' AND name IN ('decks','library_cards','deck_cards','library_card_copies')", + [], + |r| r.get(0), + ) + .unwrap(); + assert_eq!(n, 4); + } + + #[test] + fn reference_schema_applies_with_fts() { + let conn = Connection::open_in_memory().unwrap(); + conn.execute_batch(REFERENCE_SCHEMA).unwrap(); + // FTS5 + trigram tokenizer must be available in the bundled SQLite. + conn.execute( + "INSERT INTO oracle_names_fts(name, oracle_id) VALUES ('Llanowar Elves','x')", + [], + ) + .unwrap(); + let hit: i64 = conn + .query_row( + "SELECT count(*) FROM oracle_names_fts WHERE oracle_names_fts MATCH 'lan'", + [], + |r| r.get(0), + ) + .unwrap(); + assert_eq!(hit, 1); + } +} diff --git a/crates/mtgc-core/src/decks.rs b/crates/mtgc-core/src/decks.rs new file mode 100644 index 0000000..249969b --- /dev/null +++ b/crates/mtgc-core/src/decks.rs @@ -0,0 +1,287 @@ +//! Deck operations and deck membership lookups. + +use rusqlite::{params, OptionalExtension}; + +use crate::db::REF_DB; +use crate::error::Result; +use crate::model::{CardRef, ColorSet, Deck, DeckEntry}; +use crate::Catalog; + +impl Catalog { + /// Create a deck. `commander_oracle_id` is optional. + pub fn create_deck(&self, name: &str, commander_oracle_id: Option<&str>) -> Result { + self.conn.execute( + "INSERT INTO decks(name, commander_oracle_id) VALUES (?1, ?2)", + params![name, commander_oracle_id], + )?; + let id = self.conn.last_insert_rowid(); + self.get_deck(id) + .map(|d| d.expect("just-inserted deck must exist")) + } + + pub fn get_deck(&self, id: i64) -> Result> { + Ok(self + .conn + .query_row( + "SELECT id, name, format, commander_oracle_id, notes FROM decks WHERE id = ?1", + params![id], + Self::map_deck, + ) + .optional()?) + } + + pub fn list_decks(&self) -> Result> { + let mut stmt = self.conn.prepare( + "SELECT id, name, format, commander_oracle_id, notes + FROM decks ORDER BY name COLLATE NOCASE", + )?; + let rows = stmt + .query_map([], Self::map_deck)? + .collect::, _>>()?; + Ok(rows) + } + + pub fn delete_deck(&self, id: i64) -> Result<()> { + // ON DELETE CASCADE clears deck_cards. + self.conn + .execute("DELETE FROM decks WHERE id = ?1", params![id])?; + Ok(()) + } + + /// Set how many copies of a card belong to a deck. `quantity <= 0` removes + /// the card from the deck. Upserts on (deck_id, oracle_id). + pub fn set_deck_card( + &self, + deck_id: i64, + oracle_id: &str, + quantity: i64, + is_commander: bool, + ) -> Result<()> { + if quantity <= 0 { + self.conn.execute( + "DELETE FROM deck_cards WHERE deck_id = ?1 AND oracle_id = ?2", + params![deck_id, oracle_id], + )?; + return Ok(()); + } + self.conn.execute( + "INSERT INTO deck_cards(deck_id, oracle_id, quantity, is_commander) + VALUES (?1, ?2, ?3, ?4) + ON CONFLICT(deck_id, oracle_id) DO UPDATE SET + quantity = excluded.quantity, + is_commander = excluded.is_commander", + params![deck_id, oracle_id, quantity, is_commander as i64], + )?; + Ok(()) + } + + /// All cards in a deck — owned or not — with their in-deck quantity and how + /// many copies are owned (0 if the card isn't in the library). Callers split + /// this into "in your library" vs "not owned" groups. + pub fn deck_contents(&self, deck_id: i64) -> Result> { + let mut stmt = self.conn.prepare(&format!( + "SELECT dc.quantity, dc.is_commander, COALESCE(lc.quantity_owned, 0), + oc.oracle_id, oc.name, oc.mana_value, oc.colors, oc.color_identity, + oc.type_line, {image} + FROM deck_cards dc + JOIN {REF_DB}.oracle_cards oc ON oc.oracle_id = dc.oracle_id + LEFT JOIN library_cards lc ON lc.oracle_id = dc.oracle_id + WHERE dc.deck_id = ?1 + ORDER BY oc.name COLLATE NOCASE", + image = crate::db::image_subquery() + ))?; + let rows = stmt + .query_map(params![deck_id], |r| { + Ok(DeckEntry { + quantity: r.get(0)?, + is_commander: r.get::<_, i64>(1)? != 0, + quantity_owned: r.get(2)?, + card: CardRef { + oracle_id: r.get(3)?, + name: r.get(4)?, + mana_value: r.get(5)?, + colors: ColorSet(r.get::<_, i64>(6)? as u8), + color_identity: ColorSet(r.get::<_, i64>(7)? as u8), + type_line: r.get(8)?, + image_uri: r.get::<_, Option>(9)?.unwrap_or_default(), + }, + }) + })? + .collect::, _>>()?; + Ok(rows) + } + + /// The decks a given card is currently assigned to. + pub fn card_decks(&self, oracle_id: &str) -> Result> { + let mut stmt = self.conn.prepare( + "SELECT d.id, d.name, d.format, d.commander_oracle_id, d.notes + FROM deck_cards dc JOIN decks d ON d.id = dc.deck_id + WHERE dc.oracle_id = ?1 + ORDER BY d.name COLLATE NOCASE", + )?; + let rows = stmt + .query_map(params![oracle_id], Self::map_deck)? + .collect::, _>>()?; + Ok(rows) + } + + fn map_deck(r: &rusqlite::Row) -> rusqlite::Result { + Ok(Deck { + id: r.get(0)?, + name: r.get(1)?, + format: r.get(2)?, + commander_oracle_id: r.get(3)?, + notes: r.get(4)?, + }) + } + + /// Set the deck's format (e.g. "Commander"). Empty string clears it. + pub fn set_deck_format(&self, deck_id: i64, format: &str) -> Result<()> { + self.conn.execute( + "UPDATE decks SET format = ?1 WHERE id = ?2", + params![format, deck_id], + )?; + Ok(()) + } + + /// Set (or clear, with `None`) the deck's commander card. Also marks/unmarks + /// the corresponding deck_cards row as the commander when present. + pub fn set_deck_commander(&self, deck_id: i64, oracle_id: Option<&str>) -> Result<()> { + self.conn.execute( + "UPDATE decks SET commander_oracle_id = ?1 WHERE id = ?2", + params![oracle_id, deck_id], + )?; + // Reset any existing commander flag, then set it on the chosen card. + self.conn.execute( + "UPDATE deck_cards SET is_commander = 0 WHERE deck_id = ?1", + params![deck_id], + )?; + if let Some(oid) = oracle_id { + self.conn.execute( + "UPDATE deck_cards SET is_commander = 1 WHERE deck_id = ?1 AND oracle_id = ?2", + params![deck_id, oid], + )?; + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::test_support; + + #[test] + fn deck_membership_visible_from_library() { + let fx = test_support::make(); + let cat = &fx.catalog; + + cat.add_owned("llanowar-elves", 4).unwrap(); + let elves = cat.create_deck("Elfball", Some("llanowar-elves")).unwrap(); + cat.set_deck_card(elves.id, "llanowar-elves", 4, false) + .unwrap(); + + // card_decks reports membership. + let decks = cat.card_decks("llanowar-elves").unwrap(); + assert_eq!(decks.len(), 1); + assert_eq!(decks[0].name, "Elfball"); + + // list_library surfaces the deck name on the card row. + let lib = cat.list_library().unwrap(); + let row = lib + .iter() + .find(|c| c.card.oracle_id == "llanowar-elves") + .unwrap(); + assert_eq!(row.decks, vec!["Elfball".to_string()]); + assert_eq!(row.quantity_assigned, 4); + assert!(!row.is_over_assigned()); + } + + #[test] + fn over_assignment_detected_across_decks() { + let fx = test_support::make(); + let cat = &fx.catalog; + cat.add_owned("sol-ring", 1).unwrap(); // own one physical copy + + let a = cat.create_deck("Deck A", None).unwrap(); + let b = cat.create_deck("Deck B", None).unwrap(); + cat.set_deck_card(a.id, "sol-ring", 1, false).unwrap(); + cat.set_deck_card(b.id, "sol-ring", 1, false).unwrap(); + + let lib = cat.list_library().unwrap(); + let sol = lib.iter().find(|c| c.card.oracle_id == "sol-ring").unwrap(); + assert_eq!(sol.quantity_assigned, 2); + assert_eq!(sol.quantity_owned, 1); + assert!(sol.is_over_assigned()); + assert_eq!(sol.decks.len(), 2); + } + + #[test] + fn format_and_commander_metadata() { + let fx = test_support::make(); + let cat = &fx.catalog; + let d = cat.create_deck("EDH", None).unwrap(); + assert_eq!(d.format, ""); + + cat.set_deck_format(d.id, "Commander").unwrap(); + cat.set_deck_card(d.id, "llanowar-elves", 1, false).unwrap(); + cat.set_deck_commander(d.id, Some("llanowar-elves")).unwrap(); + + let d = cat.get_deck(d.id).unwrap().unwrap(); + assert_eq!(d.format, "Commander"); + assert_eq!(d.commander_oracle_id.as_deref(), Some("llanowar-elves")); + + // The card is flagged as commander in the deck. + let commander = cat + .deck_contents(d.id) + .unwrap() + .into_iter() + .find(|e| e.card.oracle_id == "llanowar-elves") + .unwrap(); + assert!(commander.is_commander); + + // Clearing works. + cat.set_deck_commander(d.id, None).unwrap(); + assert_eq!( + cat.get_deck(d.id).unwrap().unwrap().commander_oracle_id, + None + ); + } + + #[test] + fn deck_contents_include_owned_and_unowned() { + let fx = test_support::make(); + let cat = &fx.catalog; + cat.add_owned("llanowar-elves", 4).unwrap(); // owned + // serra-angel intentionally NOT owned. + + let d = cat.create_deck("Mixed", None).unwrap(); + cat.set_deck_card(d.id, "llanowar-elves", 4, false).unwrap(); + cat.set_deck_card(d.id, "serra-angel", 1, true).unwrap(); + + let contents = cat.deck_contents(d.id).unwrap(); + assert_eq!(contents.len(), 2); + + let owned: Vec<_> = contents.iter().filter(|e| e.quantity_owned > 0).collect(); + let unowned: Vec<_> = contents.iter().filter(|e| e.quantity_owned == 0).collect(); + assert_eq!(owned.len(), 1); + assert_eq!(owned[0].card.name, "Llanowar Elves"); + assert_eq!(owned[0].quantity, 4); + assert_eq!(unowned.len(), 1); + assert_eq!(unowned[0].card.name, "Serra Angel"); + assert!(unowned[0].is_commander); + } + + #[test] + fn setting_zero_removes_and_cascade_on_delete() { + let fx = test_support::make(); + let cat = &fx.catalog; + let d = cat.create_deck("Temp", None).unwrap(); + cat.set_deck_card(d.id, "lightning-bolt", 2, false).unwrap(); + cat.set_deck_card(d.id, "lightning-bolt", 0, false).unwrap(); + assert!(cat.card_decks("lightning-bolt").unwrap().is_empty()); + + cat.set_deck_card(d.id, "lightning-bolt", 1, false).unwrap(); + cat.delete_deck(d.id).unwrap(); + assert!(cat.card_decks("lightning-bolt").unwrap().is_empty()); + } +} diff --git a/crates/mtgc-core/src/error.rs b/crates/mtgc-core/src/error.rs new file mode 100644 index 0000000..f731964 --- /dev/null +++ b/crates/mtgc-core/src/error.rs @@ -0,0 +1,29 @@ +//! Core error type. + +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum CoreError { + #[error(transparent)] + Sqlite(#[from] rusqlite::Error), + + #[error(transparent)] + Io(#[from] std::io::Error), + + #[error("http error: {0}")] + Http(String), + + #[error("json error: {0}")] + Json(#[from] serde_json::Error), + + #[error("{0}")] + Other(String), +} + +pub type Result = std::result::Result; + +impl From for CoreError { + fn from(e: ureq::Error) -> Self { + CoreError::Http(e.to_string()) + } +} diff --git a/crates/mtgc-core/src/filters.rs b/crates/mtgc-core/src/filters.rs new file mode 100644 index 0000000..dd5723e --- /dev/null +++ b/crates/mtgc-core/src/filters.rs @@ -0,0 +1,228 @@ +//! Criteria search over the owned library, compiled to SQL across the library +//! and attached reference dbs. + +use rusqlite::types::Value; + +use crate::db::REF_DB; +use crate::error::Result; +use crate::model::{LibraryCard, LibraryFilter}; +use crate::Catalog; + +impl Catalog { + /// Search the owned library by the given criteria. An empty filter returns + /// the whole owned library (same set as [`Catalog::list_library`]). + /// + /// Semantics: within a facet, multiple `types`/`subtypes`/`keywords` values + /// are ANDed (the card must have all of them). Commander compatibility keeps + /// only cards whose color identity is a subset of the given identity. + pub fn search_library(&self, filter: &LibraryFilter) -> Result> { + let mut sql = format!( + "SELECT lc.oracle_id, lc.quantity_owned, lc.location, + oc.name, oc.mana_value, oc.colors, oc.color_identity, oc.type_line, {image} + FROM library_cards lc + JOIN {REF_DB}.oracle_cards oc ON oc.oracle_id = lc.oracle_id + WHERE lc.quantity_owned > 0", + image = crate::db::image_subquery() + ); + let mut vals: Vec = Vec::new(); + + if let Some(text) = filter.text.as_ref().map(|t| t.trim()).filter(|t| !t.is_empty()) { + sql.push_str(" AND oc.name_lower LIKE ?"); + vals.push(Value::Text(format!("%{}%", text.to_lowercase()))); + } + if let Some(c) = filter.colors_exact { + sql.push_str(" AND oc.colors = ?"); + vals.push(Value::Integer(c.0 as i64)); + } + if let Some(c) = filter.colors_include { + sql.push_str(" AND (oc.colors & ?) = ?"); + vals.push(Value::Integer(c.0 as i64)); + vals.push(Value::Integer(c.0 as i64)); + } + if let Some(id) = filter.commander_identity { + // subset: card_identity & ~commander == 0 (see ColorSet::is_subset_of) + sql.push_str(" AND (oc.color_identity & ?) = 0"); + vals.push(Value::Integer((!id.0 & 0b11111) as i64)); + } + if let Some(min) = filter.cmc_min { + sql.push_str(" AND oc.mana_value >= ?"); + vals.push(Value::Real(min)); + } + if let Some(max) = filter.cmc_max { + sql.push_str(" AND oc.mana_value <= ?"); + vals.push(Value::Real(max)); + } + for t in &filter.types { + sql.push_str(&format!( + " AND EXISTS (SELECT 1 FROM {REF_DB}.card_types x + WHERE x.oracle_id = oc.oracle_id AND x.type = ? COLLATE NOCASE)" + )); + vals.push(Value::Text(t.clone())); + } + for s in &filter.subtypes { + sql.push_str(&format!( + " AND EXISTS (SELECT 1 FROM {REF_DB}.card_subtypes x + WHERE x.oracle_id = oc.oracle_id AND x.subtype = ? COLLATE NOCASE)" + )); + vals.push(Value::Text(s.clone())); + } + for k in &filter.keywords { + sql.push_str(&format!( + " AND EXISTS (SELECT 1 FROM {REF_DB}.card_keywords x + WHERE x.oracle_id = oc.oracle_id AND x.keyword = ? COLLATE NOCASE)" + )); + vals.push(Value::Text(k.clone())); + } + if let Some(deck_id) = filter.in_deck { + sql.push_str( + " AND EXISTS (SELECT 1 FROM deck_cards dc + WHERE dc.oracle_id = oc.oracle_id AND dc.deck_id = ?)", + ); + vals.push(Value::Integer(deck_id)); + } + + sql.push_str(" ORDER BY oc.name COLLATE NOCASE"); + + let mut stmt = self.conn.prepare(&sql)?; + let rows = stmt + .query_map( + rusqlite::params_from_iter(vals), + crate::library::row_to_library_parts, + )? + .collect::, _>>()?; + + // Enrich with deck context (shared with list_library). + let deck_names = self.deck_names_by_oracle()?; + let assigned = self.assigned_totals_by_oracle()?; + Ok(rows + .into_iter() + .map(|(oid, qty, location, card)| LibraryCard { + quantity_owned: qty, + decks: deck_names.get(&oid).cloned().unwrap_or_default(), + quantity_assigned: *assigned.get(&oid).unwrap_or(&0), + location, + card, + }) + .collect()) + } +} + +#[cfg(test)] +mod tests { + use crate::model::{ColorSet, LibraryFilter}; + use crate::test_support; + + /// Own the whole fixture set so filters have something to select from. + fn owned_fixture() -> test_support::Fixture { + let fx = test_support::make(); + for c in test_support::seed_cards() { + fx.catalog.add_owned(c.oracle_id, 1).unwrap(); + } + fx + } + + fn names(cards: &[crate::model::LibraryCard]) -> Vec { + cards.iter().map(|c| c.card.name.clone()).collect() + } + + #[test] + fn empty_filter_returns_all_owned() { + let fx = owned_fixture(); + let all = fx.catalog.search_library(&LibraryFilter::default()).unwrap(); + assert_eq!(all.len(), 5); + } + + #[test] + fn commander_identity_subset() { + let fx = owned_fixture(); + + // A B/G commander: Golgari (BG) and mono-B/mono-G and colorless are legal; + // white/red cards are not. + let f = LibraryFilter { + commander_identity: Some(ColorSet::parse("BG")), + ..Default::default() + }; + let got = names(&fx.catalog.search_library(&f).unwrap()); + assert!(got.contains(&"Deathrite Shaman".to_string())); // BG + assert!(got.contains(&"Llanowar Elves".to_string())); // G + assert!(got.contains(&"Sol Ring".to_string())); // colorless + assert!(!got.contains(&"Serra Angel".to_string())); // W + assert!(!got.contains(&"Lightning Bolt".to_string())); // R + + // Mono-white commander: the BG card is illegal. + let f = LibraryFilter { + commander_identity: Some(ColorSet::parse("W")), + ..Default::default() + }; + let got = names(&fx.catalog.search_library(&f).unwrap()); + assert!(got.contains(&"Serra Angel".to_string())); + assert!(!got.contains(&"Deathrite Shaman".to_string())); + } + + #[test] + fn type_subtype_keyword_and_cmc() { + let fx = owned_fixture(); + + let creatures = names(&fx.catalog.search_library(&LibraryFilter { + types: vec!["Creature".into()], + ..Default::default() + }).unwrap()); + assert_eq!(creatures.len(), 3); // Llanowar, Deathrite, Serra + + let elves = names(&fx.catalog.search_library(&LibraryFilter { + subtypes: vec!["Elf".into()], + ..Default::default() + }).unwrap()); + assert_eq!(elves, vec!["Deathrite Shaman", "Llanowar Elves"]); + + let flyers = names(&fx.catalog.search_library(&LibraryFilter { + keywords: vec!["Flying".into()], + ..Default::default() + }).unwrap()); + assert_eq!(flyers, vec!["Serra Angel"]); + + let cheap = fx.catalog.search_library(&LibraryFilter { + cmc_max: Some(1.0), + ..Default::default() + }).unwrap(); + assert!(cheap.iter().all(|c| c.card.mana_value <= 1.0)); + assert!(!cheap.iter().any(|c| c.card.name == "Serra Angel")); + } + + #[test] + fn colors_exact_and_include() { + let fx = owned_fixture(); + let exact_g = names(&fx.catalog.search_library(&LibraryFilter { + colors_exact: Some(ColorSet::parse("G")), + ..Default::default() + }).unwrap()); + assert_eq!(exact_g, vec!["Llanowar Elves"]); // not Deathrite (BG) + + let has_g = names(&fx.catalog.search_library(&LibraryFilter { + colors_include: Some(ColorSet::parse("G")), + ..Default::default() + }).unwrap()); + assert!(has_g.contains(&"Llanowar Elves".to_string())); + assert!(has_g.contains(&"Deathrite Shaman".to_string())); + } + + #[test] + fn text_and_in_deck() { + let fx = owned_fixture(); + let cat = &fx.catalog; + + let bolt = names(&cat.search_library(&LibraryFilter { + text: Some("bolt".into()), + ..Default::default() + }).unwrap()); + assert_eq!(bolt, vec!["Lightning Bolt"]); + + let deck = cat.create_deck("Mono-R", None).unwrap(); + cat.set_deck_card(deck.id, "lightning-bolt", 1, false).unwrap(); + let in_deck = names(&cat.search_library(&LibraryFilter { + in_deck: Some(deck.id), + ..Default::default() + }).unwrap()); + assert_eq!(in_deck, vec!["Lightning Bolt"]); + } +} diff --git a/crates/mtgc-core/src/ingest.rs b/crates/mtgc-core/src/ingest.rs new file mode 100644 index 0000000..634e5f9 --- /dev/null +++ b/crates/mtgc-core/src/ingest.rs @@ -0,0 +1,448 @@ +//! Build `reference.db` from Scryfall bulk data. +//! +//! Two passes: the *Oracle Cards* bulk (one entry per `oracle_id`) fills +//! `oracle_cards` + the normalized type/subtype/keyword tables + the name FTS; +//! the *Default Cards* bulk (one entry per printing) fills `printings`. +//! +//! Both bulk files are large JSON arrays, so we stream them element-by-element +//! rather than loading the whole array into memory. + +use std::io::{BufReader, Read}; +use std::marker::PhantomData; +use std::path::Path; + +use rusqlite::{params, Connection}; +use serde::de::{DeserializeOwned, SeqAccess, Visitor}; +use serde::Deserialize; + +use crate::db::open_reference_for_build; +use crate::error::{CoreError, Result}; +use crate::model::ColorSet; + +const BULK_INDEX_URL: &str = "https://api.scryfall.com/bulk-data"; +const USER_AGENT: &str = "mtgc/0.1 (personal library catalog)"; + +/// Row counts from a build, for reporting/verification. +#[derive(Debug, Default, Clone, Copy)] +pub struct IngestStats { + pub oracle_cards: usize, + pub printings: usize, +} + +/// Download the latest Scryfall bulk data and (re)build `reference.db` at `dest`. +/// +/// `progress` is called with human-readable status lines. +pub fn build_reference_db(dest: &Path, mut progress: impl FnMut(&str)) -> Result { + let (oracle_uri, default_uri) = fetch_bulk_uris(&mut progress)?; + + if dest.exists() { + std::fs::remove_file(dest)?; + } + let conn = open_reference_for_build(dest)?; + // Bulk-load fast; this db is disposable/rebuildable so durability is moot. + conn.pragma_update(None, "journal_mode", "MEMORY")?; + conn.pragma_update(None, "synchronous", "OFF")?; + + progress("Loading oracle cards…"); + let oracle_cards = load_oracle_cards(&conn, http_reader(&oracle_uri)?)?; + + progress("Loading printings…"); + let printings = load_printings(&conn, http_reader(&default_uri)?)?; + + progress("Optimizing…"); + conn.execute_batch("INSERT INTO oracle_names_fts(oracle_names_fts) VALUES('optimize');")?; + + Ok(IngestStats { + oracle_cards, + printings, + }) +} + +// ---- Scryfall bulk index ------------------------------------------------- + +#[derive(Deserialize)] +struct BulkIndex { + data: Vec, +} + +#[derive(Deserialize)] +struct BulkEntry { + #[serde(rename = "type")] + kind: String, + download_uri: String, +} + +fn fetch_bulk_uris(progress: &mut impl FnMut(&str)) -> Result<(String, String)> { + progress("Fetching Scryfall bulk index…"); + let index: BulkIndex = ureq::get(BULK_INDEX_URL) + .set("User-Agent", USER_AGENT) + .set("Accept", "application/json") + .call()? + .into_json()?; + + let find = |kind: &str| { + index + .data + .iter() + .find(|e| e.kind == kind) + .map(|e| e.download_uri.clone()) + .ok_or_else(|| CoreError::Other(format!("Scryfall bulk index missing '{kind}'"))) + }; + Ok((find("oracle_cards")?, find("default_cards")?)) +} + +fn http_reader(url: &str) -> Result { + let resp = ureq::get(url) + .set("User-Agent", USER_AGENT) + .call()?; + Ok(BufReader::with_capacity(1 << 20, resp.into_reader())) +} + +// ---- Streaming array parse ---------------------------------------------- + +/// Deserialize a top-level JSON array element-by-element, invoking `f` per item +/// without holding the whole array in memory. +fn stream_json_array(reader: R, f: F) -> Result<()> +where + R: Read, + T: DeserializeOwned, + F: FnMut(T) -> Result<()>, +{ + struct SeqVisitor { + f: F, + _marker: PhantomData T>, + } + + impl<'de, T, F> Visitor<'de> for SeqVisitor + where + T: DeserializeOwned, + F: FnMut(T) -> Result<()>, + { + type Value = (); + + fn expecting(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { + fmt.write_str("a JSON array of objects") + } + + fn visit_seq(mut self, mut seq: A) -> std::result::Result<(), A::Error> + where + A: SeqAccess<'de>, + { + while let Some(item) = seq.next_element::()? { + (self.f)(item).map_err(serde::de::Error::custom)?; + } + Ok(()) + } + } + + let mut de = serde_json::Deserializer::from_reader(reader); + serde::de::Deserializer::deserialize_seq( + &mut de, + SeqVisitor { + f, + _marker: PhantomData, + }, + )?; + Ok(()) +} + +// ---- Oracle pass --------------------------------------------------------- + +#[derive(Deserialize)] +struct ScryOracle { + oracle_id: Option, + name: String, + #[serde(default)] + cmc: f64, + #[serde(default)] + colors: Option>, + #[serde(default)] + color_identity: Vec, + #[serde(default)] + type_line: Option, + #[serde(default)] + oracle_text: Option, + #[serde(default)] + keywords: Vec, + #[serde(default)] + card_faces: Option>, +} + +#[derive(Deserialize)] +struct ScryFace { + #[serde(default)] + type_line: Option, + #[serde(default)] + colors: Option>, +} + +fn load_oracle_cards(conn: &Connection, reader: impl Read) -> Result { + conn.execute_batch("BEGIN")?; + let mut count = 0usize; + { + let mut ins_card = conn.prepare( + "INSERT OR IGNORE INTO oracle_cards + (oracle_id,name,name_lower,mana_value,colors,color_identity,type_line,oracle_text) + VALUES (?1,?2,?3,?4,?5,?6,?7,?8)", + )?; + let mut ins_fts = + conn.prepare("INSERT INTO oracle_names_fts(name,oracle_id) VALUES (?1,?2)")?; + let mut ins_type = + conn.prepare("INSERT INTO card_types(oracle_id,type) VALUES (?1,?2)")?; + let mut ins_sub = + conn.prepare("INSERT INTO card_subtypes(oracle_id,subtype) VALUES (?1,?2)")?; + let mut ins_kw = + conn.prepare("INSERT INTO card_keywords(oracle_id,keyword) VALUES (?1,?2)")?; + + stream_json_array::<_, ScryOracle, _>(reader, |c| { + let Some(oracle_id) = c.oracle_id.clone() else { + return Ok(()); // rare face-only records without a top-level oracle_id + }; + + // Colors: prefer top-level, else union of faces. + let colors = match &c.colors { + Some(cs) => ColorSet::from_letters(cs), + None => { + let mut m = 0u8; + if let Some(faces) = &c.card_faces { + for f in faces { + if let Some(cs) = &f.colors { + m |= ColorSet::from_letters(cs).0; + } + } + } + ColorSet(m) + } + }; + let identity = ColorSet::from_letters(&c.color_identity); + + // Type line: top-level, else join faces. + let type_line = c.type_line.clone().unwrap_or_else(|| { + c.card_faces + .as_ref() + .map(|faces| { + faces + .iter() + .filter_map(|f| f.type_line.clone()) + .collect::>() + .join(" // ") + }) + .unwrap_or_default() + }); + + let changed = ins_card.execute(params![ + oracle_id, + c.name, + c.name.to_lowercase(), + c.cmc, + colors.0 as i64, + identity.0 as i64, + type_line, + c.oracle_text.clone().unwrap_or_default(), + ])?; + // Only emit satellite rows the first time we see this oracle_id. + if changed == 1 { + ins_fts.execute(params![c.name, oracle_id])?; + let (types, subtypes) = parse_type_line(&type_line); + for t in types { + ins_type.execute(params![oracle_id, t])?; + } + for s in subtypes { + ins_sub.execute(params![oracle_id, s])?; + } + let mut kws = c.keywords.clone(); + kws.sort(); + kws.dedup(); + for k in kws { + ins_kw.execute(params![oracle_id, k])?; + } + count += 1; + } + Ok(()) + })?; + } + conn.execute_batch("COMMIT")?; + Ok(count) +} + +/// Split a Scryfall type line into (types+supertypes, subtypes). Handles split / +/// double-faced lines joined by " // ". The separator is the em dash '—'. +pub fn parse_type_line(type_line: &str) -> (Vec, Vec) { + let mut types = Vec::new(); + let mut subtypes = Vec::new(); + for part in type_line.split("//") { + let part = part.trim(); + let mut halves = part.splitn(2, '—'); + let left = halves.next().unwrap_or("").trim(); + let right = halves.next().unwrap_or("").trim(); + for t in left.split_whitespace() { + types.push(t.to_string()); + } + for s in right.split_whitespace() { + subtypes.push(s.to_string()); + } + } + types.sort(); + types.dedup(); + subtypes.sort(); + subtypes.dedup(); + (types, subtypes) +} + +// ---- Printings pass ------------------------------------------------------ + +#[derive(Deserialize)] +struct ScryPrinting { + id: String, + oracle_id: Option, + #[serde(default)] + set: String, + #[serde(default)] + collector_number: String, + #[serde(default)] + finishes: Vec, + #[serde(default)] + rarity: String, + #[serde(default)] + image_uris: Option, + #[serde(default)] + card_faces: Option>, +} + +#[derive(Deserialize)] +struct ImageUris { + #[serde(default)] + normal: Option, +} + +#[derive(Deserialize)] +struct PrintFace { + #[serde(default)] + image_uris: Option, +} + +fn load_printings(conn: &Connection, reader: impl Read) -> Result { + conn.execute_batch("BEGIN")?; + let mut count = 0usize; + { + let mut ins = conn.prepare( + "INSERT OR IGNORE INTO printings + (scryfall_id,oracle_id,set_code,collector_number,finishes,rarity,image_uri) + VALUES (?1,?2,?3,?4,?5,?6,?7)", + )?; + stream_json_array::<_, ScryPrinting, _>(reader, |p| { + let Some(oracle_id) = p.oracle_id.clone() else { + return Ok(()); + }; + let image = p + .image_uris + .as_ref() + .and_then(|i| i.normal.clone()) + .or_else(|| { + p.card_faces + .as_ref() + .and_then(|f| f.first()) + .and_then(|f| f.image_uris.as_ref()) + .and_then(|i| i.normal.clone()) + }) + .unwrap_or_default(); + ins.execute(params![ + p.id, + oracle_id, + p.set, + p.collector_number, + p.finishes.join(","), + p.rarity, + image, + ])?; + count += 1; + Ok(()) + })?; + } + conn.execute_batch("COMMIT")?; + Ok(count) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_type_lines() { + let (t, s) = parse_type_line("Legendary Creature — Elf Druid"); + assert_eq!(t, vec!["Creature", "Legendary"]); + assert_eq!(s, vec!["Druid", "Elf"]); + + let (t, s) = parse_type_line("Instant"); + assert_eq!(t, vec!["Instant"]); + assert!(s.is_empty()); + + // Split card. + let (t, s) = parse_type_line("Sorcery // Sorcery"); + assert_eq!(t, vec!["Sorcery"]); + assert!(s.is_empty()); + } + + #[test] + fn streams_array_of_objects() { + #[derive(serde::Deserialize)] + struct Item { + n: i32, + } + let json = br#"[{"n":1},{"n":2},{"n":3}]"#; + let mut sum = 0; + stream_json_array::<_, Item, _>(&json[..], |it| { + sum += it.n; + Ok(()) + }) + .unwrap(); + assert_eq!(sum, 6); + } + + #[test] + fn oracle_and_printing_load_from_fixture_json() { + // Minimal Scryfall-shaped fixtures exercising the insert paths. + let oracle = r#"[ + {"oracle_id":"oid-1","name":"Llanowar Elves","cmc":1.0, + "colors":["G"],"color_identity":["G"], + "type_line":"Creature — Elf Druid","keywords":[]}, + {"oracle_id":"oid-2","name":"Serra Angel","cmc":5.0, + "colors":["W"],"color_identity":["W"], + "type_line":"Creature — Angel","keywords":["Flying","Vigilance"]} + ]"#; + let printings = r#"[ + {"id":"p-1","oracle_id":"oid-1","set":"m19","collector_number":"310", + "finishes":["nonfoil","foil"],"rarity":"common", + "image_uris":{"normal":"http://img/1"}} + ]"#; + + let dir = tempfile::tempdir().unwrap(); + let dest = dir.path().join("reference.db"); + let conn = open_reference_for_build(&dest).unwrap(); + + let oc = load_oracle_cards(&conn, oracle.as_bytes()).unwrap(); + let pc = load_printings(&conn, printings.as_bytes()).unwrap(); + assert_eq!(oc, 2); + assert_eq!(pc, 1); + + // Keyword table populated. + let flyers: i64 = conn + .query_row( + "SELECT count(*) FROM card_keywords WHERE keyword='Flying'", + [], + |r| r.get(0), + ) + .unwrap(); + assert_eq!(flyers, 1); + + // Bitmask colors stored. + let g: i64 = conn + .query_row( + "SELECT colors FROM oracle_cards WHERE oracle_id='oid-1'", + [], + |r| r.get(0), + ) + .unwrap(); + assert_eq!(g as u8, crate::model::G); + } +} diff --git a/crates/mtgc-core/src/lib.rs b/crates/mtgc-core/src/lib.rs new file mode 100644 index 0000000..9828f60 --- /dev/null +++ b/crates/mtgc-core/src/lib.rs @@ -0,0 +1,217 @@ +//! mtgc-core — platform-agnostic core for the MTG library catalog. +//! +//! Everything that isn't UI lives here: the SQLite schema/migrations, the +//! in-memory fuzzy name search, the criteria filters, deck operations, and the +//! Scryfall ingester. It has no Tauri (or any GUI) dependency so it stays +//! unit-testable and reusable when the app is ported to mobile. + +pub mod db; +pub mod decks; +pub mod error; +pub mod filters; +pub mod ingest; +pub mod library; +pub mod model; +pub mod search; + +use std::path::Path; + +use rusqlite::Connection; + +pub use error::{CoreError, Result}; +pub use model::{CardRef, ColorSet, Deck, DeckEntry, LibraryCard, LibraryFilter}; +pub use search::NameIndex; + +/// A handle to an open catalog: the library db with the reference db attached. +/// +/// All read/write operations go through this. It owns the SQLite connection; +/// the fuzzy [`NameIndex`] is built separately (see [`Catalog::build_name_index`]) +/// so the caller can cache it in app state. +pub struct Catalog { + pub(crate) conn: Connection, +} + +impl Catalog { + /// Open the library at `library_path` (running migrations) with the + /// reference db at `reference_path` attached. + pub fn open(library_path: &Path, reference_path: &Path) -> Result { + let conn = db::open(library_path, reference_path)?; + Ok(Self { conn }) + } + + /// Build (or rebuild) the in-memory fuzzy name index from the attached + /// reference db. Cheap enough to hold in app state and rebuild after a + /// reference-data update. + pub fn build_name_index(&self) -> Result { + NameIndex::build(&self.conn) + } + + /// Borrow the underlying connection (used by tests and advanced callers). + pub fn connection(&self) -> &Connection { + &self.conn + } +} + +#[cfg(test)] +pub(crate) mod test_support { + //! Builds a tiny library+reference catalog in temp files for tests. + + use super::*; + use crate::model::ColorSet; + + pub struct Fixture { + pub catalog: Catalog, + // Keep the temp dir alive for the lifetime of the fixture. + _dir: tempfile::TempDir, + } + + /// (oracle_id, name, mana_value, colors, color_identity, type_line, + /// types, subtypes, keywords) + pub struct SeedCard { + pub oracle_id: &'static str, + pub name: &'static str, + pub mv: f64, + pub colors: &'static str, + pub identity: &'static str, + pub type_line: &'static str, + pub types: &'static [&'static str], + pub subtypes: &'static [&'static str], + pub keywords: &'static [&'static str], + } + + pub fn seed_cards() -> Vec { + vec![ + SeedCard { + oracle_id: "llanowar-elves", + name: "Llanowar Elves", + mv: 1.0, + colors: "G", + identity: "G", + type_line: "Creature — Elf Druid", + types: &["Creature"], + subtypes: &["Elf", "Druid"], + keywords: &[], + }, + SeedCard { + oracle_id: "deathrite-shaman", + name: "Deathrite Shaman", + mv: 1.0, + colors: "BG", + identity: "BG", + type_line: "Creature — Elf Shaman", + types: &["Creature"], + subtypes: &["Elf", "Shaman"], + keywords: &[], + }, + SeedCard { + oracle_id: "serra-angel", + name: "Serra Angel", + mv: 5.0, + colors: "W", + identity: "W", + type_line: "Creature — Angel", + types: &["Creature"], + subtypes: &["Angel"], + keywords: &["Flying", "Vigilance"], + }, + SeedCard { + oracle_id: "lightning-bolt", + name: "Lightning Bolt", + mv: 1.0, + colors: "R", + identity: "R", + type_line: "Instant", + types: &["Instant"], + subtypes: &[], + keywords: &[], + }, + SeedCard { + oracle_id: "sol-ring", + name: "Sol Ring", + mv: 1.0, + colors: "", + identity: "", + type_line: "Artifact", + types: &["Artifact"], + subtypes: &[], + keywords: &[], + }, + ] + } + + pub fn make() -> Fixture { + let dir = tempfile::tempdir().unwrap(); + let ref_path = dir.path().join("reference.db"); + let lib_path = dir.path().join("library.db"); + + // Build reference.db with fixture cards. + { + let rconn = db::open_reference_for_build(&ref_path).unwrap(); + for c in seed_cards() { + let colors = ColorSet::parse(c.colors).0 as i64; + let identity = ColorSet::parse(c.identity).0 as i64; + rconn + .execute( + "INSERT INTO oracle_cards + (oracle_id,name,name_lower,mana_value,colors,color_identity,type_line) + VALUES (?1,?2,?3,?4,?5,?6,?7)", + rusqlite::params![ + c.oracle_id, + c.name, + c.name.to_lowercase(), + c.mv, + colors, + identity, + c.type_line + ], + ) + .unwrap(); + rconn + .execute( + "INSERT INTO oracle_names_fts(name,oracle_id) VALUES (?1,?2)", + rusqlite::params![c.name, c.oracle_id], + ) + .unwrap(); + for t in c.types { + rconn + .execute( + "INSERT INTO card_types(oracle_id,type) VALUES (?1,?2)", + rusqlite::params![c.oracle_id, t], + ) + .unwrap(); + } + for s in c.subtypes { + rconn + .execute( + "INSERT INTO card_subtypes(oracle_id,subtype) VALUES (?1,?2)", + rusqlite::params![c.oracle_id, s], + ) + .unwrap(); + } + for k in c.keywords { + rconn + .execute( + "INSERT INTO card_keywords(oracle_id,keyword) VALUES (?1,?2)", + rusqlite::params![c.oracle_id, k], + ) + .unwrap(); + } + // One printing per card, carrying an image URL. + rconn + .execute( + "INSERT INTO printings(scryfall_id,oracle_id,image_uri) + VALUES (?1,?2,?3)", + rusqlite::params![ + format!("print-{}", c.oracle_id), + c.oracle_id, + format!("http://img/{}", c.oracle_id) + ], + ) + .unwrap(); + } + } + + let catalog = Catalog::open(&lib_path, &ref_path).unwrap(); + Fixture { catalog, _dir: dir } + } +} diff --git a/crates/mtgc-core/src/library.rs b/crates/mtgc-core/src/library.rs new file mode 100644 index 0000000..eb6dfd1 --- /dev/null +++ b/crates/mtgc-core/src/library.rs @@ -0,0 +1,192 @@ +//! Ownership operations: what cards the user has and how many. + +use std::collections::HashMap; + +use rusqlite::{params, OptionalExtension}; + +use crate::db::REF_DB; +use crate::error::Result; +use crate::model::{CardRef, ColorSet, LibraryCard}; +use crate::Catalog; + +/// Map a row shaped `(oracle_id, quantity_owned, location, name, mana_value, +/// colors, color_identity, type_line, image_uri)` into its library parts. Shared +/// by [`Catalog::list_library`] and the criteria search. +pub(crate) fn row_to_library_parts( + r: &rusqlite::Row, +) -> rusqlite::Result<(String, i64, String, CardRef)> { + let oracle_id: String = r.get(0)?; + Ok(( + oracle_id.clone(), + r.get::<_, i64>(1)?, + r.get::<_, String>(2)?, + CardRef { + oracle_id, + name: r.get(3)?, + mana_value: r.get(4)?, + colors: ColorSet(r.get::<_, i64>(5)? as u8), + color_identity: ColorSet(r.get::<_, i64>(6)? as u8), + type_line: r.get(7)?, + image_uri: r.get::<_, Option>(8)?.unwrap_or_default(), + }, + )) +} + +impl Catalog { + /// Add `delta` copies of a card to the library (increment, creating the row + /// if needed). `delta` may be negative to decrement; the quantity floors at 0. + /// Returns the new owned quantity. + pub fn add_owned(&self, oracle_id: &str, delta: i64) -> Result { + let current: i64 = self + .conn + .query_row( + "SELECT quantity_owned FROM library_cards WHERE oracle_id = ?1", + params![oracle_id], + |r| r.get(0), + ) + .optional()? + .unwrap_or(0); + self.set_owned(oracle_id, current + delta) + } + + /// Set the absolute owned quantity for a card (0 removes ownership but keeps + /// any deck assignments referencing it). + pub fn set_owned(&self, oracle_id: &str, quantity: i64) -> Result { + let q = quantity.max(0); + self.conn.execute( + "INSERT INTO library_cards(oracle_id, quantity_owned) VALUES (?1, ?2) + ON CONFLICT(oracle_id) DO UPDATE SET quantity_owned = excluded.quantity_owned", + params![oracle_id, q], + )?; + Ok(q) + } + + /// List every owned card (quantity_owned > 0) with deck context, ordered by + /// name. This is the "browse my library" view without extra criteria. + pub fn list_library(&self) -> Result> { + let deck_names = self.deck_names_by_oracle()?; + let assigned = self.assigned_totals_by_oracle()?; + + let mut stmt = self.conn.prepare(&format!( + "SELECT lc.oracle_id, lc.quantity_owned, lc.location, + oc.name, oc.mana_value, oc.colors, oc.color_identity, oc.type_line, {image} + FROM library_cards lc + JOIN {REF_DB}.oracle_cards oc ON oc.oracle_id = lc.oracle_id + WHERE lc.quantity_owned > 0 + ORDER BY oc.name COLLATE NOCASE", + image = crate::db::image_subquery() + ))?; + let rows = stmt + .query_map([], row_to_library_parts)? + .collect::, _>>()?; + + Ok(rows + .into_iter() + .map(|(oid, qty, location, card)| LibraryCard { + quantity_owned: qty, + decks: deck_names.get(&oid).cloned().unwrap_or_default(), + quantity_assigned: *assigned.get(&oid).unwrap_or(&0), + location, + card, + }) + .collect()) + } + + /// Set the free-text storage location for a card (creates the library row if + /// absent). Empty string clears it. + pub fn set_location(&self, oracle_id: &str, location: &str) -> Result<()> { + self.conn.execute( + "INSERT INTO library_cards(oracle_id, location) VALUES (?1, ?2) + ON CONFLICT(oracle_id) DO UPDATE SET location = excluded.location", + params![oracle_id, location], + )?; + Ok(()) + } + + /// Map each owned/assigned oracle_id to the deck names it appears in. + pub(crate) fn deck_names_by_oracle(&self) -> Result>> { + let mut stmt = self.conn.prepare( + "SELECT dc.oracle_id, d.name + FROM deck_cards dc JOIN decks d ON d.id = dc.deck_id + ORDER BY d.name COLLATE NOCASE", + )?; + let mut map: HashMap> = HashMap::new(); + let rows = stmt.query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?)))?; + for row in rows { + let (oid, name) = row?; + map.entry(oid).or_default().push(name); + } + Ok(map) + } + + pub(crate) fn assigned_totals_by_oracle(&self) -> Result> { + let mut stmt = self + .conn + .prepare("SELECT oracle_id, SUM(quantity) FROM deck_cards GROUP BY oracle_id")?; + let mut map = HashMap::new(); + let rows = stmt.query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, i64>(1)?)))?; + for row in rows { + let (oid, total) = row?; + map.insert(oid, total); + } + Ok(map) + } +} + +#[cfg(test)] +mod tests { + use crate::test_support; + + #[test] + fn add_increment_and_floor() { + let fx = test_support::make(); + let cat = &fx.catalog; + + assert_eq!(cat.add_owned("sol-ring", 1).unwrap(), 1); + assert_eq!(cat.add_owned("sol-ring", 2).unwrap(), 3); + assert_eq!(cat.add_owned("sol-ring", -10).unwrap(), 0); // floors at 0 + assert_eq!(cat.set_owned("sol-ring", 4).unwrap(), 4); + + let lib = cat.list_library().unwrap(); + let sol = lib.iter().find(|c| c.card.oracle_id == "sol-ring").unwrap(); + assert_eq!(sol.quantity_owned, 4); + assert!(sol.decks.is_empty()); + // Image URL is plumbed through from the printings table. + assert_eq!(sol.card.image_uri, "http://img/sol-ring"); + } + + #[test] + fn location_is_stored_and_listed() { + let fx = test_support::make(); + let cat = &fx.catalog; + cat.add_owned("sol-ring", 1).unwrap(); + cat.set_location("sol-ring", "Box 1").unwrap(); + + let lib = cat.list_library().unwrap(); + let sol = lib.iter().find(|c| c.card.oracle_id == "sol-ring").unwrap(); + assert_eq!(sol.location, "Box 1"); + + // Clearing works. + cat.set_location("sol-ring", "").unwrap(); + let lib = cat.list_library().unwrap(); + let sol = lib.iter().find(|c| c.card.oracle_id == "sol-ring").unwrap(); + assert_eq!(sol.location, ""); + } + + #[test] + fn list_is_ordered_and_excludes_zero() { + let fx = test_support::make(); + let cat = &fx.catalog; + cat.add_owned("serra-angel", 1).unwrap(); + cat.add_owned("lightning-bolt", 1).unwrap(); + cat.set_owned("sol-ring", 0).unwrap(); + + let names: Vec<_> = cat + .list_library() + .unwrap() + .into_iter() + .map(|c| c.card.name) + .collect(); + assert_eq!(names, vec!["Lightning Bolt", "Serra Angel"]); + } +} diff --git a/crates/mtgc-core/src/model.rs b/crates/mtgc-core/src/model.rs new file mode 100644 index 0000000..de02dcb --- /dev/null +++ b/crates/mtgc-core/src/model.rs @@ -0,0 +1,165 @@ +//! Shared domain types used across the core. + +use serde::{Deserialize, Serialize}; + +/// A card's color identity / colors, encoded as a 5-bit WUBRG mask. +/// +/// Bit layout (little end first): W=1, U=2, B=4, R=8, G=16. +/// Storing colors this way turns commander color-identity compatibility into a +/// single integer predicate (see [`ColorSet::is_subset_of`]). +pub const W: u8 = 1 << 0; +pub const U: u8 = 1 << 1; +pub const B: u8 = 1 << 2; +pub const R: u8 = 1 << 3; +pub const G: u8 = 1 << 4; + +/// The five colors in canonical WUBRG order, paired with their single-letter code. +pub const WUBRG: [(u8, char); 5] = [(W, 'W'), (U, 'U'), (B, 'B'), (R, 'R'), (G, 'G')]; + +/// A set of colors as a WUBRG bitmask. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] +pub struct ColorSet(pub u8); + +impl ColorSet { + pub const COLORLESS: ColorSet = ColorSet(0); + + /// Build from an iterator of single-letter color codes (e.g. Scryfall's + /// `["B", "G"]`). Unknown letters are ignored. + pub fn from_letters(letters: I) -> Self + where + I: IntoIterator, + S: AsRef, + { + let mut mask = 0u8; + for l in letters { + match l.as_ref() { + "W" | "w" => mask |= W, + "U" | "u" => mask |= U, + "B" | "b" => mask |= B, + "R" | "r" => mask |= R, + "G" | "g" => mask |= G, + _ => {} + } + } + ColorSet(mask) + } + + /// Parse from a compact string like "BG" or "wubrg". Order-insensitive. + pub fn parse(s: &str) -> Self { + Self::from_letters(s.chars().map(|c| c.to_string())) + } + + /// Canonical WUBRG string, e.g. `ColorSet(B|G)` -> "BG". Colorless -> "". + pub fn to_letters(self) -> String { + WUBRG + .iter() + .filter(|(bit, _)| self.0 & bit != 0) + .map(|(_, ch)| *ch) + .collect() + } + + pub fn is_colorless(self) -> bool { + self.0 == 0 + } + + pub fn contains(self, bit: u8) -> bool { + self.0 & bit != 0 + } + + /// True if every color in `self` is also in `other`. + /// + /// This is the commander-legality rule: a card is playable in a commander of + /// color identity `C` iff `card_identity.is_subset_of(C)`. + pub fn is_subset_of(self, other: ColorSet) -> bool { + self.0 & !other.0 == 0 + } +} + +/// A card as returned by search / library queries. Oracle-level (one per +/// `oracle_id`), enriched with the caller's ownership + deck context where relevant. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CardRef { + pub oracle_id: String, + pub name: String, + pub mana_value: f64, + pub colors: ColorSet, + pub color_identity: ColorSet, + pub type_line: String, + /// URL of a card image (Scryfall "normal" size) for one of its printings, + /// or empty if none is available. + pub image_uri: String, +} + +/// A library row: a card the user owns, with quantity and its deck memberships. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LibraryCard { + #[serde(flatten)] + pub card: CardRef, + pub quantity_owned: i64, + /// Names of decks this card is currently assigned to. + pub decks: Vec, + /// Total copies assigned across all decks. If it exceeds `quantity_owned`, + /// the physical copies are over-committed. + pub quantity_assigned: i64, + /// Free-text physical storage location (e.g. "Box 1"). Meaningful mainly for + /// cards not currently in a deck. + pub location: String, +} + +/// A card in a deck, whether or not the user owns it. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DeckEntry { + #[serde(flatten)] + pub card: CardRef, + /// Copies of this card in the deck. + pub quantity: i64, + pub is_commander: bool, + /// Copies owned in the library (0 if the card isn't in the library). + pub quantity_owned: i64, +} + +impl LibraryCard { + pub fn is_over_assigned(&self) -> bool { + self.quantity_assigned > self.quantity_owned + } +} + +/// A deck. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Deck { + pub id: i64, + pub name: String, + /// Free-text format, e.g. "Commander", "Modern" (empty if unset). + pub format: String, + /// The commander card's oracle id (meaningful for Commander/Brawl formats). + pub commander_oracle_id: Option, + pub notes: String, +} + +/// Criteria for searching the owned library. All fields are optional; an empty +/// filter matches every owned card. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(default)] +pub struct LibraryFilter { + /// Fuzzy-ish name text (applied as a case-insensitive substring here; the + /// fast fuzzy path is [`crate::search`]). + pub text: Option, + /// Cards whose `colors` exactly equal this set. + pub colors_exact: Option, + /// Cards whose `colors` include at least these. + pub colors_include: Option, + /// Commander compatibility: only cards whose color identity is a subset of + /// this set (playable under a commander of this identity). + pub commander_identity: Option, + pub cmc_min: Option, + pub cmc_max: Option, + /// Card supertype/type, e.g. "Creature", "Instant" (matched against the + /// normalized `card_types` table). + pub types: Vec, + /// Subtype, e.g. "Elf", "Equipment". + pub subtypes: Vec, + /// Keyword ability, e.g. "Flying", "Deathtouch". + pub keywords: Vec, + /// Restrict to cards assigned to this deck id. + pub in_deck: Option, +} diff --git a/crates/mtgc-core/src/search.rs b/crates/mtgc-core/src/search.rs new file mode 100644 index 0000000..3790a41 --- /dev/null +++ b/crates/mtgc-core/src/search.rs @@ -0,0 +1,108 @@ +//! In-memory fuzzy search over oracle card names (the "rapid entry" path). +//! +//! Names are loaded once from the reference db into a [`NameIndex`]; each query +//! is ranked with nucleo (fzf-quality), giving typo-tolerant ordering far better +//! than SQL `LIKE`. ~30k names rank in well under a millisecond. + +use nucleo::pattern::{CaseMatching, Normalization, Pattern}; +use nucleo::{Config, Matcher}; +use rusqlite::Connection; + +use crate::db::REF_DB; +use crate::error::Result; +use crate::model::{CardRef, ColorSet}; + +/// One searchable entry, carrying the full [`CardRef`] so a hit needs no extra +/// DB round-trip. `AsRef` yields the name that nucleo matches against. +struct Entry { + card: CardRef, +} + +impl AsRef for Entry { + fn as_ref(&self) -> &str { + &self.card.name + } +} + +/// An immutable, in-memory index of all oracle card names for fuzzy search. +pub struct NameIndex { + entries: Vec, +} + +impl NameIndex { + /// Load every oracle card from the attached reference db. + pub fn build(conn: &Connection) -> Result { + let mut stmt = conn.prepare(&format!( + "SELECT oc.oracle_id, oc.name, oc.mana_value, oc.colors, oc.color_identity, + oc.type_line, {image} + FROM {REF_DB}.oracle_cards oc", + image = crate::db::image_subquery() + ))?; + let entries = stmt + .query_map([], |r| { + Ok(Entry { + card: CardRef { + oracle_id: r.get(0)?, + name: r.get(1)?, + mana_value: r.get(2)?, + colors: ColorSet(r.get::<_, i64>(3)? as u8), + color_identity: ColorSet(r.get::<_, i64>(4)? as u8), + type_line: r.get(5)?, + image_uri: r.get::<_, Option>(6)?.unwrap_or_default(), + }, + }) + })? + .collect::, _>>()?; + Ok(Self { entries }) + } + + pub fn len(&self) -> usize { + self.entries.len() + } + + pub fn is_empty(&self) -> bool { + self.entries.is_empty() + } + + /// Rank cards by fuzzy match against `query`, returning up to `limit` of the + /// best matches (highest score first). An empty query returns nothing. + pub fn search(&self, query: &str, limit: usize) -> Vec { + if query.trim().is_empty() { + return Vec::new(); + } + let mut matcher = Matcher::new(Config::DEFAULT); + let pattern = Pattern::parse(query, CaseMatching::Ignore, Normalization::Smart); + let mut matches = pattern.match_list(self.entries.iter(), &mut matcher); + matches.truncate(limit); + matches.into_iter().map(|(e, _score)| e.card.clone()).collect() + } +} + +#[cfg(test)] +mod tests { + use crate::test_support; + + #[test] + fn typo_still_ranks_target_first() { + let fx = test_support::make(); + let idx = fx.catalog.build_name_index().unwrap(); + assert_eq!(idx.len(), 5); + + // Misspelled "Llanowar Elves". + let hits = idx.search("lanowar elvs", 5); + assert!(!hits.is_empty(), "typo query should still match"); + assert_eq!(hits[0].name, "Llanowar Elves"); + } + + #[test] + fn prefix_and_limit() { + let fx = test_support::make(); + let idx = fx.catalog.build_name_index().unwrap(); + + let hits = idx.search("light", 5); + assert_eq!(hits[0].name, "Lightning Bolt"); + + assert!(idx.search("bolt", 1).len() <= 1); + assert!(idx.search("", 10).is_empty()); + } +} diff --git a/crates/mtgc-core/src/sql/library/0001_init.sql b/crates/mtgc-core/src/sql/library/0001_init.sql new file mode 100644 index 0000000..353bbbe --- /dev/null +++ b/crates/mtgc-core/src/sql/library/0001_init.sql @@ -0,0 +1,41 @@ +-- library.db v1 — the small, portable, synced file. +-- References reference cards by stable oracle_id / scryfall_id. + +CREATE TABLE decks ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + commander_oracle_id TEXT, + notes TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +-- Oracle-level ownership: one row per distinct card. +CREATE TABLE library_cards ( + id INTEGER PRIMARY KEY, + oracle_id TEXT NOT NULL UNIQUE, + quantity_owned INTEGER NOT NULL DEFAULT 0 +); + +-- Optional per-physical-copy printing detail (the "hybrid" identity). +CREATE TABLE library_card_copies ( + id INTEGER PRIMARY KEY, + library_card_id INTEGER NOT NULL REFERENCES library_cards(id) ON DELETE CASCADE, + scryfall_id TEXT, + set_code TEXT, + collector_number TEXT, + finish TEXT, + condition TEXT +); +CREATE INDEX idx_copies_card ON library_card_copies(library_card_id); + +-- Deck membership at the oracle level. +CREATE TABLE deck_cards ( + id INTEGER PRIMARY KEY, + deck_id INTEGER NOT NULL REFERENCES decks(id) ON DELETE CASCADE, + oracle_id TEXT NOT NULL, + quantity INTEGER NOT NULL DEFAULT 1, + is_commander INTEGER NOT NULL DEFAULT 0, + UNIQUE(deck_id, oracle_id) +); +CREATE INDEX idx_deck_cards_oid ON deck_cards(oracle_id); +CREATE INDEX idx_deck_cards_deck ON deck_cards(deck_id); diff --git a/crates/mtgc-core/src/sql/library/0002_location.sql b/crates/mtgc-core/src/sql/library/0002_location.sql new file mode 100644 index 0000000..ac0075b --- /dev/null +++ b/crates/mtgc-core/src/sql/library/0002_location.sql @@ -0,0 +1,3 @@ +-- v2 — physical storage location for owned cards (e.g. "Box 1", "Binder A"). +-- Most relevant for cards not assigned to any deck. +ALTER TABLE library_cards ADD COLUMN location TEXT NOT NULL DEFAULT ''; diff --git a/crates/mtgc-core/src/sql/library/0003_deck_format.sql b/crates/mtgc-core/src/sql/library/0003_deck_format.sql new file mode 100644 index 0000000..fd19e8d --- /dev/null +++ b/crates/mtgc-core/src/sql/library/0003_deck_format.sql @@ -0,0 +1,3 @@ +-- v3 — deck format metadata (e.g. "Commander", "Modern"). The commander card +-- itself is stored in the existing decks.commander_oracle_id column. +ALTER TABLE decks ADD COLUMN format TEXT NOT NULL DEFAULT ''; diff --git a/crates/mtgc-core/src/sql/reference_schema.sql b/crates/mtgc-core/src/sql/reference_schema.sql new file mode 100644 index 0000000..4145ace --- /dev/null +++ b/crates/mtgc-core/src/sql/reference_schema.sql @@ -0,0 +1,56 @@ +-- Schema for reference.db: read-only card data built from Scryfall bulk. +-- Applied against a connection whose `main` database IS the reference db +-- (the ingester opens reference.db directly; at runtime it is ATTACHed as `ref`). + +CREATE TABLE IF NOT EXISTS oracle_cards ( + oracle_id TEXT PRIMARY KEY, + name TEXT NOT NULL, + name_lower TEXT NOT NULL, -- for cheap case-insensitive substring + mana_value REAL NOT NULL DEFAULT 0, + colors INTEGER NOT NULL DEFAULT 0, -- WUBRG bitmask + color_identity INTEGER NOT NULL DEFAULT 0, -- WUBRG bitmask + type_line TEXT NOT NULL DEFAULT '', + oracle_text TEXT NOT NULL DEFAULT '' +); +CREATE INDEX IF NOT EXISTS idx_oracle_name_lower ON oracle_cards(name_lower); +CREATE INDEX IF NOT EXISTS idx_oracle_ci ON oracle_cards(color_identity); +CREATE INDEX IF NOT EXISTS idx_oracle_mv ON oracle_cards(mana_value); + +-- Normalized attribute tables for fast type/subtype/keyword filtering. +CREATE TABLE IF NOT EXISTS card_types ( + oracle_id TEXT NOT NULL, + type TEXT NOT NULL +); +CREATE INDEX IF NOT EXISTS idx_card_types_type ON card_types(type); +CREATE INDEX IF NOT EXISTS idx_card_types_oid ON card_types(oracle_id); + +CREATE TABLE IF NOT EXISTS card_subtypes ( + oracle_id TEXT NOT NULL, + subtype TEXT NOT NULL +); +CREATE INDEX IF NOT EXISTS idx_card_subtypes_sub ON card_subtypes(subtype); +CREATE INDEX IF NOT EXISTS idx_card_subtypes_oid ON card_subtypes(oracle_id); + +CREATE TABLE IF NOT EXISTS card_keywords ( + oracle_id TEXT NOT NULL, + keyword TEXT NOT NULL +); +CREATE INDEX IF NOT EXISTS idx_card_keywords_kw ON card_keywords(keyword); +CREATE INDEX IF NOT EXISTS idx_card_keywords_oid ON card_keywords(oracle_id); + +-- One row per printing (default-language bulk). +CREATE TABLE IF NOT EXISTS printings ( + scryfall_id TEXT PRIMARY KEY, + oracle_id TEXT NOT NULL, + set_code TEXT NOT NULL DEFAULT '', + collector_number TEXT NOT NULL DEFAULT '', + finishes TEXT NOT NULL DEFAULT '', -- comma-joined, e.g. "nonfoil,foil" + rarity TEXT NOT NULL DEFAULT '', + image_uri TEXT NOT NULL DEFAULT '' +); +CREATE INDEX IF NOT EXISTS idx_printings_oid ON printings(oracle_id); + +-- Secondary trigram FTS index over names (typo-tolerant fallback; the primary +-- interactive search is the in-memory nucleo matcher). +CREATE VIRTUAL TABLE IF NOT EXISTS oracle_names_fts + USING fts5(name, oracle_id UNINDEXED, tokenize = 'trigram'); diff --git a/index.html b/index.html new file mode 100644 index 0000000..0a9e379 --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ + + + + + + MTG Library Catalog + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..ebdaa3f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1971 @@ +{ + "name": "mtgc", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mtgc", + "version": "0.1.0", + "dependencies": { + "@tauri-apps/api": "^2", + "@tauri-apps/plugin-dialog": "^2", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@tauri-apps/cli": "^2", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "typescript": "^5.6.3", + "vite": "^5.4.11" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tauri-apps/api": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.1.tgz", + "integrity": "sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA==", + "license": "Apache-2.0 OR MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, + "node_modules/@tauri-apps/cli": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.4.tgz", + "integrity": "sha512-R8xGtMpwyetawSqm9kYOuMmEqkhUbvcUy8n0aNXIxollKBLESUu5f4Fx+64hgASYm1H+jSWq6jCW6zqTnH6hqQ==", + "dev": true, + "license": "Apache-2.0 OR MIT", + "bin": { + "tauri": "tauri.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "optionalDependencies": { + "@tauri-apps/cli-darwin-arm64": "2.11.4", + "@tauri-apps/cli-darwin-x64": "2.11.4", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.4", + "@tauri-apps/cli-linux-arm64-gnu": "2.11.4", + "@tauri-apps/cli-linux-arm64-musl": "2.11.4", + "@tauri-apps/cli-linux-riscv64-gnu": "2.11.4", + "@tauri-apps/cli-linux-x64-gnu": "2.11.4", + "@tauri-apps/cli-linux-x64-musl": "2.11.4", + "@tauri-apps/cli-win32-arm64-msvc": "2.11.4", + "@tauri-apps/cli-win32-ia32-msvc": "2.11.4", + "@tauri-apps/cli-win32-x64-msvc": "2.11.4" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.4.tgz", + "integrity": "sha512-1ryOF3ZhpZ/nemHV5zVwBQBz9jDGKmKPvWPADOhc83ig0P4bMc2iER4NbC6r9sjeIZ6RVQ4g3RZIYvezhcl4TQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-darwin-x64": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.4.tgz", + "integrity": "sha512-uFsGQAAfuyz1k/yGLmkWfkBlgKAqZfxqlHmLWx81QU27RJWfmbNHCIq8T8w1e+VClleIuZUjpHWfoE4E3DLo3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.4.tgz", + "integrity": "sha512-IaHZn5CdBL21oUmjiVOS1ctw6Ip1O0pjp70FwOWmYz1myWe0SY96ZIj2FYf7pT0m8bI2h/hrs5ZbEXXh44/MkQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-gnu": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.4.tgz", + "integrity": "sha512-N41/ukTRVe6XSuUTESuFdGeOW2i7k62tK+6gHK5Kd5/q5RPvvi19GaWAVPPb9u95HSGmTChSolBfzynUsssFaA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.4.tgz", + "integrity": "sha512-v277UnT/fB64xAfSroL5N3Km3tLmvATWqJJw/wRI+g6o+HkeD0slyE7gOhNs1MbjE41R7bQOTxMVoL3aomUJmw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.4.tgz", + "integrity": "sha512-qqgNkQ2u1yZHxjhxsZaxUtRDW8dIqIYm33rx/mzwQv0SfY9x1B+iraj8vWeFiXjjSVVhEMepXSOts1TqPzvXNQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.4.tgz", + "integrity": "sha512-2VRNWl84FOH0m2giiDkO2h0QXlcMJeX+zJDpI5kDIQAx6s+geF3v48F4DXfJez4GS/FdoDGnPnw1C2iYGbQ7bQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.4.tgz", + "integrity": "sha512-o9GyhYor/nc7xarmwDE3ka2szuW3uuZzXjHWh64Q8YX5AtSgxdQkFWzrY4O8KiGtVNvFBI14H3Q49Qj5TOIP/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-arm64-msvc": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.4.tgz", + "integrity": "sha512-ld5Ehb598m0VkYyylRPNeCFsBe/km0jxis6KgMpl3IGY6I/i1RwQXO05I1AsXUXO2WC6AvB/Lw4qTf/asiuEiQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-ia32-msvc": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.4.tgz", + "integrity": "sha512-12Hxi0XX/H5VFxO/bGgHkFWhml9VMgEOu9CidjeCeTNQ1l6fpUlbiGgSP7CLI3PFtW9/FfbeHieZ+kyWK5H7CA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-x64-msvc": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.4.tgz", + "integrity": "sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/plugin-dialog": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.7.1.tgz", + "integrity": "sha512-OK1UBXYt+ojcmxMktzzuyonYIFta8CmAASpX+CA+DTGK24KlHjhYI6x2iOJ/TjZF4N7/ACK1oFmEOjIY9IhzOQ==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.11.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.42", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.42.tgz", + "integrity": "sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz", + "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.38", + "caniuse-lite": "^1.0.30001799", + "electron-to-chromium": "^1.5.376", + "node-releases": "^2.0.48", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001800", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001800.tgz", + "integrity": "sha512-MMHtuAz9Ys840zAY5F4k6fV5GaivZ9sPk+nz0mY+GYVzRBnYkN0mpqkSR92oWRQ19yQWo4HvBV/FnC16AJX8MA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.387", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.387.tgz", + "integrity": "sha512-TaxwufTFDufvPEoXdhwVrA3UdFWBeWGkYoJ1K8ldF1xe6gKfth6iRNS5lTQ5JPNOHdGQm8PT1QYKUqFLCiUefQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.50", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz", + "integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..addd2e5 --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "mtgc", + "version": "0.1.0", + "description": "Personal Magic: The Gathering library catalog", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "tauri": "tauri" + }, + "dependencies": { + "@tauri-apps/api": "^2", + "@tauri-apps/plugin-dialog": "^2", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@tauri-apps/cli": "^2", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "typescript": "^5.6.3", + "vite": "^5.4.11" + } +} diff --git a/scripts/devel-setup.sh b/scripts/devel-setup.sh new file mode 100755 index 0000000..33614c1 --- /dev/null +++ b/scripts/devel-setup.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# +# devel-setup.sh — one-time developer environment setup for mtgc. +# +# Installs the Tauri v2 Linux system libraries and the Tauri CLI, and verifies +# the toolchain (Rust, Node, pkg-config libs) needed to build the desktop shell. +# +# Safe to re-run: package installs are idempotent and the CLI install is skipped +# if already present. +# +# Usage: +# ./scripts/devel-setup.sh +# +set -euo pipefail + +info() { printf '\033[1;34m==>\033[0m %s\n' "$*"; } +warn() { printf '\033[1;33m[warn]\033[0m %s\n' "$*"; } +err() { printf '\033[1;31m[err]\033[0m %s\n' "$*" >&2; } + +# --- 1. Prerequisites already expected on the machine ---------------------- + +if ! command -v rustc >/dev/null 2>&1; then + err "Rust not found. Install from https://rustup.rs and re-run." + exit 1 +fi +info "Rust: $(rustc --version)" + +if ! command -v node >/dev/null 2>&1; then + warn "Node.js not found — needed for the Vite/React frontend." + warn "Install Node 18+ (e.g. 'sudo dnf install nodejs') before running the app." +else + info "Node: $(node --version)" +fi + +# --- 2. Tauri v2 system libraries (Fedora / dnf) --------------------------- + +FEDORA_PKGS=( + webkit2gtk4.1-devel + gtk3-devel + libappindicator-gtk3-devel + librsvg2-devel + file + openssl-devel +) + +if command -v dnf >/dev/null 2>&1; then + info "Installing Tauri system libraries via dnf (sudo required)…" + sudo dnf install -y "${FEDORA_PKGS[@]}" +else + warn "dnf not found — this script targets Fedora." + warn "Install the equivalents of: ${FEDORA_PKGS[*]}" + warn "See https://tauri.app/start/prerequisites/ for your distro." +fi + +# --- 3. Verify the libraries pkg-config will look for ---------------------- + +check_pkg() { + if pkg-config --exists "$1" 2>/dev/null; then + info "found $1" + else + warn "missing $1 (Tauri build may fail)" + fi +} +if command -v pkg-config >/dev/null 2>&1; then + check_pkg webkit2gtk-4.1 + check_pkg gtk+-3.0 +else + warn "pkg-config not found; cannot verify libraries." +fi + +# --- 4. Tauri v2 CLI ------------------------------------------------------- + +if cargo tauri --version >/dev/null 2>&1; then + info "Tauri CLI: $(cargo tauri --version 2>&1 | head -1)" +else + info "Installing Tauri v2 CLI (cargo install tauri-cli --version '^2')…" + cargo install tauri-cli --version '^2' +fi + +info "Setup complete. Next: scaffold + run the desktop shell with 'cargo tauri dev'." diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml new file mode 100644 index 0000000..a7c9a9b --- /dev/null +++ b/src-tauri/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "mtgc-app" +version = "0.1.0" +edition = "2021" +description = "MTG Library Catalog desktop app" + +[lib] +# The Tauri app logic lives in a lib so the same entry point can serve desktop +# and (later) mobile targets. +name = "mtgc_app_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +tauri = { version = "2", features = [] } +tauri-plugin-dialog = "2" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +mtgc-core = { path = "../crates/mtgc-core" } diff --git a/src-tauri/build.rs b/src-tauri/build.rs new file mode 100644 index 0000000..d860e1e --- /dev/null +++ b/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json new file mode 100644 index 0000000..8a73be3 --- /dev/null +++ b/src-tauri/capabilities/default.json @@ -0,0 +1,7 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Capabilities for the main window", + "windows": ["main"], + "permissions": ["core:default", "dialog:default"] +} diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png new file mode 100644 index 0000000..1a9cbc2 Binary files /dev/null and b/src-tauri/icons/128x128.png differ diff --git a/src-tauri/icons/128x128@2x.png b/src-tauri/icons/128x128@2x.png new file mode 100644 index 0000000..c91c3f6 Binary files /dev/null and b/src-tauri/icons/128x128@2x.png differ diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png new file mode 100644 index 0000000..534a783 Binary files /dev/null and b/src-tauri/icons/32x32.png differ diff --git a/src-tauri/icons/64x64.png b/src-tauri/icons/64x64.png new file mode 100644 index 0000000..8542f2d Binary files /dev/null and b/src-tauri/icons/64x64.png differ diff --git a/src-tauri/icons/Square107x107Logo.png b/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 0000000..a6e0435 Binary files /dev/null and b/src-tauri/icons/Square107x107Logo.png differ diff --git a/src-tauri/icons/Square142x142Logo.png b/src-tauri/icons/Square142x142Logo.png new file mode 100644 index 0000000..489f783 Binary files /dev/null and b/src-tauri/icons/Square142x142Logo.png differ diff --git a/src-tauri/icons/Square150x150Logo.png b/src-tauri/icons/Square150x150Logo.png new file mode 100644 index 0000000..bad6967 Binary files /dev/null and b/src-tauri/icons/Square150x150Logo.png differ diff --git a/src-tauri/icons/Square284x284Logo.png b/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 0000000..bbf2fcf Binary files /dev/null and b/src-tauri/icons/Square284x284Logo.png differ diff --git a/src-tauri/icons/Square30x30Logo.png b/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 0000000..f232a2f Binary files /dev/null and b/src-tauri/icons/Square30x30Logo.png differ diff --git a/src-tauri/icons/Square310x310Logo.png b/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 0000000..6056e30 Binary files /dev/null and b/src-tauri/icons/Square310x310Logo.png differ diff --git a/src-tauri/icons/Square44x44Logo.png b/src-tauri/icons/Square44x44Logo.png new file mode 100644 index 0000000..da937e4 Binary files /dev/null and b/src-tauri/icons/Square44x44Logo.png differ diff --git a/src-tauri/icons/Square71x71Logo.png b/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 0000000..7e5ffa5 Binary files /dev/null and b/src-tauri/icons/Square71x71Logo.png differ diff --git a/src-tauri/icons/Square89x89Logo.png b/src-tauri/icons/Square89x89Logo.png new file mode 100644 index 0000000..e9c6a1e Binary files /dev/null and b/src-tauri/icons/Square89x89Logo.png differ diff --git a/src-tauri/icons/StoreLogo.png b/src-tauri/icons/StoreLogo.png new file mode 100644 index 0000000..4d0a1c2 Binary files /dev/null and b/src-tauri/icons/StoreLogo.png differ diff --git a/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml b/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..2ffbf24 --- /dev/null +++ b/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..540180b Binary files /dev/null and b/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png differ diff --git a/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..4fd2052 Binary files /dev/null and b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..dc5141f Binary files /dev/null and b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..9bfbf3b Binary files /dev/null and b/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png differ diff --git a/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..e427438 Binary files /dev/null and b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..d9cbfbc Binary files /dev/null and b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..f759552 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png differ diff --git a/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..9e6f9f5 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..adb751d Binary files /dev/null and b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..426d1e7 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..35a7065 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..1e87771 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..f5e3bc1 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..5e05849 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..4afb353 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons/android/values/ic_launcher_background.xml b/src-tauri/icons/android/values/ic_launcher_background.xml new file mode 100644 index 0000000..ea9c223 --- /dev/null +++ b/src-tauri/icons/android/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #fff + \ No newline at end of file diff --git a/src-tauri/icons/icon.icns b/src-tauri/icons/icon.icns new file mode 100644 index 0000000..43d4f2c Binary files /dev/null and b/src-tauri/icons/icon.icns differ diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico new file mode 100644 index 0000000..966300b Binary files /dev/null and b/src-tauri/icons/icon.ico differ diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png new file mode 100644 index 0000000..044918e Binary files /dev/null and b/src-tauri/icons/icon.png differ diff --git a/src-tauri/icons/ios/AppIcon-20x20@1x.png b/src-tauri/icons/ios/AppIcon-20x20@1x.png new file mode 100644 index 0000000..5a435f3 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@1x.png differ diff --git a/src-tauri/icons/ios/AppIcon-20x20@2x-1.png b/src-tauri/icons/ios/AppIcon-20x20@2x-1.png new file mode 100644 index 0000000..97b23de Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@2x-1.png differ diff --git a/src-tauri/icons/ios/AppIcon-20x20@2x.png b/src-tauri/icons/ios/AppIcon-20x20@2x.png new file mode 100644 index 0000000..97b23de Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-20x20@3x.png b/src-tauri/icons/ios/AppIcon-20x20@3x.png new file mode 100644 index 0000000..d7b72b5 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@3x.png differ diff --git a/src-tauri/icons/ios/AppIcon-29x29@1x.png b/src-tauri/icons/ios/AppIcon-29x29@1x.png new file mode 100644 index 0000000..1f109b0 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@1x.png differ diff --git a/src-tauri/icons/ios/AppIcon-29x29@2x-1.png b/src-tauri/icons/ios/AppIcon-29x29@2x-1.png new file mode 100644 index 0000000..c18e4b6 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@2x-1.png differ diff --git a/src-tauri/icons/ios/AppIcon-29x29@2x.png b/src-tauri/icons/ios/AppIcon-29x29@2x.png new file mode 100644 index 0000000..c18e4b6 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-29x29@3x.png b/src-tauri/icons/ios/AppIcon-29x29@3x.png new file mode 100644 index 0000000..e7a2c86 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@3x.png differ diff --git a/src-tauri/icons/ios/AppIcon-40x40@1x.png b/src-tauri/icons/ios/AppIcon-40x40@1x.png new file mode 100644 index 0000000..97b23de Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@1x.png differ diff --git a/src-tauri/icons/ios/AppIcon-40x40@2x-1.png b/src-tauri/icons/ios/AppIcon-40x40@2x-1.png new file mode 100644 index 0000000..5091d4c Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@2x-1.png differ diff --git a/src-tauri/icons/ios/AppIcon-40x40@2x.png b/src-tauri/icons/ios/AppIcon-40x40@2x.png new file mode 100644 index 0000000..5091d4c Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-40x40@3x.png b/src-tauri/icons/ios/AppIcon-40x40@3x.png new file mode 100644 index 0000000..fcb3d85 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@3x.png differ diff --git a/src-tauri/icons/ios/AppIcon-512@2x.png b/src-tauri/icons/ios/AppIcon-512@2x.png new file mode 100644 index 0000000..6676e86 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-512@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-60x60@2x.png b/src-tauri/icons/ios/AppIcon-60x60@2x.png new file mode 100644 index 0000000..fcb3d85 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-60x60@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-60x60@3x.png b/src-tauri/icons/ios/AppIcon-60x60@3x.png new file mode 100644 index 0000000..d549526 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-60x60@3x.png differ diff --git a/src-tauri/icons/ios/AppIcon-76x76@1x.png b/src-tauri/icons/ios/AppIcon-76x76@1x.png new file mode 100644 index 0000000..7c43029 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-76x76@1x.png differ diff --git a/src-tauri/icons/ios/AppIcon-76x76@2x.png b/src-tauri/icons/ios/AppIcon-76x76@2x.png new file mode 100644 index 0000000..14f1ce8 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-76x76@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png b/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png new file mode 100644 index 0000000..82ef37a Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png differ diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs new file mode 100644 index 0000000..db38fba --- /dev/null +++ b/src-tauri/src/commands.rs @@ -0,0 +1,163 @@ +//! Tauri commands: the typed IPC surface bridging the web UI to `mtgc-core`. +//! +//! JS passes camelCase argument keys; Tauri maps them to these snake_case params. + +use std::path::PathBuf; +use std::sync::Mutex; + +use mtgc_core::{CardRef, Deck, DeckEntry, LibraryCard, LibraryFilter}; +use serde::Serialize; +use tauri::{AppHandle, Emitter, State}; + +use crate::state::AppState; + +type Db<'a> = State<'a, Mutex>; +type Res = Result; + +/// High-level status for the top bar / first-run gating. +#[derive(Serialize)] +pub struct Status { + pub library_path: String, + pub reference_path: String, + pub reference_ready: bool, + pub card_count: usize, +} + +fn status_of(s: &AppState) -> Status { + Status { + library_path: s.library_path.display().to_string(), + reference_path: s.reference_path.display().to_string(), + reference_ready: s.catalog.is_some(), + card_count: s.index.as_ref().map(|i| i.len()).unwrap_or(0), + } +} + +#[tauri::command] +pub fn get_status(db: Db) -> Res { + Ok(status_of(&db.lock().unwrap())) +} + +#[tauri::command] +pub fn set_library_path(db: Db, path: String) -> Res { + let mut s = db.lock().unwrap(); + s.library_path = PathBuf::from(path); + s.reopen()?; + Ok(status_of(&s)) +} + +/// Download Scryfall bulk data and (re)build reference.db, then reopen. +/// Runs on a blocking thread so the UI stays responsive and progress events flow. +#[tauri::command] +pub async fn update_card_data(app: AppHandle, db: Db<'_>) -> Res { + let reference_path = db.lock().unwrap().reference_path.clone(); + let app_for_progress = app.clone(); + + tauri::async_runtime::spawn_blocking(move || { + mtgc_core::ingest::build_reference_db(&reference_path, |msg| { + let _ = app_for_progress.emit("ingest-progress", msg.to_string()); + }) + }) + .await + .map_err(|e| e.to_string())? + .map_err(|e| e.to_string())?; + + let mut s = db.lock().unwrap(); + s.reopen()?; + Ok(status_of(&s)) +} + +#[tauri::command] +pub fn search_cards(db: Db, query: String, limit: usize) -> Res> { + let s = db.lock().unwrap(); + Ok(s.index()?.search(&query, limit)) +} + +#[tauri::command] +pub fn add_owned(db: Db, oracle_id: String, delta: i64) -> Res { + let s = db.lock().unwrap(); + s.catalog()?.add_owned(&oracle_id, delta).map_err(|e| e.to_string()) +} + +#[tauri::command] +pub fn set_owned(db: Db, oracle_id: String, quantity: i64) -> Res { + let s = db.lock().unwrap(); + s.catalog()? + .set_owned(&oracle_id, quantity) + .map_err(|e| e.to_string()) +} + +#[tauri::command] +pub fn set_location(db: Db, oracle_id: String, location: String) -> Res<()> { + let s = db.lock().unwrap(); + s.catalog()? + .set_location(&oracle_id, &location) + .map_err(|e| e.to_string()) +} + +#[tauri::command] +pub fn deck_contents(db: Db, deck_id: i64) -> Res> { + let s = db.lock().unwrap(); + s.catalog()?.deck_contents(deck_id).map_err(|e| e.to_string()) +} + +#[tauri::command] +pub fn search_library(db: Db, filter: LibraryFilter) -> Res> { + let s = db.lock().unwrap(); + s.catalog()?.search_library(&filter).map_err(|e| e.to_string()) +} + +#[tauri::command] +pub fn list_decks(db: Db) -> Res> { + let s = db.lock().unwrap(); + s.catalog()?.list_decks().map_err(|e| e.to_string()) +} + +#[tauri::command] +pub fn create_deck(db: Db, name: String, commander_oracle_id: Option) -> Res { + let s = db.lock().unwrap(); + s.catalog()? + .create_deck(&name, commander_oracle_id.as_deref()) + .map_err(|e| e.to_string()) +} + +#[tauri::command] +pub fn delete_deck(db: Db, deck_id: i64) -> Res<()> { + let s = db.lock().unwrap(); + s.catalog()?.delete_deck(deck_id).map_err(|e| e.to_string()) +} + +#[tauri::command] +pub fn set_deck_format(db: Db, deck_id: i64, format: String) -> Res<()> { + let s = db.lock().unwrap(); + s.catalog()? + .set_deck_format(deck_id, &format) + .map_err(|e| e.to_string()) +} + +#[tauri::command] +pub fn set_deck_commander(db: Db, deck_id: i64, oracle_id: Option) -> Res<()> { + let s = db.lock().unwrap(); + s.catalog()? + .set_deck_commander(deck_id, oracle_id.as_deref()) + .map_err(|e| e.to_string()) +} + +#[tauri::command] +pub fn set_deck_card( + db: Db, + deck_id: i64, + oracle_id: String, + quantity: i64, + is_commander: bool, +) -> Res<()> { + let s = db.lock().unwrap(); + s.catalog()? + .set_deck_card(deck_id, &oracle_id, quantity, is_commander) + .map_err(|e| e.to_string()) +} + +#[tauri::command] +pub fn card_decks(db: Db, oracle_id: String) -> Res> { + let s = db.lock().unwrap(); + s.catalog()?.card_decks(&oracle_id).map_err(|e| e.to_string()) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs new file mode 100644 index 0000000..849259b --- /dev/null +++ b/src-tauri/src/lib.rs @@ -0,0 +1,43 @@ +mod commands; +mod state; + +use std::sync::Mutex; + +use tauri::Manager; + +use state::AppState; + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + .plugin(tauri_plugin_dialog::init()) + .setup(|app| { + let data_dir = app + .path() + .app_data_dir() + .expect("resolve app data dir"); + std::fs::create_dir_all(&data_dir).ok(); + app.manage(Mutex::new(AppState::new(data_dir))); + Ok(()) + }) + .invoke_handler(tauri::generate_handler![ + commands::get_status, + commands::set_library_path, + commands::update_card_data, + commands::search_cards, + commands::add_owned, + commands::set_owned, + commands::set_location, + commands::search_library, + commands::list_decks, + commands::create_deck, + commands::delete_deck, + commands::set_deck_format, + commands::set_deck_commander, + commands::set_deck_card, + commands::deck_contents, + commands::card_decks, + ]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs new file mode 100644 index 0000000..43f9bed --- /dev/null +++ b/src-tauri/src/main.rs @@ -0,0 +1,6 @@ +// Prevent an extra console window on Windows in release. +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + mtgc_app_lib::run() +} diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs new file mode 100644 index 0000000..2bb1d81 --- /dev/null +++ b/src-tauri/src/state.rs @@ -0,0 +1,58 @@ +//! App state: the open [`Catalog`] and fuzzy [`NameIndex`], plus the db paths. + +use std::path::PathBuf; + +use mtgc_core::{Catalog, NameIndex}; + +/// Managed application state, guarded by a `Mutex` in Tauri's state store. +pub struct AppState { + pub library_path: PathBuf, + pub reference_path: PathBuf, + pub catalog: Option, + pub index: Option, +} + +impl AppState { + /// Initialize with default paths under the app data dir and open the catalog + /// if a reference db already exists. + pub fn new(data_dir: PathBuf) -> Self { + let mut s = Self { + library_path: data_dir.join("library.db"), + reference_path: data_dir.join("reference.db"), + catalog: None, + index: None, + }; + // Best-effort open; failures just leave the catalog unloaded. + let _ = s.reopen(); + s + } + + /// (Re)open the catalog + rebuild the name index. Only opens when a reference + /// db exists — attaching a non-existent file would create an empty (tableless) + /// db and make every query fail. + pub fn reopen(&mut self) -> Result<(), String> { + if self.reference_path.exists() { + let catalog = Catalog::open(&self.library_path, &self.reference_path) + .map_err(|e| e.to_string())?; + let index = catalog.build_name_index().map_err(|e| e.to_string())?; + self.index = Some(index); + self.catalog = Some(catalog); + } else { + self.catalog = None; + self.index = None; + } + Ok(()) + } + + pub fn catalog(&self) -> Result<&Catalog, String> { + self.catalog + .as_ref() + .ok_or_else(|| "Card data not loaded yet — click 'Download card data'.".to_string()) + } + + pub fn index(&self) -> Result<&NameIndex, String> { + self.index + .as_ref() + .ok_or_else(|| "Card data not loaded yet — click 'Download card data'.".to_string()) + } +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json new file mode 100644 index 0000000..9f4bd0e --- /dev/null +++ b/src-tauri/tauri.conf.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "MTG Library", + "version": "0.1.0", + "identifier": "com.garrettmills.mtgc", + "build": { + "beforeDevCommand": "npm run dev", + "devUrl": "http://localhost:1420", + "beforeBuildCommand": "npm run build", + "frontendDist": "../dist" + }, + "app": { + "windows": [ + { + "label": "main", + "title": "MTG Library", + "width": 1200, + "height": 800, + "resizable": true + } + ], + "security": { + "csp": null + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ] + } +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..453d6c8 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,170 @@ +import { useEffect, useState, useCallback, ReactNode } from "react"; +import { listen } from "@tauri-apps/api/event"; +import { open } from "@tauri-apps/plugin-dialog"; +import { api, Status } from "./api"; +import { HoverPreviewProvider, usePersistedState } from "./components"; +import { CardEntry } from "./views/CardEntry"; +import { LibraryBrowser } from "./views/LibraryBrowser"; +import { DeckView } from "./views/DeckView"; + +type Tab = "add" | "library" | "decks"; + +const svg = (children: ReactNode) => ( + + {children} + +); + +const NAV: { id: Tab; label: string; icon: ReactNode }[] = [ + { id: "add", label: "Add Cards", icon: svg() }, + { + id: "library", + label: "Library", + icon: svg( + <> + + + + + , + ), + }, + { + id: "decks", + label: "Decks", + icon: svg( + <> + + + + , + ), + }, +]; + +export default function App() { + const [status, setStatus] = useState(null); + const [tab, setTab] = usePersistedState("active-tab", "add"); + const [progress, setProgress] = useState(null); + const [busy, setBusy] = useState(false); + + const refreshStatus = useCallback(async () => { + setStatus(await api.getStatus()); + }, []); + + useEffect(() => { + refreshStatus(); + const un = listen("ingest-progress", (e) => setProgress(e.payload)); + return () => { + un.then((f) => f()); + }; + }, [refreshStatus]); + + async function updateCardData() { + setBusy(true); + setProgress("Starting…"); + try { + const s = await api.updateCardData(); + setStatus(s); + setProgress(`Loaded ${s.card_count.toLocaleString()} cards.`); + } catch (err) { + setProgress(`Failed: ${String(err)}`); + } finally { + setBusy(false); + } + } + + async function chooseLibrary() { + const path = await open({ + title: "Choose or create library.db (put it in a synced folder)", + filters: [{ name: "SQLite", extensions: ["db", "sqlite"] }], + }); + if (typeof path === "string") { + setStatus(await api.setLibraryPath(path)); + } + } + + const ready = status?.reference_ready ?? false; + + return ( + +
+ + +
+
+ {status && ( + + {status.library_path} + + )} + + {progress && {progress}} + + +
+ +
+ {!ready ? ( +
+
✦
+

Welcome to your MTG library

+

+ No card database yet. Click Download card data to fetch + the latest cards from Scryfall (a few hundred MB, about a minute). The + reference data stays on this device — only your library file is meant to + be synced. +

+ +
+ ) : tab === "add" ? ( + + ) : tab === "library" ? ( + + ) : ( + + )} +
+
+
+
+ ); +} diff --git a/src/api.ts b/src/api.ts new file mode 100644 index 0000000..99ee5e1 --- /dev/null +++ b/src/api.ts @@ -0,0 +1,109 @@ +// Typed wrappers over the Tauri command bridge into mtgc-core. +import { invoke } from "@tauri-apps/api/core"; + +// --- Types (mirror the serde-serialized core structs) --------------------- + +export interface CardRef { + oracle_id: string; + name: string; + mana_value: number; + colors: number; // WUBRG bitmask + color_identity: number; // WUBRG bitmask + type_line: string; + image_uri: string; // Scryfall "normal" image URL, or "" if unavailable +} + +export interface LibraryCard extends CardRef { + quantity_owned: number; + decks: string[]; + quantity_assigned: number; + location: string; +} + +export interface DeckEntry extends CardRef { + quantity: number; + is_commander: boolean; + quantity_owned: number; +} + +export interface Deck { + id: number; + name: string; + format: string; + commander_oracle_id: string | null; + notes: string; +} + +export interface LibraryFilter { + text?: string | null; + colors_exact?: number | null; + colors_include?: number | null; + commander_identity?: number | null; + cmc_min?: number | null; + cmc_max?: number | null; + types?: string[]; + subtypes?: string[]; + keywords?: string[]; + in_deck?: number | null; +} + +export interface Status { + library_path: string; + reference_path: string; + reference_ready: boolean; + card_count: number; +} + +// --- WUBRG bitmask helpers ------------------------------------------------ + +export const COLORS: { bit: number; code: string; label: string }[] = [ + { bit: 1, code: "W", label: "White" }, + { bit: 2, code: "U", label: "Blue" }, + { bit: 4, code: "B", label: "Black" }, + { bit: 8, code: "R", label: "Red" }, + { bit: 16, code: "G", label: "Green" }, +]; + +export function maskToLetters(mask: number): string { + return COLORS.filter((c) => mask & c.bit) + .map((c) => c.code) + .join(""); +} + +// --- Commands ------------------------------------------------------------- + +export const api = { + getStatus: () => invoke("get_status"), + setLibraryPath: (path: string) => invoke("set_library_path", { path }), + updateCardData: () => invoke("update_card_data"), + + searchCards: (query: string, limit = 20) => + invoke("search_cards", { query, limit }), + + addOwned: (oracleId: string, delta: number) => + invoke("add_owned", { oracleId, delta }), + setOwned: (oracleId: string, quantity: number) => + invoke("set_owned", { oracleId, quantity }), + setLocation: (oracleId: string, location: string) => + invoke("set_location", { oracleId, location }), + + searchLibrary: (filter: LibraryFilter) => + invoke("search_library", { filter }), + + listDecks: () => invoke("list_decks"), + createDeck: (name: string, commanderOracleId: string | null) => + invoke("create_deck", { name, commanderOracleId }), + deleteDeck: (deckId: number) => invoke("delete_deck", { deckId }), + setDeckFormat: (deckId: number, format: string) => + invoke("set_deck_format", { deckId, format }), + setDeckCommander: (deckId: number, oracleId: string | null) => + invoke("set_deck_commander", { deckId, oracleId }), + setDeckCard: ( + deckId: number, + oracleId: string, + quantity: number, + isCommander = false, + ) => invoke("set_deck_card", { deckId, oracleId, quantity, isCommander }), + deckContents: (deckId: number) => invoke("deck_contents", { deckId }), + cardDecks: (oracleId: string) => invoke("card_decks", { oracleId }), +}; diff --git a/src/components.tsx b/src/components.tsx new file mode 100644 index 0000000..4c9cda4 --- /dev/null +++ b/src/components.tsx @@ -0,0 +1,455 @@ +import { + createContext, + ReactNode, + useCallback, + useContext, + useEffect, + useRef, + useState, +} from "react"; +import { api, CardRef, COLORS, maskToLetters } from "./api"; + +/** + * Inline editor for a card's free-text storage location (e.g. "Box 1"). + * Saves on blur; used for owned cards that aren't in any deck. + */ +export function LocationField({ + oracleId, + location, + onSaved, +}: { + oracleId: string; + location: string; + onSaved?: () => void; +}) { + const [val, setVal] = useState(location); + useEffect(() => setVal(location), [location]); + + const save = () => { + if (val !== location) api.setLocation(oracleId, val).then(() => onSaved?.()); + }; + return ( + setVal(e.target.value)} + onBlur={save} + onKeyDown={(e) => e.key === "Enter" && (e.target as HTMLInputElement).blur()} + /> + ); +} + +/** Render a color-identity/colors bitmask as WUBRG pips (colorless = a "C" pip). */ +export function ColorPips({ mask }: { mask: number }) { + const letters = maskToLetters(mask); + if (letters.length === 0) { + return ( + + C + + ); + } + return ( + + {letters.split("").map((c) => ( + + {c} + + ))} + + ); +} + +/** Small card-image thumbnail. Hovering it shows the full-size preview. */ +export function CardThumb({ card }: { card: CardRef }) { + const { handlers } = useHoverPreview(); + if (!card.image_uri) { + return ( + + 🂠 + + ); + } + return ( + {card.name} + ); +} + +/** + * A search input plus a results dropdown with keyboard navigation: + * ↑/↓ move the highlight, Enter picks, mouse hover/click also work. + * `value`/`onChange` are controlled by the caller (which runs the search). + */ +export function CardSearchBox({ + placeholder, + value, + onChange, + results, + onPick, + autoFocus, + big, +}: { + placeholder: string; + value: string; + onChange: (v: string) => void; + results: CardRef[]; + onPick: (card: CardRef) => void; + autoFocus?: boolean; + /** Larger, more prominent input (used for the primary Add Cards field). */ + big?: boolean; +}) { + const [sel, setSel] = useState(0); + const inputRef = useRef(null); + useEffect(() => setSel(0), [results]); + + // Keep focus in the box after a pick so cards can be added in a rapid streak. + function pick(card: CardRef) { + onPick(card); + inputRef.current?.focus(); + } + + function onKeyDown(e: React.KeyboardEvent) { + if (e.key === "ArrowDown") { + e.preventDefault(); + setSel((s) => Math.min(s + 1, results.length - 1)); + } else if (e.key === "ArrowUp") { + e.preventDefault(); + setSel((s) => Math.max(s - 1, 0)); + } else if (e.key === "Enter" && results[sel]) { + e.preventDefault(); + pick(results[sel]); + } + } + + return ( + <> + onChange(e.target.value)} + onKeyDown={onKeyDown} + /> + {results.length > 0 && ( +
    + {results.map((c, i) => ( +
  • setSel(i)} + onClick={() => pick(c)} + > + + {c.name} + {c.type_line} +
  • + ))} +
+ )} + + ); +} + +// --- Hover preview -------------------------------------------------------- + +interface HoverApi { + enter: (card: CardRef, e: React.MouseEvent) => void; + move: (e: React.MouseEvent) => void; + leave: () => void; +} +const HoverCtx = createContext(null); + +/** + * Provides a single floating card-image preview that appears while hovering a + * card and tracks the cursor. Wrap the app once; attach `handlers(card)` from + * [`useHoverPreview`] to any element that should trigger it. + */ +export function HoverPreviewProvider({ children }: { children: ReactNode }) { + const [state, setState] = useState<{ card: CardRef; x: number; y: number } | null>(null); + + const enter = useCallback((card: CardRef, e: React.MouseEvent) => { + if (card.image_uri) setState({ card, x: e.clientX, y: e.clientY }); + }, []); + const move = useCallback((e: React.MouseEvent) => { + setState((s) => (s ? { ...s, x: e.clientX, y: e.clientY } : s)); + }, []); + const leave = useCallback(() => setState(null), []); + + return ( + + {children} + {state && } + + ); +} + +/** Returns a `handlers(card)` factory to spread onto a row/cell for hover preview. */ +export function useHoverPreview() { + const api = useContext(HoverCtx); + return { + handlers: (card: CardRef) => + api + ? { + onMouseEnter: (e: React.MouseEvent) => api.enter(card, e), + onMouseMove: (e: React.MouseEvent) => api.move(e), + onMouseLeave: () => api.leave(), + } + : {}, + }; +} + +const PREVIEW_W = 320; +const PREVIEW_H = 446; // ~ card aspect for the normal image +const GAP = 24; +const PAD = 12; + +function FloatingCard({ card, x, y }: { card: CardRef; x: number; y: number }) { + // Prefer the right of the cursor; flip left if it would overflow. + let left = x + GAP; + if (left + PREVIEW_W + PAD > window.innerWidth) left = x - PREVIEW_W - GAP; + const top = Math.max(PAD, Math.min(y - PREVIEW_H / 2, window.innerHeight - PREVIEW_H - PAD)); + return ( + {card.name} + ); +} + +// --- Grouping ------------------------------------------------------------- + +export type GroupMode = "none" | "type" | "subtype" | "cmc" | "colors"; + +export const GROUP_OPTIONS: { value: GroupMode; label: string }[] = [ + { value: "none", label: "No grouping" }, + { value: "type", label: "Card type" }, + { value: "subtype", label: "Subtype" }, + { value: "cmc", label: "Mana value" }, + { value: "colors", label: "Color identity" }, +]; + +// Priority for choosing a card's primary type from its (multi-type) type line. +const TYPE_PRIORITY = [ + "Creature", + "Planeswalker", + "Battle", + "Land", + "Artifact", + "Enchantment", + "Instant", + "Sorcery", +]; + +function primaryType(typeLine: string): string { + const left = typeLine.split("—")[0]; + for (const t of TYPE_PRIORITY) if (left.includes(t)) return t; + const words = left.trim().split(/\s+/); + return words[words.length - 1] || "Other"; +} + +function firstSubtype(typeLine: string): string { + const parts = typeLine.split("—"); + if (parts.length < 2) return "No subtype"; + return parts[1].trim().split(/\s+/)[0] || "No subtype"; +} + +function colorLabel(mask: number): string { + const letters = maskToLetters(mask); + return letters === "" ? "Colorless" : letters; +} + +/** Group cards by the chosen attribute, returning ordered groups. */ +export function groupCards( + cards: T[], + mode: GroupMode, +): { label: string; cards: T[] }[] { + if (mode === "none") return [{ label: "", cards }]; + + const keyOf = (c: T) => + mode === "type" + ? primaryType(c.type_line) + : mode === "subtype" + ? firstSubtype(c.type_line) + : mode === "cmc" + ? `Mana value ${c.mana_value}` + : colorLabel(c.color_identity); + + const map = new Map(); + for (const c of cards) { + const k = keyOf(c); + const arr = map.get(k); + if (arr) arr.push(c); + else map.set(k, [c]); + } + + const groups = [...map.entries()].map(([label, cs]) => ({ label, cards: cs })); + if (mode === "type") { + const rank = (l: string) => { + const i = TYPE_PRIORITY.indexOf(l); + return i === -1 ? TYPE_PRIORITY.length : i; + }; + groups.sort((a, b) => rank(a.label) - rank(b.label) || a.label.localeCompare(b.label)); + } else if (mode === "cmc") { + groups.sort((a, b) => a.cards[0].mana_value - b.cards[0].mana_value); + } else { + groups.sort((a, b) => a.label.localeCompare(b.label)); + } + return groups; +} + +/** Dropdown for choosing a [`GroupMode`]. */ +export function GroupBySelect({ + value, + onChange, +}: { + value: GroupMode; + onChange: (m: GroupMode) => void; +}) { + return ( + + ); +} + +// --- Persistence ---------------------------------------------------------- + +/** useState that mirrors its value to localStorage (JSON), so it survives + * remounts and app restarts. */ +export function usePersistedState(key: string, initial: T): [T, (v: T) => void] { + const [val, setVal] = useState(() => { + const raw = localStorage.getItem(key); + if (raw === null) return initial; + try { + return JSON.parse(raw) as T; + } catch { + return initial; + } + }); + const set = useCallback( + (v: T) => { + localStorage.setItem(key, JSON.stringify(v)); + setVal(v); + }, + [key], + ); + return [val, set]; +} + +// --- View mode (list vs. image tiles) ------------------------------------ + +export type ViewMode = "list" | "tiles"; + +/** View-mode state persisted to localStorage so it survives tab switches. */ +export function useViewMode(key: string): [ViewMode, (m: ViewMode) => void] { + const [mode, setMode] = useState( + () => (localStorage.getItem(key) as ViewMode) || "list", + ); + const set = useCallback( + (m: ViewMode) => { + localStorage.setItem(key, m); + setMode(m); + }, + [key], + ); + return [mode, set]; +} + +/** Segmented control switching between the list and image-tile layouts. */ +export function ViewToggle({ + mode, + onChange, +}: { + mode: ViewMode; + onChange: (m: ViewMode) => void; +}) { + return ( + + + + + ); +} + +/** A single card rendered as a full image tile, with optional corner + footer. */ +export function CardTile({ + card, + corner, + footer, +}: { + card: CardRef; + corner?: ReactNode; + footer?: ReactNode; +}) { + return ( +
+
+ {card.image_uri ? ( + {card.name} + ) : ( +
{card.name}
+ )} + {corner &&
{corner}
} +
+ {footer &&
{footer}
} +
+ ); +} + +/** Clickable WUBRG toggles that edit a bitmask value. */ +export function ColorToggles({ + value, + onChange, +}: { + value: number; + onChange: (mask: number) => void; +}) { + return ( + + {COLORS.map((c) => ( + onChange(value ^ c.bit)} + > + {c.code} + + ))} + + ); +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..81da346 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App"; +import "./styles.css"; + +ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( + + + , +); diff --git a/src/styles.css b/src/styles.css new file mode 100644 index 0000000..c206999 --- /dev/null +++ b/src/styles.css @@ -0,0 +1,715 @@ +/* ============================================================ + mtgc — design system + Terracotta accent (#C8553D) on a layered, Discord-inspired dark UI. + ============================================================ */ + +:root { + /* Layered surfaces (darkest → lightest) */ + --bg-rail: #16171a; + --bg-app: #1a1b1f; + --bg-topbar: #1e1f24; + --bg-content: #212228; + --bg-panel: #282a31; + --bg-input: #2f313a; + --bg-hover: #363841; + --bg-elev: #3a3c46; + + --border: #34363e; + --border-soft: rgba(255, 255, 255, 0.055); + + --text: #dbdee4; + --text-strong: #f5f6f8; + --muted: #9a9ea8; + --faint: #71757f; + + /* Terracotta accent */ + --accent: #c8553d; + --accent-hover: #d86e56; + --accent-active: #a8412d; + --accent-subtle: rgba(200, 85, 61, 0.15); + --accent-glow: rgba(200, 85, 61, 0.35); + + /* Complementary / status */ + --info: #46a6bd; /* teal complement */ + --gold: #e0b24a; + --success: #4bb073; + --warn: #e0a34a; + --danger: #d8503f; + + --radius: 8px; + --radius-lg: 12px; + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3); + --shadow-md: 0 6px 20px rgba(0, 0, 0, 0.4); + --shadow-lg: 0 14px 44px rgba(0, 0, 0, 0.55); + + color-scheme: dark; +} + +* { + box-sizing: border-box; +} + +html, +body, +#root { + height: 100%; +} + +body { + margin: 0; + font-family: "Inter", "Segoe UI", system-ui, -apple-system, Roboto, sans-serif; + background: var(--bg-app); + color: var(--text); + font-size: 14px; + line-height: 1.5; + -webkit-font-smoothing: antialiased; +} + +::selection { + background: var(--accent-subtle); +} + +/* Thin, unobtrusive scrollbars */ +*::-webkit-scrollbar { + width: 11px; + height: 11px; +} +*::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.35); + border-radius: 8px; + border: 3px solid transparent; + background-clip: padding-box; +} +*::-webkit-scrollbar-thumb:hover { + background: rgba(0, 0, 0, 0.5); + background-clip: padding-box; +} +*::-webkit-scrollbar-track { + background: transparent; +} + +/* -------- Controls -------- */ + +button { + background: var(--bg-input); + color: var(--text); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 7px 13px; + cursor: pointer; + font-size: 13px; + font-weight: 500; + font-family: inherit; + transition: background 0.12s, border-color 0.12s, transform 0.05s, filter 0.12s; +} +button:hover { + background: var(--bg-hover); + border-color: var(--bg-elev); +} +button:active { + transform: translateY(1px); +} +button:disabled { + opacity: 0.5; + cursor: default; + transform: none; +} +button.primary { + background: linear-gradient(180deg, var(--accent-hover), var(--accent)); + border: none; + color: #fff; + box-shadow: var(--shadow-sm); +} +button.primary:hover { + filter: brightness(1.07); + background: linear-gradient(180deg, var(--accent-hover), var(--accent)); +} +button.primary:active { + background: var(--accent-active); +} +button.big { + padding: 11px 22px; + font-size: 15px; + border-radius: 10px; +} + +input, +select { + background: var(--bg-input); + color: var(--text); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 8px 11px; + font-size: 13px; + font-family: inherit; + transition: border-color 0.12s, box-shadow 0.12s; +} +input::placeholder { + color: var(--faint); +} +input:focus, +select:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-subtle); +} + +/* ============================================================ + App shell: nav rail + main + ============================================================ */ + +.app { + display: flex; + height: 100vh; + overflow: hidden; +} + +.nav-rail { + width: 220px; + flex-shrink: 0; + background: var(--bg-rail); + border-right: 1px solid var(--border-soft); + display: flex; + flex-direction: column; + padding: 14px 10px 10px; +} +.brand { + display: flex; + align-items: center; + gap: 11px; + padding: 4px 8px 16px; +} +.brand-mark { + width: 34px; + height: 34px; + border-radius: 10px; + background: linear-gradient(150deg, var(--accent-hover), var(--accent-active)); + color: #fff; + font-weight: 800; + font-size: 18px; + display: flex; + align-items: center; + justify-content: center; + box-shadow: var(--shadow-sm); +} +.brand-text { + font-weight: 700; + font-size: 15px; + color: var(--text-strong); + letter-spacing: 0.2px; +} +.nav-items { + display: flex; + flex-direction: column; + gap: 3px; +} +.nav-item { + position: relative; + display: flex; + align-items: center; + gap: 12px; + width: 100%; + text-align: left; + padding: 9px 12px; + border: none; + border-radius: var(--radius); + background: transparent; + color: var(--muted); + font-size: 14px; + font-weight: 550; +} +.nav-item:hover { + background: rgba(255, 255, 255, 0.04); + color: var(--text); +} +.nav-item.active { + background: var(--accent-subtle); + color: var(--text-strong); +} +.nav-item.active::before { + content: ""; + position: absolute; + left: -10px; + top: 9px; + bottom: 9px; + width: 4px; + border-radius: 0 4px 4px 0; + background: var(--accent); +} +.nav-icon { + display: inline-flex; + color: inherit; +} +.nav-foot { + margin-top: auto; + display: flex; + align-items: center; + gap: 8px; + padding: 12px 10px 6px; + font-size: 12px; + color: var(--muted); + border-top: 1px solid var(--border-soft); +} +.status-dot { + width: 8px; + height: 8px; + border-radius: 50%; +} +.status-dot.on { + background: var(--success); + box-shadow: 0 0 6px var(--success); +} +.status-dot.off { + background: var(--faint); +} + +.main { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + background: var(--bg-content); +} +.topbar { + height: 52px; + flex-shrink: 0; + display: flex; + align-items: center; + gap: 10px; + padding: 0 18px; + background: var(--bg-topbar); + border-bottom: 1px solid var(--border-soft); +} +.path { + font-size: 12px; + font-family: ui-monospace, "SF Mono", Menlo, monospace; + color: var(--faint); + background: rgba(0, 0, 0, 0.25); + padding: 4px 10px; + border-radius: 6px; + max-width: 460px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.progress { + color: var(--accent-hover); + font-size: 12px; + font-weight: 500; +} +.spacer { + flex: 1; +} +.content { + flex: 1; + overflow: auto; + padding: 22px 26px; +} + +/* Empty / welcome state */ +.empty-state { + max-width: 480px; + margin: 9vh auto 0; + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + gap: 14px; +} +.empty-mark { + width: 72px; + height: 72px; + border-radius: 20px; + display: flex; + align-items: center; + justify-content: center; + font-size: 34px; + color: #fff; + background: linear-gradient(150deg, var(--accent-hover), var(--accent-active)); + box-shadow: 0 10px 30px var(--accent-glow); +} +.empty-state h2 { + margin: 4px 0 0; +} + +/* ============================================================ + Typography helpers + ============================================================ */ + +h2 { + margin: 2px 0 14px; + font-size: 19px; + font-weight: 700; + color: var(--text-strong); + letter-spacing: -0.2px; +} +h3 { + margin: 18px 0 10px; + font-size: 12px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--faint); + border-bottom: 1px solid var(--border-soft); + padding-bottom: 8px; +} +h4 { + margin: 14px 0 7px; + font-size: 13px; + font-weight: 600; + color: var(--accent-hover); +} +.muted { + color: var(--muted); +} +.hint { + color: var(--muted); + font-size: 13px; + line-height: 1.65; +} +.row { + display: flex; + gap: 8px; + align-items: center; +} +.badge { + display: inline-flex; + align-items: center; + background: var(--accent-subtle); + color: var(--accent-hover); + border-radius: 999px; + padding: 1px 9px; + font-size: 11px; + font-weight: 700; +} + +/* ============================================================ + Lists (search results, deck list) + ============================================================ */ + +.result-list { + list-style: none; + margin: 8px 0 0; + padding: 0; + border: 1px solid var(--border-soft); + border-radius: var(--radius-lg); + overflow: hidden; + background: var(--bg-panel); +} +.result-list li { + display: flex; + align-items: center; + gap: 10px; + padding: 9px 12px; + border-bottom: 1px solid var(--border-soft); + cursor: pointer; + transition: background 0.1s; +} +.result-list li:last-child { + border-bottom: none; +} +.result-list li:hover { + background: var(--bg-hover); +} +.card-name { + font-weight: 600; + color: var(--text-strong); +} +.card-type { + color: var(--muted); + font-size: 12px; +} + +/* ============================================================ + Color pips (WUBRG) + ============================================================ */ + +.pips { + display: inline-flex; + gap: 3px; +} +.pip { + width: 18px; + height: 18px; + border-radius: 50%; + font-size: 10px; + font-weight: 800; + display: inline-flex; + align-items: center; + justify-content: center; + box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.25); +} +.pip.W { background: #f4efdd; color: #4a3f2a; } +.pip.U { background: #3a8fd0; color: #fff; } +.pip.B { background: #38343d; color: #cbc3d4; } +.pip.R { background: var(--accent); color: #fff; } +.pip.G { background: #479b63; color: #fff; } +.pip.C { background: #9aa0ab; color: #23242a; } + +.color-toggles { + display: flex; + gap: 5px; +} +.color-toggles .pip { + cursor: pointer; + opacity: 0.32; + transition: opacity 0.1s, box-shadow 0.1s; +} +.color-toggles .pip.on { + opacity: 1; + box-shadow: 0 0 0 2px var(--accent-hover); +} + +/* ============================================================ + Tables + ============================================================ */ + +table { + width: 100%; + border-collapse: collapse; +} +th { + text-align: left; + padding: 8px 12px; + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--faint); + position: sticky; + top: 0; + background: var(--bg-content); + border-bottom: 1px solid var(--border); + z-index: 1; +} +td { + padding: 8px 12px; + border-bottom: 1px solid var(--border-soft); + vertical-align: middle; +} +tbody tr { + transition: background 0.08s; +} +tbody tr:hover { + background: rgba(255, 255, 255, 0.022); +} +.over-assigned { + color: var(--warn); + font-weight: 600; +} +.deck-chip { + display: inline-flex; + align-items: center; + background: var(--bg-input); + border: 1px solid var(--border); + border-radius: 6px; + padding: 2px 8px; + font-size: 11px; + color: var(--text); + margin-right: 4px; +} + +/* ============================================================ + Filters panel + ============================================================ */ + +.filters { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); + gap: 14px; + background: var(--bg-panel); + border: 1px solid var(--border-soft); + border-radius: var(--radius-lg); + padding: 16px; + margin-bottom: 16px; +} +.field { + display: flex; + flex-direction: column; + gap: 6px; +} +.field > label { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--faint); +} +.location-input { + padding: 4px 8px; + font-size: 12px; + max-width: 160px; +} + +/* ============================================================ + Card images: thumbnails, hover preview, tiles + ============================================================ */ + +.card-thumb { + width: 40px; + height: 56px; + object-fit: cover; + border-radius: 5px; + display: block; + background: var(--bg-elev); + cursor: zoom-in; + box-shadow: var(--shadow-sm); + transition: transform 0.1s, box-shadow 0.1s; +} +.card-thumb:hover { + transform: scale(1.05); + box-shadow: 0 0 0 2px var(--accent-hover), var(--shadow-md); +} +.thumb-empty { + display: inline-flex; + width: 40px; + height: 56px; + align-items: center; + justify-content: center; + color: var(--faint); + border: 1px dashed var(--border); + border-radius: 5px; + font-size: 18px; +} +.hover-card { + position: fixed; + z-index: 200; + border-radius: 14px; + box-shadow: var(--shadow-lg); + border: 1px solid rgba(255, 255, 255, 0.08); + pointer-events: none; +} + +.tile-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); + gap: 16px; +} +.tile { + display: flex; + flex-direction: column; + gap: 7px; +} +.tile-img-wrap { + position: relative; +} +.tile-img { + width: 100%; + display: block; + border-radius: 12px; + box-shadow: var(--shadow-sm); + transition: transform 0.12s, box-shadow 0.12s; +} +.tile:hover .tile-img { + transform: translateY(-3px); + box-shadow: var(--shadow-md); +} +.tile-noimg { + aspect-ratio: 488 / 680; + border: 1px solid var(--border); + border-radius: 12px; + background: var(--bg-panel); + display: flex; + align-items: center; + justify-content: center; + text-align: center; + padding: 14px; + font-weight: 600; + color: var(--muted); +} +.tile-corner { + position: absolute; + top: 8px; + right: 8px; + display: flex; + gap: 6px; +} +.tile-badge { + background: rgba(0, 0, 0, 0.78); + color: #fff; + border-radius: 8px; + padding: 2px 9px; + font-size: 12px; + font-weight: 700; + backdrop-filter: blur(2px); +} +.tile-badge.warn { + background: var(--warn); + color: #241700; +} +.tile-remove { + background: rgba(0, 0, 0, 0.78); + border: none; + color: #fff; + border-radius: 50%; + width: 24px; + height: 24px; + padding: 0; + line-height: 1; + font-size: 13px; +} +.tile-remove:hover { + background: var(--danger); +} +.tile-footer { + display: flex; + flex-wrap: wrap; + gap: 4px; + align-items: center; +} + +/* ============================================================ + Toolbar controls: group-by + view toggle + ============================================================ */ + +.group-by { + display: inline-flex; + align-items: center; + gap: 7px; + font-size: 12px; + color: var(--muted); +} +.view-toggle { + display: inline-flex; + background: var(--bg-input); + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: hidden; +} +.view-toggle button { + border: none; + border-radius: 0; + background: transparent; + font-size: 12px; + padding: 5px 12px; + color: var(--muted); +} +.view-toggle button:hover { + background: rgba(255, 255, 255, 0.04); +} +.view-toggle button.active { + background: var(--accent); + color: #fff; +} + +/* ============================================================ + Deck metadata (format + commander) + ============================================================ */ + +.deck-meta { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + gap: 20px; + background: var(--bg-panel); + border: 1px solid var(--border-soft); + border-radius: var(--radius-lg); + padding: 14px 16px; +} +.commander-box { + display: flex; + flex-direction: column; + gap: 8px; + min-width: 280px; + flex: 1; +} diff --git a/src/views/CardEntry.tsx b/src/views/CardEntry.tsx new file mode 100644 index 0000000..026ba9e --- /dev/null +++ b/src/views/CardEntry.tsx @@ -0,0 +1,83 @@ +import { useEffect, useState } from "react"; +import { api, CardRef } from "../api"; +import { CardSearchBox } from "../components"; + +/** Rapid card entry: fuzzy search-as-you-type, ↑/↓ + Enter (or click) adds one copy. */ +export function CardEntry() { + const [query, setQuery] = useState(""); + const [results, setResults] = useState([]); + const [recent, setRecent] = useState<{ name: string; qty: number; location: string }[]>([]); + // Persisted so a "box" location sticks while entering a stack of cards. + const [location, setLocation] = useState(() => localStorage.getItem("add-location") || ""); + + useEffect(() => { + localStorage.setItem("add-location", location); + }, [location]); + + // Debounced fuzzy search. + useEffect(() => { + const q = query.trim(); + if (!q) { + setResults([]); + return; + } + const t = setTimeout(async () => { + try { + setResults(await api.searchCards(q, 20)); + } catch { + setResults([]); + } + }, 80); + return () => clearTimeout(t); + }, [query]); + + async function add(card: CardRef) { + const qty = await api.addOwned(card.oracle_id, 1); + const loc = location.trim(); + if (loc) await api.setLocation(card.oracle_id, loc); + setRecent((r) => [{ name: card.name, qty, location: loc }, ...r].slice(0, 12)); + setQuery(""); + setResults([]); + } + + return ( +
+

Add Cards

+ +
+ + setLocation(e.target.value)} + /> +
+ + + + {recent.length > 0 && ( + <> +

Just added

+
    + {recent.map((r, i) => ( +
  • + {r.name} + {r.location && {r.location}} + + owned ×{r.qty} +
  • + ))} +
+ + )} +
+ ); +} diff --git a/src/views/DeckView.tsx b/src/views/DeckView.tsx new file mode 100644 index 0000000..5d98cfd --- /dev/null +++ b/src/views/DeckView.tsx @@ -0,0 +1,377 @@ +import { useEffect, useState, useCallback } from "react"; +import { api, CardRef, Deck, DeckEntry } from "../api"; +import { + CardSearchBox, + CardThumb, + CardTile, + ColorPips, + GroupBySelect, + GroupMode, + ViewToggle, + groupCards, + usePersistedState, + useViewMode, +} from "../components"; + +const FORMATS = [ + "", + "Commander", + "Brawl", + "Standard", + "Pioneer", + "Modern", + "Legacy", + "Vintage", + "Pauper", + "Other", +]; +const COMMANDER_FORMATS = ["Commander", "Brawl"]; + +/** + * Deck editor. A deck's cards are split into two groups — "In your library" + * (owned) and "Not owned" — each with its own search box, and each groupable by + * card attribute. Deck-level format + commander metadata sit at the top. + */ +export function DeckView() { + const [decks, setDecks] = useState([]); + const [selected, setSelected] = usePersistedState("deck-selected", null); + const [newName, setNewName] = useState(""); + const [contents, setContents] = useState([]); + const [view, setView] = useViewMode("deck-view"); + const [groupBy, setGroupBy] = usePersistedState("deck-group", "none"); + + const [libQuery, setLibQuery] = useState(""); + const [libResults, setLibResults] = useState([]); + const [anyQuery, setAnyQuery] = useState(""); + const [anyResults, setAnyResults] = useState([]); + const [cmdQuery, setCmdQuery] = useState(""); + const [cmdResults, setCmdResults] = useState([]); + + const deck = decks.find((d) => d.id === selected) ?? null; + + const loadDecks = useCallback(async () => { + const d = await api.listDecks(); + setDecks(d); + // Keep the persisted selection if it still exists, else fall back to the first. + if (d.length && (selected === null || !d.some((x) => x.id === selected))) { + setSelected(d[0].id); + } + }, [selected, setSelected]); + + useEffect(() => { + loadDecks(); + }, [loadDecks]); + + const loadContents = useCallback(async () => { + if (selected === null) { + setContents([]); + return; + } + setContents(await api.deckContents(selected)); + }, [selected]); + + useEffect(() => { + loadContents(); + }, [loadContents]); + + useEffect(() => { + const q = libQuery.trim(); + if (!q) return setLibResults([]); + const t = setTimeout(() => api.searchLibrary({ text: q }).then(setLibResults), 100); + return () => clearTimeout(t); + }, [libQuery]); + + useEffect(() => { + const q = anyQuery.trim(); + if (!q) return setAnyResults([]); + const t = setTimeout(() => api.searchCards(q, 12).then(setAnyResults), 80); + return () => clearTimeout(t); + }, [anyQuery]); + + useEffect(() => { + const q = cmdQuery.trim(); + if (!q) return setCmdResults([]); + const t = setTimeout(() => api.searchCards(q, 12).then(setCmdResults), 80); + return () => clearTimeout(t); + }, [cmdQuery]); + + async function createDeck() { + if (!newName.trim()) return; + const d = await api.createDeck(newName.trim(), null); + setNewName(""); + await loadDecks(); + setSelected(d.id); + } + + async function addToDeck(card: CardRef) { + if (selected === null) return; + const existing = contents.find((e) => e.oracle_id === card.oracle_id); + const qty = existing ? existing.quantity + 1 : 1; + await api.setDeckCard(selected, card.oracle_id, qty, false); + setLibQuery(""); + setLibResults([]); + setAnyQuery(""); + setAnyResults([]); + await loadContents(); + } + + async function setQty(oracleId: string, qty: number) { + if (selected === null) return; + await api.setDeckCard(selected, oracleId, qty, false); + await loadContents(); + } + + async function removeDeck(id: number) { + await api.deleteDeck(id); + if (selected === id) setSelected(null); + await loadDecks(); + } + + async function changeFormat(format: string) { + if (selected === null) return; + await api.setDeckFormat(selected, format); + await loadDecks(); + } + + async function setCommander(card: CardRef) { + if (selected === null) return; + // Ensure the commander is in the deck, then flag it. + if (!contents.find((e) => e.oracle_id === card.oracle_id)) { + await api.setDeckCard(selected, card.oracle_id, 1, true); + } + await api.setDeckCommander(selected, card.oracle_id); + setCmdQuery(""); + setCmdResults([]); + await loadContents(); + await loadDecks(); + } + + async function clearCommander() { + if (selected === null) return; + await api.setDeckCommander(selected, null); + await loadContents(); + await loadDecks(); + } + + const owned = contents.filter((e) => e.quantity_owned > 0); + const unowned = contents.filter((e) => e.quantity_owned === 0); + const commanderName = + deck?.commander_oracle_id && + (contents.find((e) => e.oracle_id === deck.commander_oracle_id)?.name ?? + deck.commander_oracle_id); + + function deckTiles(entries: DeckEntry[]) { + return ( +
+ {entries.map((e) => ( + setQty(e.oracle_id, 0)} + > + ✕ + + } + footer={ + + {e.is_commander ? "★ " : ""}in deck ×{e.quantity} + + } + /> + ))} +
+ ); + } + + function deckTable(entries: DeckEntry[]) { + return ( + + + + + + + + + + + + + + {entries.map((e) => ( + + + + + + + + + + ))} + +
ColorsNameTypeIn deckOwned
+ + + + + {e.is_commander && ★ } + {e.name} + {e.type_line} + setQty(e.oracle_id, Number(ev.target.value))} + /> + {e.quantity_owned} + +
+ ); + } + + function cardsGroup(entries: DeckEntry[]) { + if (entries.length === 0) return

None yet.

; + return groupCards(entries, groupBy).map((g) => ( +
+ {g.label && ( +

+ {g.label} {g.cards.length} +

+ )} + {view === "tiles" ? deckTiles(g.cards) : deckTable(g.cards)} +
+ )); + } + + return ( +
+
+

Decks

+
+ setNewName(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && createDeck()} + /> + +
+
    + {decks.map((d) => ( +
  • setSelected(d.id)} + > + {d.name} + {d.format && {d.format}} + + +
  • + ))} + {decks.length === 0 && ( +
  • + No decks yet. +
  • + )} +
+
+ +
+ {selected === null || !deck ? ( +

Select or create a deck.

+ ) : ( + <> +
+

{deck.name}

+
+ + +
+
+ + {/* Deck metadata: format + (for Commander/Brawl) the commander card. */} +
+ + + {COMMANDER_FORMATS.includes(deck.format) && ( +
+
+ Commander: + {commanderName || "none"} + {commanderName && ( + + )} +
+ +
+ )} +
+ +
+

+ In your library {owned.length} +

+ + {cardsGroup(owned)} +
+ +
+

+ Not owned {unowned.length} +

+ + {cardsGroup(unowned)} +
+ + )} +
+
+ ); +} diff --git a/src/views/LibraryBrowser.tsx b/src/views/LibraryBrowser.tsx new file mode 100644 index 0000000..a3df1eb --- /dev/null +++ b/src/views/LibraryBrowser.tsx @@ -0,0 +1,235 @@ +import { useEffect, useState } from "react"; +import { api, Deck, LibraryCard, LibraryFilter } from "../api"; +import { + CardThumb, + CardTile, + ColorPips, + ColorToggles, + GroupBySelect, + GroupMode, + LocationField, + ViewToggle, + groupCards, + usePersistedState, + useViewMode, +} from "../components"; + +/** Browse & filter the owned library by name and card criteria. */ +export function LibraryBrowser() { + const [text, setText] = useState(""); + const [commander, setCommander] = useState(0); + const [colorsInclude, setColorsInclude] = useState(0); + const [cmcMin, setCmcMin] = useState(""); + const [cmcMax, setCmcMax] = useState(""); + const [type, setType] = useState(""); + const [subtype, setSubtype] = useState(""); + const [keyword, setKeyword] = useState(""); + const [inDeck, setInDeck] = useState(""); + + const [decks, setDecks] = useState([]); + const [rows, setRows] = useState([]); + const [view, setView] = useViewMode("library-view"); + const [groupBy, setGroupBy] = usePersistedState("library-group", "none"); + const [refreshKey, setRefreshKey] = useState(0); + + useEffect(() => { + api.listDecks().then(setDecks); + }, []); + + useEffect(() => { + const filter: LibraryFilter = { + text: text.trim() || null, + commander_identity: commander || null, + colors_include: colorsInclude || null, + cmc_min: cmcMin === "" ? null : Number(cmcMin), + cmc_max: cmcMax === "" ? null : Number(cmcMax), + types: splitCsv(type), + subtypes: splitCsv(subtype), + keywords: splitCsv(keyword), + in_deck: inDeck === "" ? null : Number(inDeck), + }; + const t = setTimeout(() => { + api.searchLibrary(filter).then(setRows).catch(() => setRows([])); + }, 120); + return () => clearTimeout(t); + }, [text, commander, colorsInclude, cmcMin, cmcMax, type, subtype, keyword, inDeck, refreshKey]); + + const deckOrLocation = (c: LibraryCard) => + c.decks.length === 0 ? ( + setRefreshKey((k) => k + 1)} + /> + ) : ( + c.decks.map((d) => ( + + {d} + + )) + ); + + function libTable(cards: LibraryCard[]) { + return ( + + + + + + + + + + + + + + {cards.map((c) => ( + + + + + + + + + + ))} + +
ColorsNameTypeMVOwnedDecks / Location
+ + + + {c.name}{c.type_line}{c.mana_value} c.quantity_owned ? "over-assigned" : ""}> + {c.quantity_owned} + {c.quantity_assigned > c.quantity_owned && ( + + {" "} + ⚠ {c.quantity_assigned} assigned + + )} + {deckOrLocation(c)}
+ ); + } + + function libTiles(cards: LibraryCard[]) { + return ( +
+ {cards.map((c) => ( + c.quantity_owned ? "warn" : ""}`} + title="Copies owned" + > + ×{c.quantity_owned} + {c.quantity_assigned > c.quantity_owned ? " ⚠" : ""} + + } + footer={deckOrLocation(c)} + /> + ))} +
+ ); + } + + const groups = groupCards(rows, groupBy); + + return ( +
+
+

+ Library {rows.length} +

+
+ + +
+
+ +
+
+ + setText(e.target.value)} placeholder="e.g. bolt" /> +
+
+ + +
+
+ + +
+
+ +
+ setCmcMin(e.target.value)} + /> + – + setCmcMax(e.target.value)} + /> +
+
+
+ + setType(e.target.value)} placeholder="Creature, Legendary" /> +
+
+ + setSubtype(e.target.value)} placeholder="Elf" /> +
+
+ + setKeyword(e.target.value)} placeholder="Flying" /> +
+
+ + +
+
+ + {rows.length === 0 ? ( +

+ No matching cards owned. Add some in the Add Cards tab, or loosen the filters. +

+ ) : ( + groups.map((g) => ( +
+ {g.label && ( +

+ {g.label} {g.cards.length} +

+ )} + {view === "list" ? libTable(g.cards) : libTiles(g.cards)} +
+ )) + )} +
+ ); +} + +function splitCsv(s: string): string[] { + return s + .split(",") + .map((x) => x.trim()) + .filter(Boolean); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..3934b8f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..97ede7e --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..5dc2a3e --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +// Vite config tuned for Tauri: fixed port, no clearing the terminal Tauri owns. +export default defineConfig({ + plugins: [react()], + clearScreen: false, + server: { + port: 1420, + strictPort: true, + watch: { + // Don't watch the Rust side; Tauri handles that. + ignored: ["**/src-tauri/**"], + }, + }, +});