diff options
-rw-r--r-- | src-tauri/bindings/CommitHead.ts | 4 | ||||
-rw-r--r-- | src-tauri/bindings/PullRequestType.ts | 3 | ||||
-rw-r--r-- | src-tauri/bindings/PullsApiResponseElement.ts | 4 | ||||
-rw-r--r-- | src-tauri/bindings/Repo.ts | 3 | ||||
-rw-r--r-- | src-tauri/src/constants.rs | 6 | ||||
-rw-r--r-- | src-tauri/src/github/mod.rs | 1 | ||||
-rw-r--r-- | src-tauri/src/github/pull_requests.rs | 446 | ||||
-rw-r--r-- | src-tauri/src/github/release_notes.rs | 2 | ||||
-rw-r--r-- | src-tauri/src/main.rs | 4 | ||||
-rw-r--r-- | src-vue/src/components/PullRequestsSelector.vue | 103 | ||||
-rw-r--r-- | src-vue/src/plugins/modules/pull_requests.ts | 112 | ||||
-rw-r--r-- | src-vue/src/plugins/store.ts | 5 | ||||
-rw-r--r-- | src-vue/src/views/DeveloperView.vue | 7 |
13 files changed, 698 insertions, 2 deletions
diff --git a/src-tauri/bindings/CommitHead.ts b/src-tauri/bindings/CommitHead.ts new file mode 100644 index 00000000..58f31657 --- /dev/null +++ b/src-tauri/bindings/CommitHead.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 { Repo } from "./Repo"; + +export interface CommitHead { sha: string, ref: string, repo: Repo, }
\ No newline at end of file diff --git a/src-tauri/bindings/PullRequestType.ts b/src-tauri/bindings/PullRequestType.ts new file mode 100644 index 00000000..2d1fd0a8 --- /dev/null +++ b/src-tauri/bindings/PullRequestType.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 PullRequestType = "MODS" | "LAUNCHER";
\ No newline at end of file diff --git a/src-tauri/bindings/PullsApiResponseElement.ts b/src-tauri/bindings/PullsApiResponseElement.ts new file mode 100644 index 00000000..b2b5c476 --- /dev/null +++ b/src-tauri/bindings/PullsApiResponseElement.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 { CommitHead } from "./CommitHead"; + +export interface PullsApiResponseElement { number: bigint, title: string, url: string, head: CommitHead, html_url: string, }
\ No newline at end of file diff --git a/src-tauri/bindings/Repo.ts b/src-tauri/bindings/Repo.ts new file mode 100644 index 00000000..836f39d7 --- /dev/null +++ b/src-tauri/bindings/Repo.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 interface Repo { full_name: string, }
\ No newline at end of file diff --git a/src-tauri/src/constants.rs b/src-tauri/src/constants.rs index 73cb6fb3..57a10e02 100644 --- a/src-tauri/src/constants.rs +++ b/src-tauri/src/constants.rs @@ -28,3 +28,9 @@ pub const BLACKLISTED_MODS: [&str; 3] = [ // Titanfall2 game IDs on Origin/EA-App pub const TITANFALL2_ORIGIN_IDS: [&str; 2] = ["Origin.OFR.50.0001452", "Origin.OFR.50.0001456"]; + +// GitHub API endpoints for launcher/mods PRs +pub const PULLS_API_ENDPOINT_LAUNCHER: &str = + "https://api.github.com/repos/R2Northstar/NorthstarLauncher/pulls"; +pub const PULLS_API_ENDPOINT_MODS: &str = + "https://api.github.com/repos/R2Northstar/NorthstarMods/pulls"; diff --git a/src-tauri/src/github/mod.rs b/src-tauri/src/github/mod.rs index 80a1831a..942f0db2 100644 --- a/src-tauri/src/github/mod.rs +++ b/src-tauri/src/github/mod.rs @@ -1 +1,2 @@ +pub mod pull_requests; pub mod release_notes; diff --git a/src-tauri/src/github/pull_requests.rs b/src-tauri/src/github/pull_requests.rs new file mode 100644 index 00000000..2b7be30b --- /dev/null +++ b/src-tauri/src/github/pull_requests.rs @@ -0,0 +1,446 @@ +use crate::github::release_notes::fetch_github_releases_api; + +use anyhow::anyhow; +use app::check_is_valid_game_path; +use app::constants::{APP_USER_AGENT, PULLS_API_ENDPOINT_LAUNCHER, PULLS_API_ENDPOINT_MODS}; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::fs::File; +use std::io; +use std::io::prelude::*; +use std::path::Path; +use ts_rs::TS; + +#[derive(Serialize, Deserialize, Debug, Clone, TS)] +#[ts(export)] +struct Repo { + full_name: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, TS)] +#[ts(export)] +struct CommitHead { + sha: String, + #[serde(rename = "ref")] + gh_ref: String, + repo: Repo, +} + +#[derive(Serialize, Deserialize, Debug, Clone, TS)] +#[ts(export)] +pub struct PullsApiResponseElement { + number: i64, + title: String, + url: String, + head: CommitHead, + html_url: String, +} + +// GitHub API response JSON elements as structs +#[derive(Debug, Deserialize, Clone)] +struct WorkflowRun { + id: u64, + head_sha: String, +} +#[derive(Debug, Deserialize, Clone)] +struct ActionsRunsResponse { + workflow_runs: Vec<WorkflowRun>, +} + +#[derive(Debug, Deserialize, Clone)] +struct Artifact { + id: u64, + workflow_run: WorkflowRun, +} + +#[derive(Debug, Deserialize, Clone)] +struct ArtifactsResponse { + artifacts: Vec<Artifact>, +} + +#[derive(Serialize, Deserialize, Debug, Clone, TS)] +#[ts(export)] +pub enum PullRequestType { + MODS, + LAUNCHER, +} + +/// Parse pull requests from specified URL +pub async fn get_pull_requests(url: String) -> Result<Vec<PullsApiResponseElement>, String> { + let json_response = match fetch_github_releases_api(&url).await { + Ok(result) => result, + Err(err) => return Err(err.to_string()), + }; + + let pulls_response: Vec<PullsApiResponseElement> = match serde_json::from_str(&json_response) { + Ok(res) => res, + Err(err) => return Err(err.to_string()), + }; + + Ok(pulls_response) +} + +/// Gets either launcher or mods PRs +#[tauri::command] +pub async fn get_pull_requests_wrapper( + install_type: PullRequestType, +) -> Result<Vec<PullsApiResponseElement>, String> { + let api_pr_url = match install_type { + PullRequestType::MODS => PULLS_API_ENDPOINT_MODS, + PullRequestType::LAUNCHER => PULLS_API_ENDPOINT_LAUNCHER, + }; + + get_pull_requests(api_pr_url.to_string()).await +} + +fn unzip(zip_file_name: &str) -> String { + let fname = std::path::Path::new(zip_file_name); + let file = fs::File::open(fname).unwrap(); + + let mut archive = zip::ZipArchive::new(file).unwrap(); + + let mut folder_name = "".to_string(); + + for i in 0..archive.len() { + let mut file = archive.by_index(i).unwrap(); + let outpath = match file.enclosed_name() { + Some(path) => path.to_owned(), + None => continue, + }; + + { + let comment = file.comment(); + if !comment.is_empty() { + println!("File {} comment: {}", i, comment); + } + } + + if i == 0 { + // Sanity check that it's a folder + assert!((*file.name()).ends_with('/')); + + folder_name = format!("{}", outpath.display()); + println!("{}", folder_name); + } + + if (*file.name()).ends_with('/') { + fs::create_dir_all(&outpath).unwrap(); + } else { + if let Some(p) = outpath.parent() { + if !p.exists() { + fs::create_dir_all(p).unwrap(); + } + } + let mut outfile = fs::File::create(&outpath).unwrap(); + io::copy(&mut file, &mut outfile).unwrap(); + } + + // Get and Set permissions + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + + if let Some(mode) = file.unix_mode() { + fs::set_permissions(&outpath, fs::Permissions::from_mode(mode)).unwrap(); + } + } + } + folder_name +} + +pub async fn check_github_api(url: &str) -> Result<serde_json::Value, Box<dyn std::error::Error>> { + let client = reqwest::Client::new(); + let res = client + .get(url) + .header(reqwest::header::USER_AGENT, APP_USER_AGENT) + .send() + .await + .unwrap() + .text() + .await + .unwrap(); + + let json: serde_json::Value = serde_json::from_str(&res).expect("JSON was not well-formatted"); + + Ok(json) +} + +/// Downloads a file from given URL +async fn download_zip(download_url: String, location: String) -> Result<(), anyhow::Error> { + let client = reqwest::Client::new(); + let resp = client + .get(download_url) + .header(reqwest::header::USER_AGENT, APP_USER_AGENT) + .send() + .await?; + + // Error out earlier if non-successful response + if !resp.status().is_success() { + // Return error cause wrong game path + return Err(anyhow!( + "Couldn't download zip. Received error code \"{}\"", + resp.status() + )); + } + + let mut out = fs::File::create(format!("{}/ns-dev-test-helper-temp-pr-files.zip", location)) + .expect("failed to create file"); + let bytes = resp.bytes().await?; + let mut cursor = std::io::Cursor::new(bytes); + std::io::copy(&mut cursor, &mut out)?; + Ok(()) +} + +fn unzip_launcher_zip(zip_file_name: &str) -> String { + let outfolder_name = "ns-dev-test-helper-temp-pr-files"; + let fname = std::path::Path::new(zip_file_name); + let file = fs::File::open(fname).unwrap(); + + let mut archive = zip::ZipArchive::new(file).unwrap(); + + fs::create_dir_all(outfolder_name).unwrap(); + + for i in 0..archive.len() { + let mut file = archive.by_index(i).unwrap(); + let outpath = match file.enclosed_name() { + Some(path) => path.to_owned(), + None => continue, + }; + + { + let comment = file.comment(); + if !comment.is_empty() { + println!("File {} comment: {}", i, comment); + } + } + + // Only extract two hardcoded files + if *file.name() == *"NorthstarLauncher.exe" || *file.name() == *"Northstar.dll" { + println!( + "File {} extracted to \"{}\" ({} bytes)", + i, + outpath.display(), + file.size() + ); + if let Some(p) = outpath.parent() { + if !p.exists() { + fs::create_dir_all(p).unwrap(); + } + } + let mut outfile = + fs::File::create(format!("{}/{}", outfolder_name, outpath.display())).unwrap(); + std::io::copy(&mut file, &mut outfile).unwrap(); + } + + // Get and Set permissions + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + + if let Some(mode) = file.unix_mode() { + fs::set_permissions(&outpath, fs::Permissions::from_mode(mode)).unwrap(); + } + } + } + outfolder_name.to_string() +} + +/// Recursively copies files from one directory to another +fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> io::Result<()> { + fs::create_dir_all(&dst)?; + for entry in fs::read_dir(src)? { + let entry = entry?; + let ty = entry.file_type()?; + if ty.is_dir() { + copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?; + } else { + fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?; + } + } + Ok(()) +} + +/// Gets GitHub download link of a mods PR +fn get_mods_download_link(pull_request: PullsApiResponseElement) -> Result<String, anyhow::Error> { + // {pr object} -> number == pr_number + // -> head -> ref + // -> repo -> full_name + + // Use repo and branch name to get download link + let download_url = format!( + "https://github.com/{}/archive/refs/heads/{}.zip", + pull_request.head.repo.full_name, // repo name + pull_request.head.gh_ref, // branch name + ); + + Ok(download_url) +} + +/// Gets `nightly.link` artifact download link of a launcher PR +async fn get_launcher_download_link( + pull_request: PullsApiResponseElement, +) -> Result<String, String> { + // Iterate over the first 10 pages of + for i in 1..=10 { + // Crossreference with runs API + let runs_response: ActionsRunsResponse = match check_github_api(&format!( + "https://api.github.com/repos/R2Northstar/NorthstarLauncher/actions/runs?page={}", + i + )) + .await + { + Ok(result) => serde_json::from_value(result).unwrap(), + Err(err) => return Err(format!("{}", err)), + }; + + // Cross-reference PR head commit sha against workflow runs + for workflow_run in &runs_response.workflow_runs { + // If head commit sha of run and PR match, grab CI output + if workflow_run.head_sha == pull_request.head.sha { + // Check artifacts + let api_url = format!("https://api.github.com/repos/R2Northstar/NorthstarLauncher/actions/runs/{}/artifacts", workflow_run.id); + let artifacts_response: ArtifactsResponse = serde_json::from_value( + check_github_api(&api_url).await.expect("Failed request"), + ) + .unwrap(); + + // Iterate over artifacts + for artifact in artifacts_response.artifacts { + // Make sure run is from PR head commit + if artifact.workflow_run.head_sha == workflow_run.head_sha { + dbg!(artifact.id); + + // Download artifact + return Ok(format!("https://nightly.link/R2Northstar/NorthstarLauncher/actions/artifacts/{}.zip", artifact.id)); + } + } + } + } + } + + Err(format!( + "Couldn't grab download link for PR \"{}\". PR might be too old and therefore no CI build has been deleted. Maybe ask author to update?", + pull_request.number + )) +} + +/// Adds a batch file that allows for launching Northstar with mods PR profile +fn add_batch_file(game_install_path: &str) { + let batch_path = format!("{}/r2ns-launch-mod-pr-version.bat", game_install_path); + let path = Path::new(&batch_path); + let display = path.display(); + + // Open a file in write-only mode, returns `io::Result<File>` + let mut file = match File::create(path) { + Err(why) => panic!("couldn't create {}: {}", display, why), + Ok(file) => file, + }; + + // Write the string to `file`, returns `io::Result<()>` + let batch_file_content = + "NorthstarLauncher.exe -profile=R2Northstar-PR-test-managed-folder\r\n"; + + match file.write_all(batch_file_content.as_bytes()) { + Err(why) => panic!("couldn't write to {}: {}", display, why), + Ok(_) => println!("successfully wrote to {}", display), + } +} + +/// Downloads selected launcher PR and extracts it into game install path +#[tauri::command] +pub async fn apply_launcher_pr( + pull_request: PullsApiResponseElement, + game_install_path: &str, +) -> Result<(), String> { + // Exit early if wrong game path + check_is_valid_game_path(game_install_path)?; + + // get download link + let download_url = get_launcher_download_link(pull_request).await?; + + // download + match download_zip(download_url, ".".to_string()).await { + Ok(_) => (), + Err(err) => return Err(err.to_string()), + }; + + // extract + let zip_extract_folder_name = unzip_launcher_zip("ns-dev-test-helper-temp-pr-files.zip"); + fs::remove_file("ns-dev-test-helper-temp-pr-files.zip").unwrap(); + + // Copy downloaded folder to game install folder + match copy_dir_all(zip_extract_folder_name.clone(), game_install_path) { + Ok(_) => (), + Err(err) => { + return Err(format!("Failed copying files: {}", err)); + } + } + + // Delete old unzipped + fs::remove_dir_all(zip_extract_folder_name).unwrap(); + + println!("All done with installing launcher PR"); + Ok(()) +} + +/// Downloads selected mods PR and extracts it into profile in game install path +#[tauri::command] +pub async fn apply_mods_pr( + pull_request: PullsApiResponseElement, + game_install_path: &str, +) -> Result<(), String> { + // Exit early if wrong game path + check_is_valid_game_path(game_install_path)?; + + let download_url = match get_mods_download_link(pull_request) { + Ok(url) => url, + Err(err) => return Err(err.to_string()), + }; + + match download_zip(download_url, ".".to_string()).await { + Ok(()) => (), + Err(err) => return Err(err.to_string()), + }; + + // Extract folder and delete zip + let zip_extract_folder_name = unzip("ns-dev-test-helper-temp-pr-files.zip"); + fs::remove_file("ns-dev-test-helper-temp-pr-files.zip").unwrap(); + + // Delete previously managed folder + if std::fs::remove_dir_all(format!( + "{}/R2Northstar-PR-test-managed-folder", + game_install_path + )) + .is_err() + { + if std::path::Path::new(&format!( + "{}/R2Northstar-PR-test-managed-folder", + game_install_path + )) + .exists() + { + println!("Failed removing previous dir"); + } else { + println!("Failed removing folder that doesn't exist. Probably cause first run"); + } + }; + + // Copy downloaded folder to game install folder + copy_dir_all( + zip_extract_folder_name.clone(), + format!( + "{}/R2Northstar-PR-test-managed-folder/mods", + game_install_path + ), + ) + .unwrap(); + + // Delete old copy + std::fs::remove_dir_all(zip_extract_folder_name).unwrap(); + + // Add batch file to launch right profile + add_batch_file(game_install_path); + + println!("All done with installing mods PR"); + Ok(()) +} diff --git a/src-tauri/src/github/release_notes.rs b/src-tauri/src/github/release_notes.rs index 6dee4576..b59358ec 100644 --- a/src-tauri/src/github/release_notes.rs +++ b/src-tauri/src/github/release_notes.rs @@ -19,7 +19,7 @@ pub struct FlightCoreVersion { } // Fetches repo release API and returns response as string -async fn fetch_github_releases_api(url: &str) -> Result<String, String> { +pub async fn fetch_github_releases_api(url: &str) -> Result<String, String> { println!("Fetching releases notes from GitHub API"); let client = reqwest::Client::new(); diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index c0c8a186..c7451763 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -15,6 +15,7 @@ use app::{ }; mod github; +use github::pull_requests::{apply_launcher_pr, apply_mods_pr, get_pull_requests_wrapper}; use github::release_notes::{ check_is_flightcore_outdated, get_newest_flightcore_version, get_northstar_release_notes, }; @@ -115,6 +116,9 @@ fn main() { get_server_player_count, delete_thunderstore_mod, query_thunderstore_packages_api, + get_pull_requests_wrapper, + apply_launcher_pr, + apply_mods_pr, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-vue/src/components/PullRequestsSelector.vue b/src-vue/src/components/PullRequestsSelector.vue new file mode 100644 index 00000000..58a355f4 --- /dev/null +++ b/src-vue/src/components/PullRequestsSelector.vue @@ -0,0 +1,103 @@ +<template> + <div> + <el-collapse @change="onChange"> + <el-collapse-item title="Launcher PRs" name="1"> + <p v-if="pull_requests_launcher.length === 0"> + <el-progress + :show-text="false" + :percentage="100" + status="warning" + :indeterminate="true" + :duration="1" + style="margin: 15px" + /> + </p> + <el-card v-else shadow="hover" v-for="pull_request in pull_requests_launcher" + v-bind:key="pull_request.url"> + <el-button type="primary" @click="installLauncherPR(pull_request)">Install</el-button> + <a target="_blank" :href="pull_request.html_url"> + {{ pull_request.number }}: {{ pull_request.title }} + </a> + </el-card> + </el-collapse-item> + + <el-collapse-item title="Mods PRs" name="2"> + <div style="margin: 15px"> + <el-alert title="Warning" type="warning" :closable="false" show-icon> + Mod PRs are installed into a separate profile. Make sure to launch via + 'r2ns-launch-mod-pr-version.bat' or via '-profile=R2Northstar-PR-test-managed-folder' to actually + run the PR version! + </el-alert> + </div> + <p v-if="pull_requests_mods.length === 0"> + <el-progress + :show-text="false" + :percentage="100" + status="warning" + :indeterminate="true" + :duration="1" + style="margin: 15px" + /> + </p> + <el-card v-else shadow="hover" v-for="pull_request in pull_requests_mods" v-bind:key="pull_request.url"> + <el-button type="primary" @click="installModsPR(pull_request)">Install</el-button> + <a target="_blank" :href="pull_request.html_url"> + {{ pull_request.number }}: {{ pull_request.title }} + </a> + </el-card> + </el-collapse-item> + </el-collapse> + </div> +</template> + +<script lang="ts"> +import { defineComponent } from 'vue' +import { PullRequestType } from '../../../src-tauri/bindings/PullRequestType'; +import { PullsApiResponseElement } from '../../../src-tauri/bindings/PullsApiResponseElement'; +import { invoke } from "@tauri-apps/api"; +import { ElNotification } from "element-plus"; + +export default defineComponent({ + name: 'PullRequestsSelector', + computed: { + pull_requests_launcher(): PullsApiResponseElement[] { + return this.$store.state.pullrequests.pull_requests_launcher; + }, + pull_requests_mods(): PullsApiResponseElement[] { + return this.$store.state.pullrequests.pull_requests_mods; + }, + }, + methods: { + onChange(e: Object) { + const openedCollapseNames = Object.values(e); + if (openedCollapseNames.includes('1') && this.pull_requests_launcher.length === 0) { + this.getPullRequests('LAUNCHER'); + } + if (openedCollapseNames.includes('2') && this.pull_requests_mods.length === 0) { + this.getPullRequests('MODS'); + } + }, + async getPullRequests(pull_request_type: PullRequestType) { + this.$store.commit('getPullRequests', pull_request_type); + }, + async installLauncherPR(pull_request: PullsApiResponseElement) { + this.$store.commit('installLauncherPR', pull_request); + }, + async installModsPR(pull_request: PullsApiResponseElement) { + this.$store.commit('installModsPR', pull_request); + }, + } +}) +</script> + +<style scoped> +.el-collapse { + border-radius: var(--el-border-radius-base); + overflow: hidden; +} + +:deep(.el-collapse-item__header) { + padding-left: 10px; + font-size: 14px; +} +</style> diff --git a/src-vue/src/plugins/modules/pull_requests.ts b/src-vue/src/plugins/modules/pull_requests.ts new file mode 100644 index 00000000..e64703d3 --- /dev/null +++ b/src-vue/src/plugins/modules/pull_requests.ts @@ -0,0 +1,112 @@ +import { ElNotification } from "element-plus"; +import { invoke } from "@tauri-apps/api"; +import { PullsApiResponseElement } from "../../../../src-tauri/bindings/PullsApiResponseElement"; +import { PullRequestType } from '../../../../src-tauri/bindings/PullRequestType'; +import { store } from "../store"; + +interface PullRequestStoreState { + searchValue: string, + pull_requests_launcher: PullsApiResponseElement[], + pull_requests_mods: PullsApiResponseElement[], +} + +export const pullRequestModule = { + state: () => ({ + pull_requests_launcher: [], + pull_requests_mods: [], + }), + mutations: { + async getPullRequests(state: PullRequestStoreState, pull_request_type: PullRequestType) { + await invoke<PullsApiResponseElement[]>("get_pull_requests_wrapper", { installType: pull_request_type }) + .then((message) => { + switch (pull_request_type) { + case "MODS": + state.pull_requests_mods = message; + break; + + case "LAUNCHER": + state.pull_requests_launcher = message; + break; + + default: + console.error("We should never end up here"); + } + }) + .catch((error) => { + ElNotification({ + title: 'Error', + message: error, + type: 'error', + position: 'bottom-right' + }); + }); + }, + async installLauncherPR(state: PullRequestStoreState, pull_request: PullsApiResponseElement) { + // Send notification telling the user to wait for the process to finish + const notification = ElNotification({ + title: `Installing launcher PR ${pull_request.number}`, + message: 'Please wait', + duration: 0, + type: 'info', + position: 'bottom-right' + }); + + await invoke("apply_launcher_pr", { pullRequest: pull_request, gameInstallPath: store.state.game_path }) + .then((message) => { + console.log(message); + // Show user notification if mod install completed. + ElNotification({ + title: `Done`, + message: `Installed ${pull_request.number}: "${pull_request.title}"`, + type: 'success', + position: 'bottom-right' + }); + }) + .catch((error) => { + ElNotification({ + title: 'Error', + message: error, + type: 'error', + position: 'bottom-right' + }); + }) + .finally(() => { + // Clear old notification + notification.close(); + }); + }, + async installModsPR(state: PullRequestStoreState, pull_request: PullsApiResponseElement) { + // Send notification telling the user to wait for the process to finish + const notification = ElNotification({ + title: `Installing mods PR ${pull_request.number}`, + message: 'Please wait', + duration: 0, + type: 'info', + position: 'bottom-right' + }); + + await invoke("apply_mods_pr", { pullRequest: pull_request, gameInstallPath: store.state.game_path }) + .then((message) => { + // Show user notification if mod install completed. + ElNotification({ + title: `Done`, + message: `Installed ${pull_request.number}: "${pull_request.title}"\nMake sure to launch via batch file or by specifying correct profile!`, + type: 'success', + position: 'bottom-right' + }); + }) + .catch((error) => { + ElNotification({ + title: 'Error', + message: error, + type: 'error', + position: 'bottom-right' + }); + }) + .finally(() => { + // Clear old notification + notification.close(); + }); + }, + } +} diff --git a/src-vue/src/plugins/store.ts b/src-vue/src/plugins/store.ts index e44b1c3f..8671d12b 100644 --- a/src-vue/src/plugins/store.ts +++ b/src-vue/src/plugins/store.ts @@ -16,6 +16,8 @@ import { ReleaseInfo } from "../../../src-tauri/bindings/ReleaseInfo"; import { ThunderstoreMod } from "../../../src-tauri/bindings/ThunderstoreMod"; import { NorthstarMod } from "../../../src-tauri/bindings/NorthstarMod"; import { searchModule } from './modules/search'; +import { pullRequestModule } from './modules/pull_requests'; +import { PullsApiResponseElement } from "../../../src-tauri/bindings/PullsApiResponseElement"; const persistentStore = new Store('flight-core-settings.json'); @@ -51,7 +53,8 @@ let notification_handle: NotificationHandle; export const store = createStore<FlightCoreStore>({ modules: { - search: searchModule + search: searchModule, + pullrequests: pullRequestModule, }, state(): FlightCoreStore { return { diff --git a/src-vue/src/views/DeveloperView.vue b/src-vue/src/views/DeveloperView.vue index b51e1bf1..11cb4f5d 100644 --- a/src-vue/src/views/DeveloperView.vue +++ b/src-vue/src/views/DeveloperView.vue @@ -55,6 +55,9 @@ <el-button type="primary" @click="clearFlightCorePersistentStore"> Delete FlightCore persistent store </el-button> + + <h3>Testing</h3> + <pull-requests-selector /> </el-scrollbar> </div> </template> @@ -66,10 +69,14 @@ import { ElNotification } from "element-plus"; import { GameInstall } from "../utils/GameInstall"; import { Store } from 'tauri-plugin-store-api'; import { ReleaseCanal } from "../utils/ReleaseCanal"; +import PullRequestsSelector from "../components/PullRequestsSelector.vue"; const persistentStore = new Store('flight-core-settings.json'); export default defineComponent({ name: "DeveloperView", + components: { + PullRequestsSelector + }, data() { return { mod_to_install_field_string : "", |