Skip to content

Commit

Permalink
fix: reactive download buttons
Browse files Browse the repository at this point in the history
- still needs good spinner (fero pls)
  • Loading branch information
sargon64 committed Sep 10, 2023
1 parent 506d964 commit 63cb7d3
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 101 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions apps/gui/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ native-dialog = "0.6.4"
anyhow = "1.0.75"
minreq = { version = "2.10.0", features = ["json-using-serde", "rustls", "https-rustls"] }
forge-lib = { git = "https://github.com/beat-forge/lib" }
futures-util = "0.3.28"


[profile.release]
Expand Down
96 changes: 65 additions & 31 deletions apps/gui/src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::{io::Error, path::Path, convert::Infallible, any};
use std::{any, convert::Infallible, io::Error, path::Path};

use crate::{structs, utils, DATABASE};
use crate::{structs::{self, InstanceUpdate}, utils, DATABASE};
use directories::{BaseDirs, UserDirs};
use entity::prelude::*;
use forge_lib::structs::v1::{unpack_v1_forgemod, ForgeModTypes};
Expand All @@ -16,21 +16,41 @@ static OCULUS_PATH: &str =
pub async fn get_instances() -> Vec<structs::Instance> {
let db = DATABASE.get().await.clone();

Instances::find()
.all(&db)
.await
.unwrap()
.into_iter()
.map(|f| structs::Instance {
id: f.id,
name: f.name,
path: f.path,
version: f.version,
is_modded: f.is_modded,
timestamp: f.timestamp,
mods: None, // mods not returned here
})
.collect()
futures_util::future::join_all(
Instances::find()
.all(&db)
.await
.unwrap()
.into_iter()
.map(|f| async move {
let db = DATABASE.get().await.clone(); // hack
structs::Instance {
id: f.id,
name: f.name,
path: f.path,
version: f.version,
is_modded: f.is_modded,
timestamp: f.timestamp,
mods: Some(
InstanceMods::find()
.filter(entity::instance_mods::Column::InstanceId.eq(f.id))
.all(&db)
.await
.unwrap()
.into_iter()
.map(|f| structs::Mod {
mod_id: f.mod_id,
timestamp: f.timestamp,
})
.collect::<Vec<structs::Mod>>(),
)
.map(|f| if f.len() == 0 { None } else { Some(f) })
.flatten(),
}
})
.collect::<Vec<_>>(),
)
.await
}

