forked from akanyan/STARTLINER
feat: phase 2
Newfound motivation
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,4 +1,3 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
@ -12,7 +11,6 @@ dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode
|
||||
.idea
|
||||
.DS_Store
|
||||
@ -21,3 +19,5 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
tsconfig.tsbuildinfo
|
@ -2,9 +2,11 @@
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 4,
|
||||
"semi": true,
|
||||
"printWidth": 80,
|
||||
"singleQuote": true,
|
||||
"importOrder": [
|
||||
"^vue$",
|
||||
"^pinia$",
|
||||
"^@?primevue(.*)$",
|
||||
"^@tauri-apps/(.*)$",
|
||||
"^(.*)vue$",
|
||||
|
16
README.md
16
README.md
@ -2,7 +2,7 @@
|
||||
|
||||
A simple and easy to use mod manager for [many games](https://silentblue.remywiki.com/ONGEKI:bright_MEMORY) using [Rainycolor Watercolor](https://rainy.patafour.zip).
|
||||
|
||||
For those who just want a glorified `start.bat` clicker with auto-updates, without VHDs, keychips etc.
|
||||
Intended for those who just want a glorified `start.bat` clicker, without VHDs, keychips etc.
|
||||
(for an all-in-one solution, check out the [BlueSteel launcher](https://yozora.bluesteel.737.jp.net/HarmonyPublic/SOS-Kongou)).
|
||||
|
||||
Made with Rust (Tauri) and Vue. Contributions welcome.
|
||||
@ -28,15 +28,21 @@ More file overrides may be supported in the future.
|
||||
|
||||
Arbitrary scripts are not supported by design and that will probably never change.
|
||||
|
||||
### Features
|
||||
|
||||
- Clean data modding
|
||||
- Multi-platform
|
||||
|
||||
### Todo
|
||||
|
||||
- Updates and auto-updates
|
||||
- CLI
|
||||
- Run from CLI
|
||||
- Support CHUNITHM
|
||||
- Support opts
|
||||
- `unwrap()unwrap()unwrap()unwrap()unwrap()unwrap()unwrap()`
|
||||
- Support segatools as a special package
|
||||
- Progress bars
|
||||
- Only rebuild the profile when needed
|
||||
|
||||
## Endgame
|
||||
|
||||
- Support segatools, IO DLLs and artemis as special packages.
|
||||
- Support IO DLLs and artemis as special packages.
|
||||
- Support other arcade games (if there is demand).
|
||||
|
@ -15,10 +15,12 @@
|
||||
"@primevue/themes": "^4.2.5",
|
||||
"@tailwindcss/vite": "^4.0.6",
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-deep-link": "~2",
|
||||
"@tauri-apps/plugin-dialog": "~2",
|
||||
"@tauri-apps/plugin-opener": "^2",
|
||||
"@tauri-apps/plugin-shell": "~2",
|
||||
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
||||
"pinia": "^3.0.1",
|
||||
"primeicons": "^7.0.0",
|
||||
"primevue": "^4.2.5",
|
||||
"roboto-fontface": "*",
|
||||
|
204
rust/Cargo.lock
generated
204
rust/Cargo.lock
generated
@ -109,12 +109,23 @@ version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"event-listener 5.4.0",
|
||||
"event-listener-strategy",
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-channel"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"event-listener 2.5.3",
|
||||
"futures-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-channel"
|
||||
version = "2.3.1"
|
||||
@ -165,6 +176,21 @@ dependencies = [
|
||||
"futures-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-global-executor"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c"
|
||||
dependencies = [
|
||||
"async-channel 2.3.1",
|
||||
"async-executor",
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"blocking",
|
||||
"futures-lite",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-io"
|
||||
version = "2.4.0"
|
||||
@ -190,7 +216,7 @@ version = "3.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"event-listener 5.4.0",
|
||||
"event-listener-strategy",
|
||||
"pin-project-lite",
|
||||
]
|
||||
@ -201,14 +227,14 @@ version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-channel 2.3.1",
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"async-signal",
|
||||
"async-task",
|
||||
"blocking",
|
||||
"cfg-if",
|
||||
"event-listener",
|
||||
"event-listener 5.4.0",
|
||||
"futures-lite",
|
||||
"rustix",
|
||||
"tracing",
|
||||
@ -243,6 +269,32 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-std"
|
||||
version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c634475f29802fde2b8f0b505b1bd00dfe4df7d4a000f0b36f7671197d5c3615"
|
||||
dependencies = [
|
||||
"async-channel 1.9.0",
|
||||
"async-global-executor",
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"crossbeam-utils",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-lite",
|
||||
"gloo-timers",
|
||||
"kv-log-macro",
|
||||
"log",
|
||||
"memchr",
|
||||
"once_cell",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"slab",
|
||||
"wasm-bindgen-futures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-task"
|
||||
version = "4.7.1"
|
||||
@ -367,7 +419,7 @@ version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-channel 2.3.1",
|
||||
"async-task",
|
||||
"futures-io",
|
||||
"futures-lite",
|
||||
@ -583,6 +635,12 @@ dependencies = [
|
||||
"inout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "closure"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6173fd61b610d15a7566dd7b7620775627441c4ab9dac8906e17cb93a24b782"
|
||||
|
||||
[[package]]
|
||||
name = "cocoa"
|
||||
version = "0.26.0"
|
||||
@ -941,6 +999,27 @@ dependencies = [
|
||||
"syn 2.0.98",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
|
||||
dependencies = [
|
||||
"derive_more-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more-impl"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.98",
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
@ -1185,6 +1264,12 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-listener"
|
||||
version = "2.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
|
||||
|
||||
[[package]]
|
||||
name = "event-listener"
|
||||
version = "5.4.0"
|
||||
@ -1202,7 +1287,7 @@ version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"event-listener 5.4.0",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
@ -1655,6 +1740,18 @@ version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
|
||||
|
||||
[[package]]
|
||||
name = "gloo-timers"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gobject-sys"
|
||||
version = "0.18.0"
|
||||
@ -2286,6 +2383,15 @@ dependencies = [
|
||||
"selectors",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kv-log-macro"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f"
|
||||
dependencies = [
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
@ -2375,6 +2481,9 @@ name = "log"
|
||||
version = "0.4.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f"
|
||||
dependencies = [
|
||||
"value-bag",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lzma-rs"
|
||||
@ -3622,7 +3731,7 @@ dependencies = [
|
||||
"wasm-streams",
|
||||
"web-sys",
|
||||
"webpki-roots",
|
||||
"windows-registry",
|
||||
"windows-registry 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3853,7 +3962,7 @@ checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"cssparser",
|
||||
"derive_more",
|
||||
"derive_more 0.99.19",
|
||||
"fxhash",
|
||||
"log",
|
||||
"matches",
|
||||
@ -4201,7 +4310,10 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-compression",
|
||||
"async-std",
|
||||
"closure",
|
||||
"derive_builder",
|
||||
"derive_more 2.0.1",
|
||||
"directories",
|
||||
"flate2",
|
||||
"futures",
|
||||
@ -4216,9 +4328,11 @@ dependencies = [
|
||||
"simple_logger",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-deep-link",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-opener",
|
||||
"tauri-plugin-shell",
|
||||
"tauri-plugin-single-instance",
|
||||
"tokio",
|
||||
"zip",
|
||||
]
|
||||
@ -4539,6 +4653,26 @@ dependencies = [
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-deep-link"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35d51ffd286073414d26353bcfc9e83e3cd63f96fa7f7a912f92f2118e5de5a6"
|
||||
dependencies = [
|
||||
"dunce",
|
||||
"rust-ini",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"tauri-utils",
|
||||
"thiserror 2.0.11",
|
||||
"tracing",
|
||||
"url",
|
||||
"windows-registry 0.3.0",
|
||||
"windows-result",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-dialog"
|
||||
version = "2.2.0"
|
||||
@ -4623,6 +4757,22 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-single-instance"
|
||||
version = "2.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47c387d4d96690131dc46d1d2827df5c222b896a2bfeb15a16267229a55c50b5"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin-deep-link",
|
||||
"thiserror 2.0.11",
|
||||
"tracing",
|
||||
"windows-sys 0.59.0",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime"
|
||||
version = "2.3.0"
|
||||
@ -5159,6 +5309,12 @@ version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
@ -5217,6 +5373,12 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "value-bag"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ef4c4aa54d5d05a279399bfa921ec387b7aba77caf7a682ae8d86785b8fdad2"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
@ -5621,7 +5783,7 @@ dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
"windows-strings 0.1.0",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
@ -5654,7 +5816,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0"
|
||||
dependencies = [
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
"windows-strings 0.1.0",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bafa604f2104cf5ae2cc2db1dee84b7e6a5d11b05f737b60def0ffdc398cbc0a"
|
||||
dependencies = [
|
||||
"windows-result",
|
||||
"windows-strings 0.2.0",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
@ -5677,6 +5850,15 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "978d65aedf914c664c510d9de43c8fd85ca745eaff1ed53edf409b479e441663"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.45.0"
|
||||
@ -6128,7 +6310,7 @@ dependencies = [
|
||||
"async-trait",
|
||||
"blocking",
|
||||
"enumflags2",
|
||||
"event-listener",
|
||||
"event-listener 5.4.0",
|
||||
"futures-core",
|
||||
"futures-lite",
|
||||
"hex",
|
||||
|
@ -34,4 +34,11 @@ regex = "1.11.1"
|
||||
zip = "2.2.2"
|
||||
tauri-plugin-dialog = "2"
|
||||
anyhow = "1.0.95"
|
||||
tauri-plugin-deep-link = "2"
|
||||
async-std = "1.13.0"
|
||||
closure = "0.3.0"
|
||||
derive_more = { version = "2.0.1", features = ["display"] }
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-single-instance = { version = "2", features = ["deep-link"] }
|
||||
|
||||
|
@ -14,6 +14,7 @@
|
||||
"shell:default",
|
||||
"opener:default",
|
||||
"dialog:default",
|
||||
"dialog:default"
|
||||
"dialog:default",
|
||||
"deep-link:default"
|
||||
]
|
||||
}
|
33
rust/src/appdata.rs
Normal file
33
rust/src/appdata.rs
Normal file
@ -0,0 +1,33 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use crate::pkg::PkgKey;
|
||||
use crate::Profile;
|
||||
use crate::pkg_store::PackageStore;
|
||||
|
||||
pub struct AppData {
|
||||
pub profile: Option<Profile>,
|
||||
pub pkgs: PackageStore,
|
||||
}
|
||||
|
||||
impl AppData {
|
||||
pub fn toggle_package(&mut self, key: PkgKey, enable: bool) -> Result<()> {
|
||||
log::debug!("toggle: {} {}", key, enable);
|
||||
|
||||
let profile = self.profile.as_mut()
|
||||
.ok_or_else(|| anyhow!("No profile"))?;
|
||||
|
||||
if enable {
|
||||
let pkg = self.pkgs.get(key.clone())?;
|
||||
let loc = pkg.loc
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow!("Attempted to enable a non-existent package"))?;
|
||||
profile.mods.insert(key);
|
||||
for d in &loc.dependencies {
|
||||
self.toggle_package(d.clone(), true)?;
|
||||
}
|
||||
} else {
|
||||
profile.mods.remove(&key);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
135
rust/src/cmd.rs
135
rust/src/cmd.rs
@ -1,81 +1,108 @@
|
||||
use log;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::pkg_remote;
|
||||
use crate::pkg_local;
|
||||
use crate::pkg::{Package, PkgKey};
|
||||
use crate::pkg_store::InstallResult;
|
||||
use crate::profile::Profile;
|
||||
use crate::AppData;
|
||||
use crate::model::Package;
|
||||
use crate::appdata::AppData;
|
||||
use crate::{liner, start};
|
||||
|
||||
use tauri::State;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn startline(
|
||||
state: State<'_, Mutex<AppData>>,
|
||||
) -> Result<Option<Profile>, ()> {
|
||||
pub async fn startline(state: State<'_, Mutex<AppData>>) -> Result<(), String> {
|
||||
log::debug!("invoke: startline");
|
||||
|
||||
let appd = state.lock().await;
|
||||
|
||||
Ok(appd.profile.clone())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn download_package(pkg: Package) -> Result<(), String> {
|
||||
log::debug!("invoke: download_package");
|
||||
|
||||
pkg_remote::download_package(pkg).await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_package(namespace: String, name: String) -> Result<(), String> {
|
||||
log::debug!("invoke: download_package");
|
||||
|
||||
pkg_local::delete_package(namespace, name).await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn reload_packages(state: State<'_, tokio::sync::Mutex<AppData>>) -> Result<(), String> {
|
||||
log::debug!("invoke: reload_packages");
|
||||
|
||||
let mut appd = state.lock().await;
|
||||
// todo: this should only fetch new things
|
||||
match pkg_local::walk_packages(false).await {
|
||||
Ok(m) => {
|
||||
appd.mods_local = m;
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
Err(e.to_string())
|
||||
}
|
||||
if let Some(p) = &appd.profile {
|
||||
// TODO if p.needsUpdate
|
||||
liner::line_up(p).await.expect("Line-up failed");
|
||||
start::start(p).map_err(|e| e.to_string()).map(|_| ())
|
||||
//Ok(())
|
||||
} else {
|
||||
Err("No profile".to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_packages(state: State<'_, Mutex<AppData>>) -> Result<Vec<Package>, ()> {
|
||||
log::debug!("invoke: get_packages");
|
||||
pub async fn install_package(state: State<'_, tokio::sync::Mutex<AppData>>, key: PkgKey) -> Result<InstallResult, String> {
|
||||
log::debug!("invoke: install_package");
|
||||
|
||||
let mut appd = state.lock().await;
|
||||
let rv = appd.pkgs.install_package(&key, true)
|
||||
.await
|
||||
.map_err(|e| e.to_string());
|
||||
|
||||
|
||||
// if rv.is_ok() {
|
||||
// _ = appd.toggle_package(key, true);
|
||||
// }
|
||||
|
||||
rv
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_package(state: State<'_, tokio::sync::Mutex<AppData>>, key: PkgKey) -> Result<(), String> {
|
||||
log::debug!("invoke: delete_package");
|
||||
|
||||
let mut appd = state.lock().await;
|
||||
appd.pkgs.delete_package(&key, true)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_package(state: State<'_, tokio::sync::Mutex<AppData>>, key: PkgKey) -> Result<Package, String> {
|
||||
log::debug!("invoke: get_package");
|
||||
|
||||
let appd = state.lock().await;
|
||||
appd.pkgs.get(key)
|
||||
.map_err(|e| e.to_string())
|
||||
.cloned()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn toggle_package(state: State<'_, tokio::sync::Mutex<AppData>>, key: PkgKey, enable: bool) -> Result<(), String> {
|
||||
log::debug!("invoke: toggle_package");
|
||||
|
||||
let mut appd = state.lock().await;
|
||||
appd.toggle_package(key, enable)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn reload_all_packages(state: State<'_, tokio::sync::Mutex<AppData>>) -> Result<(), String> {
|
||||
log::debug!("invoke: reload_all_packages");
|
||||
|
||||
let mut appd = state.lock().await;
|
||||
appd.pkgs.reload_all()
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_all_packages(state: State<'_, Mutex<AppData>>) -> Result<HashMap<PkgKey, Package>, ()> {
|
||||
log::debug!("invoke: get_all_packages");
|
||||
|
||||
let appd = state.lock().await;
|
||||
|
||||
Ok(appd.mods_local.clone())
|
||||
Ok(appd.pkgs.get_all())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_listings(state: State<'_, Mutex<AppData>>) -> Result<Vec<Package>, String> {
|
||||
log::debug!("invoke: get_listings");
|
||||
pub async fn fetch_listings(state: State<'_, Mutex<AppData>>) -> Result<(), String> {
|
||||
log::debug!("invoke: fetch_listings");
|
||||
|
||||
let mut appd = state.lock().await;
|
||||
match pkg_remote::get_listings(&mut appd).await {
|
||||
Ok(l) => Ok(l.clone()),
|
||||
Err(e) => Err(e.to_string())
|
||||
}
|
||||
appd.pkgs.fetch_listings().await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_current_profile(
|
||||
state: State<'_, Mutex<AppData>>,
|
||||
) -> Result<Option<Profile>, ()> {
|
||||
pub async fn get_current_profile(state: State<'_, Mutex<AppData>>) -> Result<Option<Profile>, ()> {
|
||||
log::debug!("invoke: get_current_profile");
|
||||
|
||||
let appd = state.lock().await;
|
||||
@ -83,9 +110,7 @@ pub async fn get_current_profile(
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn save_profile(
|
||||
state: State<'_, Mutex<AppData>>
|
||||
) -> Result<(), ()> {
|
||||
pub async fn save_profile(state: State<'_, Mutex<AppData>>) -> Result<(), ()> {
|
||||
log::debug!("invoke: save_profile");
|
||||
|
||||
let appd = state.lock().await;
|
||||
@ -101,7 +126,7 @@ pub async fn save_profile(
|
||||
#[tauri::command]
|
||||
pub async fn init_profile(
|
||||
state: State<'_, Mutex<AppData>>,
|
||||
path: PathBuf
|
||||
path: PathBuf,
|
||||
) -> Result<Profile, String> {
|
||||
log::debug!("invoke: init_profile");
|
||||
|
||||
@ -112,4 +137,4 @@ pub async fn init_profile(
|
||||
appd.profile = Some(new_profile.clone());
|
||||
|
||||
Ok(new_profile)
|
||||
}
|
||||
}
|
||||
|
57
rust/src/download_handler.rs
Normal file
57
rust/src/download_handler.rs
Normal file
@ -0,0 +1,57 @@
|
||||
use std::{collections::HashSet, path::PathBuf};
|
||||
use tauri::{AppHandle, Emitter};
|
||||
use tokio::fs::File;
|
||||
use anyhow::{anyhow, Result};
|
||||
|
||||
use crate::pkg::{Package, PkgKey, Remote};
|
||||
|
||||
pub struct DownloadHandler {
|
||||
set: HashSet<String>,
|
||||
app: AppHandle
|
||||
}
|
||||
|
||||
impl DownloadHandler {
|
||||
pub fn new(app: AppHandle) -> DownloadHandler {
|
||||
DownloadHandler {
|
||||
set: HashSet::new(),
|
||||
app
|
||||
}
|
||||
}
|
||||
|
||||
pub fn download_zip(&mut self, zip_path: &PathBuf, pkg: &Package) -> Result<()> {
|
||||
let rmt = pkg.rmt.as_ref()
|
||||
.ok_or_else(|| anyhow!("Attempted to download a package without remote data"))?
|
||||
.clone();
|
||||
if self.set.contains(zip_path.to_string_lossy().as_ref()) {
|
||||
Err(anyhow!("Already downloading"))
|
||||
} else {
|
||||
self.set.insert(zip_path.to_string_lossy().to_string());
|
||||
tauri::async_runtime::spawn(Self::download_zip_proc(self.app.clone(), zip_path.clone(), pkg.key(), rmt));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn download_zip_proc(app: AppHandle, zip_path: PathBuf, pkg_key: PkgKey, rmt: Remote) -> Result<()> {
|
||||
use futures::StreamExt;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
// let zip_path_part = zip_path.add_extension("part");
|
||||
let mut zip_path_part = zip_path.to_owned();
|
||||
zip_path_part.set_extension("zip.part");
|
||||
|
||||
let mut cache_file_w = File::create(&zip_path_part).await?;
|
||||
let mut byte_stream = reqwest::get(&rmt.download_url).await?.bytes_stream();
|
||||
|
||||
log::info!("Downloading: {}", rmt.download_url);
|
||||
while let Some(item) = byte_stream.next().await {
|
||||
let i = item?;
|
||||
cache_file_w.write_all(&mut i.as_ref()).await?;
|
||||
}
|
||||
cache_file_w.sync_all().await?;
|
||||
tokio::fs::rename(&zip_path_part, &zip_path).await?;
|
||||
|
||||
app.emit("download-end", pkg_key)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
152
rust/src/lib.rs
152
rust/src/lib.rs
@ -1,30 +1,34 @@
|
||||
mod cfg;
|
||||
mod model;
|
||||
mod cmd;
|
||||
mod pkg_remote;
|
||||
mod pkg_local;
|
||||
mod util;
|
||||
mod model;
|
||||
mod pkg;
|
||||
mod pkg_store;
|
||||
mod profile;
|
||||
mod util;
|
||||
mod start;
|
||||
mod liner;
|
||||
mod download_handler;
|
||||
mod appdata;
|
||||
|
||||
use tokio::{fs, try_join};
|
||||
use tokio::sync::Mutex;
|
||||
use model::Package;
|
||||
use tauri::Manager;
|
||||
use closure::closure;
|
||||
use appdata::AppData;
|
||||
use pkg::PkgKey;
|
||||
use pkg_store::PackageStore;
|
||||
use profile::Profile;
|
||||
|
||||
struct AppData {
|
||||
profile: Option<Profile>,
|
||||
mods_local: Vec<Package>,
|
||||
mods_store: Vec<Package>,
|
||||
}
|
||||
use tauri::{Listener, Manager};
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
use tokio::{sync::Mutex, fs, try_join};
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub async fn run(args: Vec<String>) {
|
||||
simple_logger::init_with_env().unwrap();
|
||||
pub async fn run(_args: Vec<String>) {
|
||||
simple_logger::init_with_env()
|
||||
.expect("Unable to initialize the logger");
|
||||
|
||||
log::info!(
|
||||
"Running from {}",
|
||||
std::env::current_dir().unwrap_or_default().to_str().unwrap_or_default()
|
||||
std::env::current_dir()
|
||||
.unwrap_or_default()
|
||||
.to_str()
|
||||
.unwrap_or_default()
|
||||
);
|
||||
|
||||
try_join!(
|
||||
@ -33,35 +37,87 @@ pub async fn run(args: Vec<String>) {
|
||||
fs::create_dir_all(util::cache_dir())
|
||||
).expect("Unable to create working directories");
|
||||
|
||||
let app_data = AppData {
|
||||
profile: pkg_local::load_config(),
|
||||
mods_local: pkg_local::walk_packages(true).await.expect("Unable to scan local packages"),
|
||||
mods_store: [].to_vec(),
|
||||
};
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
|
||||
let _ = app
|
||||
.get_webview_window("main")
|
||||
.expect("No main window")
|
||||
.set_focus();
|
||||
if args.len() == 2 {
|
||||
// Todo deindent this chimera
|
||||
let url = &args[1];
|
||||
if &url[..13] == "rainycolor://" {
|
||||
let regex = regex::Regex::new(
|
||||
r"rainycolor://v1/install/rainy\.patafour\.zip/([^/]+)/([^/]+)/[0-9]+\.[0-9]+\.[0-9]+/"
|
||||
).expect("Invalid regex");
|
||||
if let Some(caps) = regex.captures(url) {
|
||||
if caps.len() == 3 {
|
||||
let apph = app.clone();
|
||||
let key = PkgKey(format!("{}-{}", caps.get(1).unwrap().as_str(), caps.get(2).unwrap().as_str()));
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let mutex = apph.state::<Mutex<AppData>>();
|
||||
let mut appd = mutex.lock().await;
|
||||
_ = appd.pkgs.fetch_listings().await;
|
||||
if let Err(e) = appd.pkgs.install_package(&key, true).await {
|
||||
log::warn!("Fail: {}", e.to_string());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
.plugin(tauri_plugin_deep_link::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.setup(|app| {
|
||||
let app_data = AppData {
|
||||
profile: Profile::load(),
|
||||
pkgs: PackageStore::new(app.handle().clone())
|
||||
};
|
||||
|
||||
if args.len() == 1 {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.setup(|app| {
|
||||
app.manage(Mutex::new(app_data));
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
cmd::get_packages,
|
||||
cmd::reload_packages,
|
||||
cmd::get_listings,
|
||||
cmd::download_package,
|
||||
cmd::delete_package,
|
||||
cmd::get_current_profile,
|
||||
cmd::init_profile,
|
||||
cmd::save_profile,
|
||||
cmd::startline
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
} else {
|
||||
panic!("Not implemented");
|
||||
}
|
||||
app.manage(Mutex::new(app_data));
|
||||
app.deep_link().register_all()?;
|
||||
|
||||
let apph = app.handle();
|
||||
|
||||
app.listen("download-end", closure!(clone apph, |ev| {
|
||||
let raw = ev.payload();
|
||||
let key = PkgKey(raw[1..raw.len()-1].to_owned());
|
||||
let apph = apph.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let mutex = apph.state::<Mutex<AppData>>();
|
||||
let mut appd = mutex.lock().await;
|
||||
_ = appd.pkgs.install_package(&key, true).await;
|
||||
});
|
||||
}));
|
||||
|
||||
app.listen("install-end", closure!(clone apph, |ev| {
|
||||
let payload = serde_json::from_str::<pkg_store::Payload>(&ev.payload());
|
||||
let apph = apph.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let mutex = apph.state::<Mutex<AppData>>();
|
||||
let mut appd = mutex.lock().await;
|
||||
_ = appd.toggle_package(payload.unwrap().pkg, true);
|
||||
});
|
||||
}));
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
cmd::get_package,
|
||||
cmd::get_all_packages,
|
||||
cmd::reload_all_packages,
|
||||
cmd::fetch_listings,
|
||||
cmd::install_package,
|
||||
cmd::delete_package,
|
||||
cmd::toggle_package,
|
||||
cmd::get_current_profile,
|
||||
cmd::init_profile,
|
||||
cmd::save_profile,
|
||||
cmd::startline
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
50
rust/src/liner.rs
Normal file
50
rust/src/liner.rs
Normal file
@ -0,0 +1,50 @@
|
||||
use tokio::task::JoinSet;
|
||||
use anyhow::Result;
|
||||
use tokio::fs;
|
||||
use crate::util;
|
||||
use crate::profile::Profile;
|
||||
|
||||
pub async fn line_up(p: &Profile) -> Result<()> {
|
||||
let dir_out = util::profile_dir(&p);
|
||||
log::info!("Preparing {}", dir_out.to_string_lossy());
|
||||
|
||||
let mut futures = JoinSet::new();
|
||||
if dir_out.join("BepInEx").exists() {
|
||||
futures.spawn(fs::remove_dir_all(dir_out.join("BepInEx")));
|
||||
}
|
||||
if dir_out.join("option").exists() {
|
||||
futures.spawn(fs::remove_dir_all(dir_out.join("option")));
|
||||
}
|
||||
while let Some(_) = futures.join_next().await {}
|
||||
|
||||
fs::create_dir_all(dir_out.join("option")).await?;
|
||||
|
||||
log::debug!("--");
|
||||
for m in &p.mods {
|
||||
log::debug!("{}", m.0);
|
||||
let dir_out = util::profile_dir(&p);
|
||||
let (namespace, name) = m.0.split_at(m.0.find("-").expect("Invalid mod definition"));
|
||||
let bpx = util::pkg_dir_of(namespace, &name[1..])
|
||||
.join("app")
|
||||
.join("BepInEx");
|
||||
if bpx.exists() {
|
||||
util::copy_recursive(&bpx, &dir_out.join("BepInEx"))?;
|
||||
}
|
||||
|
||||
let opt = util::pkg_dir_of(namespace, &name[1..]).join("option");
|
||||
if opt.exists() {
|
||||
let x = opt.read_dir().unwrap().next().unwrap()?;
|
||||
if x.metadata()?.is_dir() {
|
||||
fs::symlink(&x.path(), &dir_out.join("option").join(x.file_name())).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
log::debug!("--");
|
||||
|
||||
for opt in p.path.join("option").read_dir()? {
|
||||
let opt = opt?;
|
||||
fs::symlink(&opt.path(), &dir_out.join("option").join(opt.file_name())).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
use serde::Deserialize;
|
||||
use crate::pkg::PkgKeyVersion;
|
||||
|
||||
// manifest.json
|
||||
|
||||
@ -8,4 +9,5 @@ pub struct PackageManifest {
|
||||
pub name: String,
|
||||
pub version_number: String,
|
||||
pub description: String,
|
||||
}
|
||||
pub dependencies: Vec<PkgKeyVersion>
|
||||
}
|
||||
|
@ -1,33 +1,7 @@
|
||||
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub enum Game {
|
||||
Ongeki,
|
||||
Chunithm,
|
||||
}
|
||||
|
||||
impl Serialize for Game {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
match self {
|
||||
Game::Ongeki => serializer.serialize_str("ongeki"),
|
||||
Game::Chunithm => serializer.serialize_str("chunithm"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Game {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Game, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
match s.as_str() {
|
||||
"chunithm" => Ok(Game::Chunithm),
|
||||
"ongeki" => Ok(Game::Ongeki),
|
||||
_ => Err(de::Error::custom("unknown game")),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,23 +1,3 @@
|
||||
pub mod local;
|
||||
pub mod misc;
|
||||
pub mod rainy;
|
||||
|
||||
use derive_builder::Builder;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Builder, Default, Serialize, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub struct Package {
|
||||
pub namespace: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub package_url: String,
|
||||
pub download_url: String,
|
||||
pub path: String,
|
||||
pub enabled: bool,
|
||||
pub icon: String,
|
||||
pub version: String,
|
||||
pub version_available: String,
|
||||
pub deprecated: bool,
|
||||
pub dependencies: Vec<Package>,
|
||||
}
|
||||
pub mod rainy;
|
@ -1,4 +1,5 @@
|
||||
use serde::Deserialize;
|
||||
use crate::pkg::PkgKeyVersion;
|
||||
|
||||
// /c/{game}/api/v1/package
|
||||
|
||||
@ -14,34 +15,10 @@ pub struct V1Package {
|
||||
#[derive(Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub struct V1Version {
|
||||
// no namespace
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub version_number: String,
|
||||
pub icon: String,
|
||||
pub dependencies: Vec<String>,
|
||||
pub dependencies: Vec<PkgKeyVersion>,
|
||||
pub download_url: String,
|
||||
}
|
||||
|
||||
// /api/experimental/{namespace}/{name}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub struct V0Package {
|
||||
pub owner: String,
|
||||
pub package_url: String,
|
||||
pub is_deprecated: bool,
|
||||
pub latest: V0Version,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub struct V0Version {
|
||||
pub namespace: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub version_number: String,
|
||||
pub icon: String,
|
||||
pub dependencies: Vec<String>,
|
||||
pub download_url: String,
|
||||
}
|
||||
}
|
168
rust/src/pkg.rs
Normal file
168
rust/src/pkg.rs
Normal file
@ -0,0 +1,168 @@
|
||||
use anyhow::{Result, anyhow, bail};
|
||||
use derive_more::Display;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::{Path, PathBuf};
|
||||
use tokio::fs;
|
||||
use crate::{model::{local, rainy}, util};
|
||||
|
||||
// {namespace}-{name}
|
||||
#[derive(Eq, Hash, PartialEq, Clone, Serialize, Deserialize, Display)]
|
||||
pub struct PkgKey(pub String);
|
||||
|
||||
// {namespace}-{name}-{version}
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct PkgKeyVersion(String);
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub struct Package {
|
||||
pub namespace: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub icon: String,
|
||||
pub loc: Option<Local>,
|
||||
pub rmt: Option<Remote>
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize)]
|
||||
pub enum Kind {
|
||||
#[default] Mod,
|
||||
UnsupportedMod
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub struct Local {
|
||||
pub version: String,
|
||||
pub path: PathBuf,
|
||||
pub dependencies: Vec<PkgKey>,
|
||||
pub kind: Kind
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub struct Remote {
|
||||
pub version: String,
|
||||
pub package_url: String,
|
||||
pub download_url: String,
|
||||
pub deprecated: bool,
|
||||
pub dependencies: Vec<PkgKey>
|
||||
}
|
||||
|
||||
impl Package {
|
||||
pub fn from_rainy(mut p: rainy::V1Package) -> Option<Package> {
|
||||
if p.versions.len() == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let v = p.versions.swap_remove(0);
|
||||
|
||||
Some(Package {
|
||||
namespace: p.owner,
|
||||
name: v.name,
|
||||
description: v.description,
|
||||
icon: v.icon,
|
||||
loc: None,
|
||||
rmt: Some(Remote {
|
||||
package_url: p.package_url,
|
||||
download_url: v.download_url,
|
||||
deprecated: p.is_deprecated,
|
||||
version: v.version_number,
|
||||
dependencies: Self::sanitize_deps(v.dependencies)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn from_dir(dir: PathBuf) -> Result<Package> {
|
||||
let str = fs::read_to_string(dir.join("manifest.json")).await?;
|
||||
let mft: local::PackageManifest = serde_json::from_str(&str)?;
|
||||
|
||||
let icon = dir.join("icon.png")
|
||||
.as_os_str()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_owned();
|
||||
|
||||
let dependencies = Self::sanitize_deps(mft.dependencies);
|
||||
|
||||
Ok(Package {
|
||||
namespace: Self::dir_to_namespace(&dir)?,
|
||||
name: mft.name.clone(),
|
||||
description: mft.description.clone(),
|
||||
icon,
|
||||
loc: Some(Local {
|
||||
version: mft.version_number,
|
||||
path: dir.to_owned(),
|
||||
kind: Kind::Mod,
|
||||
dependencies
|
||||
}),
|
||||
rmt: None
|
||||
})
|
||||
}
|
||||
|
||||
pub fn key(&self) -> PkgKey {
|
||||
PkgKey(format!("{}-{}", self.namespace, self.name))
|
||||
}
|
||||
|
||||
pub fn path(&self) -> PathBuf {
|
||||
util::pkg_dir().join(self.key().0)
|
||||
}
|
||||
|
||||
pub fn _dir_to_key(dir: &Path) -> Result<String> {
|
||||
let (key, _) = Self::parse_dir_name(dir)?;
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
pub fn dir_to_namespace(dir: &Path) -> Result<String> {
|
||||
let (_, n) = Self::parse_dir_name(dir)?;
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
fn manifest(dir: &Path) -> Result<local::PackageManifest> {
|
||||
serde_json::from_reader(std::fs::File::open(dir.join("manifest.json"))?)
|
||||
.map_err(|err| anyhow!("Invalid manifest: {}", err))
|
||||
}
|
||||
|
||||
fn parse_dir_name(dir: &Path) -> Result<(String, String)> {
|
||||
let mft = Self::manifest(dir)?;
|
||||
let regex = regex::Regex::new(r"([A-Za-z0-9_]+)-([A-Za-z0-9_]+)$")?;
|
||||
let dir_name = dir.file_name()
|
||||
.to_owned()
|
||||
.ok_or_else(|| anyhow!("Invalid directory name"))?
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow!("Illegal directory name"))?;
|
||||
|
||||
let namespace;
|
||||
|
||||
if let Some(caps) = regex.captures(dir_name) {
|
||||
let name_match = caps.get(2)
|
||||
.ok_or_else(|| anyhow!("Invalid directory name"))?;
|
||||
|
||||
if name_match.as_str() != mft.name {
|
||||
bail!("Invalid manifest or directory name");
|
||||
}
|
||||
|
||||
namespace = caps.get(1)
|
||||
.ok_or_else(|| anyhow!("Invalid directory name?"))?
|
||||
.as_str()
|
||||
.to_owned();
|
||||
|
||||
Ok((format!("{}-{}", namespace, mft.name), namespace))
|
||||
} else {
|
||||
bail!("Error reading {}: invalid directory name", dir_name);
|
||||
}
|
||||
}
|
||||
|
||||
fn sanitize_deps(mut deps: Vec<PkgKeyVersion>) -> Vec<PkgKey> {
|
||||
let regex = regex::Regex::new(r"([A-Za-z0-9_]+)-([A-Za-z0-9_]+)-[0-9\.]+$")
|
||||
.expect("Invalid regex");
|
||||
|
||||
for i in 0..deps.len() {
|
||||
let caps = regex.captures(&deps[i].0)
|
||||
.expect("Invalid dependency");
|
||||
deps[i] = PkgKeyVersion(format!("{}-{}", caps.get(1).unwrap().as_str(), caps.get(2).unwrap().as_str()));
|
||||
}
|
||||
let rv: Vec<PkgKey> = unsafe { std::mem::transmute(deps) };
|
||||
rv
|
||||
}
|
||||
}
|
@ -1,106 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use std::fs::{self, File};
|
||||
|
||||
use crate::{pkg_remote, profile::Profile, model::{self, local, Package}, util};
|
||||
|
||||
pub fn load_config() -> Option<Profile> {
|
||||
let path = util::get_dirs().config_dir().join("profile-ongeki-default.json");
|
||||
if let Ok(s) = fs::read_to_string(path) {
|
||||
Some(serde_json::from_str(&s).expect("Invalid profile json"))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn walk_packages(fetch_remote: bool) -> Result<Vec<Package>> {
|
||||
let mut res = [].to_vec();
|
||||
|
||||
let packages = fs::read_dir(util::get_dirs().data_dir().join("pkg"))?;
|
||||
|
||||
for package in packages {
|
||||
let dir = package.unwrap().path();
|
||||
let mft: local::PackageManifest = serde_json::from_reader(
|
||||
File::open(dir.join("manifest.json"))?
|
||||
)?;
|
||||
let regex = regex::Regex::new(r"([A-Za-z0-9_]+)-([A-Za-z0-9_]+)$")?;
|
||||
let dir_name = dir.file_name().to_owned().unwrap().to_str().unwrap();
|
||||
let namespace;
|
||||
|
||||
if let Some(caps) = regex.captures(dir_name) {
|
||||
if caps.len() != 3 || caps.get(2).unwrap().as_str() != mft.name {
|
||||
log::error!(
|
||||
"Error reading {}: invalid manifest or directory name",
|
||||
dir_name
|
||||
);
|
||||
continue;
|
||||
}
|
||||
namespace = caps.get(1).unwrap().as_str();
|
||||
} else {
|
||||
log::error!("Error reading {}: invalid directory name", dir_name);
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut builder = model::PackageBuilder::default();
|
||||
|
||||
builder
|
||||
.name(mft.name.to_owned())
|
||||
.namespace(namespace.to_owned())
|
||||
.enabled(false);
|
||||
|
||||
builder.package_url(format!(
|
||||
"https://rainy.patafour.zip/package/{}/{}",
|
||||
namespace, mft.name
|
||||
));
|
||||
|
||||
builder
|
||||
.version(mft.version_number.clone())
|
||||
.description(mft.description)
|
||||
.icon(
|
||||
dir.join("icon.png")
|
||||
.as_os_str()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_owned(),
|
||||
)
|
||||
.path(dir.as_os_str().to_str().unwrap().to_owned())
|
||||
.dependencies([].to_vec());
|
||||
|
||||
builder
|
||||
.version_available(mft.version_number)
|
||||
.download_url("".to_owned())
|
||||
.deprecated(false);
|
||||
|
||||
if fetch_remote == true {
|
||||
if let Ok(rem) = pkg_remote::get_remote_meta(namespace, &mft.name).await {
|
||||
builder.version_available(rem.latest.version_number);
|
||||
builder.download_url(rem.latest.download_url);
|
||||
builder.deprecated(rem.is_deprecated);
|
||||
}
|
||||
}
|
||||
|
||||
match builder.build() {
|
||||
Ok(r) => {
|
||||
res.push(r);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Bad package: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn delete_package(namespace: String, name: String) -> Result<(), tokio::io::Error> {
|
||||
let path = util::get_dirs()
|
||||
.data_dir()
|
||||
.join("pkg")
|
||||
.join(format!("{}-{}", namespace, name));
|
||||
|
||||
if path.exists() && path.join("manifest.json").exists() {
|
||||
log::debug!("rm -r'ing {}", path.to_string_lossy());
|
||||
tokio::fs::remove_dir_all(&path).await
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,105 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use tokio::fs::{self, File};
|
||||
use crate::{pkg_local, util, AppData};
|
||||
use crate::model::{rainy, Package};
|
||||
|
||||
async fn fetch_listings() -> Result<Vec<Package>> {
|
||||
use async_compression::futures::bufread::GzipDecoder;
|
||||
use futures::{
|
||||
io::{self, BufReader, ErrorKind},
|
||||
prelude::*,
|
||||
};
|
||||
let response = reqwest::get("https://rainy.patafour.zip/c/ongeki/api/v1/package/")
|
||||
.await?;
|
||||
let reader = response
|
||||
.bytes_stream()
|
||||
.map_err(|e| io::Error::new(ErrorKind::Other, e))
|
||||
.into_async_read();
|
||||
let mut decoder = GzipDecoder::new(BufReader::new(reader));
|
||||
let mut data = String::new();
|
||||
decoder.read_to_string(&mut data).await?;
|
||||
|
||||
let listings: Vec<rainy::V1Package> = serde_json::from_str(&data).expect("Fuck2");
|
||||
|
||||
let mut res: Vec<Package> = [].to_vec();
|
||||
for l in listings {
|
||||
if let Some(v) = l.versions.last() {
|
||||
let mut p = Package::default();
|
||||
p.name = v.name.clone();
|
||||
p.namespace = l.owner.clone();
|
||||
p.description = v.description.clone();
|
||||
p.version = v.version_number.clone();
|
||||
p.icon = v.icon.clone();
|
||||
p.package_url = l.package_url.clone();
|
||||
p.download_url = v.download_url.clone();
|
||||
res.push(p);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn get_listings(appd: &mut AppData) -> Result<&Vec<Package>> {
|
||||
if appd.mods_store.len() == 0 {
|
||||
let listings = fetch_listings().await?;
|
||||
appd.mods_store = listings;
|
||||
}
|
||||
Ok(&appd.mods_store)
|
||||
}
|
||||
|
||||
pub async fn get_remote_meta(
|
||||
namespace: &str,
|
||||
name: &str,
|
||||
) -> Result<rainy::V0Package> {
|
||||
let url = format!(
|
||||
"https://rainy.patafour.zip/api/experimental/package/{}/{}/",
|
||||
namespace, name
|
||||
);
|
||||
let res = reqwest::get(url).await?.text().await?;
|
||||
let package: rainy::V0Package = serde_json::from_str(&res)?;
|
||||
|
||||
Ok(package)
|
||||
}
|
||||
|
||||
pub async fn download_package(pkg: Package) -> Result<()> {
|
||||
use futures::StreamExt;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
let zip_path = util::cache_dir().join(format!(
|
||||
"{}-{}-{}.zip",
|
||||
pkg.namespace, pkg.name, pkg.version
|
||||
));
|
||||
|
||||
if !zip_path.exists() {
|
||||
// let zip_path_part = zip_path.add_extension("part");
|
||||
let mut zip_path_part = zip_path.clone();
|
||||
zip_path_part.set_extension("zip.part");
|
||||
let mut cache_file_w = File::create(&zip_path_part).await?;
|
||||
let mut byte_stream = reqwest::get(&pkg.download_url)
|
||||
.await?
|
||||
.bytes_stream();
|
||||
|
||||
log::info!("downloading: {}", pkg.download_url);
|
||||
while let Some(item) = byte_stream.next().await {
|
||||
let i = item?;
|
||||
cache_file_w.write_all(&mut i.as_ref()).await?;
|
||||
}
|
||||
cache_file_w.sync_all().await?;
|
||||
tokio::fs::rename(&zip_path_part, &zip_path).await?;
|
||||
}
|
||||
|
||||
let cache_file_r = std::fs::File::open(&zip_path)?;
|
||||
let mut archive = zip::ZipArchive::new(cache_file_r)?;
|
||||
|
||||
pkg_local::delete_package(pkg.namespace.clone(), pkg.name.clone()).await?;
|
||||
|
||||
let path = util::get_dirs()
|
||||
.data_dir()
|
||||
.join("pkg")
|
||||
.join(format!("{}-{}", pkg.namespace, pkg.name));
|
||||
|
||||
fs::create_dir(&path).await?;
|
||||
archive.extract(path)?;
|
||||
|
||||
Ok(())
|
||||
}
|
204
rust/src/pkg_store.rs
Normal file
204
rust/src/pkg_store.rs
Normal file
@ -0,0 +1,204 @@
|
||||
use std::collections::HashMap;
|
||||
use anyhow::{Result, anyhow};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{AppHandle, Emitter};
|
||||
use tokio::fs;
|
||||
use tokio::task::JoinSet;
|
||||
use crate::model::rainy;
|
||||
use crate::pkg::{Package, PkgKey};
|
||||
use crate::util;
|
||||
use crate::download_handler::DownloadHandler;
|
||||
|
||||
pub struct PackageStore {
|
||||
store: HashMap<PkgKey, Package>,
|
||||
has_fetched: bool,
|
||||
app: AppHandle,
|
||||
dlh: DownloadHandler
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Payload {
|
||||
pub pkg: PkgKey
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub enum InstallResult {
|
||||
Ready, Deferred
|
||||
}
|
||||
|
||||
impl PackageStore {
|
||||
pub fn new(app: AppHandle) -> PackageStore {
|
||||
PackageStore {
|
||||
store: HashMap::new(),
|
||||
has_fetched: false,
|
||||
app: app.clone(),
|
||||
dlh: DownloadHandler::new(app)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self, key: PkgKey) -> Result<&Package> {
|
||||
self.store.get(&key)
|
||||
.ok_or_else(|| anyhow!("Invalid package key"))
|
||||
}
|
||||
|
||||
pub fn get_all(&self) -> HashMap<PkgKey, Package> {
|
||||
self.store.clone()
|
||||
}
|
||||
|
||||
pub async fn reload_package(&mut self, key: PkgKey) {
|
||||
let dir = util::pkg_dir().join(&key.0);
|
||||
if let Ok(pkg) = Package::from_dir(dir).await {
|
||||
self.update_package(key, pkg);
|
||||
} else {
|
||||
log::error!("couldn't reload {}", key);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn reload_all(&mut self) -> Result<()> {
|
||||
let dirents = std::fs::read_dir(util::pkg_dir())?;
|
||||
let mut futures = JoinSet::new();
|
||||
|
||||
for dir in dirents {
|
||||
if let Ok(dir) = dir {
|
||||
let path = dir.path();
|
||||
futures.spawn(Package::from_dir(path));
|
||||
}
|
||||
}
|
||||
|
||||
while let Some(res) = futures.join_next().await {
|
||||
if let Ok(Ok(pkg)) = res {
|
||||
self.update_package(pkg.key(), pkg);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn fetch_listings(&mut self) -> Result<()> {
|
||||
if self.has_fetched {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
use async_compression::futures::bufread::GzipDecoder;
|
||||
use futures::{
|
||||
io::{self, BufReader, ErrorKind},
|
||||
prelude::*,
|
||||
};
|
||||
|
||||
let response = reqwest::get("https://rainy.patafour.zip/c/ongeki/api/v1/package/").await?;
|
||||
let reader = response
|
||||
.bytes_stream()
|
||||
.map_err(|e| io::Error::new(ErrorKind::Other, e))
|
||||
.into_async_read();
|
||||
|
||||
let mut decoder = GzipDecoder::new(BufReader::new(reader));
|
||||
let mut data = String::new();
|
||||
decoder.read_to_string(&mut data).await?;
|
||||
|
||||
let listings: Vec<rainy::V1Package> = serde_json::from_str(&data)
|
||||
.expect("Invalid JSON");
|
||||
|
||||
for listing in listings {
|
||||
// This is None if the package has no versions for whatever reason
|
||||
if let Some(r) = Package::from_rainy(listing) {
|
||||
//log::warn!("D {}", &r.rmt.as_ref().unwrap().dependencies.first().unwrap_or(&"Nothing".to_owned()));
|
||||
match self.store.get_mut(&r.key()) {
|
||||
Some(l) => {
|
||||
l.rmt = r.rmt;
|
||||
}
|
||||
None => {
|
||||
self.store.insert(r.key(), r);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.has_fetched = true;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn install_package(&mut self, key: &PkgKey, force: bool) -> Result<InstallResult> {
|
||||
log::debug!("Installing {}", key);
|
||||
|
||||
let pkg = self.store.get(key)
|
||||
.ok_or_else(|| anyhow!("Attempted to install a nonexistent pkg"))?
|
||||
.clone();
|
||||
|
||||
if pkg.loc.is_some() && !force {
|
||||
return Ok(InstallResult::Ready);
|
||||
}
|
||||
let rmt = pkg.rmt.as_ref() //clone()
|
||||
.ok_or_else(|| anyhow!("Attempted to install a pkg without remote data"))?;
|
||||
|
||||
for dep in &rmt.dependencies {
|
||||
self.app.emit("install-start", Payload {
|
||||
pkg: dep.to_owned()
|
||||
})?;
|
||||
Box::pin(self.install_package(&dep, false)).await?;
|
||||
}
|
||||
|
||||
let zip_path = util::cache_dir().join(format!(
|
||||
"{}-{}-{}.zip",
|
||||
pkg.namespace, pkg.name, rmt.version
|
||||
));
|
||||
|
||||
if !zip_path.exists() {
|
||||
self.dlh.download_zip(&zip_path, &pkg)?;
|
||||
return Ok(InstallResult::Deferred);
|
||||
}
|
||||
|
||||
let cache_file_r = std::fs::File::open(&zip_path)?;
|
||||
let mut archive = zip::ZipArchive::new(cache_file_r)?;
|
||||
|
||||
self.delete_package(key, false).await?;
|
||||
|
||||
let path = pkg.path();
|
||||
fs::create_dir(&path).await?;
|
||||
archive.extract(path)?;
|
||||
self.reload_package(key.to_owned()).await;
|
||||
|
||||
self.app.emit("install-end", Payload {
|
||||
pkg: key.to_owned()
|
||||
})?;
|
||||
|
||||
log::info!("Installed {}", key);
|
||||
|
||||
Ok(InstallResult::Ready)
|
||||
}
|
||||
|
||||
pub async fn delete_package(&mut self, key: &PkgKey, force: bool) -> Result<()> {
|
||||
let pkg = self.store.get_mut(key)
|
||||
.ok_or_else(|| anyhow!("Attempted to delete a nonexistent pkg"))?;
|
||||
let path = pkg.path();
|
||||
|
||||
if path.exists() && path.join("manifest.json").exists() {
|
||||
// TODO don't rm -r - use a file whitelist
|
||||
log::debug!("rm -r'ing {}", path.to_string_lossy());
|
||||
pkg.loc = None;
|
||||
let rv = tokio::fs::remove_dir_all(&path).await
|
||||
.map_err(|e| anyhow!("Could not delete a package: {}", e));
|
||||
|
||||
if rv.is_ok() {
|
||||
self.app.emit("install-end", Payload {
|
||||
pkg: key.to_owned()
|
||||
})?;
|
||||
log::info!("Deleted {}", key);
|
||||
}
|
||||
rv
|
||||
} else {
|
||||
if force {
|
||||
Err(anyhow!("Nothing to delete"))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_package(&mut self, key: PkgKey, mut new: Package) {
|
||||
if let Some(old) = self.store.get(&key) {
|
||||
new.rmt = old.rmt.clone();
|
||||
}
|
||||
self.store.insert(key, new);
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
use std::path::PathBuf;
|
||||
use std::{collections::HashSet, path::{Path, PathBuf}};
|
||||
|
||||
use crate::{model::misc, pkg::PkgKey, util};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::fs;
|
||||
use crate::{model::misc, util};
|
||||
|
||||
// {game}-profile-{name}.json
|
||||
|
||||
@ -12,22 +12,53 @@ pub struct Profile {
|
||||
pub game: misc::Game,
|
||||
pub path: PathBuf,
|
||||
pub name: String,
|
||||
pub mods: Vec<String>,
|
||||
pub mods: HashSet<PkgKey>,
|
||||
pub wine_runtime: Option<PathBuf>,
|
||||
pub wine_prefix: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Profile {
|
||||
pub fn new(path: PathBuf) -> Profile {
|
||||
Profile {
|
||||
game: misc::Game::Ongeki,
|
||||
path: path,
|
||||
path: path.parent().unwrap().to_owned(),
|
||||
name: "ongeki-default".to_owned(),
|
||||
mods: [].to_vec()
|
||||
mods: HashSet::new(),
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
wine_runtime: Some(Path::new("/usr/bin/wine").to_path_buf()),
|
||||
#[cfg(target_os = "windows")]
|
||||
wine_runtime: None,
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
wine_prefix: Some(
|
||||
directories::UserDirs::new()
|
||||
.expect("No home directory")
|
||||
.home_dir()
|
||||
.join(".wine"),
|
||||
),
|
||||
#[cfg(target_os = "windows")]
|
||||
wine_prefix: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load() -> Option<Profile> {
|
||||
let path = util::get_dirs()
|
||||
.config_dir()
|
||||
.join("profile-ongeki-default.json");
|
||||
if let Ok(s) = std::fs::read_to_string(path) {
|
||||
Some(serde_json::from_str(&s).expect("Invalid profile json"))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn save(&self) {
|
||||
let path = util::get_dirs().config_dir().join("profile-ongeki-default.json");
|
||||
let path = util::get_dirs()
|
||||
.config_dir()
|
||||
.join("profile-ongeki-default.json");
|
||||
let s = serde_json::to_string_pretty(self).unwrap();
|
||||
fs::write(&path, s).await.unwrap();
|
||||
log::info!("Written to {}", path.to_string_lossy());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
16
rust/src/start.rs
Normal file
16
rust/src/start.rs
Normal file
@ -0,0 +1,16 @@
|
||||
use anyhow::Result;
|
||||
use tokio::process::{Child, Command};
|
||||
use crate::profile::Profile;
|
||||
use crate::util;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn start(p: &Profile) -> Result<Child> {
|
||||
Ok(Command::new(p.wine_runtime.as_ref().unwrap())
|
||||
.env(
|
||||
"SEGATOOLS_CONFIG_PATH",
|
||||
util::profile_dir(&p).join("segatools.ini"),
|
||||
)
|
||||
.env("WINEPREFIX", p.wine_prefix.as_ref().unwrap())
|
||||
.arg(p.path.join("start.bat"))
|
||||
.spawn()?)
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
use std::path::PathBuf;
|
||||
use directories::ProjectDirs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::profile::Profile;
|
||||
|
||||
pub fn get_dirs() -> ProjectDirs {
|
||||
ProjectDirs::from("org", "7EVENDAYSHOLIDAYS", "STARTLINER")
|
||||
@ -14,6 +16,31 @@ pub fn pkg_dir() -> PathBuf {
|
||||
get_dirs().data_dir().join("pkg").to_owned()
|
||||
}
|
||||
|
||||
pub fn pkg_dir_of(namespace: &str, name: &str) -> PathBuf {
|
||||
pkg_dir().join(format!("{}-{}", namespace, name)).to_owned()
|
||||
}
|
||||
|
||||
pub fn profile_dir(p: &Profile) -> PathBuf {
|
||||
get_dirs()
|
||||
.data_dir()
|
||||
.join("profile-".to_owned() + &p.name)
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
pub fn cache_dir() -> PathBuf {
|
||||
get_dirs().cache_dir().to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn copy_recursive(src: &Path, dst: &Path) -> std::io::Result<()> {
|
||||
std::fs::create_dir_all(&dst).unwrap();
|
||||
for entry in std::fs::read_dir(src)? {
|
||||
let entry = entry?;
|
||||
let meta = entry.metadata()?;
|
||||
if meta.is_dir() {
|
||||
copy_recursive(&entry.path(), &dst.join(entry.file_name()))?;
|
||||
} else {
|
||||
std::fs::copy(&entry.path(), &dst.join(entry.file_name()))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1,48 +1,53 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "STARTLINER",
|
||||
"version": "0.1.0",
|
||||
"identifier": "moe.tendokyu.akanyan.startliner",
|
||||
"build": {
|
||||
"beforeDevCommand": "bun run dev",
|
||||
"devUrl": "http://localhost:1420",
|
||||
"beforeBuildCommand": "bun run build",
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
"plugins": {
|
||||
"fs": {
|
||||
"requireLiteralLeadingDot": false
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "STARTLINER",
|
||||
"version": "0.1.0",
|
||||
"identifier": "moe.tendokyu.akanyan.startliner",
|
||||
"build": {
|
||||
"beforeDevCommand": "bun run dev",
|
||||
"devUrl": "http://localhost:1420",
|
||||
"beforeBuildCommand": "bun run build",
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
"plugins": {
|
||||
"fs": {
|
||||
"requireLiteralLeadingDot": false
|
||||
},
|
||||
"deep-link": {
|
||||
"desktop": {
|
||||
"schemes": ["rainycolor"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "STARTLINER",
|
||||
"width": 600,
|
||||
"height": 500,
|
||||
"minWidth": 600,
|
||||
"minHeight": 500
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": {
|
||||
"img-src": "'self' asset: https: blob: data:"
|
||||
},
|
||||
"assetProtocol": {
|
||||
"enable": true,
|
||||
"scope": ["**/*", "**/.*/**/*"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "STARTLINER",
|
||||
"width": 600,
|
||||
"height": 500,
|
||||
"minWidth": 600,
|
||||
"minHeight": 500
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": {
|
||||
"img-src": "'self' asset: https: blob: data:"
|
||||
},
|
||||
"assetProtocol": {
|
||||
"enable": true,
|
||||
"scope": ["**/*", "**/.*/**/*"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { Ref, onMounted, ref } from 'vue';
|
||||
import { updatePrimaryPalette } from '@primevue/themes';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import Button from 'primevue/button';
|
||||
import Tab from 'primevue/tab';
|
||||
import TabList from 'primevue/tablist';
|
||||
@ -8,37 +7,23 @@ import TabPanel from 'primevue/tabpanel';
|
||||
import TabPanels from 'primevue/tabpanels';
|
||||
import Tabs from 'primevue/tabs';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { onOpenUrl } from '@tauri-apps/plugin-deep-link';
|
||||
import { open } from '@tauri-apps/plugin-dialog';
|
||||
import ModList from './ModList.vue';
|
||||
import ModStore from './ModStore.vue';
|
||||
import Options from './Options.vue';
|
||||
import { Profile } from '../types';
|
||||
import { usePkgStore } from '../stores';
|
||||
import { changePrimaryColor } from '../util';
|
||||
|
||||
const changePrimaryColor = (game: 'ongeki' | 'chunithm') => {
|
||||
const color = game === 'ongeki' ? 'pink' : 'yellow';
|
||||
const store = usePkgStore();
|
||||
store.setupListeners();
|
||||
|
||||
updatePrimaryPalette({
|
||||
50: `{${color}.50}`,
|
||||
100: `{${color}.100}`,
|
||||
200: `{${color}.200}`,
|
||||
300: `{${color}.300}`,
|
||||
400: `{${color}.400}`,
|
||||
500: `{${color}.500}`,
|
||||
600: `{${color}.600}`,
|
||||
700: `{${color}.700}`,
|
||||
800: `{${color}.800}`,
|
||||
900: `{${color}.900}`,
|
||||
950: `{${color}.950}`,
|
||||
});
|
||||
};
|
||||
|
||||
let profile: Ref<Profile | null> = ref(null);
|
||||
let key = ref(0);
|
||||
const currentTab = ref('3');
|
||||
|
||||
const loadProfile = async () => {
|
||||
profile = await invoke('get_current_profile');
|
||||
await store.reloadProfile();
|
||||
|
||||
if (profile === null) {
|
||||
if (store.profile === null) {
|
||||
const file = await open({
|
||||
multiple: false,
|
||||
directory: false,
|
||||
@ -50,33 +35,46 @@ const loadProfile = async () => {
|
||||
],
|
||||
});
|
||||
if (file !== null) {
|
||||
profile = await invoke('init_profile', { path: file });
|
||||
await store.initProfile(file);
|
||||
}
|
||||
}
|
||||
key.value += 1;
|
||||
if (store.profile !== null) {
|
||||
changePrimaryColor(store.profile.game);
|
||||
currentTab.value = '0';
|
||||
}
|
||||
|
||||
await store.reloadAll();
|
||||
};
|
||||
|
||||
const isDisabled = () => profile === null;
|
||||
const isProfileDisabled = computed(() => store.profile === null);
|
||||
|
||||
const startline = () => {
|
||||
invoke('startline');
|
||||
|
||||
//startDisabled.value = true;
|
||||
};
|
||||
|
||||
onOpenUrl((urls) => {
|
||||
console.log('deep link:', urls);
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await loadProfile();
|
||||
});
|
||||
|
||||
changePrimaryColor('ongeki');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<Tabs lazy value="3" class="h-screen">
|
||||
<Tabs lazy :value="currentTab" class="h-screen">
|
||||
<div class="fixed w-full flex z-100">
|
||||
<TabList class="grow">
|
||||
<Tab :disabled="isDisabled()" :key="key" value="0"
|
||||
<Tab :disabled="isProfileDisabled" value="0"
|
||||
><div class="pi pi-list-check"></div
|
||||
></Tab>
|
||||
<Tab :disabled="isDisabled()" :key="key" value="1"
|
||||
<Tab :disabled="isProfileDisabled" value="1"
|
||||
><div class="pi pi-download"></div
|
||||
></Tab>
|
||||
<Tab :disabled="isDisabled()" :key="key" value="2"
|
||||
<Tab :disabled="isProfileDisabled" value="2"
|
||||
><div class="pi pi-cog"></div
|
||||
></Tab>
|
||||
<Tab value="3"
|
||||
@ -84,18 +82,19 @@ changePrimaryColor('ongeki');
|
||||
></Tab>
|
||||
<div class="grow"></div>
|
||||
<Button
|
||||
disabled
|
||||
:disabled="false"
|
||||
icon="pi pi-play"
|
||||
label="START"
|
||||
aria-label="start"
|
||||
size="small"
|
||||
class="m-2.5"
|
||||
@click="startline()"
|
||||
/>
|
||||
</TabList>
|
||||
</div>
|
||||
<TabPanels class="w-full grow mt-[3rem]">
|
||||
<TabPanel value="0">
|
||||
<ModList :profile="profile!" />
|
||||
<ModList />
|
||||
</TabPanel>
|
||||
<TabPanel value="1">
|
||||
<ModStore />
|
||||
@ -106,6 +105,7 @@ changePrimaryColor('ongeki');
|
||||
<TabPanel value="3">
|
||||
UNDER CONSTRUCTION<br /><br />
|
||||
<Button
|
||||
:disabled="!isProfileDisabled"
|
||||
label="Create profile"
|
||||
icon="pi pi-plus"
|
||||
aria-label="open-executable"
|
||||
|
58
src/components/InstallButton.vue
Normal file
58
src/components/InstallButton.vue
Normal file
@ -0,0 +1,58 @@
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { Package } from '../types';
|
||||
import { pkgKey } from '../util';
|
||||
|
||||
const props = defineProps({
|
||||
pkg: Object as () => Package,
|
||||
});
|
||||
|
||||
const install = async () => {
|
||||
if (props.pkg === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
await invoke('install_package', { key: pkgKey(props.pkg) });
|
||||
|
||||
//if (rv === 'Deferred') { /* download progress */ }
|
||||
};
|
||||
|
||||
const remove = async () => {
|
||||
if (props.pkg === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
await invoke('delete_package', {
|
||||
key: pkgKey(props.pkg),
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Button
|
||||
v-if="pkg?.loc"
|
||||
rounded
|
||||
icon="pi pi-trash"
|
||||
severity="danger"
|
||||
aria-label="remove"
|
||||
size="small"
|
||||
class="self-center ml-4"
|
||||
style="width: 2rem; height: 2rem"
|
||||
:loading="false"
|
||||
v-on:click="remove()"
|
||||
/>
|
||||
|
||||
<Button
|
||||
v-else
|
||||
rounded
|
||||
icon="pi pi-plus"
|
||||
severity="success"
|
||||
aria-label="install"
|
||||
size="small"
|
||||
class="self-center ml-4"
|
||||
style="width: 2rem; height: 2rem"
|
||||
:loading="false"
|
||||
v-on:click="install()"
|
||||
/>
|
||||
</template>
|
@ -1,32 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import { Reactive, onMounted, reactive } from 'vue';
|
||||
import Fieldset from 'primevue/fieldset';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import ModListEntry from './ModListEntry.vue';
|
||||
import { ModEntry, Profile } from '../types';
|
||||
import { usePkgStore } from '../stores';
|
||||
import { Profile } from '../types';
|
||||
|
||||
const mods: Reactive<{ [key: string]: ModEntry[] }> = reactive({});
|
||||
|
||||
const props = defineProps({
|
||||
defineProps({
|
||||
profile: Object as () => Profile,
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
const modsRaw: ModEntry[] = await invoke('get_packages');
|
||||
modsRaw.forEach((m) => {
|
||||
if (props.profile?.mods.includes(`${m.namespace}-${m.name}`)) {
|
||||
m.enabled = true;
|
||||
}
|
||||
});
|
||||
Object.assign(
|
||||
mods,
|
||||
Object.groupBy(modsRaw, ({ namespace }) => namespace)
|
||||
const pkgs = usePkgStore();
|
||||
|
||||
const group = () => {
|
||||
const a = Object.assign(
|
||||
{},
|
||||
Object.groupBy(pkgs.allLocal, ({ namespace }) => namespace)
|
||||
);
|
||||
});
|
||||
return a;
|
||||
};
|
||||
|
||||
pkgs.reloadProfile();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fieldset v-for="(namespace, key) in mods" :legend="key.toString()">
|
||||
<ModListEntry v-for="m in namespace" :mod="m" />
|
||||
<Fieldset v-for="(namespace, key) in group()" :legend="key.toString()">
|
||||
<ModListEntry v-for="p in namespace" :pkg="p" />
|
||||
</Fieldset>
|
||||
</template>
|
||||
|
@ -2,32 +2,33 @@
|
||||
import Button from 'primevue/button';
|
||||
import ToggleSwitch from 'primevue/toggleswitch';
|
||||
import { open } from '@tauri-apps/plugin-shell';
|
||||
import InstallButton from './InstallButton.vue';
|
||||
import ModTitlecard from './ModTitlecard.vue';
|
||||
import { ModEntry } from '../types';
|
||||
import { usePkgStore } from '../stores';
|
||||
import { Package } from '../types';
|
||||
|
||||
defineProps({
|
||||
mod: Object as () => ModEntry,
|
||||
const store = usePkgStore();
|
||||
|
||||
const props = defineProps({
|
||||
pkg: Object as () => Package,
|
||||
});
|
||||
|
||||
const toggle = (value: boolean) => {
|
||||
store.toggle(props.pkg, value);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center">
|
||||
<ModTitlecard showVersion :localIcon="true" :mod="mod" />
|
||||
<ModTitlecard showVersion :pkg="pkg" />
|
||||
<ToggleSwitch
|
||||
class="scale-[1.33] shrink-0"
|
||||
inputId="switch"
|
||||
:modelValue="mod?.enabled"
|
||||
/>
|
||||
<Button
|
||||
rounded
|
||||
disabled
|
||||
icon="pi pi-trash"
|
||||
severity="danger"
|
||||
aria-label="delete"
|
||||
size="small"
|
||||
class="ml-5 self-center shrink-0"
|
||||
style="width: 2rem; height: 2rem"
|
||||
:disabled="!pkg?.loc"
|
||||
:modelValue="store.isEnabled(pkg)"
|
||||
v-on:value-change="toggle"
|
||||
/>
|
||||
<InstallButton />
|
||||
<Button
|
||||
rounded
|
||||
icon="pi pi-folder"
|
||||
@ -36,7 +37,7 @@ defineProps({
|
||||
size="small"
|
||||
class="ml-2 shrink-0"
|
||||
style="width: 2rem; height: 2rem"
|
||||
v-on:click="open(mod?.path ?? '')"
|
||||
v-on:click="pkg?.loc && open(pkg.loc.path ?? '')"
|
||||
/>
|
||||
<Button
|
||||
rounded
|
||||
@ -46,7 +47,7 @@ defineProps({
|
||||
size="small"
|
||||
class="ml-2 shrink-0"
|
||||
style="width: 2rem; height: 2rem"
|
||||
v-on:click="open(mod?.package_url ?? '')"
|
||||
v-on:click="pkg?.rmt && open(pkg.rmt.package_url ?? '')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -1,35 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { Reactive, onMounted, reactive } from 'vue';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { onMounted } from 'vue';
|
||||
import ModStoreEntry from './ModStoreEntry.vue';
|
||||
import { ModEntry } from '../types';
|
||||
import { usePkgStore } from '../stores';
|
||||
|
||||
const local: Reactive<{ [key: string]: ModEntry }> = reactive({});
|
||||
const listings: Reactive<ModEntry[]> = reactive([]);
|
||||
const pkgs = usePkgStore();
|
||||
|
||||
const reload = async () => {
|
||||
const modsRaw: ModEntry[] = await invoke('get_packages');
|
||||
Object.keys(local).forEach((key) => {
|
||||
delete local[key];
|
||||
});
|
||||
for (const m of modsRaw) {
|
||||
local[`${m.namespace}-${m.name}`] = m;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
Object.assign(listings, await invoke('get_listings'));
|
||||
reload();
|
||||
onMounted(() => {
|
||||
pkgs.fetch();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-for="l in listings" class="flex flex-row">
|
||||
<ModStoreEntry
|
||||
:mod="l"
|
||||
:isLocal="local[`${l.namespace}-${l.name}`] !== undefined"
|
||||
v-on:updated="reload()"
|
||||
/>
|
||||
<div v-for="p in pkgs.allRemote" class="flex flex-row">
|
||||
<ModStoreEntry :pkg="p" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -1,48 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import Button from 'primevue/button';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { open } from '@tauri-apps/plugin-shell';
|
||||
import InstallButton from './InstallButton.vue';
|
||||
import ModTitlecard from './ModTitlecard.vue';
|
||||
import { ModEntry } from '../types';
|
||||
import { Package } from '../types';
|
||||
|
||||
const emit = defineEmits(['updated']);
|
||||
const props = defineProps({
|
||||
mod: Object as () => ModEntry,
|
||||
isLocal: Boolean,
|
||||
defineProps({
|
||||
pkg: Object as () => Package,
|
||||
});
|
||||
|
||||
const isLoading = ref(false);
|
||||
|
||||
const handlePackageButton = async () => {
|
||||
isLoading.value = true;
|
||||
if (!props.isLocal) {
|
||||
await invoke('download_package', { pkg: props.mod });
|
||||
} else {
|
||||
await invoke('delete_package', {
|
||||
namespace: props.mod?.namespace,
|
||||
name: props.mod?.name,
|
||||
});
|
||||
}
|
||||
await invoke('reload_packages');
|
||||
emit('updated');
|
||||
isLoading.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ModTitlecard :mod="mod" showNamespace />
|
||||
<Button
|
||||
rounded
|
||||
:icon="isLocal ? 'pi pi-trash' : 'pi pi-plus'"
|
||||
:severity="isLocal ? 'danger' : 'success'"
|
||||
aria-label="install"
|
||||
size="small"
|
||||
class="self-center"
|
||||
style="width: 2rem; height: 2rem"
|
||||
:loading="isLoading"
|
||||
v-on:click="handlePackageButton()"
|
||||
/>
|
||||
<ModTitlecard :pkg="pkg" showNamespace />
|
||||
<InstallButton :pkg="pkg" />
|
||||
<Button
|
||||
rounded
|
||||
icon="pi pi-external-link"
|
||||
@ -51,7 +21,8 @@ const handlePackageButton = async () => {
|
||||
size="small"
|
||||
class="self-center ml-2"
|
||||
style="width: 2rem; height: 2rem"
|
||||
v-on:click="open(mod?.package_url ?? '')"
|
||||
:disabled="!pkg?.rmt"
|
||||
v-on:click="open(pkg?.rmt?.package_url ?? '')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
@ -1,19 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import { convertFileSrc } from '@tauri-apps/api/core';
|
||||
import { ModEntry } from '../types';
|
||||
import { Package } from '../types';
|
||||
|
||||
defineProps({
|
||||
mod: Object as () => ModEntry,
|
||||
modelValue: Boolean,
|
||||
localIcon: Boolean,
|
||||
const props = defineProps({
|
||||
pkg: Object as () => Package,
|
||||
showNamespace: Boolean,
|
||||
showVersion: Boolean,
|
||||
});
|
||||
|
||||
const iconSrc = () => {
|
||||
const icon = props.pkg?.icon;
|
||||
|
||||
if (icon === undefined) {
|
||||
return '';
|
||||
} else if (icon.startsWith('https://')) {
|
||||
return icon;
|
||||
} else {
|
||||
return convertFileSrc(icon);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<img
|
||||
:src="localIcon ? convertFileSrc(mod?.icon ?? '') : mod?.icon"
|
||||
:src="iconSrc()"
|
||||
class="self-center rounded-sm"
|
||||
width="32px"
|
||||
height="32px"
|
||||
@ -21,23 +31,23 @@ defineProps({
|
||||
<label class="m-3 align-middle text grow z-5 h-50px" for="switch">
|
||||
<div>
|
||||
<span class="text-lg">
|
||||
{{ mod?.name ?? 'Untitled' }}
|
||||
{{ pkg?.name ?? 'Untitled' }}
|
||||
</span>
|
||||
<span
|
||||
v-if="showNamespace && mod?.namespace"
|
||||
v-if="showNamespace && pkg?.namespace"
|
||||
class="text-sm opacity-75"
|
||||
>
|
||||
by {{ mod.namespace }}
|
||||
by {{ pkg.namespace }}
|
||||
</span>
|
||||
<span
|
||||
v-if="showVersion && mod?.version"
|
||||
v-if="showVersion && pkg?.loc?.version"
|
||||
class="text-sm opacity-75 m-2"
|
||||
>
|
||||
{{ mod.version ?? '?.?.?' }}
|
||||
{{ pkg?.loc?.version ?? '?.?.?' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{{ mod?.description ?? 'No description' }}
|
||||
{{ pkg?.description ?? 'No description' }}
|
||||
</div>
|
||||
</label>
|
||||
</template>
|
||||
|
@ -1,13 +1,16 @@
|
||||
import { createApp } from 'vue';
|
||||
import { createPinia } from 'pinia';
|
||||
import { definePreset } from '@primevue/themes';
|
||||
import Theme from '@primevue/themes/aura';
|
||||
import PrimeVue from 'primevue/config';
|
||||
import App from './components/App.vue';
|
||||
|
||||
const pinia = createPinia();
|
||||
const app = createApp(App);
|
||||
|
||||
const Preset = definePreset(Theme, {});
|
||||
|
||||
app.use(pinia);
|
||||
app.use(PrimeVue, {
|
||||
theme: {
|
||||
preset: Preset,
|
||||
|
106
src/stores.ts
Normal file
106
src/stores.ts
Normal file
@ -0,0 +1,106 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import { Package, Profile } from './types';
|
||||
import { pkgKey } from './util';
|
||||
|
||||
type InstallStatus = {
|
||||
pkg: string;
|
||||
};
|
||||
|
||||
export const usePkgStore = defineStore('pkg', {
|
||||
state: (): { pkg: { [key: string]: Package }; prf: Profile | null } => {
|
||||
return {
|
||||
pkg: {},
|
||||
prf: null,
|
||||
};
|
||||
},
|
||||
getters: {
|
||||
fromDepString: (state) => (str: string) => state.pkg[str] ?? null,
|
||||
fromName: (state) => (namespace: string, name: string) =>
|
||||
state.pkg[`${namespace}-${name}`] ?? null,
|
||||
all: (state) => Object.values(state),
|
||||
allLocal: (state) => Object.values(state.pkg).filter((p) => p.loc),
|
||||
allRemote: (state) => Object.values(state.pkg).filter((p) => p.rmt),
|
||||
profile: (state) => state.prf,
|
||||
isEnabled: (state) => (pkg: Package | undefined) =>
|
||||
pkg !== undefined && state.prf?.mods.includes(pkgKey(pkg)),
|
||||
},
|
||||
actions: {
|
||||
setupListeners() {
|
||||
listen<InstallStatus>('install-start', async (ev) => {
|
||||
await this.reload(ev.payload.pkg);
|
||||
});
|
||||
|
||||
listen<InstallStatus>('install-end', async (ev) => {
|
||||
await this.reload(ev.payload.pkg);
|
||||
await this.reloadProfile();
|
||||
});
|
||||
},
|
||||
|
||||
async reloadAll() {
|
||||
await invoke('reload_all_packages');
|
||||
const data = (await invoke('get_all_packages')) as {
|
||||
[key: string]: Package;
|
||||
};
|
||||
|
||||
for (const k of Object.keys(this)) {
|
||||
delete this.pkg[k];
|
||||
}
|
||||
|
||||
Object.assign(this.pkg, data);
|
||||
|
||||
if (this.prf !== null)
|
||||
for (const [k, v] of Object.entries(this)) {
|
||||
if (this.prf.mods.includes(k)) {
|
||||
if (v.loc) {
|
||||
v.loc.enabled = true;
|
||||
} else {
|
||||
console.error(`${k} enabled but not present`);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async reload(pkgOrKey: string | Package) {
|
||||
const key =
|
||||
typeof pkgOrKey === 'string' ? pkgOrKey : pkgKey(pkgOrKey);
|
||||
const rv: Package = await invoke('get_package', {
|
||||
key,
|
||||
});
|
||||
if (this.pkg[key] === undefined) {
|
||||
this.pkg[key] = {} as Package;
|
||||
} else {
|
||||
this.pkg[key].loc = null;
|
||||
this.pkg[key].rmt = null;
|
||||
}
|
||||
Object.assign(this.pkg[key], rv);
|
||||
},
|
||||
|
||||
async initProfile(path: string) {
|
||||
this.prf = await invoke('init_profile', { path });
|
||||
},
|
||||
|
||||
async saveProfile() {
|
||||
await invoke('save_profile');
|
||||
},
|
||||
|
||||
async reloadProfile() {
|
||||
this.prf = await invoke('get_current_profile');
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
await invoke('fetch_listings');
|
||||
await this.reloadAll();
|
||||
},
|
||||
|
||||
async toggle(pkg: Package | undefined, enable: boolean) {
|
||||
if (pkg === undefined) {
|
||||
return;
|
||||
}
|
||||
await invoke('toggle_package', { key: pkgKey(pkg), enable });
|
||||
await this.reload(pkg);
|
||||
await this.saveProfile();
|
||||
},
|
||||
},
|
||||
});
|
22
src/types.ts
22
src/types.ts
@ -1,17 +1,25 @@
|
||||
export interface ModEntry {
|
||||
export interface Package {
|
||||
namespace: string;
|
||||
name: string;
|
||||
description: string;
|
||||
package_url: string;
|
||||
icon: string;
|
||||
path: string;
|
||||
version: string;
|
||||
version_available: string;
|
||||
enabled: boolean;
|
||||
loc: {
|
||||
version: string;
|
||||
path: string;
|
||||
dependencies: string[];
|
||||
} | null;
|
||||
rmt: {
|
||||
version: string;
|
||||
package_url: string;
|
||||
download_url: string;
|
||||
deprecated: boolean;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface Profile {
|
||||
name: string;
|
||||
game: 'ongeki' | 'chunithm';
|
||||
game: 'Ongeki' | 'Chunithm';
|
||||
mods: string[];
|
||||
}
|
||||
|
||||
export type PackageC = Map<string, Package>;
|
||||
|
22
src/util.ts
Normal file
22
src/util.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { updatePrimaryPalette } from '@primevue/themes';
|
||||
import { Package } from './types';
|
||||
|
||||
export const changePrimaryColor = (game: 'Ongeki' | 'Chunithm') => {
|
||||
const color = game === 'Ongeki' ? 'pink' : 'yellow';
|
||||
|
||||
updatePrimaryPalette({
|
||||
50: `{${color}.50}`,
|
||||
100: `{${color}.100}`,
|
||||
200: `{${color}.200}`,
|
||||
300: `{${color}.300}`,
|
||||
400: `{${color}.400}`,
|
||||
500: `{${color}.500}`,
|
||||
600: `{${color}.600}`,
|
||||
700: `{${color}.700}`,
|
||||
800: `{${color}.800}`,
|
||||
900: `{${color}.900}`,
|
||||
950: `{${color}.950}`,
|
||||
});
|
||||
};
|
||||
|
||||
export const pkgKey = (pkg: Package) => `${pkg.namespace}-${pkg.name}`;
|
Reference in New Issue
Block a user