feat: global progress bar

Also fix me having no foresight and executing things
inside log::debug! macros
This commit is contained in:
2025-04-17 07:44:05 +00:00
parent 658a69a1e2
commit e9550e8eee
13 changed files with 127 additions and 33 deletions

View File

@ -4,6 +4,5 @@
### Long-term
- Progress bars and other GUI sugar
- artemis as a special package
- Other arcade games (if there is demand)

View File

@ -1,5 +1,4 @@
use std::{collections::HashSet, path::PathBuf};
use futures::Stream;
use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Emitter};
use tokio::fs::File;
@ -15,7 +14,7 @@ pub struct DownloadHandler {
#[derive(Serialize, Deserialize, Clone)]
pub struct DownloadTick {
pkg_key: PkgKey,
ratio: f32
ratio: f32,
}
impl DownloadHandler {
@ -50,14 +49,15 @@ impl DownloadHandler {
let mut cache_file_w = File::create(&zip_path_part).await?;
let mut byte_stream = reqwest::get(&rmt.download_url).await?.bytes_stream();
let first_hint = byte_stream.size_hint().0 as f32;
let mut total_bytes = 0;
log::info!("downloading: {}", rmt.download_url);
while let Some(item) = byte_stream.next().await {
let i = item?;
app.emit("download-tick", DownloadTick {
total_bytes += i.len();
_ = app.emit("download-progress", DownloadTick {
pkg_key: pkg_key.clone(),
ratio: 1.0f32 - (byte_stream.size_hint().0 as f32) / first_hint
ratio: (total_bytes as f32) / (rmt.file_size as f32),
})?;
cache_file_w.write_all(&mut i.as_ref()).await?;
}

View File

@ -102,14 +102,15 @@ pub async fn run(_args: Vec<String>) {
});
app.listen("download-end", closure!(clone apph, |ev| {
log::debug!("download-end triggered: {}", ev.payload());
let raw = ev.payload();
log::debug!("download-end triggered: {}", raw);
let key = PkgKey(raw[1..raw.len()-1].to_owned());
let apph = apph.clone();
tauri::async_runtime::spawn(async move {
let mutex = apph.state::<Mutex<AppData>>();
let mut appd = mutex.lock().await;
log::debug!("download-end install {:?}", appd.pkgs.install_package(&key, true, false).await);
let res = appd.pkgs.install_package(&key, true, false).await;
log::debug!("download-end install {:?}", res);
});
}));
@ -126,19 +127,21 @@ pub async fn run(_args: Vec<String>) {
}));
app.listen("install-end-prelude", closure!(clone apph, |ev| {
log::debug!("install-end-prelude triggered: {}", ev.payload());
let payload = serde_json::from_str::<Payload>(ev.payload());
log::debug!("install-end-prelude triggered: {:?}", payload);
let apph = apph.clone();
if let Ok(payload) = payload {
tauri::async_runtime::spawn(async move {
let mutex = apph.state::<Mutex<AppData>>();
let mut appd = mutex.lock().await;
let res = appd.toggle_package(payload.pkg.clone(), ToggleAction::EnableSelf);
log::debug!(
"install-end-prelude toggle {:?}",
appd.toggle_package(payload.pkg.clone(), ToggleAction::EnableSelf)
res
);
use tauri::Emitter;
log::debug!("install-end {:?}", apph.emit("install-end", payload));
let res = apph.emit("install-end", payload);
log::debug!("install-end {:?}", res);
});
} else {
log::error!("install-end-prelude: invalid payload: {}", ev.payload());
@ -232,7 +235,8 @@ pub async fn run(_args: Vec<String>) {
let mutex = app.state::<Mutex<AppData>>();
let appd = mutex.lock().await;
if let Some(p) = &appd.profile {
log::debug!("save: {:?}", p.save());
let res = p.save();
log::debug!("save: {:?}", res);
app.exit(0);
}
});

View File

@ -22,4 +22,5 @@ pub struct V1Version {
pub icon: String,
pub dependencies: BTreeSet<PkgKeyVersion>,
pub download_url: String,
pub file_size: i64,
}

View File

@ -81,6 +81,7 @@ pub struct Remote {
pub nsfw: bool,
pub categories: Vec<String>,
pub dependencies: BTreeSet<PkgKey>,
pub file_size: i64,
}
impl PkgKey {
@ -112,7 +113,8 @@ impl Package {
nsfw: p.has_nsfw_content,
version: v.version_number,
categories: p.categories,
dependencies: Self::sanitize_deps(v.dependencies)
dependencies: Self::sanitize_deps(v.dependencies),
file_size: v.file_size
}),
source: PackageSource::Rainy,
})

View File

@ -21,7 +21,7 @@ pub struct PackageStore {
offline: bool,
}
#[derive(Clone, Serialize, Deserialize)]
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct Payload {
pub pkg: PkgKey
}

View File

@ -88,6 +88,60 @@ listen<string>('launch-error', (event) => {
errorMessage.value = event.payload;
errorHeader.value = 'Launch error';
});
interface DownloadingStatus {
ratio: number;
pkg_key: string;
}
const downloading_status: Ref<DownloadingStatus[]> = ref([]);
const download_value = computed(() => {
return (
downloading_status.value.map((v) => v.ratio).reduce((a, v) => a * v) *
100
);
});
const downloadProgressText = computed(() => {
if (download_value.value < 7) {
return '';
}
let pkgs = `${downloading_status.value.length} package${downloading_status.value.length === 1 ? '' : 's'}`;
if (download_value.value < 14) {
return pkgs;
} else {
return `${pkgs} (${Math.floor(download_value.value)}%)`;
}
});
listen<DownloadingStatus>('download-progress', (event) => {
let status = downloading_status.value.find(
(v) => v.pkg_key === event.payload.pkg_key
);
if (status === undefined) {
status = {
ratio: 0,
pkg_key: event.payload.pkg_key,
};
downloading_status.value.push(status);
}
status.ratio = event.payload.ratio;
const remove = () => {
if (status !== undefined) {
downloading_status.value = downloading_status.value.filter(
(v) => v.pkg_key !== event.payload.pkg_key
);
}
};
if (status.ratio === 1.0) {
remove();
}
setTimeout(() => remove, 10_000);
});
</script>
<template>
@ -102,6 +156,16 @@ listen<string>('launch-error', (event) => {
: 'main-scale-xl'
"
>
<div
v-if="downloading_status.length > 0"
class="download-progress-bg"
></div>
<ProgressBar
v-if="downloading_status.length > 0"
:value="download_value"
class="download-progress"
>{{ downloadProgressText }}</ProgressBar
>
<ConfirmDialog>
<template #message="{ message }">
<ScrollPanel
@ -353,4 +417,23 @@ body {
.p-progressbar-label {
transition-duration: 0s !important;
}
.download-progress {
position: fixed !important;
bottom: 0;
left: 5vw;
width: 90vw;
z-index: 10000 !important;
margin: 20px auto;
}
.download-progress-bg {
position: fixed;
bottom: 0;
left: 0;
width: 100vw;
height: 60px;
background-color: var(--p-surface-900);
border-top: 1px solid var(--p-surface-600);
z-index: 998;
}
</style>

View File

@ -1,4 +1,5 @@
<script setup lang="ts">
import { ref } from 'vue';
import Button from 'primevue/button';
import { invoke } from '../invoke';
import { usePkgStore } from '../stores';
@ -11,20 +12,26 @@ const props = defineProps({
pkg: Object as () => Package,
});
const deleting = ref(false);
const remove = async () => {
if (props.pkg === undefined) {
return;
}
deleting.value = true;
await invoke('delete_package', {
key: pkgKey(props.pkg),
});
deleting.value = false;
};
</script>
<template>
<Button
v-if="pkg?.loc && !pkg?.js.busy"
v-if="pkg?.loc && !pkg?.js.downloading"
rounded
icon="pi pi-trash"
severity="danger"
@ -32,7 +39,7 @@ const remove = async () => {
size="small"
class="self-center ml-4"
style="width: 2rem; height: 2rem"
:loading="pkg?.js.busy"
:loading="deleting"
v-on:click="remove()"
/>
@ -45,7 +52,7 @@ const remove = async () => {
size="small"
class="self-center ml-4"
style="width: 2rem; height: 2rem"
:loading="pkg?.js.busy"
:loading="pkg?.js.downloading"
v-on:click="async () => await pkgs.install(pkg)"
/>
</template>

View File

@ -15,7 +15,6 @@ const props = defineProps({
const pkgs = usePkgStore();
const prf = usePrfStore();
const groupCallIndex = ref(0);
const empty = ref(false);
const gameSublist: Ref<string[]> = ref([]);
@ -46,10 +45,7 @@ const group = computed(() => {
({ namespace }) => namespace
)
);
if (groupCallIndex.value > 0) {
empty.value = Object.keys(res).length === 0;
}
groupCallIndex.value += 1;
empty.value = Object.keys(res).length === 0;
return res;
});

View File

@ -20,17 +20,15 @@ const install = async () => {
});
} catch (err) {
if (props.pkg !== undefined) {
props.pkg.js.busy = false;
props.pkg.js.downloading = false;
}
}
//if (rv === 'Deferred') { /* download progress */ }
};
</script>
<template>
<Button
v-if="needsUpdate(pkg) && !pkg?.js.busy"
v-if="needsUpdate(pkg) && !pkg?.js.downloading"
rounded
icon="pi pi-download"
severity="success"

View File

@ -76,7 +76,7 @@ const themeModel = computed({
>
<ToggleSwitch v-model="verboseModel" />
</OptionRow>
<OptionRow title="Light theme">
<OptionRow title="Theme">
<SelectButton
v-model="themeModel"
:options="[

View File

@ -119,13 +119,13 @@ export const usePkgStore = defineStore('pkg', {
listen<InstallStatus>('install-start', async (ev) => {
const key = ev.payload.pkg;
await this.reload(key);
this.pkg[key].js.busy = true;
this.pkg[key].js.downloading = true;
});
listen<InstallStatus>('install-end', async (ev) => {
const key = ev.payload.pkg;
await this.reload(key);
this.pkg[key].js.busy = false;
this.pkg[key].js.downloading = false;
});
},
@ -152,17 +152,22 @@ export const usePkgStore = defineStore('pkg', {
async reloadWith(key: string, pkg: Package) {
if (this.pkg[key] === undefined) {
this.pkg[key] = { js: { busy: false } } as Package;
this.pkg[key] = { js: { downloading: false } } as Package;
} else {
this.pkg[key].loc = null;
this.pkg[key].rmt = null;
}
Object.assign(this.pkg[key], pkg);
if (!pkg.js) {
pkg.js = { downloading: false };
}
if (pkg.rmt !== null) {
pkg.rmt.categories.forEach((c) =>
this.availableCategories.add(c)
);
pkg.js.downloading = false;
}
},
@ -193,9 +198,8 @@ export const usePkgStore = defineStore('pkg', {
force: true,
});
} catch (err) {
console.error(err);
if (pkg !== undefined) {
pkg.js.busy = false;
pkg.js.downloading = false;
}
}
},

View File

@ -19,7 +19,7 @@ export interface Package {
icon: string;
} | null;
js: {
busy: boolean;
downloading: boolean;
};
}