#[tauri::command]
Expand All @@ -44,11 +64,7 @@ pub async fn get_instance(id: i32) -> structs::Instance {
.await
.unwrap();

let instance = Instances::find_by_id(id)
.one(&db)
.await
.unwrap()
.unwrap();
let instance = Instances::find_by_id(id).one(&db).await.unwrap().unwrap();

structs::Instance {
id: instance.id,
Expand Down Expand Up @@ -195,7 +211,9 @@ pub async fn detect_instances() -> Vec<structs::Instance> {
let db_instance = entity::instances::ActiveModel {
name: Set(path.file_name().unwrap().to_str().unwrap().to_string()),
path: Set(path.to_str().unwrap().to_string()),
version: Set(utils::get_game_version(path.to_str().unwrap().to_string())),
version: Set(utils::get_game_version(
path.to_str().unwrap().to_string(),
)),
is_modded: Set(false),
..Default::default()
};
Expand All @@ -218,7 +236,7 @@ pub async fn detect_instances() -> Vec<structs::Instance> {
let base_path = bsm_config["installation-folder"].as_str().unwrap(); // this is the base path for bs manager, not including BSManager
let mut bs_manager = Path::new(base_path).join("BSManager");
bs_manager.push("BSInstances");
dbg!(&bs_manager);
dbg!(&bs_manager);

for entry in std::fs::read_dir(bs_manager).unwrap() {
let entry = entry.unwrap();
Expand All @@ -245,7 +263,9 @@ pub async fn detect_instances() -> Vec<structs::Instance> {
let db_instance = entity::instances::ActiveModel {
name: Set(path.file_name().unwrap().to_str().unwrap().to_string()),
path: Set(path.to_str().unwrap().to_string()),
version: Set(utils::get_game_version(path.to_str().unwrap().to_string())),
version: Set(utils::get_game_version(
path.to_str().unwrap().to_string(),
)),
is_modded: Set(false),
..Default::default()
};
Expand Down Expand Up @@ -277,8 +297,10 @@ pub async fn add_instance(name: String) -> structs::Instance {
let uder = UserDirs::new().unwrap();
let path = native_dialog::FileDialog::new()
.set_location(uder.home_dir())
.show_open_single_dir().unwrap()
.ok_or_else(|| Error::new(std::io::ErrorKind::NotFound, "Directory Not found")).unwrap();
.show_open_single_dir()
.unwrap()
.ok_or_else(|| Error::new(std::io::ErrorKind::NotFound, "Directory Not found"))
.unwrap();
let db = DATABASE.get().await.clone();
let instance = entity::instances::ActiveModel {
name: Set(name.clone()),
Expand All @@ -288,7 +310,8 @@ pub async fn add_instance(name: String) -> structs::Instance {
..Default::default()
}
.insert(&db)
.await.unwrap();
.await
.unwrap();

structs::Instance {
id: instance.id,
Expand All @@ -307,7 +330,8 @@ pub async fn remove_instance(name: String) -> bool {
if let Some(i) = Instances::find()
.filter(entity::instances::Column::Name.eq(name))
.one(&db)
.await.unwrap()
.await
.unwrap()
{
i.delete(&db).await.unwrap();
return true;
Expand All @@ -317,6 +341,7 @@ pub async fn remove_instance(name: String) -> bool {

#[tauri::command]
pub async fn install_mod(
window: tauri::Window,
instance_id: i32,
mod_id: String,
// mod_version: Option<String>,
Expand Down Expand Up @@ -383,10 +408,19 @@ pub async fn install_mod(
// }

dbg!("nop - mod install not implemented yet");
let instance = Instances::find_by_id(instance_id).one(&DATABASE.get().await.clone()).await.unwrap().unwrap();
let instance = Instances::find_by_id(instance_id)
.one(&DATABASE.get().await.clone())
.await
.unwrap()
.unwrap();
let im = entity::instance_mods::ActiveModel {
instance_id: Set(instance_id),
mod_id: Set(mod_id),
..Default::default()
}.insert(&DATABASE.get().await.clone()).await.unwrap();
}
.insert(&DATABASE.get().await.clone())
.await
.unwrap();

window.emit("instance-update", InstanceUpdate::new(instance_id, structs::InstanceUpdateKind::ModInstalled(im.mod_id))).unwrap();
}
8 changes: 7 additions & 1 deletion apps/gui/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,16 @@ async fn main() {
)
.ok();
});
// db needs to block on startup, as migrations are !sync

// db needs to block on startup, as migrations are !async
let db = DATABASE.get().await.clone();
migration::Migrator::up(&db, None).await.unwrap();

// detect instances on startup
tokio::spawn(async {
commands::detect_instances().await;
});

tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
commands::get_instances,
Expand Down
18 changes: 18 additions & 0 deletions apps/gui/src-tauri/src/structs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,22 @@ pub struct Instance {
pub struct Mod {
pub mod_id: String,
pub timestamp: DateTimeUtc,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct InstanceUpdate {
pub instance_id: i32,
pub kind: InstanceUpdateKind,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum InstanceUpdateKind {
/// ModInstalled(<mod_id>)
ModInstalled(String),
}

impl InstanceUpdate {
pub fn new(instance_id: i32, kind: InstanceUpdateKind) -> Self {
Self { instance_id, kind }
}
}
107 changes: 51 additions & 56 deletions apps/gui/src/lib/components/ModListItem.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,80 +2,70 @@
import { DownloadIcon, CheckmarkIcon } from 'ui/icons';
import { Button } from 'ui/button';
export let iter: number;
export const _iter: number | undefined = undefined;
export let mod: SearchMod;
export let instance_id: number;
export let name: string;
export let slug: string;
export let author: {
username: string;
};
export let description: string;
// export let category: string;
// export let downloads: any;
export let icon: string;
export let id: string;
export let instance_id: string;
export let installed: boolean;
export let selected: boolean;
import { selectedItems } from '$lib/stores';
import { invoke } from '@tauri-apps/api/tauri';
import { fly } from 'svelte/transition';
import { quintOut } from 'svelte/easing';
import { ModStatus, type InstanceUpdate, type ModInstalled, type SearchMod } from '$lib/types';
import { listen } from '@tauri-apps/api/event';
import { onDestroy } from 'svelte';
let items: string[] = [];
selectedItems.subscribe((val) => {
items = val;
selected = val.includes(slug);
});
// Event handler to toggle selection
const toggleSelection = () => {
selectedItems.update((val) => {
if (val.includes(slug)) {
return val.filter((item) => item !== slug);
} else {
return [...val, slug];
}
});
};
const installMod = (e: any) => {
e.stopPropagation();
invoke('install_mod', { instanceId: parseInt(instance_id), modId: id }).then((res: any) => {
// check if mod is already installed
if (mod.status != ModStatus.NotInstalled) return;
mod.status = ModStatus.Installing;
invoke('install_mod', { instanceId: instance_id, modId: mod.id }).then((res: any) => {
console.log(res);
});
};
//listen for button state updates
const unlisten = listen("instance-update", async (event) => {
console.log(JSON.stringify(event.payload));
if ((<InstanceUpdate> event.payload).instance_id === instance_id) {
if ((<InstanceUpdate<ModInstalled>>event.payload).kind.ModInstalled === mod.id) {
mod.status = ModStatus.Installed;
}
}
});
onDestroy(async () => {
(await unlisten)();
});
</script>

<div
data-installed={installed}
data-installed={mod.status}
data-instance-id={instance_id}
data-mod-id={id}
data-mod-id={mod.id}
class={`beatforge-focusable-element beatforge-mod-list-item bg-primary-800 my-2 flex w-full min-w-0 flex-row items-center gap-2 rounded-lg p-2 outline-none transition-all duration-[120ms] hover:duration-[0ms] md:my-0 md:gap-4 md:rounded-none md:p-4`}
>
<div
class="beatforge-mod-list-item-image bg-primary-600 mr-2 flex h-12 w-12 flex-shrink-0 overflow-hidden rounded-md md:h-16 md:w-16"
>
<img
alt={`Listing for mod ${name}, created by ${author}`}
src={icon ? icon : '/images/unknown.svg'}
alt={`Listing for mod ${mod.name}, created by ${mod.author.display_name ?? mod.author.username}`}
src={'/images/unknown.svg'}
class="h-full w-full object-cover"
/>
/> <!-- todo: add icon support -->
</div>
<div class="beatforge-mod-list-item-info flex w-full min-w-0 flex-col gap-1 truncate md:gap-1">
<div class="beatforge-mod-list-item-info-title flex items-center gap-2">
<h2 class="text-md font-black md:text-xl">{name}</h2>
<p class="text-primary-200 text-xs">by {author}</p>
{#if installed}
<h2 class="text-md font-black md:text-xl">{mod.name}</h2>
<p class="text-primary-200 text-xs">by {mod.author.display_name ?? mod.author.username}</p>
{#if mod.status == ModStatus.Installed}
<div class="">Installed</div>
{/if}
</div>
<div class="beatforge-mod-list-item-description flex min-w-0">
<p class="text-primary-200 min-w-0 truncate text-xs font-medium">
{description}
{mod.description}
</p>
</div>
</div>
Expand All @@ -93,20 +83,25 @@
<div class="text-primary-200 flex flex-row items-center gap-1 text-xs">
{category}
</div> -->
<Button on:click={installMod} variant={installed ? 'secondary' : 'primary'}>
<slot name="leading">
{#if installed}
<CheckmarkIcon customClasses="flex-shrink-0 w-4 h-4" />
{#key mod.status}
<Button on:click={installMod} variant={mod.status == ModStatus.Installed? 'secondary' : 'primary'}>
<slot name="leading">
{#if mod.status == ModStatus.Installed}
<CheckmarkIcon customClasses="flex-shrink-0 w-4 h-4" />
{:else if mod.status == ModStatus.Installing}
<!-- todo: add spinner -->
<div></div>
{:else}
<DownloadIcon customClasses="flex-shrink-0 w-4 h-4" />
{/if}
</slot>

{#if mod.status == ModStatus.Installed}
Installed
{:else}
<DownloadIcon customClasses="flex-shrink-0 w-4 h-4" />
Download
{/if}
</slot>

{#if installed}
Installed
{:else}
Download
{/if}
</Button>
</Button>
{/key}
</div>
</div>
Loading

0 comments on commit 63cb7d3

Please sign in to comment.