mirror of
https://github.com/Drop-OSS/drop-app.git
synced 2025-11-10 04:22:13 +10:00
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:
@ -1,92 +1,55 @@
|
||||
<template>
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="mb-3 inline-flex gap-x-2">
|
||||
<div
|
||||
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="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>
|
||||
<input
|
||||
type="text"
|
||||
v-model="searchQuery"
|
||||
<input 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"
|
||||
placeholder="Search library..."
|
||||
/>
|
||||
placeholder="Search library..." />
|
||||
</div>
|
||||
<button
|
||||
@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"
|
||||
>
|
||||
<button @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">
|
||||
<ArrowPathIcon class="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<TransitionGroup name="list" tag="ul" class="flex flex-col gap-y-1.5">
|
||||
<NuxtLink
|
||||
v-for="nav in filteredNavigation"
|
||||
:key="nav.id"
|
||||
:class="[
|
||||
<NuxtLink v-for="(nav, navIndex) in filteredNavigation" :key="nav.id" :class="[
|
||||
'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.index === currentNavigation
|
||||
navIndex === 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-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-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=""
|
||||
/>
|
||||
<div 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="" />
|
||||
</div>
|
||||
<div class="flex flex-col flex-1">
|
||||
<p
|
||||
class="truncate text-xs font-display leading-5 flex-1 font-semibold"
|
||||
>
|
||||
<p class="truncate text-xs font-display leading-5 flex-1 font-semibold">
|
||||
{{ nav.label }}
|
||||
</p>
|
||||
<p
|
||||
class="text-xs font-medium"
|
||||
:class="[gameStatusTextStyle[games[nav.id].status.value.type]]"
|
||||
>
|
||||
<p class="text-xs font-medium" :class="[gameStatusTextStyle[games[nav.id].status.value.type]]">
|
||||
{{ gameStatusText[games[nav.id].status.value.type] }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</TransitionGroup>
|
||||
<div
|
||||
v-if="loading"
|
||||
class="h-full grow flex p-8 justify-center text-zinc-100"
|
||||
>
|
||||
<div v-if="loading" class="h-full grow flex p-8 justify-center text-zinc-100">
|
||||
<div role="status">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
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"
|
||||
>
|
||||
<svg aria-hidden="true" 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
|
||||
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
|
||||
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>
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
@ -159,7 +122,18 @@ async function calculateGames(clearAll = false) {
|
||||
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(() =>
|
||||
rawGames.value.map((game) => {
|
||||
@ -180,9 +154,11 @@ const navigation = computed(() =>
|
||||
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(() => {
|
||||
if (!searchQuery.value)
|
||||
@ -197,9 +173,7 @@ listen("update_library", async (event) => {
|
||||
console.log("Updating library");
|
||||
let oldNavigation = navigation.value[currentNavigation.value];
|
||||
await calculateGames();
|
||||
recalculateNavigation();
|
||||
if (oldNavigation !== navigation.value[currentNavigation.value]) {
|
||||
console.log("Triggered");
|
||||
if (oldNavigation.route !== navigation.value[currentNavigation.value].route) {
|
||||
router.push("/library");
|
||||
}
|
||||
});
|
||||
|
||||
@ -124,7 +124,12 @@ impl DownloadManagerBuilder {
|
||||
self.current_download_agent = None;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@ -215,10 +220,6 @@ impl DownloadManagerBuilder {
|
||||
&& self.download_queue.read().front().unwrap()
|
||||
== &self.current_download_agent.as_ref().unwrap().metadata()
|
||||
{
|
||||
debug!(
|
||||
"Current download agent: {:?}",
|
||||
self.current_download_agent.as_ref().unwrap().metadata()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -325,8 +326,8 @@ impl DownloadManagerBuilder {
|
||||
|
||||
self.stop_and_wait_current_download();
|
||||
self.remove_and_cleanup_front_download(¤t_agent.metadata());
|
||||
self.push_ui_queue_update();
|
||||
}
|
||||
self.push_ui_queue_update();
|
||||
self.set_status(DownloadManagerStatus::Error);
|
||||
}
|
||||
fn manage_cancel_signal(&mut self, meta: &DownloadableMetadata) {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
use std::{
|
||||
fmt::{Display, Formatter},
|
||||
io,
|
||||
io, sync::Arc,
|
||||
};
|
||||
|
||||
use serde_with::SerializeDisplay;
|
||||
@ -14,9 +14,10 @@ pub enum ApplicationDownloadError {
|
||||
NotInitialized,
|
||||
Communication(RemoteAccessError),
|
||||
DiskFull(u64, u64),
|
||||
#[allow(dead_code)]
|
||||
Checksum,
|
||||
Lock,
|
||||
IoError(io::ErrorKind),
|
||||
IoError(Arc<io::Error>),
|
||||
DownloadError,
|
||||
}
|
||||
|
||||
|
||||
@ -484,19 +484,16 @@ impl GameDownloadAgent {
|
||||
|
||||
self.control_flag.set(DownloadThreadControlFlag::Go);
|
||||
|
||||
let mut db_lock = borrow_db_mut_checked();
|
||||
db_lock.applications.transient_statuses.insert(
|
||||
self.metadata(),
|
||||
ApplicationTransientStatus::Validating {
|
||||
let status = ApplicationTransientStatus::Validating {
|
||||
version_name: self.version.clone(),
|
||||
},
|
||||
);
|
||||
push_game_update(
|
||||
app_handle,
|
||||
&self.metadata().id,
|
||||
None,
|
||||
GameStatusManager::fetch_state(&self.metadata().id, &db_lock),
|
||||
);
|
||||
};
|
||||
|
||||
let mut db_lock = borrow_db_mut_checked();
|
||||
db_lock
|
||||
.applications
|
||||
.transient_statuses
|
||||
.insert(self.metadata(), status.clone());
|
||||
push_game_update(app_handle, &self.metadata().id, None, (None, Some(status)));
|
||||
}
|
||||
|
||||
pub fn validate(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
|
||||
|
||||
@ -17,6 +17,7 @@ use std::fs::{Permissions, set_permissions};
|
||||
use std::io::Read;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
fs::{File, OpenOptions},
|
||||
io::{self, BufWriter, Seek, SeekFrom, Write},
|
||||
@ -191,12 +192,13 @@ pub fn download_game_bucket(
|
||||
.to_str()
|
||||
.unwrap();
|
||||
|
||||
|
||||
|
||||
for (i, raw_length) in lengths.split(",").enumerate() {
|
||||
let length = raw_length.parse::<usize>().unwrap_or(0);
|
||||
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);
|
||||
};
|
||||
if drop.length != length {
|
||||
@ -210,11 +212,11 @@ pub fn download_game_bucket(
|
||||
|
||||
let mut pipeline =
|
||||
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
|
||||
.copy()
|
||||
.map_err(|e| ApplicationDownloadError::IoError(e.kind()))?;
|
||||
.map_err(|e| ApplicationDownloadError::IoError(Arc::new(e)))?;
|
||||
if !completed {
|
||||
return Ok(false);
|
||||
}
|
||||
@ -225,18 +227,20 @@ pub fn download_game_bucket(
|
||||
for drop in bucket.drops.iter() {
|
||||
let permissions = Permissions::from_mode(drop.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
|
||||
.finish()
|
||||
.map_err(|e| ApplicationDownloadError::IoError(e.kind()))?;
|
||||
.map_err(|e| ApplicationDownloadError::IoError(Arc::new(e)))?;
|
||||
|
||||
for (index, drop) in bucket.drops.iter().enumerate() {
|
||||
let res = hex::encode(**checksums.get(index).unwrap());
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -14,6 +14,7 @@ use crate::database::models::data::{
|
||||
ApplicationTransientStatus, DownloadableMetadata, GameDownloadStatus, GameVersion,
|
||||
};
|
||||
use crate::download_manager::download_manager_frontend::DownloadStatus;
|
||||
use crate::error::drop_server_error::DropServerError;
|
||||
use crate::error::library_error::LibraryError;
|
||||
use crate::error::remote_access_error::RemoteAccessError;
|
||||
use crate::games::state::{GameStatusManager, GameStatusWithTransient};
|
||||
@ -89,7 +90,10 @@ pub async fn fetch_library_logic(
|
||||
.await?;
|
||||
|
||||
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:?}");
|
||||
return Err(RemoteAccessError::InvalidResponse(err));
|
||||
}
|
||||
@ -358,8 +362,7 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
|
||||
db_handle
|
||||
.applications
|
||||
.transient_statuses
|
||||
.entry(meta.clone())
|
||||
.and_modify(|v| *v = ApplicationTransientStatus::Uninstalling {});
|
||||
.insert(meta.clone(), ApplicationTransientStatus::Uninstalling {});
|
||||
|
||||
push_game_update(
|
||||
app_handle,
|
||||
@ -393,8 +396,7 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
|
||||
db_handle
|
||||
.applications
|
||||
.transient_statuses
|
||||
.entry(meta.clone())
|
||||
.and_modify(|v| *v = ApplicationTransientStatus::Uninstalling {});
|
||||
.insert(meta.clone(), ApplicationTransientStatus::Uninstalling {});
|
||||
|
||||
drop(db_handle);
|
||||
|
||||
@ -412,8 +414,7 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
|
||||
db_handle
|
||||
.applications
|
||||
.game_statuses
|
||||
.entry(meta.id.clone())
|
||||
.and_modify(|e| *e = GameDownloadStatus::Remote {});
|
||||
.insert(meta.id.clone(), GameDownloadStatus::Remote {});
|
||||
let _ = db_handle.applications.transient_statuses.remove(&meta);
|
||||
|
||||
push_game_update(
|
||||
@ -425,8 +426,6 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
|
||||
|
||||
debug!("uninstalled game id {}", &meta.id);
|
||||
app_handle.emit("update_library", ()).unwrap();
|
||||
|
||||
drop(db_handle);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@ -499,6 +498,7 @@ pub fn on_game_complete(
|
||||
.game_statuses
|
||||
.insert(meta.id.clone(), status.clone());
|
||||
drop(db_handle);
|
||||
|
||||
app_handle
|
||||
.emit(
|
||||
&format!("update_game/{}", meta.id),
|
||||
@ -519,6 +519,12 @@ pub fn push_game_update(
|
||||
version: Option<GameVersion>,
|
||||
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
|
||||
.emit(
|
||||
&format!("update_game/{game_id}"),
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use std::path::PathBuf;
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
use futures_lite::io;
|
||||
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());
|
||||
}
|
||||
}
|
||||
Err(ApplicationDownloadError::IoError(io::Error::other(
|
||||
Err(ApplicationDownloadError::IoError(Arc::new(io::Error::other(
|
||||
"could not find disk of path",
|
||||
).kind()))
|
||||
))))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user