diff options
-rw-r--r-- | src-tauri/bindings/InstallProgress.ts | 4 | ||||
-rw-r--r-- | src-tauri/bindings/InstallState.ts | 3 | ||||
-rw-r--r-- | src-tauri/src/lib.rs | 74 | ||||
-rw-r--r-- | src-tauri/src/main.rs | 6 | ||||
-rw-r--r-- | src-vue/src/components/InstallProgressBar.vue | 102 | ||||
-rw-r--r-- | src-vue/src/components/PlayButton.vue | 48 | ||||
-rw-r--r-- | src-vue/src/i18n/lang/en.json | 4 | ||||
-rw-r--r-- | src-vue/src/views/PlayView.vue | 17 | ||||
-rw-r--r-- | src-vue/src/views/RepairView.vue | 11 |
9 files changed, 231 insertions, 38 deletions
diff --git a/src-tauri/bindings/InstallProgress.ts b/src-tauri/bindings/InstallProgress.ts new file mode 100644 index 00000000..7bea9bb8 --- /dev/null +++ b/src-tauri/bindings/InstallProgress.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { InstallState } from "./InstallState"; + +export interface InstallProgress { current_downloaded: bigint, total_size: bigint, state: InstallState, }
\ No newline at end of file diff --git a/src-tauri/bindings/InstallState.ts b/src-tauri/bindings/InstallState.ts new file mode 100644 index 00000000..21dbc0c7 --- /dev/null +++ b/src-tauri/bindings/InstallState.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type InstallState = "DOWNLOADING" | "EXTRACTING" | "DONE";
\ No newline at end of file diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index a3553448..56bc590c 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,4 +1,4 @@ -use std::{env, fs, path::Path, time::Duration}; +use std::{cell::RefCell, env, fs, path::Path, time::Duration, time::Instant}; use anyhow::{anyhow, Context, Result}; @@ -52,6 +52,22 @@ pub struct NorthstarServer { pub player_count: i32, } +#[derive(Serialize, Deserialize, Debug, Clone, TS)] +#[ts(export)] +pub enum InstallState { + DOWNLOADING, + EXTRACTING, + DONE, +} + +#[derive(Serialize, Deserialize, Debug, Clone, TS)] +#[ts(export)] +struct InstallProgress { + current_downloaded: u64, + total_size: u64, + state: InstallState, +} + /// Check version number of a mod pub fn check_mod_version_number(path_to_mod_folder: String) -> Result<String, anyhow::Error> { // println!("{}", format!("{}/mod.json", path_to_mod_folder)); @@ -187,7 +203,11 @@ fn extract(zip_file: std::fs::File, target: &std::path::Path) -> Result<()> { ///Install N* from the provided mod /// ///Checks cache, else downloads the latest version -async fn do_install(nmod: &thermite::model::ModVersion, game_path: &std::path::Path) -> Result<()> { +async fn do_install( + window: tauri::Window, + nmod: &thermite::model::ModVersion, + game_path: &std::path::Path, +) -> Result<()> { let filename = format!("northstar-{}.zip", nmod.version); let download_directory = format!("{}/___flightcore-temp-download-dir/", game_path.display()); @@ -196,7 +216,43 @@ async fn do_install(nmod: &thermite::model::ModVersion, game_path: &std::path::P let download_path = format!("{}/{}", download_directory, filename); log::info!("Download path: {download_path}"); - let nfile = thermite::core::manage::download_file(&nmod.url, download_path).unwrap(); + let last_emit = RefCell::new(Instant::now()); // Keep track of the last time a signal was emitted + let nfile = thermite::core::manage::download_file_with_progress( + &nmod.url, + download_path, + |delta, current, total| { + if delta != 0 { + // Only emit a signal once every 100ms + // This way we don't bombard the frontend with events on fast download speeds + let time_since_last_emit = Instant::now().duration_since(*last_emit.borrow()); + if time_since_last_emit >= Duration::from_millis(100) { + window + .emit( + "northstar-install-download-progress", + InstallProgress { + current_downloaded: current, + total_size: total, + state: InstallState::DOWNLOADING, + }, + ) + .unwrap(); + *last_emit.borrow_mut() = Instant::now(); + } + } + }, + ) + .unwrap(); + + window + .emit( + "northstar-install-download-progress", + InstallProgress { + current_downloaded: 0, + total_size: 0, + state: InstallState::EXTRACTING, + }, + ) + .unwrap(); log::info!("Extracting Northstar..."); extract(nfile, game_path)?; @@ -206,11 +262,22 @@ async fn do_install(nmod: &thermite::model::ModVersion, game_path: &std::path::P std::fs::remove_dir_all(download_directory).unwrap(); log::info!("Done installing Northstar!"); + window + .emit( + "northstar-install-download-progress", + InstallProgress { + current_downloaded: 0, + total_size: 0, + state: InstallState::DONE, + }, + ) + .unwrap(); Ok(()) } pub async fn install_northstar( + window: tauri::Window, game_path: &str, northstar_package_name: Option<String>, ) -> Result<String, String> { @@ -235,6 +302,7 @@ pub async fn install_northstar( log::info!("Install path \"{}\"", game_path); match do_install( + window, nmod.versions.get(&nmod.latest).unwrap(), std::path::Path::new(game_path), ) diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 16b951ec..76e8833e 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -284,11 +284,12 @@ async fn get_host_os_caller() -> String { #[tauri::command] /// Installs Northstar to the given path async fn install_northstar_caller( + window: tauri::Window, game_path: String, northstar_package_name: Option<String>, ) -> Result<bool, String> { log::info!("Running"); - match install_northstar(&game_path, northstar_package_name).await { + match install_northstar(window, &game_path, northstar_package_name).await { Ok(_) => Ok(true), Err(err) => { log::error!("{}", err); @@ -300,13 +301,14 @@ async fn install_northstar_caller( #[tauri::command] /// Update Northstar install in the given path async fn update_northstar_caller( + window: tauri::Window, game_path: String, northstar_package_name: Option<String>, ) -> Result<bool, String> { log::info!("Updating Northstar"); // Simply re-run install with up-to-date version for upate - match install_northstar(&game_path, northstar_package_name).await { + match install_northstar(window, &game_path, northstar_package_name).await { Ok(_) => Ok(true), Err(err) => { log::error!("{}", err); diff --git a/src-vue/src/components/InstallProgressBar.vue b/src-vue/src/components/InstallProgressBar.vue new file mode 100644 index 00000000..d0c2047c --- /dev/null +++ b/src-vue/src/components/InstallProgressBar.vue @@ -0,0 +1,102 @@ +<script lang="ts"> +import { defineComponent } from 'vue'; +import { appWindow } from '@tauri-apps/api/window'; +import { InstallProgress } from '../../../src-tauri/bindings/InstallProgress'; + +export default defineComponent({ + name: 'InstallProgressBar', + computed: { + progressBarStyle(): string { + return !this.install_or_update ? 'hide-progress' : ''; + } + }, + data() { + return { + percentage: 0, + color: '#409EFF', + install_or_update: false, + status: "unknown", + current_downloaded: -1, + total_size: -1, + }; + }, + methods: { + formatBytes(bytes: number, decimals = 2) { + if (bytes === 0) return '0 Bytes'; + const k = 1000; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; + }, + formatText() { + if (this.status == "DOWNLOADING") { + const current_downloaded_string = this.formatBytes(this.current_downloaded); + const total_size_string = this.formatBytes(this.total_size); + const status = this.$t("generic.downloading"); + return `${status}: ${current_downloaded_string}/${total_size_string}`; + } + if (this.status == "EXTRACTING") { + return this.$t("generic.extracting"); + } + return "Inactive"; // Needed to keep same size format when progress bar is hidden + } + }, + mounted() { + appWindow.listen<InstallProgress>( + 'northstar-install-download-progress', + ({ event, payload }) => { + this.install_or_update = true; + let progress = payload; + this.status = progress.state; + if (progress.state == "DOWNLOADING") { + this.percentage = ((Number(progress.current_downloaded) / Number(progress.total_size)) * 100); + this.color = '#409EFF'; + this.current_downloaded = Number(progress.current_downloaded); + this.total_size = Number(progress.total_size); + } + if (progress.state == "EXTRACTING") { + this.percentage = 100; + this.color = '#67C23A'; + } + if (progress.state == "DONE") { + // Clear state again + this.install_or_update = false + } + } + ); + } +}); +</script> + +<template> + <el-progress + :class="progressBarStyle" + :format="formatText" + :percentage="percentage" + :color="color" + :indeterminate="status === 'EXTRACTING'" + :duration="1" + > + </el-progress> +</template> + +<style scoped> +.el-progress { + margin-top: 10px; +} + +/* Set progress bar width */ +.el-progress:deep(.el-progress-bar) { + width: 200px; + flex-grow: initial; +} + +.el-progress:deep(.el-progress__text) { + line-height: 1.2; +} + +.hide-progress { + opacity: 0; +} +</style> diff --git a/src-vue/src/components/PlayButton.vue b/src-vue/src/components/PlayButton.vue index 3efcc9f5..208b4703 100644 --- a/src-vue/src/components/PlayButton.vue +++ b/src-vue/src/components/PlayButton.vue @@ -83,10 +83,10 @@ export default defineComponent({ return this.showReleaseSwitch ? 'border-radius: 2px 0 0 2px;' : 'border-radius: 2px'; - } + }, }, methods: { - launchGame() { + async launchGame() { this.$store.commit('launchGame'); } } @@ -94,30 +94,31 @@ export default defineComponent({ </script> <template> - <el-button :disabled="northstarIsRunning" - type="primary" size="large" @click="launchGame" - class="fc_launch__button" :style="buttonRadiusStyle"> - {{ playButtonLabel }} - </el-button> - <el-select v-if="showReleaseSwitch" :disabled="northstarIsRunning" - v-model="currentCanal" placeholder="Select"> - <el-option-group - v-for="group in selectOptions" - :key="group.label" - :label="group.label" - > - <el-option - v-for="item in group.options" - :key="item.value" - :label="item.label" - :value="item.value" - /> - </el-option-group> - </el-select> + <nav> + <el-button :disabled="northstarIsRunning" + type="primary" size="large" @click="launchGame" + class="fc_launch__button" :style="buttonRadiusStyle"> + {{ playButtonLabel }} + </el-button> + <el-select v-if="showReleaseSwitch" :disabled="northstarIsRunning" + v-model="currentCanal" placeholder="Select"> + <el-option-group + v-for="group in selectOptions" + :key="group.label" + :label="group.label" + > + <el-option + v-for="item in group.options" + :key="item.value" + :label="item.label" + :value="item.value" + /> + </el-option-group> + </el-select> + </nav> </template> <style scoped> - button { text-transform: uppercase; padding: 30px; @@ -130,7 +131,6 @@ button { } /* Release canal selector */ - .el-select { width: 0; margin-right: 50px; diff --git a/src-vue/src/i18n/lang/en.json b/src-vue/src/i18n/lang/en.json index 5e1974cc..3553d2a5 100644 --- a/src-vue/src/i18n/lang/en.json +++ b/src-vue/src/i18n/lang/en.json @@ -12,7 +12,9 @@ "no": "No", "error": "Error", "cancel": "Cancel", - "informationShort": "Info" + "informationShort": "Info", + "downloading": "Downloading", + "extracting": "Extracting" }, "play": { diff --git a/src-vue/src/views/PlayView.vue b/src-vue/src/views/PlayView.vue index 2f3562ef..76f4f328 100644 --- a/src-vue/src/views/PlayView.vue +++ b/src-vue/src/views/PlayView.vue @@ -2,10 +2,12 @@ import { Tabs } from "../utils/Tabs"; import PlayButton from '../components/PlayButton.vue'; import { defineComponent } from "vue"; +import InstallProgressBar from "../components/InstallProgressBar.vue"; export default defineComponent({ components: { - PlayButton + PlayButton, + InstallProgressBar }, computed: { northstarIsRunning(): boolean { @@ -45,7 +47,9 @@ export default defineComponent({ {{ $t('play.unable_to_load_playercount') }} </div> </div> - <div> + + <!-- Align play button and services state container --> + <div style="display: flex"> <PlayButton /> <div v-if="$store.state.developer_mode" id="fc_services__status"> <div> @@ -58,12 +62,13 @@ export default defineComponent({ </div> </div> </div> + <InstallProgressBar /> </div> </template> <style scoped> .fc_launch__container { - margin: 50px; + margin: 50px 50px 30px 50px; position: fixed; bottom: 0; } @@ -101,13 +106,9 @@ export default defineComponent({ border-color: var(--el-color-primary); } - #fc_services__status { - display: inline-block; - position: fixed; - padding: 10px 20px; color: #e8edef; - bottom: 43px; + align-self: end; } .fc_version__line { diff --git a/src-vue/src/views/RepairView.vue b/src-vue/src/views/RepairView.vue index 9de9a7f3..3d449d70 100644 --- a/src-vue/src/views/RepairView.vue +++ b/src-vue/src/views/RepairView.vue @@ -33,6 +33,7 @@ <script lang="ts"> import { defineComponent } from "vue"; import { GameInstall } from "../utils/GameInstall"; +import { InstallProgress } from "../../../src-tauri/bindings/InstallProgress"; import { invoke } from "@tauri-apps/api"; import { ReleaseCanal } from "../utils/ReleaseCanal"; import { Store } from 'tauri-plugin-store-api'; @@ -76,6 +77,16 @@ export default defineComponent({ ); let install_northstar_result = invoke("install_northstar_caller", { gamePath: game_install.game_path, northstarPackageName: ReleaseCanal.RELEASE }); + + appWindow.listen<InstallProgress>( + 'northstar-install-download-progress', + ({ event, payload }) => { + let typed_payload = payload; + console.log("current_downloaded:", typed_payload.current_downloaded); + console.log("total_size: ", typed_payload.total_size); + console.log("state: ", typed_payload.state); + } + ); await install_northstar_result .then((message) => { // Send notification |