fix: assorted fixes

This commit is contained in:
DecDuck
2025-01-20 11:42:09 +11:00
parent 92729701c3
commit 89ea34c94e
16 changed files with 166 additions and 109 deletions

View File

@ -20,10 +20,18 @@ import {
const router = useRouter(); const router = useRouter();
const state = useAppState(); const state = useAppState();
try {
state.value = JSON.parse(await invoke("fetch_state")); state.value = JSON.parse(await invoke("fetch_state"));
} catch (e) {
console.error("failed to parse state", e);
}
router.beforeEach(async () => { router.beforeEach(async () => {
try {
state.value = JSON.parse(await invoke("fetch_state")); state.value = JSON.parse(await invoke("fetch_state"));
} catch (e) {
console.error("failed to parse state", e);
}
}); });
setupHooks(); setupHooks();

View File

@ -2,7 +2,13 @@ import { listen } from "@tauri-apps/api/event";
import type { DownloadableMetadata } from "~/types"; import type { DownloadableMetadata } from "~/types";
export type QueueState = { export type QueueState = {
queue: Array<{ meta: DownloadableMetadata; status: string; progress: number | null }>; queue: Array<{
meta: DownloadableMetadata;
status: string;
progress: number | null;
current: number;
max: number;
}>;
status: string; status: string;
}; };

View File

@ -48,9 +48,7 @@ export function initialNavigation(state: Ref<AppState>) {
switch (state.value.status) { switch (state.value.status) {
case AppStatus.NotConfigured: case AppStatus.NotConfigured:
router.push({ path: "/setup" }).then(() => { router.push({ path: "/setup" });
console.log("Pushed Setup");
});
break; break;
case AppStatus.SignedOut: case AppStatus.SignedOut:
router.push("/auth"); router.push("/auth");

View File

@ -1,4 +1,7 @@
<template /> <template />
<script setup lang="ts"> <script setup lang="ts">
definePageMeta({
layout: false
})
</script> </script>

View File

@ -242,7 +242,7 @@
" "
:loading="installLoading" :loading="installLoading"
type="submit" type="submit"
class="w-full sm:w-fit" class="ml-2 w-full sm:w-fit"
> >
Install Install
</LoadingButton> </LoadingButton>

View File

@ -1,23 +1,37 @@
<template> <template>
<div class="bg-zinc-950 p-4 min-h-full space-y-4"> <div class="bg-zinc-950 p-4 min-h-full space-y-4">
<div class="h-16 overflow-hidden relative rounded-xl flex flex-row border border-zinc-900">
<div <div
class="bg-zinc-900 z-10 w-32 flex flex-col gap-x-2 text-blue-400 font-display items-left justify-center pl-2"> class="h-16 overflow-hidden relative rounded-xl flex flex-row border border-zinc-900"
<span class="font-semibold">{{ formatKilobytes(stats.speed) }}</span> >
<span v-if="stats.time > 0" class="text-sm">{{ formatTime(stats.time) }} left</span> <div
class="bg-zinc-900 z-10 w-32 flex flex-col gap-x-2 text-blue-400 font-display items-left justify-center pl-2"
>
<span class="font-semibold">{{ formatKilobytes(stats.speed) }}/s</span>
<span v-if="stats.time > 0" class="text-sm"
>{{ formatTime(stats.time) }} left</span
>
</div> </div>
<div class="absolute inset-0 h-full flex flex-row items-end justify-end"> <div class="absolute inset-0 h-full flex flex-row items-end justify-end">
<div v-for="bar in speedHistory" :style="{ height: `${bar / speedMax * 100}%` }" <div
class="w-[8px] bg-blue-600/40" /> v-for="bar in speedHistory"
:style="{ height: `${(bar / speedMax) * 100}%` }"
class="w-[8px] bg-blue-600/40"
/>
</div> </div>
</div> </div>
<draggable v-model="queue.queue" @end="onEnd"> <draggable v-model="queue.queue" @end="onEnd">
<template #item="{ element }: { element: (typeof queue.value.queue)[0] }"> <template #item="{ element }: { element: (typeof queue.value.queue)[0] }">
<li v-if="games[element.meta.id]" :key="element.meta.id" <li
class="mb-4 bg-zinc-900 rounded-lg flex flex-row justify-between gap-x-6 py-5 px-4"> v-if="games[element.meta.id]"
:key="element.meta.id"
class="mb-4 bg-zinc-900 rounded-lg flex flex-row justify-between gap-x-6 py-5 px-4"
>
<div class="w-full flex items-center max-w-md gap-x-4 relative"> <div class="w-full flex items-center max-w-md gap-x-4 relative">
<img class="size-24 flex-none bg-zinc-800 object-cover rounded" :src="games[element.meta.id].cover" alt="" /> <img
class="size-24 flex-none bg-zinc-800 object-cover rounded"
:src="games[element.meta.id].cover"
alt=""
/>
<div class="min-w-0 flex-auto"> <div class="min-w-0 flex-auto">
<p class="text-xl font-semibold text-zinc-100"> <p class="text-xl font-semibold text-zinc-100">
<NuxtLink :href="`/library/${element.meta.id}`" class=""> <NuxtLink :href="`/library/${element.meta.id}`" class="">
@ -35,47 +49,68 @@
<p class="text-md text-zinc-500 uppercase font-display font-bold"> <p class="text-md text-zinc-500 uppercase font-display font-bold">
{{ element.status }} {{ element.status }}
</p> </p>
<div v-if="element.progress" class="mt-1 w-96 bg-zinc-800 rounded-lg overflow-hidden"> <div
<div class="h-2 bg-blue-600" :style="{ width: `${element.progress * 100}%` }" /> v-if="element.progress"
class="mt-1 w-96 bg-zinc-800 rounded-lg overflow-hidden"
>
<div
class="h-2 bg-blue-600"
:style="{ width: `${element.progress * 100}%` }"
/>
</div> </div>
<span
class="mt-2 inline-flex items-center gap-x-1 text-zinc-400 text-sm font-display"
><span class="text-zinc-300">{{
formatKilobytes(element.current / 1000)
}}</span>
/
<span class="">{{ formatKilobytes(element.max / 1000) }}</span
><ServerIcon class="size-5"
/></span>
</div> </div>
<button @click="() => cancelGame(element.meta)" class="group"> <button @click="() => cancelGame(element.meta)" class="group">
<XMarkIcon class="transition size-8 flex-none text-zinc-600 group-hover:text-zinc-300" <XMarkIcon
aria-hidden="true" /> class="transition size-8 flex-none text-zinc-600 group-hover:text-zinc-300"
aria-hidden="true"
/>
</button> </button>
</div> </div>
</li> </li>
<p v-else>Loading...</p> <p v-else>Loading...</p>
</template> </template>
</draggable> </draggable>
<div class="text-zinc-600 uppercase font-semibold font-display w-full text-center" v-if="queue.queue.length == 0"> <div
class="text-zinc-600 uppercase font-semibold font-display w-full text-center"
v-if="queue.queue.length == 0"
>
No items in the queue No items in the queue
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { XMarkIcon } from "@heroicons/vue/20/solid"; import { ServerIcon, XMarkIcon } from "@heroicons/vue/20/solid";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import type { DownloadableMetadata, Game, GameStatus } from "~/types"; import type { DownloadableMetadata, Game, GameStatus } from "~/types";
const windowWidth = ref(window.innerWidth); const windowWidth = ref(window.innerWidth);
window.addEventListener('resize', (event) => { window.addEventListener("resize", (event) => {
windowWidth.value = window.innerWidth; windowWidth.value = window.innerWidth;
}) });
const queue = useQueueState(); const queue = useQueueState();
const stats = useStatsState(); const stats = useStatsState();
const speedHistory = useState<Array<number>>(() => []); const speedHistory = useState<Array<number>>(() => []);
const speedHistoryMax = computed(() => windowWidth.value / 8); const speedHistoryMax = computed(() => windowWidth.value / 8);
const speedMax = computed(() => speedHistory.value.reduce((a, b) => a > b ? a : b) * 1.3); const speedMax = computed(
() => speedHistory.value.reduce((a, b) => (a > b ? a : b)) * 1.3
);
const previousGameId = ref<string | undefined>(); const previousGameId = ref<string | undefined>();
const games: Ref<{ const games: Ref<{
[key: string]: { game: Game; status: Ref<GameStatus>; cover: string }; [key: string]: { game: Game; status: Ref<GameStatus>; cover: string };
}> = ref({}); }> = ref({});
function resetHistoryGraph() { function resetHistoryGraph() {
speedHistory.value = []; speedHistory.value = [];
stats.value = { time: 0, speed: 0 }; stats.value = { time: 0, speed: 0 };
@ -97,8 +132,7 @@ function checkReset(v: QueueState) {
return; return;
} }
// If it's a different game now // If it's a different game now
if (currentGame != previousGameId.value if (currentGame != previousGameId.value) {
) {
previousGameId.value = currentGame; previousGameId.value = currentGame;
resetHistoryGraph(); resetHistoryGraph();
return; return;
@ -115,10 +149,12 @@ watch(stats, (v) => {
speedHistory.value.splice(0, 1); speedHistory.value.splice(0, 1);
} }
checkReset(queue.value); checkReset(queue.value);
}) });
function loadGamesForQueue(v: typeof queue.value) { function loadGamesForQueue(v: typeof queue.value) {
for (const { meta: { id } } of v.queue) { for (const {
meta: { id },
} of v.queue) {
if (games.value[id]) return; if (games.value[id]) return;
(async () => { (async () => {
const gameData = await useGame(id); const gameData = await useGame(id);
@ -152,7 +188,7 @@ function formatKilobytes(bytes: number): string {
unitIndex++; unitIndex++;
} }
return `${value.toFixed(1)} ${units[unitIndex]}/s`; return `${value.toFixed(1)} ${units[unitIndex]}`;
} }
function formatTime(seconds: number): string { function formatTime(seconds: number): string {
@ -162,7 +198,7 @@ function formatTime(seconds: number): string {
const minutes = Math.floor(seconds / 60); const minutes = Math.floor(seconds / 60);
if (minutes < 60) { if (minutes < 60) {
return `${minutes}m ${Math.round(seconds % 60)}s` return `${minutes}m ${Math.round(seconds % 60)}s`;
} }
const hours = Math.floor(minutes / 60); const hours = Math.floor(minutes / 60);

View File

@ -1,7 +1,7 @@
export default defineNuxtPlugin((nuxtApp) => { export default defineNuxtPlugin((nuxtApp) => {
// Also possible // Also possible
nuxtApp.hook("vue:error", (error, instance, info) => { nuxtApp.hook("vue:error", (error, instance, info) => {
console.log(error); console.error(error, info);
const router = useRouter(); const router = useRouter();
router.replace(`/error`); router.replace(`/error`);
}); });

View File

@ -1,7 +1,8 @@
use std::{ use std::{
collections::HashMap, collections::HashMap,
fs::{self, create_dir_all}, fs::{self, create_dir_all},
path::PathBuf, hash::Hash,
path::{Path, PathBuf},
sync::{LazyLock, Mutex, RwLockReadGuard, RwLockWriteGuard}, sync::{LazyLock, Mutex, RwLockReadGuard, RwLockWriteGuard},
}; };
@ -91,10 +92,18 @@ impl Database {
Self { Self {
applications: DatabaseApplications { applications: DatabaseApplications {
install_dirs: vec![games_base_dir.into()], install_dirs: vec![games_base_dir.into()],
..Default::default() game_statuses: HashMap::new(),
game_versions: HashMap::new(),
installed_game_version: HashMap::new(),
transient_statuses: HashMap::new(),
}, },
prev_database, prev_database,
..Default::default() base_url: "".to_owned(),
auth: None,
settings: Settings {
autostart: false,
max_download_threads: 4,
},
} }
} }
} }

View File

@ -350,6 +350,8 @@ impl DownloadManagerBuilder {
meta: DownloadableMetadata::clone(key), meta: DownloadableMetadata::clone(key),
status: val.status(), status: val.status(),
progress: val.progress().get_progress(), progress: val.progress().get_progress(),
current: val.progress().sum(),
max: val.progress().get_max(),
} }
}) })
.collect(); .collect();

View File

@ -23,7 +23,7 @@ pub struct ProgressObject {
//last_update: Arc<RwLock<Instant>>, //last_update: Arc<RwLock<Instant>>,
last_update_time: Arc<AtomicInstant>, last_update_time: Arc<AtomicInstant>,
bytes_last_update: Arc<AtomicUsize>, bytes_last_update: Arc<AtomicUsize>,
rolling: RollingProgressWindow<256>, rolling: RollingProgressWindow<250>,
} }
pub struct ProgressHandle { pub struct ProgressHandle {
@ -46,6 +46,15 @@ impl ProgressHandle {
.fetch_add(amount, std::sync::atomic::Ordering::Relaxed); .fetch_add(amount, std::sync::atomic::Ordering::Relaxed);
calculate_update(&self.progress_object); calculate_update(&self.progress_object);
} }
pub fn skip(&self, amount: usize) {
self.progress
.fetch_add(amount, std::sync::atomic::Ordering::Relaxed);
// Offset the bytes at last offset by this amount
self.progress_object
.bytes_last_update
.fetch_add(amount, Ordering::Relaxed);
// Dont' fire update
}
} }
impl ProgressObject { impl ProgressObject {
@ -97,15 +106,15 @@ impl ProgressObject {
} }
#[throttle(1, Duration::from_millis(20))] #[throttle(1, Duration::from_millis(20))]
pub fn calculate_update(progress_object: &ProgressObject) { pub fn calculate_update(progress: &ProgressObject) {
let last_update_time = progress_object let last_update_time = progress
.last_update_time .last_update_time
.swap(Instant::now(), Ordering::SeqCst); .swap(Instant::now(), Ordering::SeqCst);
let time_since_last_update = Instant::now().duration_since(last_update_time).as_millis(); let time_since_last_update = Instant::now().duration_since(last_update_time).as_millis();
let current_bytes_downloaded = progress_object.sum(); let current_bytes_downloaded = progress.sum();
let max = progress_object.get_max(); let max = progress.get_max();
let bytes_at_last_update = progress_object let bytes_at_last_update = progress
.bytes_last_update .bytes_last_update
.swap(current_bytes_downloaded, Ordering::Relaxed); .swap(current_bytes_downloaded, Ordering::Relaxed);
@ -115,8 +124,8 @@ pub fn calculate_update(progress_object: &ProgressObject) {
let bytes_remaining = max - current_bytes_downloaded; // bytes let bytes_remaining = max - current_bytes_downloaded; // bytes
progress_object.update_window(kilobytes_per_second); progress.update_window(kilobytes_per_second);
push_update(progress_object, bytes_remaining); push_update(progress, bytes_remaining);
} }
#[throttle(1, Duration::from_millis(500))] #[throttle(1, Duration::from_millis(500))]

View File

@ -127,25 +127,17 @@ impl GameDownloadAgent {
} }
fn download_manifest(&self) -> Result<(), ApplicationDownloadError> { fn download_manifest(&self) -> Result<(), ApplicationDownloadError> {
let base_url = DB.fetch_base_url();
let manifest_url = base_url
.join(
format!(
"/api/v1/client/game/manifest?id={}&version={}",
self.id,
encode(&self.version)
)
.as_str(),
)
.unwrap();
let header = generate_authorization_header(); let header = generate_authorization_header();
let client = reqwest::blocking::Client::new(); let client = reqwest::blocking::Client::new();
let response = client let response = make_request(
.get(manifest_url.to_string()) &client,
.header("Authorization", header) &["/api/v1/client/game/manifest"],
&[("id", &self.id), ("version", &self.version)],
|f| f.header("Authorization", header),
)
.map_err(|e| ApplicationDownloadError::Communication(e))?
.send() .send()
.unwrap(); .map_err(|e| ApplicationDownloadError::Communication(e.into()))?;
if response.status() != 200 { if response.status() != 200 {
return Err(ApplicationDownloadError::Communication( return Err(ApplicationDownloadError::Communication(
@ -266,9 +258,10 @@ impl GameDownloadAgent {
let progress = self.progress.get(index); let progress = self.progress.get(index);
let progress_handle = ProgressHandle::new(progress, self.progress.clone()); let progress_handle = ProgressHandle::new(progress, self.progress.clone());
// If we've done this one already, skip it // If we've done this one already, skip it
if self.completed_contexts.lock().unwrap().contains(&index) { if self.completed_contexts.lock().unwrap().contains(&index) {
progress_handle.add(context.length); progress_handle.skip(context.length);
continue; continue;
} }
@ -319,8 +312,10 @@ impl GameDownloadAgent {
// If we're not out of contexts, we're not done, so we don't fire completed // If we're not out of contexts, we're not done, so we don't fire completed
if completed_lock_len != contexts.len() { if completed_lock_len != contexts.len() {
info!( info!(
"download agent for {} exited without completing", "download agent for {} exited without completing ({}/{})",
self.id.clone() self.id.clone(),
completed_lock_len,
contexts.len(),
); );
self.stored_manifest self.stored_manifest
.set_completed_contexts(self.completed_contexts.lock().unwrap().as_slice()); .set_completed_contexts(self.completed_contexts.lock().unwrap().as_slice());
@ -385,6 +380,7 @@ impl Downloadable for GameDownloadAgent {
.unwrap(); .unwrap();
} }
// TODO: fix this function. It doesn't restart the download properly, nor does it reset the state properly
fn on_incomplete(&self, app_handle: &tauri::AppHandle) { fn on_incomplete(&self, app_handle: &tauri::AppHandle) {
let meta = self.metadata(); let meta = self.metadata();
*self.status.lock().unwrap() = DownloadStatus::Queued; *self.status.lock().unwrap() = DownloadStatus::Queued;

View File

@ -133,7 +133,6 @@ pub fn download_game_chunk(
if response.status() != 200 { if response.status() != 200 {
let err = response.json().unwrap(); let err = response.json().unwrap();
warn!("{:?}", err);
return Err(ApplicationDownloadError::Communication( return Err(ApplicationDownloadError::Communication(
RemoteAccessError::InvalidResponse(err), RemoteAccessError::InvalidResponse(err),
)); ));

View File

@ -53,6 +53,8 @@ pub struct QueueUpdateEventQueueData {
pub meta: DownloadableMetadata, pub meta: DownloadableMetadata,
pub status: DownloadStatus, pub status: DownloadStatus,
pub progress: f64, pub progress: f64,
pub current: usize,
pub max: usize,
} }
#[derive(serde::Serialize, Clone)] #[derive(serde::Serialize, Clone)]
@ -81,15 +83,12 @@ pub struct GameVersionOption {
} }
pub fn fetch_library_logic(app: AppHandle) -> Result<Vec<Game>, RemoteAccessError> { pub fn fetch_library_logic(app: AppHandle) -> Result<Vec<Game>, RemoteAccessError> {
let base_url = DB.fetch_base_url();
let library_url = base_url.join("/api/v1/client/user/library")?;
let header = generate_authorization_header(); let header = generate_authorization_header();
let client = reqwest::blocking::Client::new(); let client = reqwest::blocking::Client::new();
let response = client let response = make_request(&client, &["/api/v1/client/user/library"], &[], |f| {
.get(library_url.to_string()) f.header("Authorization", header)
.header("Authorization", header) })?
.send()?; .send()?;
if response.status() != 200 { if response.status() != 200 {
@ -290,25 +289,22 @@ pub fn on_game_complete(
app_handle: &AppHandle, app_handle: &AppHandle,
) -> Result<(), RemoteAccessError> { ) -> Result<(), RemoteAccessError> {
// Fetch game version information from remote // Fetch game version information from remote
let base_url = DB.fetch_base_url();
if meta.version.is_none() { if meta.version.is_none() {
return Err(RemoteAccessError::GameNotFound); return Err(RemoteAccessError::GameNotFound);
} }
let endpoint = base_url.join(
format!(
"/api/v1/client/metadata/version?id={}&version={}",
meta.id,
encode(meta.version.as_ref().unwrap())
)
.as_str(),
)?;
let header = generate_authorization_header(); let header = generate_authorization_header();
let client = reqwest::blocking::Client::new(); let client = reqwest::blocking::Client::new();
let response = client let response = make_request(
.get(endpoint.to_string()) &client,
.header("Authorization", header) &["/api/v1/client/metadata/version"],
&[
("id", &meta.id),
("version", meta.version.as_ref().unwrap()),
],
|f| f.header("Authorization", header),
)?
.send()?; .send()?;
let data: GameVersion = response.json()?; let data: GameVersion = response.json()?;

View File

@ -46,6 +46,7 @@ use remote::auth::{self, generate_authorization_header, recieve_handshake};
use remote::commands::{ use remote::commands::{
auth_initiate, gen_drop_url, manual_recieve_handshake, retry_connect, sign_out, use_remote, auth_initiate, gen_drop_url, manual_recieve_handshake, retry_connect, sign_out, use_remote,
}; };
use remote::requests::make_request;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::path::Path; use std::path::Path;
use std::sync::Arc; use std::sync::Arc;
@ -355,22 +356,15 @@ pub fn run() {
Ok(()) Ok(())
}) })
.register_asynchronous_uri_scheme_protocol("object", move |_ctx, request, responder| { .register_asynchronous_uri_scheme_protocol("object", move |_ctx, request, responder| {
let base_url = DB.fetch_base_url();
// Drop leading / // Drop leading /
let object_id = &request.uri().path()[1..]; let object_id = &request.uri().path()[1..];
let object_url = base_url
.join("/api/v1/client/object/")
.unwrap()
.join(object_id)
.unwrap();
let header = generate_authorization_header(); let header = generate_authorization_header();
let client: reqwest::blocking::Client = reqwest::blocking::Client::new(); let client: reqwest::blocking::Client = reqwest::blocking::Client::new();
let response = client let response = make_request(&client, &["/api/v1/client/object/", object_id], &[], |f| {
.get(object_url.to_string()) f.header("Authorization", header)
.header("Authorization", header) })
.unwrap()
.send(); .send();
if response.is_err() { if response.is_err() {
warn!( warn!(

View File

@ -15,6 +15,8 @@ use crate::{
AppState, AppStatus, User, DB, AppState, AppStatus, User, DB,
}; };
use super::requests::make_request;
#[derive(Serialize)] #[derive(Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct InitiateRequestBody { struct InitiateRequestBody {
@ -67,16 +69,15 @@ pub fn generate_authorization_header() -> String {
pub fn fetch_user() -> Result<User, RemoteAccessError> { pub fn fetch_user() -> Result<User, RemoteAccessError> {
let base_url = DB.fetch_base_url(); let base_url = DB.fetch_base_url();
let endpoint = base_url.join("/api/v1/client/user")?;
let header = generate_authorization_header(); let header = generate_authorization_header();
let client = reqwest::blocking::Client::new(); let client = reqwest::blocking::Client::new();
let response = client let response = make_request(&client, &["/api/v1/client/user"], &[], |f| {
.get(endpoint.to_string()) f.header("Authorization", header)
.header("Authorization", header) })?
.send()?; .send()?;
if response.status() != 200 { if response.status() != 200 {
let err: DropServerError = response.json().unwrap(); let err: DropServerError = response.json()?;
warn!("{:?}", err); warn!("{:?}", err);
if err.status_message == "Nonce expired" { if err.status_message == "Nonce expired" {

View File

@ -4,17 +4,17 @@ use crate::{database::db::DatabaseImpls, error::remote_access_error::RemoteAcces
pub fn make_request<T: AsRef<str>, F: FnOnce(RequestBuilder) -> RequestBuilder>( pub fn make_request<T: AsRef<str>, F: FnOnce(RequestBuilder) -> RequestBuilder>(
client: &Client, client: &Client,
endpoints: &[T], path_components: &[T],
params: &[(T, T)], query: &[(T, T)],
f: F, f: F,
) -> Result<RequestBuilder, RemoteAccessError> { ) -> Result<RequestBuilder, RemoteAccessError> {
let mut base_url = DB.fetch_base_url(); let mut base_url = DB.fetch_base_url();
for endpoint in endpoints { for endpoint in path_components {
base_url = base_url.join(endpoint.as_ref())?; base_url = base_url.join(endpoint.as_ref())?;
} }
{ {
let mut queries = base_url.query_pairs_mut(); let mut queries = base_url.query_pairs_mut();
for (param, val) in params { for (param, val) in query {
queries.append_pair(param.as_ref(), val.as_ref()); queries.append_pair(param.as_ref(), val.as_ref());
} }
} }