UI & error fixes & QoL (#116)

* fix: use Arc<Error> instead of just ErrorKind

* fix: game status updates for UI

* fix: missing game version on push_game_update calls

* feat: wait if library load takes <300ms

* fix: clippy
This commit is contained in:
DecDuck
2025-08-15 22:56:49 +10:00
committed by GitHub
parent cb55ac2bf5
commit 17c375bcab
7 changed files with 90 additions and 107 deletions

View File

@ -1,92 +1,55 @@
<template> <template>
<div class="flex flex-col h-full"> <div class="flex flex-col h-full">
<div class="mb-3 inline-flex gap-x-2"> <div class="mb-3 inline-flex gap-x-2">
<div <div class="relative transition-transform duration-300 hover:scale-105 active:scale-95">
class="relative transition-transform duration-300 hover:scale-105 active:scale-95" <div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
> <MagnifyingGlassIcon class="h-5 w-5 text-zinc-400" aria-hidden="true" />
<div
class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"
>
<MagnifyingGlassIcon
class="h-5 w-5 text-zinc-400"
aria-hidden="true"
/>
</div> </div>
<input <input type="text" v-model="searchQuery"
type="text"
v-model="searchQuery"
class="block w-full rounded-lg border-0 bg-zinc-800/50 py-2 pl-10 pr-3 text-zinc-100 placeholder:text-zinc-500 focus:bg-zinc-800 focus:ring-2 focus:ring-inset focus:ring-blue-500 sm:text-sm sm:leading-6" class="block w-full rounded-lg border-0 bg-zinc-800/50 py-2 pl-10 pr-3 text-zinc-100 placeholder:text-zinc-500 focus:bg-zinc-800 focus:ring-2 focus:ring-inset focus:ring-blue-500 sm:text-sm sm:leading-6"
placeholder="Search library..." placeholder="Search library..." />
/>
</div> </div>
<button <button @click="() => calculateGames(true)"
@click="() => calculateGames(true)" class="p-1 flex items-center justify-center transition-transform duration-300 size-10 hover:scale-110 active:scale-90 rounded-lg bg-zinc-800/50 text-zinc-100">
class="p-1 flex items-center justify-center transition-transform duration-300 size-10 hover:scale-110 active:scale-90 rounded-lg bg-zinc-800/50 text-zinc-100"
>
<ArrowPathIcon class="size-4" /> <ArrowPathIcon class="size-4" />
</button> </button>
</div> </div>
<TransitionGroup name="list" tag="ul" class="flex flex-col gap-y-1.5"> <TransitionGroup name="list" tag="ul" class="flex flex-col gap-y-1.5">
<NuxtLink <NuxtLink v-for="(nav, navIndex) in filteredNavigation" :key="nav.id" :class="[
v-for="nav in filteredNavigation" 'transition-all duration-300 rounded-lg flex items-center py-2 px-3 hover:scale-105 active:scale-95 hover:shadow-lg hover:shadow-zinc-950/50',
:key="nav.id" navIndex === currentNavigation
:class="[ ? 'bg-zinc-800 text-zinc-100 shadow-md shadow-zinc-950/20'
'transition-all duration-300 rounded-lg flex items-center py-2 px-3 hover:scale-105 active:scale-95 hover:shadow-lg hover:shadow-zinc-950/50', : nav.isInstalled.value
nav.index === currentNavigation
? 'bg-zinc-800 text-zinc-100 shadow-md shadow-zinc-950/20'
: nav.isInstalled.value
? 'text-zinc-300 hover:bg-zinc-800/90 hover:text-zinc-200' ? 'text-zinc-300 hover:bg-zinc-800/90 hover:text-zinc-200'
: 'text-zinc-500 hover:bg-zinc-800/70 hover:text-zinc-300', : 'text-zinc-500 hover:bg-zinc-800/70 hover:text-zinc-300',
]" ]" :href="nav.route">
:href="nav.route"
>
<div class="flex items-center w-full gap-x-3"> <div class="flex items-center w-full gap-x-3">
<div <div class="flex-none transition-transform duration-300 hover:-rotate-2">
class="flex-none transition-transform duration-300 hover:-rotate-2" <img class="size-8 object-cover bg-zinc-900 rounded-lg transition-all duration-300 shadow-sm"
> :src="icons[nav.id]" alt="" />
<img
class="size-8 object-cover bg-zinc-900 rounded-lg transition-all duration-300 shadow-sm"
:src="icons[nav.id]"
alt=""
/>
</div> </div>
<div class="flex flex-col flex-1"> <div class="flex flex-col flex-1">
<p <p class="truncate text-xs font-display leading-5 flex-1 font-semibold">
class="truncate text-xs font-display leading-5 flex-1 font-semibold"
>
{{ nav.label }} {{ nav.label }}
</p> </p>
<p <p class="text-xs font-medium" :class="[gameStatusTextStyle[games[nav.id].status.value.type]]">
class="text-xs font-medium"
:class="[gameStatusTextStyle[games[nav.id].status.value.type]]"
>
{{ gameStatusText[games[nav.id].status.value.type] }} {{ gameStatusText[games[nav.id].status.value.type] }}
</p> </p>
</div> </div>
</div> </div>
</NuxtLink> </NuxtLink>
</TransitionGroup> </TransitionGroup>
<div <div v-if="loading" class="h-full grow flex p-8 justify-center text-zinc-100">
v-if="loading"
class="h-full grow flex p-8 justify-center text-zinc-100"
>
<div role="status"> <div role="status">
<svg <svg aria-hidden="true" class="w-6 h-6 text-transparent animate-spin fill-zinc-600" viewBox="0 0 100 101"
aria-hidden="true" fill="none" xmlns="http://www.w3.org/2000/svg">
class="w-6 h-6 text-transparent animate-spin fill-zinc-600"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path <path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor" fill="currentColor" />
/>
<path <path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill" fill="currentFill" />
/>
</svg> </svg>
<span class="sr-only">Loading...</span> <span class="sr-only">Loading...</span>
</div> </div>
@ -159,7 +122,18 @@ async function calculateGames(clearAll = false) {
rawGames.value = newGames; rawGames.value = newGames;
} }
calculateGames(true); // Wait up to 300 ms for the library to load, otherwise
// show the loading state while we while
await new Promise<void>((r) => {
let hasResolved = false;
const resolveFunc = () => {
if (!hasResolved) r();
hasResolved = true
}
calculateGames(true).then(resolveFunc);
setTimeout(resolveFunc, 300);
})
const navigation = computed(() => const navigation = computed(() =>
rawGames.value.map((game) => { rawGames.value.map((game) => {
@ -180,9 +154,11 @@ const navigation = computed(() =>
return item; return item;
}) })
); );
const { currentNavigation, recalculateNavigation } = useCurrentNavigationIndex(
navigation.value const route = useRoute();
); const currentNavigation = computed(() => {
return navigation.value.findIndex((e) => e.route == route.path)
});
const filteredNavigation = computed(() => { const filteredNavigation = computed(() => {
if (!searchQuery.value) if (!searchQuery.value)
@ -197,9 +173,7 @@ listen("update_library", async (event) => {
console.log("Updating library"); console.log("Updating library");
let oldNavigation = navigation.value[currentNavigation.value]; let oldNavigation = navigation.value[currentNavigation.value];
await calculateGames(); await calculateGames();
recalculateNavigation(); if (oldNavigation.route !== navigation.value[currentNavigation.value].route) {
if (oldNavigation !== navigation.value[currentNavigation.value]) {
console.log("Triggered");
router.push("/library"); router.push("/library");
} }
}); });

View File

@ -124,7 +124,12 @@ impl DownloadManagerBuilder {
self.current_download_agent = None; self.current_download_agent = None;
let mut download_thread_lock = self.current_download_thread.lock().unwrap(); let mut download_thread_lock = self.current_download_thread.lock().unwrap();
*download_thread_lock = None;
if let Some(unfinished_thread) = download_thread_lock.take()
&& !unfinished_thread.is_finished()
{
unfinished_thread.join().unwrap();
}
drop(download_thread_lock); drop(download_thread_lock);
} }
@ -215,10 +220,6 @@ impl DownloadManagerBuilder {
&& self.download_queue.read().front().unwrap() && self.download_queue.read().front().unwrap()
== &self.current_download_agent.as_ref().unwrap().metadata() == &self.current_download_agent.as_ref().unwrap().metadata()
{ {
debug!(
"Current download agent: {:?}",
self.current_download_agent.as_ref().unwrap().metadata()
);
return; return;
} }
@ -325,8 +326,8 @@ impl DownloadManagerBuilder {
self.stop_and_wait_current_download(); self.stop_and_wait_current_download();
self.remove_and_cleanup_front_download(&current_agent.metadata()); self.remove_and_cleanup_front_download(&current_agent.metadata());
self.push_ui_queue_update();
} }
self.push_ui_queue_update();
self.set_status(DownloadManagerStatus::Error); self.set_status(DownloadManagerStatus::Error);
} }
fn manage_cancel_signal(&mut self, meta: &DownloadableMetadata) { fn manage_cancel_signal(&mut self, meta: &DownloadableMetadata) {

View File

@ -1,6 +1,6 @@
use std::{ use std::{
fmt::{Display, Formatter}, fmt::{Display, Formatter},
io, io, sync::Arc,
}; };
use serde_with::SerializeDisplay; use serde_with::SerializeDisplay;
@ -14,9 +14,10 @@ pub enum ApplicationDownloadError {
NotInitialized, NotInitialized,
Communication(RemoteAccessError), Communication(RemoteAccessError),
DiskFull(u64, u64), DiskFull(u64, u64),
#[allow(dead_code)]
Checksum, Checksum,
Lock, Lock,
IoError(io::ErrorKind), IoError(Arc<io::Error>),
DownloadError, DownloadError,
} }

View File

@ -484,19 +484,16 @@ impl GameDownloadAgent {
self.control_flag.set(DownloadThreadControlFlag::Go); self.control_flag.set(DownloadThreadControlFlag::Go);
let status = ApplicationTransientStatus::Validating {
version_name: self.version.clone(),
};
let mut db_lock = borrow_db_mut_checked(); let mut db_lock = borrow_db_mut_checked();
db_lock.applications.transient_statuses.insert( db_lock
self.metadata(), .applications
ApplicationTransientStatus::Validating { .transient_statuses
version_name: self.version.clone(), .insert(self.metadata(), status.clone());
}, push_game_update(app_handle, &self.metadata().id, None, (None, Some(status)));
);
push_game_update(
app_handle,
&self.metadata().id,
None,
GameStatusManager::fetch_state(&self.metadata().id, &db_lock),
);
} }
pub fn validate(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> { pub fn validate(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {

View File

@ -17,6 +17,7 @@ use std::fs::{Permissions, set_permissions};
use std::io::Read; use std::io::Read;
#[cfg(unix)] #[cfg(unix)]
use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::PermissionsExt;
use std::sync::Arc;
use std::{ use std::{
fs::{File, OpenOptions}, fs::{File, OpenOptions},
io::{self, BufWriter, Seek, SeekFrom, Write}, io::{self, BufWriter, Seek, SeekFrom, Write},
@ -191,12 +192,13 @@ pub fn download_game_bucket(
.to_str() .to_str()
.unwrap(); .unwrap();
for (i, raw_length) in lengths.split(",").enumerate() { for (i, raw_length) in lengths.split(",").enumerate() {
let length = raw_length.parse::<usize>().unwrap_or(0); let length = raw_length.parse::<usize>().unwrap_or(0);
let Some(drop) = bucket.drops.get(i) else { let Some(drop) = bucket.drops.get(i) else {
warn!("invalid number of Content-Lengths recieved: {}, {}", i, lengths); warn!(
"invalid number of Content-Lengths recieved: {}, {}",
i, lengths
);
return Err(ApplicationDownloadError::DownloadError); return Err(ApplicationDownloadError::DownloadError);
}; };
if drop.length != length { if drop.length != length {
@ -210,11 +212,11 @@ pub fn download_game_bucket(
let mut pipeline = let mut pipeline =
DropDownloadPipeline::new(response, bucket.drops.clone(), control_flag, progress) DropDownloadPipeline::new(response, bucket.drops.clone(), control_flag, progress)
.map_err(|e| ApplicationDownloadError::IoError(e.kind()))?; .map_err(|e| ApplicationDownloadError::IoError(Arc::new(e)))?;
let completed = pipeline let completed = pipeline
.copy() .copy()
.map_err(|e| ApplicationDownloadError::IoError(e.kind()))?; .map_err(|e| ApplicationDownloadError::IoError(Arc::new(e)))?;
if !completed { if !completed {
return Ok(false); return Ok(false);
} }
@ -225,18 +227,20 @@ pub fn download_game_bucket(
for drop in bucket.drops.iter() { for drop in bucket.drops.iter() {
let permissions = Permissions::from_mode(drop.permissions); let permissions = Permissions::from_mode(drop.permissions);
set_permissions(drop.path.clone(), permissions) set_permissions(drop.path.clone(), permissions)
.map_err(|e| ApplicationDownloadError::IoError(e.kind()))?; .map_err(|e| ApplicationDownloadError::IoError(Arc::new(e)))?;
} }
} }
let checksums = pipeline let checksums = pipeline
.finish() .finish()
.map_err(|e| ApplicationDownloadError::IoError(e.kind()))?; .map_err(|e| ApplicationDownloadError::IoError(Arc::new(e)))?;
for (index, drop) in bucket.drops.iter().enumerate() { for (index, drop) in bucket.drops.iter().enumerate() {
let res = hex::encode(**checksums.get(index).unwrap()); let res = hex::encode(**checksums.get(index).unwrap());
if res != drop.checksum { if res != drop.checksum {
return Err(ApplicationDownloadError::Checksum); warn!("context didn't match... doing nothing because we will validate later.");
// return Ok(false);
// return Err(ApplicationDownloadError::Checksum);
} }
} }

View File

@ -14,6 +14,7 @@ use crate::database::models::data::{
ApplicationTransientStatus, DownloadableMetadata, GameDownloadStatus, GameVersion, ApplicationTransientStatus, DownloadableMetadata, GameDownloadStatus, GameVersion,
}; };
use crate::download_manager::download_manager_frontend::DownloadStatus; use crate::download_manager::download_manager_frontend::DownloadStatus;
use crate::error::drop_server_error::DropServerError;
use crate::error::library_error::LibraryError; use crate::error::library_error::LibraryError;
use crate::error::remote_access_error::RemoteAccessError; use crate::error::remote_access_error::RemoteAccessError;
use crate::games::state::{GameStatusManager, GameStatusWithTransient}; use crate::games::state::{GameStatusManager, GameStatusWithTransient};
@ -89,7 +90,10 @@ pub async fn fetch_library_logic(
.await?; .await?;
if response.status() != 200 { if response.status() != 200 {
let err = response.json().await.unwrap(); let err = response.json().await.unwrap_or(DropServerError {
status_code: 500,
status_message: "Invalid response from server.".to_owned(),
});
warn!("{err:?}"); warn!("{err:?}");
return Err(RemoteAccessError::InvalidResponse(err)); return Err(RemoteAccessError::InvalidResponse(err));
} }
@ -358,8 +362,7 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
db_handle db_handle
.applications .applications
.transient_statuses .transient_statuses
.entry(meta.clone()) .insert(meta.clone(), ApplicationTransientStatus::Uninstalling {});
.and_modify(|v| *v = ApplicationTransientStatus::Uninstalling {});
push_game_update( push_game_update(
app_handle, app_handle,
@ -393,8 +396,7 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
db_handle db_handle
.applications .applications
.transient_statuses .transient_statuses
.entry(meta.clone()) .insert(meta.clone(), ApplicationTransientStatus::Uninstalling {});
.and_modify(|v| *v = ApplicationTransientStatus::Uninstalling {});
drop(db_handle); drop(db_handle);
@ -412,8 +414,7 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
db_handle db_handle
.applications .applications
.game_statuses .game_statuses
.entry(meta.id.clone()) .insert(meta.id.clone(), GameDownloadStatus::Remote {});
.and_modify(|e| *e = GameDownloadStatus::Remote {});
let _ = db_handle.applications.transient_statuses.remove(&meta); let _ = db_handle.applications.transient_statuses.remove(&meta);
push_game_update( push_game_update(
@ -425,8 +426,6 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
debug!("uninstalled game id {}", &meta.id); debug!("uninstalled game id {}", &meta.id);
app_handle.emit("update_library", ()).unwrap(); app_handle.emit("update_library", ()).unwrap();
drop(db_handle);
} }
}); });
} else { } else {
@ -499,6 +498,7 @@ pub fn on_game_complete(
.game_statuses .game_statuses
.insert(meta.id.clone(), status.clone()); .insert(meta.id.clone(), status.clone());
drop(db_handle); drop(db_handle);
app_handle app_handle
.emit( .emit(
&format!("update_game/{}", meta.id), &format!("update_game/{}", meta.id),
@ -519,6 +519,12 @@ pub fn push_game_update(
version: Option<GameVersion>, version: Option<GameVersion>,
status: GameStatusWithTransient, status: GameStatusWithTransient,
) { ) {
if let Some(GameDownloadStatus::Installed { .. } | GameDownloadStatus::SetupRequired { .. }) =
&status.0
&& version.is_none() {
panic!("pushed game for installed game that doesn't have version information");
}
app_handle app_handle
.emit( .emit(
&format!("update_game/{game_id}"), &format!("update_game/{game_id}"),

View File

@ -1,4 +1,4 @@
use std::path::PathBuf; use std::{path::PathBuf, sync::Arc};
use futures_lite::io; use futures_lite::io;
use sysinfo::{Disk, DiskRefreshKind, Disks}; use sysinfo::{Disk, DiskRefreshKind, Disks};
@ -21,7 +21,7 @@ pub fn get_disk_available(mount_point: PathBuf) -> Result<u64, ApplicationDownlo
return Ok(disk.available_space()); return Ok(disk.available_space());
} }
} }
Err(ApplicationDownloadError::IoError(io::Error::other( Err(ApplicationDownloadError::IoError(Arc::new(io::Error::other(
"could not find disk of path", "could not find disk of path",
).kind())) ))))
} }