Implement Nexus integration #54
18 changed files with 1225 additions and 23 deletions
|
@ -12,6 +12,7 @@
|
||||||
- dtmm: show dialog for critical errors
|
- dtmm: show dialog for critical errors
|
||||||
- dtmm: check mod order before deployment
|
- dtmm: check mod order before deployment
|
||||||
- dtmt: add mod dependencies to config
|
- dtmt: add mod dependencies to config
|
||||||
|
- dtmm: match mods to Nexus and check for updates
|
||||||
|
|
||||||
=== Fixed
|
=== 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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
|
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.21.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64ct"
|
name = "base64ct"
|
||||||
version = "1.6.0"
|
version = "1.6.0"
|
||||||
|
@ -756,6 +762,7 @@ dependencies = [
|
||||||
"dtmt-shared",
|
"dtmt-shared",
|
||||||
"futures",
|
"futures",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
|
"nexusmods",
|
||||||
"oodle-sys",
|
"oodle-sys",
|
||||||
"path-slash",
|
"path-slash",
|
||||||
"sdk",
|
"sdk",
|
||||||
|
@ -768,6 +775,7 @@ dependencies = [
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-error",
|
"tracing-error",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
"usvg",
|
||||||
"zip",
|
"zip",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -831,6 +839,15 @@ dependencies = [
|
||||||
"wio",
|
"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]]
|
[[package]]
|
||||||
name = "endian-type"
|
name = "endian-type"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
|
@ -1021,6 +1038,15 @@ version = "0.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
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]]
|
[[package]]
|
||||||
name = "fs_extra"
|
name = "fs_extra"
|
||||||
version = "1.3.0"
|
version = "1.3.0"
|
||||||
|
@ -1373,6 +1399,25 @@ dependencies = [
|
||||||
"syn",
|
"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]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.12.3"
|
version = "0.12.3"
|
||||||
|
@ -1409,6 +1454,87 @@ dependencies = [
|
||||||
"digest",
|
"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]]
|
[[package]]
|
||||||
name = "im"
|
name = "im"
|
||||||
version = "15.1.0"
|
version = "15.1.0"
|
||||||
|
@ -1516,6 +1642,12 @@ dependencies = [
|
||||||
"windows-sys 0.45.0",
|
"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]]
|
[[package]]
|
||||||
name = "is-terminal"
|
name = "is-terminal"
|
||||||
version = "0.4.4"
|
version = "0.4.4"
|
||||||
|
@ -1722,6 +1854,12 @@ dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mime"
|
||||||
|
version = "0.3.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "minimal-lexical"
|
name = "minimal-lexical"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
|
@ -1755,6 +1893,41 @@ version = "0.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3"
|
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]]
|
[[package]]
|
||||||
name = "nibble_vec"
|
name = "nibble_vec"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
@ -1986,6 +2159,51 @@ version = "0.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
|
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]]
|
[[package]]
|
||||||
name = "os_str_bytes"
|
name = "os_str_bytes"
|
||||||
version = "6.4.1"
|
version = "6.4.1"
|
||||||
|
@ -2092,6 +2310,12 @@ dependencies = [
|
||||||
"sha2",
|
"sha2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "percent-encoding"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pest"
|
name = "pest"
|
||||||
version = "2.5.6"
|
version = "2.5.6"
|
||||||
|
@ -2397,6 +2621,43 @@ version = "0.6.28"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
|
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]]
|
[[package]]
|
||||||
name = "resvg"
|
name = "resvg"
|
||||||
version = "0.25.0"
|
version = "0.25.0"
|
||||||
|
@ -2532,6 +2793,15 @@ dependencies = [
|
||||||
"winapi-util",
|
"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]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
|
@ -2565,6 +2835,29 @@ dependencies = [
|
||||||
"tracing-error",
|
"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]]
|
[[package]]
|
||||||
name = "self_cell"
|
name = "self_cell"
|
||||||
version = "0.10.2"
|
version = "0.10.2"
|
||||||
|
@ -2597,6 +2890,17 @@ dependencies = [
|
||||||
"syn",
|
"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]]
|
[[package]]
|
||||||
name = "serde_sjson"
|
name = "serde_sjson"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
|
@ -2606,6 +2910,18 @@ dependencies = [
|
||||||
"serde",
|
"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]]
|
[[package]]
|
||||||
name = "sha1"
|
name = "sha1"
|
||||||
version = "0.10.5"
|
version = "0.10.5"
|
||||||
|
@ -2686,6 +3002,16 @@ version = "1.10.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
|
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]]
|
[[package]]
|
||||||
name = "steamid-ng"
|
name = "steamid-ng"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
|
@ -2925,6 +3251,21 @@ dependencies = [
|
||||||
"displaydoc",
|
"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]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.26.0"
|
version = "1.26.0"
|
||||||
|
@ -2939,6 +3280,7 @@ dependencies = [
|
||||||
"num_cpus",
|
"num_cpus",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"signal-hook-registry",
|
"signal-hook-registry",
|
||||||
|
"socket2",
|
||||||
"tokio-macros",
|
"tokio-macros",
|
||||||
"tracing",
|
"tracing",
|
||||||
"windows-sys 0.45.0",
|
"windows-sys 0.45.0",
|
||||||
|
@ -2955,6 +3297,16 @@ dependencies = [
|
||||||
"syn",
|
"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]]
|
[[package]]
|
||||||
name = "tokio-stream"
|
name = "tokio-stream"
|
||||||
version = "0.1.12"
|
version = "0.1.12"
|
||||||
|
@ -2966,6 +3318,20 @@ dependencies = [
|
||||||
"tokio",
|
"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]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "0.5.11"
|
version = "0.5.11"
|
||||||
|
@ -2992,6 +3358,12 @@ dependencies = [
|
||||||
"winnow",
|
"winnow",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower-service"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing"
|
name = "tracing"
|
||||||
version = "0.1.37"
|
version = "0.1.37"
|
||||||
|
@ -3075,6 +3447,12 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "try-lock"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ttf-parser"
|
name = "ttf-parser"
|
||||||
version = "0.17.1"
|
version = "0.17.1"
|
||||||
|
@ -3210,6 +3588,15 @@ version = "1.0.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4"
|
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]]
|
[[package]]
|
||||||
name = "unicode-script"
|
name = "unicode-script"
|
||||||
version = "0.5.5"
|
version = "0.5.5"
|
||||||
|
@ -3234,13 +3621,25 @@ version = "0.1.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
|
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]]
|
[[package]]
|
||||||
name = "usvg"
|
name = "usvg"
|
||||||
version = "0.25.0"
|
version = "0.25.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "585bb2d87c8fd6041a479dea01479dcf9094e61b5f9af221606927e61a2bd939"
|
checksum = "585bb2d87c8fd6041a479dea01479dcf9094e61b5f9af221606927e61a2bd939"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64 0.13.1",
|
||||||
"data-url",
|
"data-url",
|
||||||
"flate2",
|
"flate2",
|
||||||
"fontdb",
|
"fontdb",
|
||||||
|
@ -3278,6 +3677,12 @@ version = "0.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
|
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vcpkg"
|
||||||
|
version = "0.2.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version-compare"
|
name = "version-compare"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
|
@ -3322,6 +3727,16 @@ dependencies = [
|
||||||
"winapi-util",
|
"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]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
version = "0.11.0+wasi-snapshot-preview1"
|
version = "0.11.0+wasi-snapshot-preview1"
|
||||||
|
@ -3353,6 +3768,18 @@ dependencies = [
|
||||||
"wasm-bindgen-shared",
|
"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]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro"
|
name = "wasm-bindgen-macro"
|
||||||
version = "0.2.84"
|
version = "0.2.84"
|
||||||
|
|
|
@ -15,6 +15,7 @@ dtmt-shared = { path = "../../lib/dtmt-shared", version = "*" }
|
||||||
futures = "0.3.25"
|
futures = "0.3.25"
|
||||||
oodle-sys = { path = "../../lib/oodle-sys", version = "*" }
|
oodle-sys = { path = "../../lib/oodle-sys", version = "*" }
|
||||||
sdk = { path = "../../lib/sdk", version = "*" }
|
sdk = { path = "../../lib/sdk", version = "*" }
|
||||||
|
nexusmods = { path = "../../lib/nexusmods", version = "*" }
|
||||||
serde_sjson = { path = "../../lib/serde_sjson", version = "*" }
|
serde_sjson = { path = "../../lib/serde_sjson", version = "*" }
|
||||||
serde = { version = "1.0.152", features = ["derive", "rc"] }
|
serde = { version = "1.0.152", features = ["derive", "rc"] }
|
||||||
tokio = { version = "1.23.0", features = ["rt", "fs", "tracing", "sync"] }
|
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"
|
strip-ansi-escapes = "0.1.1"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
colors-transform = "0.2.11"
|
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::im::Vector;
|
||||||
use druid::{FileInfo, ImageBuf};
|
use druid::{FileInfo, ImageBuf};
|
||||||
use dtmt_shared::ModConfig;
|
use dtmt_shared::ModConfig;
|
||||||
|
use nexusmods::Api as NexusApi;
|
||||||
use tokio::fs::{self, DirEntry};
|
use tokio::fs::{self, DirEntry};
|
||||||
use tokio::runtime::Runtime;
|
use tokio::runtime::Runtime;
|
||||||
use tokio_stream::wrappers::ReadDirStream;
|
use tokio_stream::wrappers::ReadDirStream;
|
||||||
use tokio_stream::StreamExt;
|
use tokio_stream::StreamExt;
|
||||||
use zip::ZipArchive;
|
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 crate::util::config::{ConfigSerialize, LoadOrderEntry};
|
||||||
|
|
||||||
use super::read_sjson_file;
|
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()))?;
|
.wrap_err_with(|| format!("Failed to read file {}", info.path.display()))?;
|
||||||
let data = Cursor::new(data);
|
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")?;
|
let mut archive = ZipArchive::new(data).wrap_err("Failed to open ZIP archive")?;
|
||||||
|
|
||||||
if tracing::enabled!(tracing::Level::DEBUG) {
|
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))
|
.extract(Arc::as_ref(&mod_dir))
|
||||||
.wrap_err_with(|| format!("Failed to extract archive to {}", mod_dir.display()))?;
|
.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
|
let packages = files
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(name, files)| Arc::new(PackageInfo::new(name, files.into_iter().collect())))
|
.map(|(name, files)| Arc::new(PackageInfo::new(name, files.into_iter().collect())))
|
||||||
.collect();
|
.collect();
|
||||||
let info = ModInfo::new(mod_cfg, packages, image);
|
let info = ModInfo::new(mod_cfg, packages, image, nexus);
|
||||||
|
|
||||||
Ok(info)
|
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> {
|
async fn read_mod_dir_entry(res: Result<DirEntry>) -> Result<ModInfo> {
|
||||||
let entry = res?;
|
let entry = res?;
|
||||||
let config_path = entry.path().join("dtmt.cfg");
|
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 index_path = entry.path().join("files.sjson");
|
||||||
|
|
||||||
let cfg: ModConfig = read_sjson_file(&config_path)
|
let cfg: ModConfig = read_sjson_file(&config_path)
|
||||||
.await
|
.await
|
||||||
.wrap_err_with(|| format!("Failed to read mod config '{}'", config_path.display()))?;
|
.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)
|
let files: HashMap<String, Vec<String>> = read_sjson_file(&index_path)
|
||||||
.await
|
.await
|
||||||
.wrap_err_with(|| format!("Failed to read file index '{}'", index_path.display()))?;
|
.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()
|
.into_iter()
|
||||||
.map(|(name, files)| Arc::new(PackageInfo::new(name, files.into_iter().collect())))
|
.map(|(name, files)| Arc::new(PackageInfo::new(name, files.into_iter().collect())))
|
||||||
.collect();
|
.collect();
|
||||||
let info = ModInfo::new(cfg, packages, image);
|
let info = ModInfo::new(cfg, packages, image, nexus);
|
||||||
Ok(info)
|
Ok(info)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -334,3 +376,49 @@ pub(crate) fn check_mod_order(state: &ActionState) -> Result<()> {
|
||||||
|
|
||||||
Ok(())
|
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::app::*;
|
||||||
use crate::controller::game::*;
|
use crate::controller::game::*;
|
||||||
use crate::state::AsyncAction;
|
use crate::state::AsyncAction;
|
||||||
|
use crate::state::ACTION_FINISH_CHECK_UPDATE;
|
||||||
use crate::state::ACTION_FINISH_SAVE_SETTINGS;
|
use crate::state::ACTION_FINISH_SAVE_SETTINGS;
|
||||||
use crate::state::ACTION_SHOW_ERROR_DIALOG;
|
use crate::state::ACTION_SHOW_ERROR_DIALOG;
|
||||||
use crate::state::{
|
use crate::state::{
|
||||||
|
@ -120,6 +121,29 @@ async fn handle_action(
|
||||||
.submit_command(ACTION_FINISH_SAVE_SETTINGS, (), Target::Auto)
|
.submit_command(ACTION_FINISH_SAVE_SETTINGS, (), Target::Auto)
|
||||||
.expect("failed to send command");
|
.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"]
|
#![recursion_limit = "256"]
|
||||||
#![feature(let_chains)]
|
#![feature(let_chains)]
|
||||||
|
#![feature(arc_unwrap_or_clone)]
|
||||||
#![windows_subsystem = "windows"]
|
#![windows_subsystem = "windows"]
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
@ -91,6 +92,7 @@ fn main() -> Result<()> {
|
||||||
config.path,
|
config.path,
|
||||||
game_dir.unwrap_or_default(),
|
game_dir.unwrap_or_default(),
|
||||||
config.data_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())
|
state.mods = load_mods(state.get_mod_dir(), config.mod_order.iter())
|
||||||
.wrap_err("Failed to load mods")?;
|
.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::{
|
use druid::im::{HashMap, Vector};
|
||||||
im::{HashMap, Vector},
|
use druid::{Data, ImageBuf, Lens, WindowHandle, WindowId};
|
||||||
Data, ImageBuf, Lens, WindowHandle, WindowId,
|
|
||||||
};
|
|
||||||
use dtmt_shared::ModConfig;
|
use dtmt_shared::ModConfig;
|
||||||
|
use nexusmods::Mod as NexusMod;
|
||||||
|
|
||||||
use super::SelectedModLens;
|
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)]
|
#[derive(Clone, Data, Debug, Lens)]
|
||||||
pub(crate) struct ModInfo {
|
pub(crate) struct ModInfo {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
@ -87,6 +108,8 @@ pub(crate) struct ModInfo {
|
||||||
#[data(ignore)]
|
#[data(ignore)]
|
||||||
pub resources: ModResourceInfo,
|
pub resources: ModResourceInfo,
|
||||||
pub depends: Vector<ModDependency>,
|
pub depends: Vector<ModDependency>,
|
||||||
|
#[data(ignore)]
|
||||||
|
pub nexus: Option<NexusInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ModInfo {
|
impl ModInfo {
|
||||||
|
@ -94,6 +117,7 @@ impl ModInfo {
|
||||||
cfg: ModConfig,
|
cfg: ModConfig,
|
||||||
packages: Vector<Arc<PackageInfo>>,
|
packages: Vector<Arc<PackageInfo>>,
|
||||||
image: Option<ImageBuf>,
|
image: Option<ImageBuf>,
|
||||||
|
nexus: Option<NexusInfo>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: cfg.id,
|
id: cfg.id,
|
||||||
|
@ -112,6 +136,7 @@ impl ModInfo {
|
||||||
localization: cfg.resources.localization,
|
localization: cfg.resources.localization,
|
||||||
},
|
},
|
||||||
depends: cfg.depends.into_iter().map(ModDependency::from).collect(),
|
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_reset_in_progress: bool,
|
||||||
pub is_save_in_progress: bool,
|
pub is_save_in_progress: bool,
|
||||||
pub is_next_save_pending: bool,
|
pub is_next_save_pending: bool,
|
||||||
|
pub is_update_in_progress: bool,
|
||||||
pub game_dir: Arc<PathBuf>,
|
pub game_dir: Arc<PathBuf>,
|
||||||
pub data_dir: Arc<PathBuf>,
|
pub data_dir: Arc<PathBuf>,
|
||||||
|
pub nexus_api_key: Arc<String>,
|
||||||
pub log: Arc<String>,
|
pub log: Arc<String>,
|
||||||
|
|
||||||
#[lens(ignore)]
|
#[lens(ignore)]
|
||||||
|
@ -145,7 +172,12 @@ impl State {
|
||||||
#[allow(non_upper_case_globals)]
|
#[allow(non_upper_case_globals)]
|
||||||
pub const selected_mod: SelectedModLens = SelectedModLens;
|
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();
|
let ctx = sdk::Context::new();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
@ -158,9 +190,11 @@ impl State {
|
||||||
is_reset_in_progress: false,
|
is_reset_in_progress: false,
|
||||||
is_save_in_progress: false,
|
is_save_in_progress: false,
|
||||||
is_next_save_pending: false,
|
is_next_save_pending: false,
|
||||||
|
is_update_in_progress: false,
|
||||||
config_path: Arc::new(config_path),
|
config_path: Arc::new(config_path),
|
||||||
game_dir: Arc::new(game_dir),
|
game_dir: Arc::new(game_dir),
|
||||||
data_dir: Arc::new(data_dir),
|
data_dir: Arc::new(data_dir),
|
||||||
|
nexus_api_key: Arc::new(nexus_api_key),
|
||||||
log: Arc::new(String::new()),
|
log: Arc::new(String::new()),
|
||||||
windows: HashMap::new(),
|
windows: HashMap::new(),
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,6 +39,11 @@ pub(crate) const ACTION_START_SAVE_SETTINGS: Selector =
|
||||||
pub(crate) const ACTION_FINISH_SAVE_SETTINGS: Selector =
|
pub(crate) const ACTION_FINISH_SAVE_SETTINGS: Selector =
|
||||||
Selector::new("dtmm.action.finish-save-settings");
|
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_SET_DIRTY: Selector = Selector::new("dtmm.action.set-dirty");
|
||||||
|
|
||||||
pub(crate) const ACTION_SHOW_ERROR_DIALOG: Selector<SingleUse<Report>> =
|
pub(crate) const ACTION_SHOW_ERROR_DIALOG: Selector<SingleUse<Report>> =
|
||||||
|
@ -56,6 +61,7 @@ pub(crate) struct ActionState {
|
||||||
pub mod_dir: Arc<PathBuf>,
|
pub mod_dir: Arc<PathBuf>,
|
||||||
pub config_path: Arc<PathBuf>,
|
pub config_path: Arc<PathBuf>,
|
||||||
pub ctx: Arc<sdk::Context>,
|
pub ctx: Arc<sdk::Context>,
|
||||||
|
pub nexus_api_key: Arc<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<State> for ActionState {
|
impl From<State> for ActionState {
|
||||||
|
@ -67,6 +73,7 @@ impl From<State> for ActionState {
|
||||||
data_dir: state.data_dir,
|
data_dir: state.data_dir,
|
||||||
config_path: state.config_path,
|
config_path: state.config_path,
|
||||||
ctx: state.ctx,
|
ctx: state.ctx,
|
||||||
|
nexus_api_key: state.nexus_api_key,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,6 +84,7 @@ pub(crate) enum AsyncAction {
|
||||||
AddMod(ActionState, FileInfo),
|
AddMod(ActionState, FileInfo),
|
||||||
DeleteMod(ActionState, Arc<ModInfo>),
|
DeleteMod(ActionState, Arc<ModInfo>),
|
||||||
SaveSettings(ActionState),
|
SaveSettings(ActionState),
|
||||||
|
CheckUpdates(ActionState),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct Delegate {
|
pub(crate) struct Delegate {
|
||||||
|
@ -302,6 +310,50 @@ impl AppDelegate<State> for Delegate {
|
||||||
state.windows.insert(id, handle);
|
state.windows.insert(id, handle);
|
||||||
Handled::Yes
|
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 => {
|
cmd => {
|
||||||
if cfg!(debug_assertions) {
|
if cfg!(debug_assertions) {
|
||||||
tracing::warn!("Unknown command: {:?}", cmd);
|
tracing::warn!("Unknown command: {:?}", cmd);
|
||||||
|
|
|
@ -3,7 +3,7 @@ use std::sync::Arc;
|
||||||
use druid::im::Vector;
|
use druid::im::Vector;
|
||||||
use druid::{Data, Lens};
|
use druid::{Data, Lens};
|
||||||
|
|
||||||
use super::{ModInfo, State};
|
use super::{ModInfo, NexusInfo, State};
|
||||||
|
|
||||||
pub(crate) struct SelectedModLens;
|
pub(crate) struct SelectedModLens;
|
||||||
|
|
||||||
|
@ -73,3 +73,51 @@ impl<T: Data> Lens<Vector<T>, Vector<(usize, T)>> for IndexedVectorLens {
|
||||||
ret
|
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_DARK, 0xd6, 0x5d, 0x0e);
|
||||||
make_color!(COLOR_ORANGE_LIGHT, 0xfe, 0x80, 0x19);
|
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);
|
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_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,
|
data: &State,
|
||||||
env: &Env,
|
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);
|
ctx.submit_command(ACTION_START_SAVE_SETTINGS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,23 +2,24 @@ use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use druid::im::Vector;
|
use druid::im::Vector;
|
||||||
|
use druid::lens;
|
||||||
use druid::widget::{
|
use druid::widget::{
|
||||||
Checkbox, CrossAxisAlignment, Either, Flex, Image, Label, LineBreaking, List,
|
Checkbox, CrossAxisAlignment, Either, Flex, Image, Label, LineBreaking, List,
|
||||||
MainAxisAlignment, Maybe, Scroll, SizedBox, Split, Svg, SvgData, TextBox, ViewSwitcher,
|
MainAxisAlignment, Maybe, Scroll, SizedBox, Split, Svg, SvgData, TextBox, ViewSwitcher,
|
||||||
};
|
};
|
||||||
use druid::{lens, Data, ImageBuf, LifeCycleCtx};
|
|
||||||
use druid::{
|
use druid::{
|
||||||
Color, FileDialogOptions, FileSpec, FontDescriptor, FontFamily, LensExt, SingleUse, Widget,
|
Color, FileDialogOptions, FileSpec, FontDescriptor, FontFamily, LensExt, SingleUse, Widget,
|
||||||
WidgetExt, WindowDesc, WindowId,
|
WidgetExt, WindowDesc, WindowId,
|
||||||
};
|
};
|
||||||
|
use druid::{Data, ImageBuf, LifeCycleCtx};
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
|
|
||||||
use crate::state::{
|
use crate::state::{
|
||||||
ModInfo, State, View, ACTION_ADD_MOD, ACTION_SELECTED_MOD_DOWN, ACTION_SELECTED_MOD_UP,
|
ModInfo, NexusInfo, NexusInfoLens, State, View, ACTION_ADD_MOD, ACTION_SELECTED_MOD_DOWN,
|
||||||
ACTION_SELECT_MOD, ACTION_SET_WINDOW_HANDLE, ACTION_START_DELETE_SELECTED_MOD,
|
ACTION_SELECTED_MOD_UP, ACTION_SELECT_MOD, ACTION_SET_WINDOW_HANDLE, ACTION_START_CHECK_UPDATE,
|
||||||
ACTION_START_DEPLOY, ACTION_START_RESET_DEPLOYMENT,
|
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::border::Border;
|
||||||
use crate::ui::widget::button::Button;
|
use crate::ui::widget::button::Button;
|
||||||
use crate::ui::widget::controller::{
|
use crate::ui::widget::controller::{
|
||||||
|
@ -49,6 +50,12 @@ fn build_top_bar() -> impl Widget<State> {
|
||||||
state.current_view = View::Settings;
|
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 deploy_button = {
|
||||||
let icon = Svg::new(SvgData::from_str(theme::icons::ALERT_CIRCLE).expect("invalid SVG"))
|
let icon = Svg::new(SvgData::from_str(theme::icons::ALERT_CIRCLE).expect("invalid SVG"))
|
||||||
.fix_height(druid::theme::TEXT_SIZE_NORMAL);
|
.fix_height(druid::theme::TEXT_SIZE_NORMAL);
|
||||||
|
@ -85,6 +92,8 @@ fn build_top_bar() -> impl Widget<State> {
|
||||||
)
|
)
|
||||||
.with_child(
|
.with_child(
|
||||||
Flex::row()
|
Flex::row()
|
||||||
|
.with_child(check_update_button)
|
||||||
|
.with_default_spacer()
|
||||||
.with_child(deploy_button)
|
.with_child(deploy_button)
|
||||||
.with_default_spacer()
|
.with_default_spacer()
|
||||||
.with_child(reset_button),
|
.with_child(reset_button),
|
||||||
|
@ -118,10 +127,42 @@ fn build_mod_list() -> impl Widget<State> {
|
||||||
let name =
|
let name =
|
||||||
Label::raw().lens(lens!((usize, Arc<ModInfo>, bool), 1).then(ModInfo::name.in_arc()));
|
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()
|
Flex::row()
|
||||||
.must_fill_main_axis(true)
|
.must_fill_main_axis(true)
|
||||||
.with_child(checkbox)
|
.with_child(checkbox)
|
||||||
.with_child(name)
|
.with_flex_child(fields, 1.)
|
||||||
.padding((5.0, 4.0))
|
.padding((5.0, 4.0))
|
||||||
.background(theme::keys::KEY_MOD_LIST_ITEM_BG_COLOR)
|
.background(theme::keys::KEY_MOD_LIST_ITEM_BG_COLOR)
|
||||||
.on_click(|ctx, (i, _, _), _env| ctx.submit_command(ACTION_SELECT_MOD.with(*i)))
|
.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());
|
.lens(ModInfo::name.in_arc());
|
||||||
let summary = Label::raw()
|
let summary = Label::raw()
|
||||||
.with_line_break_mode(LineBreaking::WordWrap)
|
.with_line_break_mode(LineBreaking::WordWrap)
|
||||||
.lens(ModInfo::summary.in_arc());
|
.lens(NexusInfoLens::new(NexusInfo::summary, ModInfo::summary).in_arc());
|
||||||
|
|
||||||
// TODO: Image/icon?
|
// TODO: Image/icon?
|
||||||
|
|
||||||
let version_line = Label::dynamic(|info: &Arc<ModInfo>, _| {
|
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)
|
format!("Version: {}, by {author}", info.version)
|
||||||
} else {
|
} else {
|
||||||
format!("Version: {}", info.version)
|
format!("Version: {}", info.version)
|
||||||
|
@ -359,12 +406,22 @@ fn build_view_settings() -> impl Widget<State> {
|
||||||
)
|
)
|
||||||
.expand_width();
|
.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()
|
let content = Flex::column()
|
||||||
.must_fill_main_axis(true)
|
.must_fill_main_axis(true)
|
||||||
.cross_axis_alignment(CrossAxisAlignment::Start)
|
.cross_axis_alignment(CrossAxisAlignment::Start)
|
||||||
.with_child(data_dir_setting)
|
.with_child(data_dir_setting)
|
||||||
.with_default_spacer()
|
.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)
|
SizedBox::new(content)
|
||||||
.width(800.)
|
.width(800.)
|
||||||
|
|
|
@ -28,6 +28,7 @@ impl<'a> From<&'a ModInfo> for LoadOrderEntrySerialize<'a> {
|
||||||
pub(crate) struct ConfigSerialize<'a> {
|
pub(crate) struct ConfigSerialize<'a> {
|
||||||
game_dir: &'a Path,
|
game_dir: &'a Path,
|
||||||
data_dir: &'a Path,
|
data_dir: &'a Path,
|
||||||
|
nexus_api_key: &'a String,
|
||||||
mod_order: Vec<LoadOrderEntrySerialize<'a>>,
|
mod_order: Vec<LoadOrderEntrySerialize<'a>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,6 +37,7 @@ impl<'a> From<&'a ActionState> for ConfigSerialize<'a> {
|
||||||
Self {
|
Self {
|
||||||
game_dir: &state.game_dir,
|
game_dir: &state.game_dir,
|
||||||
data_dir: &state.data_dir,
|
data_dir: &state.data_dir,
|
||||||
|
nexus_api_key: &state.nexus_api_key,
|
||||||
mod_order: state
|
mod_order: state
|
||||||
.mods
|
.mods
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -58,6 +60,7 @@ pub(crate) struct Config {
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
pub data_dir: Option<PathBuf>,
|
pub data_dir: Option<PathBuf>,
|
||||||
pub game_dir: Option<PathBuf>,
|
pub game_dir: Option<PathBuf>,
|
||||||
|
pub nexus_api_key: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub mod_order: Vec<LoadOrderEntry>,
|
pub mod_order: Vec<LoadOrderEntry>,
|
||||||
}
|
}
|
||||||
|
@ -140,6 +143,7 @@ where
|
||||||
path: default_path,
|
path: default_path,
|
||||||
data_dir: Some(get_default_data_dir()),
|
data_dir: Some(get_default_data_dir()),
|
||||||
game_dir: None,
|
game_dir: None,
|
||||||
|
nexus_api_key: None,
|
||||||
mod_order: Vec::new(),
|
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