Implement Nexus integration #54
18 changed files with 1225 additions and 23 deletions
|
@ -12,6 +12,7 @@
|
|||
- dtmm: show dialog for critical errors
|
||||
- dtmm: check mod order before deployment
|
||||
- dtmt: add mod dependencies to config
|
||||
- dtmm: match mods to Nexus and check for updates
|
||||
|
||||
=== Fixed
|
||||
|
||||
|
|
429
Cargo.lock
generated
429
Cargo.lock
generated
|
@ -130,6 +130,12 @@ version = "0.13.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a"
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.6.0"
|
||||
|
@ -756,6 +762,7 @@ dependencies = [
|
|||
"dtmt-shared",
|
||||
"futures",
|
||||
"lazy_static",
|
||||
"nexusmods",
|
||||
"oodle-sys",
|
||||
"path-slash",
|
||||
"sdk",
|
||||
|
@ -768,6 +775,7 @@ dependencies = [
|
|||
"tracing",
|
||||
"tracing-error",
|
||||
"tracing-subscriber",
|
||||
"usvg",
|
||||
"zip",
|
||||
]
|
||||
|
||||
|
@ -831,6 +839,15 @@ dependencies = [
|
|||
"wio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "endian-type"
|
||||
version = "0.1.2"
|
||||
|
@ -1021,6 +1038,15 @@ version = "0.1.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fs_extra"
|
||||
version = "1.3.0"
|
||||
|
@ -1373,6 +1399,25 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.3.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5be7b54589b581f624f566bf5d8eb2bab1db736c51528720b6bd36b96b55924d"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"http",
|
||||
"indexmap",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
|
@ -1409,6 +1454,87 @@ dependencies = [
|
|||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
"itoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
|
||||
|
||||
[[package]]
|
||||
name = "httpdate"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "0.14.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc5e554ff619822309ffd57d8734d77cd5ce6238bc956f037ea06c58238c9899"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-tls"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"hyper",
|
||||
"native-tls",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
|
||||
dependencies = [
|
||||
"unicode-bidi",
|
||||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "im"
|
||||
version = "15.1.0"
|
||||
|
@ -1516,6 +1642,12 @@ dependencies = [
|
|||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30e22bd8629359895450b59ea7a776c850561b96a3b1d31321c1949d9e6c9146"
|
||||
|
||||
[[package]]
|
||||
name = "is-terminal"
|
||||
version = "0.4.4"
|
||||
|
@ -1722,6 +1854,12 @@ dependencies = [
|
|||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
|
@ -1755,6 +1893,41 @@ version = "0.7.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3"
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"log",
|
||||
"openssl",
|
||||
"openssl-probe",
|
||||
"openssl-sys",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nexusmods"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"lazy_static",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"time",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nibble_vec"
|
||||
version = "0.1.0"
|
||||
|
@ -1986,6 +2159,51 @@ version = "0.3.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd2523381e46256e40930512c7fd25562b9eae4812cb52078f155e87217c9d1e"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"openssl-macros",
|
||||
"openssl-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-macros"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.81"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "176be2629957c157240f68f61f2d0053ad3a4ecfdd9ebf1e6521d18d9635cf67"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "os_str_bytes"
|
||||
version = "6.4.1"
|
||||
|
@ -2092,6 +2310,12 @@ dependencies = [
|
|||
"sha2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
|
||||
|
||||
[[package]]
|
||||
name = "pest"
|
||||
version = "2.5.6"
|
||||
|
@ -2397,6 +2621,43 @@ version = "0.6.28"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.11.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21eed90ec8570952d53b772ecf8f206aa1ec9a3d76b2521c56c42973f2d91ee9"
|
||||
dependencies = [
|
||||
"base64 0.21.0",
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"hyper",
|
||||
"hyper-tls",
|
||||
"ipnet",
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime",
|
||||
"native-tls",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"winreg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "resvg"
|
||||
version = "0.25.0"
|
||||
|
@ -2532,6 +2793,15 @@ dependencies = [
|
|||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
version = "0.1.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3"
|
||||
dependencies = [
|
||||
"windows-sys 0.42.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.1.0"
|
||||
|
@ -2565,6 +2835,29 @@ dependencies = [
|
|||
"tracing-error",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "2.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"core-foundation",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework-sys"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "self_cell"
|
||||
version = "0.10.2"
|
||||
|
@ -2597,6 +2890,17 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.94"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c533a59c9d8a93a09c6ab31f0fd5e5f4dd1b8fc9434804029839884765d04ea"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_sjson"
|
||||
version = "1.0.0"
|
||||
|
@ -2606,6 +2910,18 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.10.5"
|
||||
|
@ -2686,6 +3002,16 @@ version = "1.10.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.4.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "steamid-ng"
|
||||
version = "1.0.0"
|
||||
|
@ -2925,6 +3251,21 @@ dependencies = [
|
|||
"displaydoc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
|
||||
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.26.0"
|
||||
|
@ -2939,6 +3280,7 @@ dependencies = [
|
|||
"num_cpus",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"tracing",
|
||||
"windows-sys 0.45.0",
|
||||
|
@ -2955,6 +3297,16 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-native-tls"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
|
||||
dependencies = [
|
||||
"native-tls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-stream"
|
||||
version = "0.1.12"
|
||||
|
@ -2966,6 +3318,20 @@ dependencies = [
|
|||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.5.11"
|
||||
|
@ -2992,6 +3358,12 @@ dependencies = [
|
|||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-service"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.37"
|
||||
|
@ -3075,6 +3447,12 @@ dependencies = [
|
|||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "try-lock"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
|
||||
|
||||
[[package]]
|
||||
name = "ttf-parser"
|
||||
version = "0.17.1"
|
||||
|
@ -3210,6 +3588,15 @@ version = "1.0.8"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
version = "0.1.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
|
||||
dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-script"
|
||||
version = "0.5.5"
|
||||
|
@ -3234,13 +3621,25 @@ version = "0.1.10"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna",
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "usvg"
|
||||
version = "0.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "585bb2d87c8fd6041a479dea01479dcf9094e61b5f9af221606927e61a2bd939"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.13.1",
|
||||
"data-url",
|
||||
"flate2",
|
||||
"fontdb",
|
||||
|
@ -3278,6 +3677,12 @@ version = "0.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
|
||||
|
||||
[[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.1.1"
|
||||
|
@ -3322,6 +3727,16 @@ dependencies = [
|
|||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "want"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"try-lock",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
|
@ -3353,6 +3768,18 @@ dependencies = [
|
|||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.84"
|
||||
|
|
|
@ -15,6 +15,7 @@ dtmt-shared = { path = "../../lib/dtmt-shared", version = "*" }
|
|||
futures = "0.3.25"
|
||||
oodle-sys = { path = "../../lib/oodle-sys", version = "*" }
|
||||
sdk = { path = "../../lib/sdk", version = "*" }
|
||||
nexusmods = { path = "../../lib/nexusmods", version = "*" }
|
||||
serde_sjson = { path = "../../lib/serde_sjson", version = "*" }
|
||||
serde = { version = "1.0.152", features = ["derive", "rc"] }
|
||||
tokio = { version = "1.23.0", features = ["rt", "fs", "tracing", "sync"] }
|
||||
|
@ -28,3 +29,4 @@ time = { version = "0.3.20", features = ["serde", "serde-well-known", "local-off
|
|||
strip-ansi-escapes = "0.1.1"
|
||||
lazy_static = "1.4.0"
|
||||
colors-transform = "0.2.11"
|
||||
usvg = "0.25.0"
|
||||
|
|
|
@ -8,13 +8,14 @@ use color_eyre::{Help, Report, Result};
|
|||
use druid::im::Vector;
|
||||
use druid::{FileInfo, ImageBuf};
|
||||
use dtmt_shared::ModConfig;
|
||||
use nexusmods::Api as NexusApi;
|
||||
use tokio::fs::{self, DirEntry};
|
||||
use tokio::runtime::Runtime;
|
||||
use tokio_stream::wrappers::ReadDirStream;
|
||||
use tokio_stream::StreamExt;
|
||||
use zip::ZipArchive;
|
||||
|
||||
use crate::state::{ActionState, ModInfo, ModOrder, PackageInfo};
|
||||
use crate::state::{ActionState, ModInfo, ModOrder, NexusInfo, PackageInfo};
|
||||
use crate::util::config::{ConfigSerialize, LoadOrderEntry};
|
||||
|
||||
use super::read_sjson_file;
|
||||
|
@ -26,6 +27,26 @@ pub(crate) async fn import_mod(state: ActionState, info: FileInfo) -> Result<Mod
|
|||
.wrap_err_with(|| format!("Failed to read file {}", info.path.display()))?;
|
||||
let data = Cursor::new(data);
|
||||
|
||||
let nexus = if let Some((_, id, _, _)) = info
|
||||
.path
|
||||
.file_name()
|
||||
.and_then(|s| s.to_str())
|
||||
.and_then(NexusApi::parse_file_name)
|
||||
{
|
||||
if !state.nexus_api_key.is_empty() {
|
||||
let api = NexusApi::new(state.nexus_api_key.to_string())?;
|
||||
let mod_info = api
|
||||
.mods_id(id)
|
||||
.await
|
||||
.wrap_err_with(|| format!("Failed to query mod {} from Nexus", id))?;
|
||||
Some(NexusInfo::from(mod_info))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut archive = ZipArchive::new(data).wrap_err("Failed to open ZIP archive")?;
|
||||
|
||||
if tracing::enabled!(tracing::Level::DEBUG) {
|
||||
|
@ -137,11 +158,19 @@ pub(crate) async fn import_mod(state: ActionState, info: FileInfo) -> Result<Mod
|
|||
.extract(Arc::as_ref(&mod_dir))
|
||||
.wrap_err_with(|| format!("Failed to extract archive to {}", mod_dir.display()))?;
|
||||
|
||||
if let Some(nexus) = &nexus {
|
||||
let data = serde_sjson::to_string(nexus).wrap_err("Failed to serialize Nexus info")?;
|
||||
let path = mod_dir.join(&mod_cfg.id).join("nexus.sjson");
|
||||
fs::write(&path, data.as_bytes())
|
||||
.await
|
||||
.wrap_err_with(|| format!("Failed to write Nexus info to '{}'", path.display()))?;
|
||||
}
|
||||
|
||||
let packages = files
|
||||
.into_iter()
|
||||
.map(|(name, files)| Arc::new(PackageInfo::new(name, files.into_iter().collect())))
|
||||
.collect();
|
||||
let info = ModInfo::new(mod_cfg, packages, image);
|
||||
let info = ModInfo::new(mod_cfg, packages, image, nexus);
|
||||
|
||||
Ok(info)
|
||||
}
|
||||
|
@ -181,12 +210,25 @@ pub(crate) async fn save_settings(state: ActionState) -> Result<()> {
|
|||
async fn read_mod_dir_entry(res: Result<DirEntry>) -> Result<ModInfo> {
|
||||
let entry = res?;
|
||||
let config_path = entry.path().join("dtmt.cfg");
|
||||
let nexus_path = entry.path().join("nexus.sjson");
|
||||
let index_path = entry.path().join("files.sjson");
|
||||
|
||||
let cfg: ModConfig = read_sjson_file(&config_path)
|
||||
.await
|
||||
.wrap_err_with(|| format!("Failed to read mod config '{}'", config_path.display()))?;
|
||||
|
||||
let nexus: Option<NexusInfo> = match read_sjson_file(&nexus_path)
|
||||
.await
|
||||
.wrap_err_with(|| format!("Failed to read Nexus info '{}'", nexus_path.display()))
|
||||
{
|
||||
Ok(nexus) => Some(nexus),
|
||||
Err(err) if err.is::<std::io::Error>() => match err.downcast_ref::<std::io::Error>() {
|
||||
Some(err) if err.kind() == std::io::ErrorKind::NotFound => None,
|
||||
_ => return Err(err),
|
||||
},
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
|
||||
let files: HashMap<String, Vec<String>> = read_sjson_file(&index_path)
|
||||
.await
|
||||
.wrap_err_with(|| format!("Failed to read file index '{}'", index_path.display()))?;
|
||||
|
@ -222,7 +264,7 @@ async fn read_mod_dir_entry(res: Result<DirEntry>) -> Result<ModInfo> {
|
|||
.into_iter()
|
||||
.map(|(name, files)| Arc::new(PackageInfo::new(name, files.into_iter().collect())))
|
||||
.collect();
|
||||
let info = ModInfo::new(cfg, packages, image);
|
||||
let info = ModInfo::new(cfg, packages, image, nexus);
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
|
@ -334,3 +376,49 @@ pub(crate) fn check_mod_order(state: &ActionState) -> Result<()> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(info, api), fields(id = info.id, name = info.name, version = info.version))]
|
||||
async fn check_mod_update(info: Arc<ModInfo>, api: Arc<NexusApi>) -> Result<Option<ModInfo>> {
|
||||
let Some(nexus) = &info.nexus else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let updated_info = api
|
||||
.mods_id(nexus.id)
|
||||
.await
|
||||
.wrap_err_with(|| format!("Failed to query mod {} from Nexus", nexus.id))?;
|
||||
|
||||
let mut info = Arc::unwrap_or_clone(info);
|
||||
info.nexus = Some(NexusInfo::from(updated_info));
|
||||
|
||||
Ok(Some(info))
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(state))]
|
||||
pub(crate) async fn check_updates(state: ActionState) -> Result<Vec<ModInfo>> {
|
||||
if state.nexus_api_key.is_empty() {
|
||||
eyre::bail!("Nexus API key not set. Cannot check for updates.");
|
||||
}
|
||||
|
||||
let api = NexusApi::new(state.nexus_api_key.to_string())
|
||||
.wrap_err("Failed to initialize Nexus API")?;
|
||||
let api = Arc::new(api);
|
||||
|
||||
let tasks = state
|
||||
.mods
|
||||
.iter()
|
||||
.map(|info| check_mod_update(info.clone(), api.clone()));
|
||||
|
||||
let results = futures::future::join_all(tasks).await;
|
||||
let updates = results
|
||||
.into_iter()
|
||||
.filter_map(|res| match res {
|
||||
Ok(info) => info,
|
||||
Err(err) => {
|
||||
tracing::error!("{:?}", err);
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
Ok(updates)
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ use tokio::sync::RwLock;
|
|||
use crate::controller::app::*;
|
||||
use crate::controller::game::*;
|
||||
use crate::state::AsyncAction;
|
||||
use crate::state::ACTION_FINISH_CHECK_UPDATE;
|
||||
use crate::state::ACTION_FINISH_SAVE_SETTINGS;
|
||||
use crate::state::ACTION_SHOW_ERROR_DIALOG;
|
||||
use crate::state::{
|
||||
|
@ -120,6 +121,29 @@ async fn handle_action(
|
|||
.submit_command(ACTION_FINISH_SAVE_SETTINGS, (), Target::Auto)
|
||||
.expect("failed to send command");
|
||||
}),
|
||||
AsyncAction::CheckUpdates(state) => tokio::spawn(async move {
|
||||
let updates = match check_updates(state)
|
||||
.await
|
||||
.wrap_err("Failed to check for updates")
|
||||
{
|
||||
Ok(updates) => updates,
|
||||
Err(err) => {
|
||||
tracing::error!("{:?}", err);
|
||||
send_error(event_sink.clone(), err).await;
|
||||
vec![]
|
||||
}
|
||||
};
|
||||
|
||||
event_sink
|
||||
.write()
|
||||
.await
|
||||
.submit_command(
|
||||
ACTION_FINISH_CHECK_UPDATE,
|
||||
SingleUse::new(updates),
|
||||
Target::Auto,
|
||||
)
|
||||
.expect("failed to send command");
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
#![recursion_limit = "256"]
|
||||
#![feature(let_chains)]
|
||||
#![feature(arc_unwrap_or_clone)]
|
||||
#![windows_subsystem = "windows"]
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
@ -91,6 +92,7 @@ fn main() -> Result<()> {
|
|||
config.path,
|
||||
game_dir.unwrap_or_default(),
|
||||
config.data_dir.unwrap_or_default(),
|
||||
config.nexus_api_key.unwrap_or_default(),
|
||||
);
|
||||
state.mods = load_mods(state.get_mod_dir(), config.mod_order.iter())
|
||||
.wrap_err("Failed to load mods")?;
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
use std::{path::PathBuf, sync::Arc};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use druid::{
|
||||
im::{HashMap, Vector},
|
||||
Data, ImageBuf, Lens, WindowHandle, WindowId,
|
||||
};
|
||||
use druid::im::{HashMap, Vector};
|
||||
use druid::{Data, ImageBuf, Lens, WindowHandle, WindowId};
|
||||
use dtmt_shared::ModConfig;
|
||||
use nexusmods::Mod as NexusMod;
|
||||
|
||||
use super::SelectedModLens;
|
||||
|
||||
|
@ -69,6 +69,27 @@ impl From<dtmt_shared::ModDependency> for ModDependency {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Data, Debug, Lens, serde::Serialize, serde::Deserialize)]
|
||||
pub(crate) struct NexusInfo {
|
||||
pub id: u64,
|
||||
pub version: String,
|
||||
pub author: String,
|
||||
pub summary: Arc<String>,
|
||||
pub description: Arc<String>,
|
||||
}
|
||||
|
||||
impl From<NexusMod> for NexusInfo {
|
||||
fn from(value: NexusMod) -> Self {
|
||||
Self {
|
||||
id: value.mod_id,
|
||||
version: value.version,
|
||||
author: value.author,
|
||||
summary: Arc::new(value.summary),
|
||||
description: Arc::new(value.description),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Data, Debug, Lens)]
|
||||
pub(crate) struct ModInfo {
|
||||
pub id: String,
|
||||
|
@ -87,6 +108,8 @@ pub(crate) struct ModInfo {
|
|||
#[data(ignore)]
|
||||
pub resources: ModResourceInfo,
|
||||
pub depends: Vector<ModDependency>,
|
||||
#[data(ignore)]
|
||||
pub nexus: Option<NexusInfo>,
|
||||
}
|
||||
|
||||
impl ModInfo {
|
||||
|
@ -94,6 +117,7 @@ impl ModInfo {
|
|||
cfg: ModConfig,
|
||||
packages: Vector<Arc<PackageInfo>>,
|
||||
image: Option<ImageBuf>,
|
||||
nexus: Option<NexusInfo>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: cfg.id,
|
||||
|
@ -112,6 +136,7 @@ impl ModInfo {
|
|||
localization: cfg.resources.localization,
|
||||
},
|
||||
depends: cfg.depends.into_iter().map(ModDependency::from).collect(),
|
||||
nexus,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -126,8 +151,10 @@ pub(crate) struct State {
|
|||
pub is_reset_in_progress: bool,
|
||||
pub is_save_in_progress: bool,
|
||||
pub is_next_save_pending: bool,
|
||||
pub is_update_in_progress: bool,
|
||||
pub game_dir: Arc<PathBuf>,
|
||||
pub data_dir: Arc<PathBuf>,
|
||||
pub nexus_api_key: Arc<String>,
|
||||
pub log: Arc<String>,
|
||||
|
||||
#[lens(ignore)]
|
||||
|
@ -145,7 +172,12 @@ impl State {
|
|||
#[allow(non_upper_case_globals)]
|
||||
pub const selected_mod: SelectedModLens = SelectedModLens;
|
||||
|
||||
pub fn new(config_path: PathBuf, game_dir: PathBuf, data_dir: PathBuf) -> Self {
|
||||
pub fn new(
|
||||
config_path: PathBuf,
|
||||
game_dir: PathBuf,
|
||||
data_dir: PathBuf,
|
||||
nexus_api_key: String,
|
||||
) -> Self {
|
||||
let ctx = sdk::Context::new();
|
||||
|
||||
Self {
|
||||
|
@ -158,9 +190,11 @@ impl State {
|
|||
is_reset_in_progress: false,
|
||||
is_save_in_progress: false,
|
||||
is_next_save_pending: false,
|
||||
is_update_in_progress: false,
|
||||
config_path: Arc::new(config_path),
|
||||
game_dir: Arc::new(game_dir),
|
||||
data_dir: Arc::new(data_dir),
|
||||
nexus_api_key: Arc::new(nexus_api_key),
|
||||
log: Arc::new(String::new()),
|
||||
windows: HashMap::new(),
|
||||
}
|
||||
|
|
|
@ -39,6 +39,11 @@ pub(crate) const ACTION_START_SAVE_SETTINGS: Selector =
|
|||
pub(crate) const ACTION_FINISH_SAVE_SETTINGS: Selector =
|
||||
Selector::new("dtmm.action.finish-save-settings");
|
||||
|
||||
pub(crate) const ACTION_START_CHECK_UPDATE: Selector =
|
||||
Selector::new("dtmm.action.start-check-update");
|
||||
pub(crate) const ACTION_FINISH_CHECK_UPDATE: Selector<SingleUse<Vec<ModInfo>>> =
|
||||
Selector::new("dtmm.action.finish-check-update");
|
||||
|
||||
pub(crate) const ACTION_SET_DIRTY: Selector = Selector::new("dtmm.action.set-dirty");
|
||||
|
||||
pub(crate) const ACTION_SHOW_ERROR_DIALOG: Selector<SingleUse<Report>> =
|
||||
|
@ -56,6 +61,7 @@ pub(crate) struct ActionState {
|
|||
pub mod_dir: Arc<PathBuf>,
|
||||
pub config_path: Arc<PathBuf>,
|
||||
pub ctx: Arc<sdk::Context>,
|
||||
pub nexus_api_key: Arc<String>,
|
||||
}
|
||||
|
||||
impl From<State> for ActionState {
|
||||
|
@ -67,6 +73,7 @@ impl From<State> for ActionState {
|
|||
data_dir: state.data_dir,
|
||||
config_path: state.config_path,
|
||||
ctx: state.ctx,
|
||||
nexus_api_key: state.nexus_api_key,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -77,6 +84,7 @@ pub(crate) enum AsyncAction {
|
|||
AddMod(ActionState, FileInfo),
|
||||
DeleteMod(ActionState, Arc<ModInfo>),
|
||||
SaveSettings(ActionState),
|
||||
CheckUpdates(ActionState),
|
||||
}
|
||||
|
||||
pub(crate) struct Delegate {
|
||||
|
@ -302,6 +310,50 @@ impl AppDelegate<State> for Delegate {
|
|||
state.windows.insert(id, handle);
|
||||
Handled::Yes
|
||||
}
|
||||
cmd if cmd.is(ACTION_START_CHECK_UPDATE) => {
|
||||
if self
|
||||
.sender
|
||||
.send(AsyncAction::CheckUpdates(state.clone().into()))
|
||||
.is_ok()
|
||||
{
|
||||
state.is_update_in_progress = true;
|
||||
} else {
|
||||
tracing::error!("Failed to queue action to check updates");
|
||||
}
|
||||
Handled::Yes
|
||||
}
|
||||
cmd if cmd.is(ACTION_FINISH_CHECK_UPDATE) => {
|
||||
let mut updates = cmd
|
||||
.get(ACTION_FINISH_CHECK_UPDATE)
|
||||
.and_then(SingleUse::take)
|
||||
.expect("command type matched but didn't contain the expected value");
|
||||
|
||||
if tracing::enabled!(tracing::Level::DEBUG) {
|
||||
let mods: Vec<_> = updates
|
||||
.iter()
|
||||
.map(|info| {
|
||||
format!(
|
||||
"{}: {} -> {:?}",
|
||||
info.name,
|
||||
info.version,
|
||||
info.nexus.as_ref().map(|n| &n.version)
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
tracing::info!("Mod updates:\n{}", mods.join("\n"));
|
||||
}
|
||||
|
||||
for mod_info in state.mods.iter_mut() {
|
||||
if let Some(index) = updates.iter().position(|i2| i2.id == mod_info.id) {
|
||||
let update = updates.swap_remove(index);
|
||||
*mod_info = Arc::new(update);
|
||||
}
|
||||
}
|
||||
|
||||
state.is_update_in_progress = false;
|
||||
Handled::Yes
|
||||
}
|
||||
cmd => {
|
||||
if cfg!(debug_assertions) {
|
||||
tracing::warn!("Unknown command: {:?}", cmd);
|
||||
|
|
|
@ -3,7 +3,7 @@ use std::sync::Arc;
|
|||
use druid::im::Vector;
|
||||
use druid::{Data, Lens};
|
||||
|
||||
use super::{ModInfo, State};
|
||||
use super::{ModInfo, NexusInfo, State};
|
||||
|
||||
pub(crate) struct SelectedModLens;
|
||||
|
||||
|
@ -73,3 +73,51 @@ impl<T: Data> Lens<Vector<T>, Vector<(usize, T)>> for IndexedVectorLens {
|
|||
ret
|
||||
}
|
||||
}
|
||||
|
||||
/// A Lens that first checks a key in a mod's `NexusInfo`, then falls back to
|
||||
/// the regular one.
|
||||
pub(crate) struct NexusInfoLens<T, L, R>
|
||||
where
|
||||
L: Lens<NexusInfo, T>,
|
||||
R: Lens<ModInfo, T>,
|
||||
{
|
||||
value: L,
|
||||
fallback: R,
|
||||
_marker: std::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T: Data, L, R> NexusInfoLens<T, L, R>
|
||||
where
|
||||
L: Lens<NexusInfo, T>,
|
||||
R: Lens<ModInfo, T>,
|
||||
{
|
||||
pub fn new(value: L, fallback: R) -> Self {
|
||||
Self {
|
||||
value,
|
||||
fallback,
|
||||
_marker: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Data, L, R> Lens<ModInfo, T> for NexusInfoLens<T, L, R>
|
||||
where
|
||||
L: Lens<NexusInfo, T>,
|
||||
R: Lens<ModInfo, T>,
|
||||
{
|
||||
fn with<V, F: FnOnce(&T) -> V>(&self, data: &ModInfo, f: F) -> V {
|
||||
if let Some(nexus) = &data.nexus {
|
||||
self.value.with(nexus, f)
|
||||
} else {
|
||||
self.fallback.with(data, f)
|
||||
}
|
||||
}
|
||||
|
||||
fn with_mut<V, F: FnOnce(&mut T) -> V>(&self, data: &mut ModInfo, f: F) -> V {
|
||||
if let Some(nexus) = &mut data.nexus {
|
||||
self.value.with_mut(nexus, f)
|
||||
} else {
|
||||
self.fallback.with_mut(data, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -63,7 +63,7 @@ pub mod gruvbox_dark {
|
|||
make_color!(COLOR_ORANGE_DARK, 0xd6, 0x5d, 0x0e);
|
||||
make_color!(COLOR_ORANGE_LIGHT, 0xfe, 0x80, 0x19);
|
||||
|
||||
make_color!(COLOR_ACCENT, COLOR_RED_LIGHT);
|
||||
make_color!(COLOR_ACCENT, COLOR_BLUE_LIGHT);
|
||||
make_color!(COLOR_ACCENT_FG, COLOR_BG0_H);
|
||||
}
|
||||
|
||||
|
|
|
@ -1 +1,41 @@
|
|||
use druid::Color;
|
||||
use usvg::{
|
||||
Error, Fill, LineCap, LineJoin, NodeKind, NonZeroPositiveF64, Options, Paint, Stroke, Tree,
|
||||
};
|
||||
|
||||
pub static ALERT_CIRCLE: &str = include_str!("../../../assets/icons/icons/alert-circle.svg");
|
||||
pub static ALERT_TRIANGLE: &str = include_str!("../../../assets/icons/icons/alert-triangle.svg");
|
||||
|
||||
pub fn parse_svg(svg: &str) -> Result<Tree, Error> {
|
||||
let opt = Options::default();
|
||||
Tree::from_str(svg, &opt.to_ref())
|
||||
}
|
||||
|
||||
pub fn recolor_icon(tree: Tree, stroke: bool, color: Color) -> Tree {
|
||||
let (red, green, blue, _) = color.as_rgba8();
|
||||
|
||||
let mut children = tree.root.children();
|
||||
// The first element is always some kind of background placeholder
|
||||
children.next();
|
||||
|
||||
for node in children {
|
||||
if let NodeKind::Path(ref mut path) = *node.borrow_mut() {
|
||||
if stroke {
|
||||
path.stroke = Some(Stroke {
|
||||
paint: Paint::Color(usvg::Color { red, green, blue }),
|
||||
width: NonZeroPositiveF64::new(2.).expect("the value is not zero"),
|
||||
linecap: LineCap::Round,
|
||||
linejoin: LineJoin::Round,
|
||||
..Default::default()
|
||||
});
|
||||
} else {
|
||||
path.fill = Some(Fill {
|
||||
paint: Paint::Color(usvg::Color { red, green, blue }),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tree
|
||||
}
|
||||
|
|
|
@ -78,7 +78,7 @@ impl<W: Widget<State>> Controller<State, W> for DirtyStateController {
|
|||
data: &State,
|
||||
env: &Env,
|
||||
) {
|
||||
if compare_state_fields!(old_data, data, mods, game_dir, data_dir) {
|
||||
if compare_state_fields!(old_data, data, mods, game_dir, data_dir, nexus_api_key) {
|
||||
ctx.submit_command(ACTION_START_SAVE_SETTINGS);
|
||||
}
|
||||
|
||||
|
|
|
@ -2,23 +2,24 @@ use std::str::FromStr;
|
|||
use std::sync::Arc;
|
||||
|
||||
use druid::im::Vector;
|
||||
use druid::lens;
|
||||
use druid::widget::{
|
||||
Checkbox, CrossAxisAlignment, Either, Flex, Image, Label, LineBreaking, List,
|
||||
MainAxisAlignment, Maybe, Scroll, SizedBox, Split, Svg, SvgData, TextBox, ViewSwitcher,
|
||||
};
|
||||
use druid::{lens, Data, ImageBuf, LifeCycleCtx};
|
||||
use druid::{
|
||||
Color, FileDialogOptions, FileSpec, FontDescriptor, FontFamily, LensExt, SingleUse, Widget,
|
||||
WidgetExt, WindowDesc, WindowId,
|
||||
};
|
||||
use druid::{Data, ImageBuf, LifeCycleCtx};
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
use crate::state::{
|
||||
ModInfo, State, View, ACTION_ADD_MOD, ACTION_SELECTED_MOD_DOWN, ACTION_SELECTED_MOD_UP,
|
||||
ACTION_SELECT_MOD, ACTION_SET_WINDOW_HANDLE, ACTION_START_DELETE_SELECTED_MOD,
|
||||
ACTION_START_DEPLOY, ACTION_START_RESET_DEPLOYMENT,
|
||||
ModInfo, NexusInfo, NexusInfoLens, State, View, ACTION_ADD_MOD, ACTION_SELECTED_MOD_DOWN,
|
||||
ACTION_SELECTED_MOD_UP, ACTION_SELECT_MOD, ACTION_SET_WINDOW_HANDLE, ACTION_START_CHECK_UPDATE,
|
||||
ACTION_START_DELETE_SELECTED_MOD, ACTION_START_DEPLOY, ACTION_START_RESET_DEPLOYMENT,
|
||||
};
|
||||
use crate::ui::theme::{self, ColorExt};
|
||||
use crate::ui::theme::{self, ColorExt, COLOR_YELLOW_LIGHT};
|
||||
use crate::ui::widget::border::Border;
|
||||
use crate::ui::widget::button::Button;
|
||||
use crate::ui::widget::controller::{
|
||||
|
@ -49,6 +50,12 @@ fn build_top_bar() -> impl Widget<State> {
|
|||
state.current_view = View::Settings;
|
||||
});
|
||||
|
||||
let check_update_button = Button::with_label("Check for updates")
|
||||
.on_click(|ctx, _: &mut State, _| {
|
||||
ctx.submit_command(ACTION_START_CHECK_UPDATE);
|
||||
})
|
||||
.disabled_if(|data, _| data.is_update_in_progress);
|
||||
|
||||
let deploy_button = {
|
||||
let icon = Svg::new(SvgData::from_str(theme::icons::ALERT_CIRCLE).expect("invalid SVG"))
|
||||
.fix_height(druid::theme::TEXT_SIZE_NORMAL);
|
||||
|
@ -85,6 +92,8 @@ fn build_top_bar() -> impl Widget<State> {
|
|||
)
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_child(check_update_button)
|
||||
.with_default_spacer()
|
||||
.with_child(deploy_button)
|
||||
.with_default_spacer()
|
||||
.with_child(reset_button),
|
||||
|
@ -118,10 +127,42 @@ fn build_mod_list() -> impl Widget<State> {
|
|||
let name =
|
||||
Label::raw().lens(lens!((usize, Arc<ModInfo>, bool), 1).then(ModInfo::name.in_arc()));
|
||||
|
||||
let version = {
|
||||
let icon = {
|
||||
let tree =
|
||||
theme::icons::parse_svg(theme::icons::ALERT_TRIANGLE).expect("invalid SVG");
|
||||
|
||||
let tree = theme::icons::recolor_icon(tree, true, COLOR_YELLOW_LIGHT);
|
||||
|
||||
Svg::new(Arc::new(tree)).fix_height(druid::theme::TEXT_SIZE_NORMAL)
|
||||
};
|
||||
|
||||
Either::new(
|
||||
|info, _| {
|
||||
info.nexus
|
||||
.as_ref()
|
||||
.map(|n| info.version != n.version)
|
||||
.unwrap_or(false)
|
||||
},
|
||||
Flex::row()
|
||||
.with_child(icon)
|
||||
.with_spacer(3.)
|
||||
.with_child(Label::raw().lens(ModInfo::version.in_arc())),
|
||||
Label::raw().lens(ModInfo::version.in_arc()),
|
||||
)
|
||||
.lens(lens!((usize, Arc<ModInfo>, bool), 1))
|
||||
};
|
||||
|
||||
let fields = Flex::row()
|
||||
.must_fill_main_axis(true)
|
||||
.main_axis_alignment(MainAxisAlignment::SpaceBetween)
|
||||
.with_child(name)
|
||||
.with_child(version);
|
||||
|
||||
Flex::row()
|
||||
.must_fill_main_axis(true)
|
||||
.with_child(checkbox)
|
||||
.with_child(name)
|
||||
.with_flex_child(fields, 1.)
|
||||
.padding((5.0, 4.0))
|
||||
.background(theme::keys::KEY_MOD_LIST_ITEM_BG_COLOR)
|
||||
.on_click(|ctx, (i, _, _), _env| ctx.submit_command(ACTION_SELECT_MOD.with(*i)))
|
||||
|
@ -253,12 +294,18 @@ fn build_mod_details_info() -> impl Widget<State> {
|
|||
.lens(ModInfo::name.in_arc());
|
||||
let summary = Label::raw()
|
||||
.with_line_break_mode(LineBreaking::WordWrap)
|
||||
.lens(ModInfo::summary.in_arc());
|
||||
.lens(NexusInfoLens::new(NexusInfo::summary, ModInfo::summary).in_arc());
|
||||
|
||||
// TODO: Image/icon?
|
||||
|
||||
let version_line = Label::dynamic(|info: &Arc<ModInfo>, _| {
|
||||
if let Some(author) = &info.author {
|
||||
let author = info
|
||||
.nexus
|
||||
.as_ref()
|
||||
.map(|n| &n.author)
|
||||
.or(info.author.as_ref());
|
||||
|
||||
if let Some(author) = &author {
|
||||
format!("Version: {}, by {author}", info.version)
|
||||
} else {
|
||||
format!("Version: {}", info.version)
|
||||
|
@ -359,12 +406,22 @@ fn build_view_settings() -> impl Widget<State> {
|
|||
)
|
||||
.expand_width();
|
||||
|
||||
let nexus_apy_key_setting = Flex::row()
|
||||
.must_fill_main_axis(true)
|
||||
.main_axis_alignment(MainAxisAlignment::Start)
|
||||
.with_child(Label::new("Nexus API Key:"))
|
||||
.with_default_spacer()
|
||||
.with_flex_child(TextBox::new().expand_width().lens(State::nexus_api_key), 1.)
|
||||
.expand_width();
|
||||
|
||||
let content = Flex::column()
|
||||
.must_fill_main_axis(true)
|
||||
.cross_axis_alignment(CrossAxisAlignment::Start)
|
||||
.with_child(data_dir_setting)
|
||||
.with_default_spacer()
|
||||
.with_child(game_dir_setting);
|
||||
.with_child(game_dir_setting)
|
||||
.with_default_spacer()
|
||||
.with_child(nexus_apy_key_setting);
|
||||
|
||||
SizedBox::new(content)
|
||||
.width(800.)
|
||||
|
|
|
@ -28,6 +28,7 @@ impl<'a> From<&'a ModInfo> for LoadOrderEntrySerialize<'a> {
|
|||
pub(crate) struct ConfigSerialize<'a> {
|
||||
game_dir: &'a Path,
|
||||
data_dir: &'a Path,
|
||||
nexus_api_key: &'a String,
|
||||
mod_order: Vec<LoadOrderEntrySerialize<'a>>,
|
||||
}
|
||||
|
||||
|
@ -36,6 +37,7 @@ impl<'a> From<&'a ActionState> for ConfigSerialize<'a> {
|
|||
Self {
|
||||
game_dir: &state.game_dir,
|
||||
data_dir: &state.data_dir,
|
||||
nexus_api_key: &state.nexus_api_key,
|
||||
mod_order: state
|
||||
.mods
|
||||
.iter()
|
||||
|
@ -58,6 +60,7 @@ pub(crate) struct Config {
|
|||
pub path: PathBuf,
|
||||
pub data_dir: Option<PathBuf>,
|
||||
pub game_dir: Option<PathBuf>,
|
||||
pub nexus_api_key: Option<String>,
|
||||
#[serde(default)]
|
||||
pub mod_order: Vec<LoadOrderEntry>,
|
||||
}
|
||||
|
@ -140,6 +143,7 @@ where
|
|||
path: default_path,
|
||||
data_dir: Some(get_default_data_dir()),
|
||||
game_dir: None,
|
||||
nexus_api_key: None,
|
||||
mod_order: Vec::new(),
|
||||
};
|
||||
|
||||
|
|
21
lib/nexusmods/Cargo.toml
Normal file
21
lib/nexusmods/Cargo.toml
Normal file
|
@ -0,0 +1,21 @@
|
|||
[package]
|
||||
name = "nexusmods"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
futures = "0.3.26"
|
||||
lazy_static = "1.4.0"
|
||||
regex = "1.7.1"
|
||||
reqwest = { version = "0.11.14" }
|
||||
serde = { version = "1.0.152", features = ["derive"] }
|
||||
serde_json = "1.0.94"
|
||||
thiserror = "1.0.39"
|
||||
time = { version = "0.3.20", features = ["serde"] }
|
||||
tracing = "0.1.37"
|
||||
url = { version = "2.3.1", features = ["serde"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.26.0", features = ["rt", "macros"] }
|
291
lib/nexusmods/src/lib.rs
Normal file
291
lib/nexusmods/src/lib.rs
Normal file
|
@ -0,0 +1,291 @@
|
|||
use std::collections::HashMap;
|
||||
use std::convert::Infallible;
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use reqwest::header::{HeaderMap, HeaderValue, InvalidHeaderValue};
|
||||
use reqwest::{Client, RequestBuilder, Url};
|
||||
use serde::Deserialize;
|
||||
use thiserror::Error;
|
||||
|
||||
mod types;
|
||||
use time::OffsetDateTime;
|
||||
pub use types::*;
|
||||
|
||||
// TODO: Add OS information
|
||||
const USER_AGENT: &str = concat!("DTMM/", env!("CARGO_PKG_VERSION"));
|
||||
const GAME_ID: &str = "warhammer40kdarktide";
|
||||
|
||||
lazy_static! {
|
||||
static ref BASE_URL: Url = Url::parse("https://api.nexusmods.com/v1/").unwrap();
|
||||
static ref BASE_URL_GAME: Url =
|
||||
Url::parse("https://api.nexusmods.com/v1/games/warhammer40kdarktide/").unwrap();
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("HTTP error: {0:?}")]
|
||||
HTTP(#[from] reqwest::Error),
|
||||
#[error("invalid URL: {0:?}")]
|
||||
URLParseError(#[from] url::ParseError),
|
||||
#[error("failed to deserialize '{error}': {json}")]
|
||||
Deserialize {
|
||||
json: String,
|
||||
error: serde_json::Error,
|
||||
},
|
||||
#[error(transparent)]
|
||||
InvalidHeaderValue(#[from] InvalidHeaderValue),
|
||||
#[error("this error cannot happen")]
|
||||
Infallible(#[from] Infallible),
|
||||
#[error("invalid NXM URL '{}': {0}", .1.as_str())]
|
||||
InvalidNXM(&'static str, Url),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
pub struct Nxm {
|
||||
pub mod_id: u64,
|
||||
pub file_id: u64,
|
||||
pub user_id: u64,
|
||||
pub key: String,
|
||||
pub expires: OffsetDateTime,
|
||||
}
|
||||
|
||||
pub struct Api {
|
||||
client: Client,
|
||||
}
|
||||
|
||||
impl Api {
|
||||
pub fn new(key: String) -> Result<Self> {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("accept", HeaderValue::from_static("application/json"));
|
||||
headers.insert("apikey", HeaderValue::from_str(&key)?);
|
||||
|
||||
let client = Client::builder()
|
||||
.user_agent(USER_AGENT)
|
||||
.default_headers(headers)
|
||||
.build()?;
|
||||
|
||||
Ok(Self { client })
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
async fn send<T>(&self, req: RequestBuilder) -> Result<T>
|
||||
where
|
||||
T: for<'a> Deserialize<'a>,
|
||||
{
|
||||
let res = req.send().await?.error_for_status()?;
|
||||
tracing::trace!(?res);
|
||||
|
||||
let json = res.text().await?;
|
||||
serde_json::from_str(&json).map_err(|error| Error::Deserialize { json, error })
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn user_validate(&self) -> Result<User> {
|
||||
let url = BASE_URL.join("users/validate.json")?;
|
||||
let req = self.client.get(url);
|
||||
self.send(req).await
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn mods_updated(&self, period: UpdatePeriod) -> Result<Vec<UpdateInfo>> {
|
||||
let url = BASE_URL_GAME.join("mods/updated.json")?;
|
||||
let req = self.client.get(url).query(&[period]);
|
||||
self.send(req).await
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn mods_id(&self, id: u64) -> Result<Mod> {
|
||||
let url = BASE_URL_GAME.join(&format!("mods/{}.json", id))?;
|
||||
let req = self.client.get(url);
|
||||
self.send(req).await
|
||||
}
|
||||
|
||||
pub fn parse_file_name<S: AsRef<str>>(
|
||||
name: S,
|
||||
) -> Option<(String, u64, String, OffsetDateTime)> {
|
||||
lazy_static! {
|
||||
static ref RE: Regex = Regex::new(r#"^(?P<name>.+?)-(?P<mod_id>[1-9]\d*)-(?P<version>.+?)-(?P<updated>[1-9]\d*)(?:\.\w+)?$"#).unwrap();
|
||||
}
|
||||
|
||||
RE.captures(name.as_ref()).and_then(|cap| {
|
||||
let name = cap.name("name").map(|s| s.as_str().to_string())?;
|
||||
let mod_id = cap.name("mod_id").and_then(|s| s.as_str().parse().ok())?;
|
||||
let version = cap.name("version").map(|s| s.as_str().to_string())?;
|
||||
let updated = cap
|
||||
.name("updated")
|
||||
.and_then(|s| s.as_str().parse().ok())
|
||||
.and_then(|s| OffsetDateTime::from_unix_timestamp(s).ok())?;
|
||||
|
||||
Some((name, mod_id, version, updated))
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn mods_download_link(
|
||||
&self,
|
||||
mod_id: u64,
|
||||
file_id: u64,
|
||||
key: String,
|
||||
expires: OffsetDateTime,
|
||||
) -> Result<Vec<DownloadLink>> {
|
||||
let url =
|
||||
BASE_URL_GAME.join(&format!("mods/{mod_id}/files/{file_id}/download_link.json"))?;
|
||||
let req = self
|
||||
.client
|
||||
.get(url)
|
||||
.query(&[("key", key)])
|
||||
.query(&[("expires", expires.unix_timestamp())]);
|
||||
self.send(req).await
|
||||
}
|
||||
|
||||
pub async fn handle_nxm(&self, url: Url) -> Result<(Mod, Vec<u8>)> {
|
||||
let nxm = Self::parse_nxm(url.clone())?;
|
||||
|
||||
let user = self.user_validate().await?;
|
||||
|
||||
if nxm.user_id != user.user_id {
|
||||
return Err(Error::InvalidNXM("user_id mismtach", url));
|
||||
}
|
||||
|
||||
let (mod_data, download_info) = futures::try_join!(
|
||||
self.mods_id(nxm.mod_id),
|
||||
self.mods_download_link(nxm.mod_id, nxm.file_id, nxm.key, nxm.expires)
|
||||
)?;
|
||||
|
||||
let Some(download_url) = download_info.get(0).map(|i| i.uri.clone()) else {
|
||||
return Err(Error::InvalidNXM("no download link", url));
|
||||
};
|
||||
|
||||
let req = self.client.get(download_url);
|
||||
let data = req.send().await?.bytes().await?;
|
||||
|
||||
Ok((mod_data, data.to_vec()))
|
||||
}
|
||||
|
||||
pub fn parse_nxm(nxm: Url) -> Result<Nxm> {
|
||||
if nxm.scheme() != "nxm" {
|
||||
return Err(Error::InvalidNXM("Invalid scheme", nxm));
|
||||
}
|
||||
|
||||
// Now it makes sense, why Nexus calls this field `game_domain_name`, when it's just
|
||||
// another path segmentin the regular API calls.
|
||||
if nxm.host_str() != Some(GAME_ID) {
|
||||
return Err(Error::InvalidNXM("Invalid game domain name", nxm));
|
||||
}
|
||||
|
||||
let Some(mut segments) = nxm.path_segments() else {
|
||||
return Err(Error::InvalidNXM("Cannot be a base", nxm));
|
||||
};
|
||||
|
||||
if segments.next() != Some("mods") {
|
||||
return Err(Error::InvalidNXM("Unexpected path segment", nxm));
|
||||
}
|
||||
|
||||
let Some(mod_id) = segments.next().and_then(|id| id.parse().ok()) else {
|
||||
return Err(Error::InvalidNXM("Invalid mod ID", nxm));
|
||||
};
|
||||
|
||||
if segments.next() != Some("files") {
|
||||
return Err(Error::InvalidNXM("Unexpected path segment", nxm));
|
||||
}
|
||||
|
||||
let Some(file_id) = segments.next().and_then(|id| id.parse().ok()) else {
|
||||
return Err(Error::InvalidNXM("Invalid file ID", nxm));
|
||||
};
|
||||
|
||||
let mut query = HashMap::new();
|
||||
let pairs = nxm.query_pairs();
|
||||
|
||||
for (key, val) in pairs {
|
||||
query.insert(key, val);
|
||||
}
|
||||
|
||||
let Some(key) = query.get("key") else {
|
||||
return Err(Error::InvalidNXM("Missing 'key'", nxm));
|
||||
};
|
||||
|
||||
let expires = query
|
||||
.get("expires")
|
||||
.and_then(|expires| expires.parse().ok())
|
||||
.and_then(|expires| OffsetDateTime::from_unix_timestamp(expires).ok());
|
||||
let Some(expires) = expires else {
|
||||
return Err(Error::InvalidNXM("Missing 'expires'", nxm));
|
||||
};
|
||||
|
||||
let user_id = query.get("user_id").and_then(|id| id.parse().ok());
|
||||
let Some(user_id) = user_id else {
|
||||
return Err(Error::InvalidNXM("Missing 'user_id'", nxm));
|
||||
};
|
||||
|
||||
Ok(Nxm {
|
||||
mod_id,
|
||||
file_id,
|
||||
key: key.to_string(),
|
||||
expires,
|
||||
user_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use reqwest::Url;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use crate::Api;
|
||||
|
||||
fn make_api() -> Api {
|
||||
let key = std::env::var("NEXUSMODS_API_KEY").expect("'NEXUSMODS_API_KEY' env var missing");
|
||||
Api::new(key).expect("failed to build API client")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mods_updated() {
|
||||
let client = make_api();
|
||||
client
|
||||
.mods_updated(Default::default())
|
||||
.await
|
||||
.expect("failed to query 'mods_updated'");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn user_validate() {
|
||||
let client = make_api();
|
||||
client
|
||||
.user_validate()
|
||||
.await
|
||||
.expect("failed to query 'user_validate'");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mods_id() {
|
||||
let client = make_api();
|
||||
let dmf_id = 8;
|
||||
client
|
||||
.mods_id(dmf_id)
|
||||
.await
|
||||
.expect("failed to query 'mods_id'");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_file_name() {
|
||||
let file = "Darktide Mod Framework-8-23-3-04-1677966575.zip";
|
||||
let (name, mod_id, version, updated) = Api::parse_file_name(file).unwrap();
|
||||
|
||||
assert_eq!(name, String::from("Darktide Mod Framework"));
|
||||
assert_eq!(mod_id, 8);
|
||||
assert_eq!(version, String::from("23-3-04"));
|
||||
assert_eq!(
|
||||
updated,
|
||||
OffsetDateTime::from_unix_timestamp(1677966575).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_nxm() {
|
||||
let nxm = Url::parse("nxm://warhammer40kdarktide/mods/8/files/1000172397?key=VZ86Guj_LosPvtkD90-ZQg&expires=1678359882&user_id=1234567").expect("invalid NXM example");
|
||||
Api::parse_nxm(nxm).expect("failed to parse nxm link");
|
||||
}
|
||||
}
|
111
lib/nexusmods/src/types.rs
Normal file
111
lib/nexusmods/src/types.rs
Normal file
|
@ -0,0 +1,111 @@
|
|||
use reqwest::Url;
|
||||
use serde::ser::SerializeTuple;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct User {
|
||||
pub user_id: u64,
|
||||
pub name: String,
|
||||
pub profile_url: Url,
|
||||
// pub is_premium: bool,
|
||||
// pub is_supporter: bool,
|
||||
// pub email: String,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ModStatus {
|
||||
Published,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Deserialize)]
|
||||
pub enum EndorseStatus {
|
||||
Endorsed,
|
||||
Undecided,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ModEndorsement {
|
||||
pub endorse_status: EndorseStatus,
|
||||
#[serde(with = "time::serde::timestamp::option")]
|
||||
pub timestamp: Option<OffsetDateTime>,
|
||||
pub version: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Mod {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub summary: String,
|
||||
pub picture_url: Url,
|
||||
pub uid: u64,
|
||||
pub mod_id: u64,
|
||||
pub category_id: u64,
|
||||
pub version: String,
|
||||
#[serde(with = "time::serde::timestamp")]
|
||||
pub created_timestamp: OffsetDateTime,
|
||||
// created_time: OffsetDateTime,
|
||||
#[serde(with = "time::serde::timestamp")]
|
||||
pub updated_timestamp: OffsetDateTime,
|
||||
// updated_time: OffsetDateTime,
|
||||
pub author: String,
|
||||
pub uploaded_by: String,
|
||||
pub uploaded_users_profile_url: Url,
|
||||
pub status: ModStatus,
|
||||
pub available: bool,
|
||||
pub endorsement: ModEndorsement,
|
||||
// pub mod_downloads: u64,
|
||||
// pub mod_unique_downloads: u64,
|
||||
// pub game_id: u64,
|
||||
// pub allow_rating: bool,
|
||||
// pub domain_name: String,
|
||||
// pub endorsement_count: u64,
|
||||
// pub contains_adult_content: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct DownloadLink {
|
||||
pub name: String,
|
||||
pub short_name: String,
|
||||
#[serde(alias = "URI")]
|
||||
pub uri: Url,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UpdateInfo {
|
||||
pub mod_id: u64,
|
||||
#[serde(with = "time::serde::timestamp")]
|
||||
pub latest_file_update: OffsetDateTime,
|
||||
#[serde(with = "time::serde::timestamp")]
|
||||
pub latest_mod_activity: OffsetDateTime,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum UpdatePeriod {
|
||||
Day,
|
||||
Week,
|
||||
Month,
|
||||
}
|
||||
|
||||
impl Default for UpdatePeriod {
|
||||
fn default() -> Self {
|
||||
Self::Week
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for UpdatePeriod {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let mut tup = serializer.serialize_tuple(2)?;
|
||||
tup.serialize_element("period")?;
|
||||
tup.serialize_element(match self {
|
||||
Self::Day => "1d",
|
||||
Self::Week => "1w",
|
||||
Self::Month => "1m",
|
||||
})?;
|
||||
tup.end()
|
||||
}
|
||||
}
|
|
@ -1 +1 @@
|
|||
Subproject commit 6e413b7bf5fde09ca66bfcee1394443264c99ab1
|
||||
Subproject commit 73d2b23ce50e75b184f5092ad515e97a0adbe6da
|
Loading…
Add table
Reference in a new issue