forked from akanyan/STARTLINER
feat: less bad installations
This commit is contained in:
15
README.md
15
README.md
@ -1,12 +1,23 @@
|
|||||||
## STARTLINER
|
## STARTLINER
|
||||||
|
|
||||||
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).
|
wipwipwipwipwipwipwipwipwipwipwipwipwipwipwipwipwipwipwipwipwipwipwipwipwipwipwipwipwipwipwipwipwipwipwipwip
|
||||||
|
|
||||||
|
A simple (_not yet_) and easy to use (_not yet_) mod manager for [many games](https://silentblue.remywiki.com/ONGEKI:bright_MEMORY) (_more to come_) using [Rainycolor Watercolor](https://rainy.patafour.zip).
|
||||||
|
|
||||||
Intended for those who just want a glorified `start.bat` clicker, 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)).
|
(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.
|
Made with Rust (Tauri) and Vue. Contributions welcome.
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
wipwipwipwipwipwipwipwipwipwipwipwip
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bun install
|
||||||
|
bun run tauri dev
|
||||||
|
```
|
||||||
|
|
||||||
### Package format
|
### Package format
|
||||||
|
|
||||||
- [Package format requirements](https://rainy.patafour.zip/package/create/docs/)
|
- [Package format requirements](https://rainy.patafour.zip/package/create/docs/)
|
||||||
@ -31,7 +42,7 @@ Arbitrary scripts are not supported by design and that will probably never chang
|
|||||||
### Features
|
### Features
|
||||||
|
|
||||||
- Clean data modding
|
- Clean data modding
|
||||||
- Multi-platform
|
- Technically multi-platform
|
||||||
|
|
||||||
### Todo
|
### Todo
|
||||||
|
|
||||||
|
@ -26,6 +26,13 @@ impl AppData {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
profile.mods.remove(&key);
|
profile.mods.remove(&key);
|
||||||
|
for (ckey, pkg) in self.pkgs.get_all() {
|
||||||
|
if let Some(loc) = pkg.loc {
|
||||||
|
if loc.dependencies.contains(&key) {
|
||||||
|
self.toggle_package(ckey, false)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -29,24 +29,17 @@ pub async fn startline(state: State<'_, Mutex<AppData>>) -> Result<(), String> {
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn install_package(state: State<'_, tokio::sync::Mutex<AppData>>, key: PkgKey) -> Result<InstallResult, String> {
|
pub async fn install_package(state: State<'_, tokio::sync::Mutex<AppData>>, key: PkgKey) -> Result<InstallResult, String> {
|
||||||
log::debug!("invoke: install_package");
|
log::debug!("invoke: install_package({})", key);
|
||||||
|
|
||||||
let mut appd = state.lock().await;
|
let mut appd = state.lock().await;
|
||||||
let rv = appd.pkgs.install_package(&key, true)
|
appd.pkgs.install_package(&key, true, true)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string());
|
.map_err(|e| e.to_string())
|
||||||
|
|
||||||
|
|
||||||
// if rv.is_ok() {
|
|
||||||
// _ = appd.toggle_package(key, true);
|
|
||||||
// }
|
|
||||||
|
|
||||||
rv
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn delete_package(state: State<'_, tokio::sync::Mutex<AppData>>, key: PkgKey) -> Result<(), String> {
|
pub async fn delete_package(state: State<'_, tokio::sync::Mutex<AppData>>, key: PkgKey) -> Result<(), String> {
|
||||||
log::debug!("invoke: delete_package");
|
log::debug!("invoke: delete_package({})", key);
|
||||||
|
|
||||||
let mut appd = state.lock().await;
|
let mut appd = state.lock().await;
|
||||||
appd.pkgs.delete_package(&key, true)
|
appd.pkgs.delete_package(&key, true)
|
||||||
@ -56,7 +49,7 @@ pub async fn delete_package(state: State<'_, tokio::sync::Mutex<AppData>>, key:
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_package(state: State<'_, tokio::sync::Mutex<AppData>>, key: PkgKey) -> Result<Package, String> {
|
pub async fn get_package(state: State<'_, tokio::sync::Mutex<AppData>>, key: PkgKey) -> Result<Package, String> {
|
||||||
log::debug!("invoke: get_package");
|
log::debug!("invoke: get_package({})", key);
|
||||||
|
|
||||||
let appd = state.lock().await;
|
let appd = state.lock().await;
|
||||||
appd.pkgs.get(key)
|
appd.pkgs.get(key)
|
||||||
@ -66,7 +59,7 @@ pub async fn get_package(state: State<'_, tokio::sync::Mutex<AppData>>, key: Pkg
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn toggle_package(state: State<'_, tokio::sync::Mutex<AppData>>, key: PkgKey, enable: bool) -> Result<(), String> {
|
pub async fn toggle_package(state: State<'_, tokio::sync::Mutex<AppData>>, key: PkgKey, enable: bool) -> Result<(), String> {
|
||||||
log::debug!("invoke: toggle_package");
|
log::debug!("invoke: toggle_package({}, {})", key, enable);
|
||||||
|
|
||||||
let mut appd = state.lock().await;
|
let mut appd = state.lock().await;
|
||||||
appd.toggle_package(key, enable)
|
appd.toggle_package(key, enable)
|
||||||
|
@ -23,6 +23,7 @@ impl DownloadHandler {
|
|||||||
.ok_or_else(|| anyhow!("Attempted to download a package without remote data"))?
|
.ok_or_else(|| anyhow!("Attempted to download a package without remote data"))?
|
||||||
.clone();
|
.clone();
|
||||||
if self.set.contains(zip_path.to_string_lossy().as_ref()) {
|
if self.set.contains(zip_path.to_string_lossy().as_ref()) {
|
||||||
|
// Todo when there is a clear cache button, it should clear the set
|
||||||
Err(anyhow!("Already downloading"))
|
Err(anyhow!("Already downloading"))
|
||||||
} else {
|
} else {
|
||||||
self.set.insert(zip_path.to_string_lossy().to_string());
|
self.set.insert(zip_path.to_string_lossy().to_string());
|
||||||
|
@ -58,7 +58,7 @@ pub async fn run(_args: Vec<String>) {
|
|||||||
let mutex = apph.state::<Mutex<AppData>>();
|
let mutex = apph.state::<Mutex<AppData>>();
|
||||||
let mut appd = mutex.lock().await;
|
let mut appd = mutex.lock().await;
|
||||||
_ = appd.pkgs.fetch_listings().await;
|
_ = appd.pkgs.fetch_listings().await;
|
||||||
if let Err(e) = appd.pkgs.install_package(&key, true).await {
|
if let Err(e) = appd.pkgs.install_package(&key, true, true).await {
|
||||||
log::warn!("Fail: {}", e.to_string());
|
log::warn!("Fail: {}", e.to_string());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -89,7 +89,7 @@ pub async fn run(_args: Vec<String>) {
|
|||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
let mutex = apph.state::<Mutex<AppData>>();
|
let mutex = apph.state::<Mutex<AppData>>();
|
||||||
let mut appd = mutex.lock().await;
|
let mut appd = mutex.lock().await;
|
||||||
_ = appd.pkgs.install_package(&key, true).await;
|
_ = appd.pkgs.install_package(&key, true, false).await;
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::path::Path;
|
||||||
use anyhow::{Result, anyhow};
|
use anyhow::{Result, anyhow};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tauri::{AppHandle, Emitter};
|
use tauri::{AppHandle, Emitter};
|
||||||
@ -118,7 +119,7 @@ impl PackageStore {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn install_package(&mut self, key: &PkgKey, force: bool) -> Result<InstallResult> {
|
pub async fn install_package(&mut self, key: &PkgKey, force: bool, install_deps: bool) -> Result<InstallResult> {
|
||||||
log::debug!("Installing {}", key);
|
log::debug!("Installing {}", key);
|
||||||
|
|
||||||
let pkg = self.store.get(key)
|
let pkg = self.store.get(key)
|
||||||
@ -128,14 +129,18 @@ impl PackageStore {
|
|||||||
if pkg.loc.is_some() && !force {
|
if pkg.loc.is_some() && !force {
|
||||||
return Ok(InstallResult::Ready);
|
return Ok(InstallResult::Ready);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.app.emit("install-start", Payload {
|
||||||
|
pkg: key.to_owned()
|
||||||
|
})?;
|
||||||
|
|
||||||
let rmt = pkg.rmt.as_ref() //clone()
|
let rmt = pkg.rmt.as_ref() //clone()
|
||||||
.ok_or_else(|| anyhow!("Attempted to install a pkg without remote data"))?;
|
.ok_or_else(|| anyhow!("Attempted to install a pkg without remote data"))?;
|
||||||
|
|
||||||
for dep in &rmt.dependencies {
|
if install_deps {
|
||||||
self.app.emit("install-start", Payload {
|
for dep in &rmt.dependencies {
|
||||||
pkg: dep.to_owned()
|
Box::pin(self.install_package(&dep, false, true)).await?;
|
||||||
})?;
|
}
|
||||||
Box::pin(self.install_package(&dep, false)).await?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let zip_path = util::cache_dir().join(format!(
|
let zip_path = util::cache_dir().join(format!(
|
||||||
@ -145,6 +150,7 @@ impl PackageStore {
|
|||||||
|
|
||||||
if !zip_path.exists() {
|
if !zip_path.exists() {
|
||||||
self.dlh.download_zip(&zip_path, &pkg)?;
|
self.dlh.download_zip(&zip_path, &pkg)?;
|
||||||
|
log::debug!("Deferring {}", key);
|
||||||
return Ok(InstallResult::Deferred);
|
return Ok(InstallResult::Deferred);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,16 +174,16 @@ impl PackageStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_package(&mut self, key: &PkgKey, force: bool) -> Result<()> {
|
pub async fn delete_package(&mut self, key: &PkgKey, force: bool) -> Result<()> {
|
||||||
|
log::debug!("Will delete {} {}", key, force);
|
||||||
|
|
||||||
let pkg = self.store.get_mut(key)
|
let pkg = self.store.get_mut(key)
|
||||||
.ok_or_else(|| anyhow!("Attempted to delete a nonexistent pkg"))?;
|
.ok_or_else(|| anyhow!("Attempted to delete a nonexistent pkg"))?;
|
||||||
let path = pkg.path();
|
let path = pkg.path();
|
||||||
|
|
||||||
if path.exists() && path.join("manifest.json").exists() {
|
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;
|
pkg.loc = None;
|
||||||
let rv = tokio::fs::remove_dir_all(&path).await
|
|
||||||
.map_err(|e| anyhow!("Could not delete a package: {}", e));
|
let rv = Self::clean_up_package(&path).await;
|
||||||
|
|
||||||
if rv.is_ok() {
|
if rv.is_ok() {
|
||||||
self.app.emit("install-end", Payload {
|
self.app.emit("install-end", Payload {
|
||||||
@ -201,4 +207,41 @@ impl PackageStore {
|
|||||||
}
|
}
|
||||||
self.store.insert(key, new);
|
self.store.insert(key, new);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn clean_up_dir(path: impl AsRef<Path>, name: &str) -> Result<()> {
|
||||||
|
let path = path.as_ref().join(name);
|
||||||
|
if path.exists() {
|
||||||
|
tokio::fs::remove_dir_all(path)
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow!("Could not delete /{}: {}", name, e))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn clean_up_file(path: impl AsRef<Path>, name: &str, force: bool) -> Result<()> {
|
||||||
|
let path = path.as_ref().join(name);
|
||||||
|
if force || path.exists() {
|
||||||
|
tokio::fs::remove_file(path)
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow!("Could not delete /{}: {}", name, e))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn clean_up_package(path: impl AsRef<Path>) -> Result<()> {
|
||||||
|
// todo case sensitivity for linux
|
||||||
|
Self::clean_up_dir(&path, "app").await?;
|
||||||
|
Self::clean_up_dir(&path, "option").await?;
|
||||||
|
Self::clean_up_file(&path, "icon.png", true).await?;
|
||||||
|
Self::clean_up_file(&path, "manifest.json", true).await?;
|
||||||
|
Self::clean_up_file(&path, "README.md", true).await?;
|
||||||
|
|
||||||
|
tokio::fs::remove_dir(path.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow!("Could not delete {}: {}", path.as_ref().to_string_lossy(), e))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ impl Profile {
|
|||||||
mods: HashSet::new(),
|
mods: HashSet::new(),
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
wine_runtime: Some(Path::new("/usr/bin/wine").to_path_buf()),
|
wine_runtime: Some(std::path::Path::new("/usr/bin/wine").to_path_buf()),
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
wine_runtime: None,
|
wine_runtime: None,
|
||||||
|
|
||||||
|
@ -1,24 +1,27 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use std::process::Stdio;
|
|
||||||
use tokio::task::JoinSet;
|
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
use crate::profile::Profile;
|
use crate::profile::Profile;
|
||||||
use crate::util;
|
use crate::util;
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
pub fn start(p: &Profile) -> Result<Child> {
|
pub fn start(p: &Profile) -> Result<()> {
|
||||||
Ok(Command::new(p.wine_runtime.as_ref().unwrap())
|
Command::new(p.wine_runtime.as_ref().unwrap())
|
||||||
.env(
|
.env(
|
||||||
"SEGATOOLS_CONFIG_PATH",
|
"SEGATOOLS_CONFIG_PATH",
|
||||||
util::profile_dir(&p).join("segatools.ini"),
|
util::profile_dir(&p).join("segatools.ini"),
|
||||||
)
|
)
|
||||||
.env("WINEPREFIX", p.wine_prefix.as_ref().unwrap())
|
.env("WINEPREFIX", p.wine_prefix.as_ref().unwrap())
|
||||||
.arg(p.exe_dir.join("start.bat"))
|
.arg(p.exe_dir.join("start.bat"))
|
||||||
.spawn()?)
|
.spawn()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
pub fn start(p: &Profile) -> Result<()> {
|
pub fn start(p: &Profile) -> Result<()> {
|
||||||
|
use std::process::Stdio;
|
||||||
|
use tokio::task::JoinSet;
|
||||||
|
|
||||||
let ini_path = util::profile_dir(&p).join("segatools.ini");
|
let ini_path = util::profile_dir(&p).join("segatools.ini");
|
||||||
|
|
||||||
log::debug!("With path {}", ini_path.to_string_lossy());
|
log::debug!("With path {}", ini_path.to_string_lossy());
|
||||||
|
@ -35,7 +35,7 @@
|
|||||||
},
|
},
|
||||||
"assetProtocol": {
|
"assetProtocol": {
|
||||||
"enable": true,
|
"enable": true,
|
||||||
"scope": ["**", "**/*", "**/.*/**/*", "**\\*", "**\\.*\\**\\*"]
|
"scope": ["**", "**/*", "**/.*/**/*"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -13,7 +13,14 @@ const install = async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await invoke('install_package', { key: pkgKey(props.pkg) });
|
try {
|
||||||
|
await invoke('install_package', { key: pkgKey(props.pkg) });
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
if (props.pkg !== undefined) {
|
||||||
|
props.pkg.js.busy = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//if (rv === 'Deferred') { /* download progress */ }
|
//if (rv === 'Deferred') { /* download progress */ }
|
||||||
};
|
};
|
||||||
@ -39,7 +46,7 @@ const remove = async () => {
|
|||||||
size="small"
|
size="small"
|
||||||
class="self-center ml-4"
|
class="self-center ml-4"
|
||||||
style="width: 2rem; height: 2rem"
|
style="width: 2rem; height: 2rem"
|
||||||
:loading="false"
|
:loading="pkg?.js.busy"
|
||||||
v-on:click="remove()"
|
v-on:click="remove()"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -52,7 +59,7 @@ const remove = async () => {
|
|||||||
size="small"
|
size="small"
|
||||||
class="self-center ml-4"
|
class="self-center ml-4"
|
||||||
style="width: 2rem; height: 2rem"
|
style="width: 2rem; height: 2rem"
|
||||||
:loading="false"
|
:loading="pkg?.js.busy"
|
||||||
v-on:click="install()"
|
v-on:click="install()"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
@ -13,8 +13,9 @@ const props = defineProps({
|
|||||||
pkg: Object as () => Package,
|
pkg: Object as () => Package,
|
||||||
});
|
});
|
||||||
|
|
||||||
const toggle = (value: boolean) => {
|
const toggle = async (value: boolean) => {
|
||||||
store.toggle(props.pkg, value);
|
store.toggle(props.pkg, value);
|
||||||
|
await store.reloadProfile();
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -29,12 +29,18 @@ export const usePkgStore = defineStore('pkg', {
|
|||||||
actions: {
|
actions: {
|
||||||
setupListeners() {
|
setupListeners() {
|
||||||
listen<InstallStatus>('install-start', async (ev) => {
|
listen<InstallStatus>('install-start', async (ev) => {
|
||||||
await this.reload(ev.payload.pkg);
|
const key = ev.payload.pkg;
|
||||||
|
await this.reload(key);
|
||||||
|
this.pkg[key].js.busy = true;
|
||||||
|
console.log('install-start' + key);
|
||||||
});
|
});
|
||||||
|
|
||||||
listen<InstallStatus>('install-end', async (ev) => {
|
listen<InstallStatus>('install-end', async (ev) => {
|
||||||
await this.reload(ev.payload.pkg);
|
const key = ev.payload.pkg;
|
||||||
|
await this.reload(key);
|
||||||
await this.reloadProfile();
|
await this.reloadProfile();
|
||||||
|
this.pkg[key].js.busy = false;
|
||||||
|
console.log('install-end' + key);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -44,37 +50,28 @@ export const usePkgStore = defineStore('pkg', {
|
|||||||
[key: string]: Package;
|
[key: string]: Package;
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const k of Object.keys(this)) {
|
for (const [k, v] of Object.entries(data)) {
|
||||||
delete this.pkg[k];
|
this.reloadWith(k, v);
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
async reload(pkgOrKey: string | Package) {
|
||||||
const key =
|
const key =
|
||||||
typeof pkgOrKey === 'string' ? pkgOrKey : pkgKey(pkgOrKey);
|
typeof pkgOrKey === 'string' ? pkgOrKey : pkgKey(pkgOrKey);
|
||||||
const rv: Package = await invoke('get_package', {
|
const pkg: Package = await invoke('get_package', {
|
||||||
key,
|
key,
|
||||||
});
|
});
|
||||||
|
this.reloadWith(key, pkg);
|
||||||
|
},
|
||||||
|
|
||||||
|
async reloadWith(key: string, pkg: Package) {
|
||||||
if (this.pkg[key] === undefined) {
|
if (this.pkg[key] === undefined) {
|
||||||
this.pkg[key] = {} as Package;
|
this.pkg[key] = { js: { busy: false } } as Package;
|
||||||
} else {
|
} else {
|
||||||
this.pkg[key].loc = null;
|
this.pkg[key].loc = null;
|
||||||
this.pkg[key].rmt = null;
|
this.pkg[key].rmt = null;
|
||||||
}
|
}
|
||||||
Object.assign(this.pkg[key], rv);
|
Object.assign(this.pkg[key], pkg);
|
||||||
},
|
},
|
||||||
|
|
||||||
async initProfile(exePath: string) {
|
async initProfile(exePath: string) {
|
||||||
|
@ -14,6 +14,9 @@ export interface Package {
|
|||||||
download_url: string;
|
download_url: string;
|
||||||
deprecated: boolean;
|
deprecated: boolean;
|
||||||
} | null;
|
} | null;
|
||||||
|
js: {
|
||||||
|
busy: boolean;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Profile {
|
export interface Profile {
|
||||||
|
Reference in New Issue
Block a user