backend: majority of version management implemented

This commit is contained in:
Tyler Wilding 2023-02-16 23:19:10 -05:00
parent a027e59e75
commit fee811df72
No known key found for this signature in database
GPG key ID: 77CB07796494137E
8 changed files with 299 additions and 118 deletions

View file

@ -1,18 +1,19 @@
use futures_util::StreamExt;
use std::{collections::HashMap, error::Error, path::Path};
use std::{io::Cursor, path::Path};
use tokio::{fs::File, io::AsyncWriteExt};
use crate::config::LauncherConfig;
use crate::{config::LauncherConfig, util};
#[tauri::command]
pub async fn list_downloaded_official_versions(
pub async fn list_downloaded_versions(
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
version_folder: String,
) -> Result<Vec<String>, ()> {
let config_lock = config.lock().await;
match &config_lock.installation_dir {
None => Ok(Vec::new()),
Some(path) => {
let expected_path = Path::new(&path).join("/versions/official");
let expected_path = Path::new(path).join("versions").join(version_folder);
if !expected_path.is_dir() {
Ok(Vec::new())
} else {
@ -24,7 +25,11 @@ pub async fn list_downloaded_official_versions(
e.ok().and_then(|d| {
let p = d.path();
if p.is_dir() {
Some(p.to_string_lossy().into_owned())
Some(
p.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or("".into()),
)
} else {
None
}
@ -48,15 +53,12 @@ pub async fn download_official_version(
match &config_lock.installation_dir {
None => Ok(()),
Some(path) => {
println!("BLAH - {}", path);
// TODO - make the dir
let expected_path = Path::new(&path).join("versions/official/test");
println!("{}", expected_path.display());
// TODO - severe lack of safety here!
// TODO - make the dir and the file name
let expected_path = Path::new(&path).join("versions/official/test.zip");
let client = reqwest::Client::new();
println!("{}", url);
let mut req = client.get(url);
let res = req.send().await.expect("");
println!("{:?}", res);
let total = res.content_length().expect("");
let mut file = File::create(expected_path).await.expect("");
@ -66,7 +68,62 @@ pub async fn download_official_version(
let chunk = chunk.expect("");
file.write_all(&chunk).await.expect("");
}
let target_dir = Path::new(&path).join("versions/official/").join(version);
let zip_path = Path::new(&path).join("versions/official/test.zip");
let archive: Vec<u8> = std::fs::read(&zip_path.clone()).unwrap();
zip_extract::extract(Cursor::new(archive), &target_dir, true).expect("");
std::fs::remove_file(zip_path).expect("TODO");
Ok(())
}
}
}
#[tauri::command]
pub async fn go_to_version_folder(
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
version_folder: String,
) -> Result<(), ()> {
let config_lock = config.lock().await;
match &config_lock.installation_dir {
None => Err(()),
Some(path) => {
let expected_path = Path::new(path).join("versions").join(version_folder);
util::open_dir_in_os(expected_path.to_string_lossy().into_owned());
Ok(())
}
}
}
#[tauri::command]
pub async fn save_active_version_change(
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
version_folder: String,
new_active_version: String,
) -> Result<(), ()> {
let mut config_lock = config.lock().await;
// TODO - error checking
config_lock.set_active_version_folder(version_folder);
config_lock.set_active_version(new_active_version);
Ok(())
}
#[tauri::command]
pub async fn get_active_version(
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
) -> Result<Option<String>, ()> {
let config_lock = config.lock().await;
Ok(config_lock.active_version.clone())
}
#[tauri::command]
pub async fn get_active_version_folder(
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
) -> Result<Option<String>, ()> {
let config_lock = config.lock().await;
Ok(config_lock.active_version_folder.clone())
}

View file

@ -38,6 +38,7 @@ impl SupportedGame {
pub struct GameConfig {
is_installed: bool,
version: Option<String>,
version_folder: Option<String>,
}
impl GameConfig {
@ -45,6 +46,7 @@ impl GameConfig {
Self {
is_installed: false,
version: None,
version_folder: None,
}
}
}
@ -101,6 +103,8 @@ pub struct LauncherConfig {
pub games: SupportedGames,
pub last_active_game: Option<SupportedGame>,
pub installation_dir: Option<String>,
pub active_version: Option<String>,
pub active_version_folder: Option<String>,
}
// TODO - what is _loaded?
@ -118,6 +122,8 @@ impl LauncherConfig {
games: SupportedGames::default(),
last_active_game: None,
installation_dir: None,
active_version: None,
active_version_folder: Some("official".to_string()),
}
}
@ -181,4 +187,14 @@ impl LauncherConfig {
self.installation_dir = Some(new_dir);
self.save_config();
}
pub fn set_active_version(&mut self, new_version: String) {
self.active_version = Some(new_version);
self.save_config();
}
pub fn set_active_version_folder(&mut self, new_version_folder: String) {
self.active_version_folder = Some(new_version_folder);
self.save_config();
}
}

View file

@ -10,6 +10,7 @@ use std::env;
mod commands;
mod config;
mod textures;
mod util;
use commands::{
close_splashscreen, copy_dir, get_highest_simd, get_install_directory, open_dir, open_repl,
set_install_directory,
@ -19,6 +20,8 @@ use textures::{extract_textures, get_all_texture_packs};
pub type FFIResult<T> = Result<T, String>;
fn main() {
// TODO - switch to https://github.com/daboross/fern so we can setup easy logging
// to a file as well
if env::var_os("RUST_LOG").is_none() {
env::set_var("RUST_LOG", "debug");
}
@ -44,8 +47,12 @@ fn main() {
get_install_directory,
set_install_directory,
// Version Management,
commands::versions::list_downloaded_official_versions,
commands::versions::list_downloaded_versions,
commands::versions::download_official_version,
commands::versions::go_to_version_folder,
commands::versions::save_active_version_change,
commands::versions::get_active_version,
commands::versions::get_active_version_folder,
// Requirements Checking
get_highest_simd,
open_dir,

View file

@ -1,86 +1,87 @@
use serde::{Deserialize, Serialize};
use std::{
fs,
io::{self, Cursor, Read},
path::{Path, PathBuf},
fs,
io::{self, Cursor, Read},
path::{Path, PathBuf},
};
#[derive(Serialize, Deserialize, Debug)]
pub struct TexturePack {
author: String,
description: String,
version: String,
path: Option<PathBuf>,
author: String,
description: String,
version: String,
path: Option<PathBuf>,
}
#[tauri::command]
pub async fn extract_textures(app_handle: tauri::AppHandle, textures_array: Vec<String>) {
let text_dir = app_handle
.path_resolver()
.app_dir()
.unwrap()
.join("data/texture_replacements");
let text_dir = app_handle
.path_resolver()
.app_dir()
.unwrap()
.join("data/texture_replacements");
let target_dir = PathBuf::from(text_dir.clone()); // Doesn't need to exist
let target_dir = PathBuf::from(text_dir.clone()); // Doesn't need to exist
for path in textures_array {
println!("Extracting texture pack: {:?}", path.clone());
// for path in textures_array {
// println!("Extracting texture pack: {:?}", path.clone());
let archive: Vec<u8> = fs::read(&path.clone()).unwrap();
// The third parameter allows you to strip away toplevel directories.
// If `archive` contained a single directory, its contents would be extracted instead.
match zip_extract::extract(Cursor::new(archive), &target_dir, true) {
Ok(_) => continue,
Err(err) => println!("{:?}", err),
}
}
// let archive: Vec<u8> = fs::read(&path.clone()).unwrap();
// // The third parameter allows you to strip away toplevel directories.
// // If `archive` contained a single directory, its contents would be extracted instead.
// match zip_extract::extract(Cursor::new(archive), &target_dir, true) {
// Ok(_) => continue,
// Err(err) => println!("{:?}", err),
// }
// }
}
fn read_texture_json_file(file_path: PathBuf) -> Result<TexturePack, io::Error> {
let zipfile = std::fs::File::open(&file_path)?;
let mut zip = zip::ZipArchive::new(zipfile).unwrap();
let zipfile = std::fs::File::open(&file_path)?;
let mut zip = zip::ZipArchive::new(zipfile).unwrap();
// TODO: Figure out some top level schenanigans here similar to the zip extract ignoring toplevel
let mut contents = String::new();
zip.by_name("texture_replacements/about.json")?
.read_to_string(&mut contents)?;
// TODO: Figure out some top level schenanigans here similar to the zip extract ignoring toplevel
let mut contents = String::new();
zip
.by_name("texture_replacements/about.json")?
.read_to_string(&mut contents)?;
let pack: TexturePack = TexturePack {
path: Some(file_path),
..serde_json::from_str(&contents).unwrap()
};
Ok(pack)
let pack: TexturePack = TexturePack {
path: Some(file_path),
..serde_json::from_str(&contents).unwrap()
};
Ok(pack)
}
#[tauri::command]
pub fn get_all_texture_packs(dir: String) -> Vec<TexturePack> {
let dir_path = Path::new(&dir).exists();
if !dir_path {
println!("Textures directory doesn't exist, creating it now.");
fs::create_dir(dir.clone()).unwrap();
return Vec::new();
}
let dir_path = Path::new(&dir).exists();
if !dir_path {
println!("Textures directory doesn't exist, creating it now.");
fs::create_dir(dir.clone()).unwrap();
return Vec::new();
}
let entries = fs::read_dir(dir).unwrap();
let entries = fs::read_dir(dir).unwrap();
let mut texture_pack_data: Vec<TexturePack> = Vec::new();
for entry in entries {
let path = entry.unwrap().path();
match path.extension() {
Some(ext) if ext == "zip" => {
let files = match read_texture_json_file(path.clone()) {
Ok(pack) => pack,
Err(_e) => {
// if the about.json file isn't inside of the expected directory this error happens
// TODO: add this error to a logs file so players know when they install a bad texture pack
println!("File doesn't have proper about.json: {:?}", path);
continue;
}
};
texture_pack_data.push(files);
}
_ => continue,
}
let mut texture_pack_data: Vec<TexturePack> = Vec::new();
for entry in entries {
let path = entry.unwrap().path();
match path.extension() {
Some(ext) if ext == "zip" => {
let files = match read_texture_json_file(path.clone()) {
Ok(pack) => pack,
Err(_e) => {
// if the about.json file isn't inside of the expected directory this error happens
// TODO: add this error to a logs file so players know when they install a bad texture pack
println!("File doesn't have proper about.json: {:?}", path);
continue;
}
};
texture_pack_data.push(files);
}
_ => continue,
}
return texture_pack_data;
}
return texture_pack_data;
}

25
src-tauri/src/util.rs Normal file
View file

@ -0,0 +1,25 @@
use std::process::Command;
#[cfg(target_os = "windows")]
pub fn open_dir_in_os(dir: String) {
Command::new("explorer")
.arg(dir) // <- Specify the directory you'd like to open.
.spawn()
.unwrap();
}
#[cfg(target_os = "linux")]
pub fn open_dir_in_os(dir: String) {
Command::new("xdg-open")
.arg(dir) // <- Specify the directory you'd like to open.
.spawn()
.unwrap();
}
#[cfg(target_os = "macos")]
pub fn open_dir_in_os(dir: String) {
Command::new("open")
.arg(dir) // <- Specify the directory you'd like to open.
.spawn()
.unwrap();
}

View file

@ -28,9 +28,6 @@
"icons/icon.icns",
"icons/icon.ico"
],
"resources": [
"data/"
],
"externalBin": [
"bin/extractor",
"bin/gk",

View file

@ -1,8 +1,16 @@
import { invoke } from "@tauri-apps/api/tauri";
export async function listDownloadedOfficialVersions(): Promise<string[]> {
export enum VersionFolders {
OFFICIAL = "official",
UNOFFICIAL = "unofficial",
DEVEL = "devel",
}
export async function listDownloadedVersions(
folder: VersionFolders
): Promise<string[]> {
try {
return await invoke("list_downloaded_official_versions", {});
return await invoke("list_downloaded_versions", { versionFolder: folder });
} catch (e) {
console.log("TODO AH!");
}
@ -18,3 +26,41 @@ export async function downloadOfficialVersion(
console.log("TODO AH!");
}
}
export async function openVersionFolder(folder: VersionFolders) {
try {
return await invoke("go_to_version_folder", { versionFolder: folder });
} catch (e) {
console.log("TODO AH!");
}
}
export async function saveActiveVersionChange(
folder: VersionFolders,
newVersion: String
) {
try {
return await invoke("save_active_version_change", {
versionFolder: folder,
newActiveVersion: newVersion,
});
} catch (e) {
console.log("TODO AH!");
}
}
export async function getActiveVersion() {
try {
return await invoke("get_active_version", {});
} catch (e) {
console.log("TODO AH!");
}
}
export async function getActiveVersionFolder() {
try {
return await invoke("get_active_version_folder", {});
} catch (e) {
console.log("TODO AH!");
}
}

View file

@ -11,11 +11,18 @@
import Icon from "@iconify/svelte";
import { onMount } from "svelte";
import { each } from "svelte/internal";
import { downloadOfficialVersion, listDownloadedOfficialVersions } from "$lib/rpc/versions";
import {
downloadOfficialVersion,
getActiveVersion,
listDownloadedVersions,
openVersionFolder,
saveActiveVersionChange,
VersionFolders,
} from "$lib/rpc/versions";
let componentLoaded = false;
let currentOfficialVersion = "v0.1.31";
let selectedOfficialVersion = "v0.1.31";
let currentOfficialVersion = undefined;
let selectedOfficialVersion = undefined;
const tabItemActiveClasses =
"inline-block text-sm font-bold text-center disabled:cursor-not-allowed p-4 text-orange-500 border-b-2 border-orange-500 dark:text-orange-500 dark:border-orange-500";
@ -28,12 +35,14 @@
githubLink: string | undefined;
downloadUrl: string | undefined; // TODO - windows/mac/linux
isDownloaded: boolean;
isActive: boolean;
}
let officialReleases: Release[] = [];
onMount(async () => {
// TODO - check when this is null
currentOfficialVersion = await getActiveVersion();
selectedOfficialVersion = currentOfficialVersion;
await refreshOfficialVersionList(undefined);
// TODO - spinner
componentLoaded = true;
@ -41,7 +50,9 @@
async function refreshOfficialVersionList(evt) {
// Check the backend to see if the folder has any versions
const installedVersions = await listDownloadedOfficialVersions();
const installedVersions = await listDownloadedVersions(
VersionFolders.OFFICIAL
);
officialReleases = [];
for (const version of installedVersions) {
officialReleases = [
@ -51,8 +62,7 @@
date: undefined,
githubLink: undefined,
downloadUrl: undefined,
isDownloaded: true,
isActive: true, // TODO
isDownloaded: true
},
];
}
@ -75,7 +85,8 @@
if (existingRelease.version == release.tag_name) {
existingRelease.date = release.published_at;
existingRelease.githubLink = release.html_url;
existingRelease.downloadUrl = "https://github.com/open-goal/jak-project/releases/download/v0.1.32/opengoal-windows-v0.1.32.zip";
existingRelease.downloadUrl =
"https://github.com/open-goal/jak-project/releases/download/v0.1.32/opengoal-windows-v0.1.32.zip";
foundExistingRelease = true;
break;
}
@ -89,24 +100,28 @@
version: release.tag_name,
date: release.published_at,
githubLink: release.html_url,
downloadUrl: "https://github.com/open-goal/jak-project/releases/download/v0.1.32/opengoal-windows-v0.1.32.zip",
isDownloaded: false,
isActive: false, // TODO
downloadUrl:
"https://github.com/open-goal/jak-project/releases/download/v0.1.32/opengoal-windows-v0.1.32.zip",
isDownloaded: false
},
];
}
// Sort releases by published date
officialReleases.sort((a, b) => b.date.localeCompare(a.date));
officialReleases = officialReleases.sort((a, b) => b.date.localeCompare(a.date));
selectedOfficialVersion = "v0.1.32";
}
async function saveOfficialVersionChange(evt) {
// TODO - tauri side
await saveActiveVersionChange(
VersionFolders.OFFICIAL,
selectedOfficialVersion
);
currentOfficialVersion = selectedOfficialVersion;
}
async function openOfficialVersionFolder(evt) {
// TODO - tauri side
openVersionFolder(VersionFolders.OFFICIAL);
}
async function onDownloadOfficialVersion(version: String, url: String) {
@ -135,16 +150,19 @@
</div>
<div class="flex">
{#if currentOfficialVersion != selectedOfficialVersion}
<Button class="!p-2 mr-2 dark:bg-green-500 hover:dark:bg-green-600" on:click={saveOfficialVersionChange}>
<Icon
icon="material-symbols:save"
width="20"
height="20"
alt="save official version change"
/>
</Button>
<Button
btnClass="!p-2 mr-2 rounded-md dark:bg-green-500 hover:dark:bg-green-600"
on:click={saveOfficialVersionChange}
>
<Icon
icon="material-symbols:save"
width="20"
height="20"
alt="save official version change"
/>
</Button>
{/if}
<Button class="!p-2 mr-2" on:click={refreshOfficialVersionList}>
<Button btnClass="!p-2 mr-2 rounded-md dark:bg-orange-500 hover:dark:bg-orange-600" on:click={refreshOfficialVersionList}>
<Icon
icon="material-symbols:refresh"
width="20"
@ -152,7 +170,7 @@
alt="refresh official version list"
/>
</Button>
<Button class="!p-2">
<Button btnClass="!p-2 rounded-md dark:bg-orange-500 hover:dark:bg-orange-600" on:click={openOfficialVersionFolder}>
<Icon
icon="material-symbols:folder-open-rounded"
width="20"
@ -174,23 +192,32 @@
<TableHeadCell>Date</TableHeadCell>
<TableHeadCell>Github Link</TableHeadCell>
</TableHead>
<TableBody class="divide-y">
{#each officialReleases as release}
<TableBody tableBodyClass="divide-y">
{#each officialReleases as release (release.version)}
<TableBodyRow>
<TableBodyCell class="!p-4 py-0">
<Radio
class="disabled:cursor-not-allowed"
bind:group={selectedOfficialVersion}
value={release.version}
disabled={!release.isDownloaded}
name="official-release"
/>
<TableBodyCell tdClass="px-6 py-2 whitespace-nowrap font-medium">
{#if release.isDownloaded}
<Radio
class="disabled:cursor-not-allowed p-0"
bind:group={selectedOfficialVersion}
value={release.version}
disabled={!release.isDownloaded}
name="official-release"
/>
{/if}
</TableBodyCell>
<TableBodyCell
tdClass="px-6 py-0 whitespace-nowrap font-medium"
tdClass="px-6 py-2 whitespace-nowrap font-medium"
style="line-height: 0;"
>
<Button class="dark:bg-transparent hover:dark:bg-transparent focus:ring-0 focus:ring-offset-0" on:click={() => onDownloadOfficialVersion(release.version, release.downloadUrl)}>
<Button
btnClass="dark:bg-transparent hover:dark:bg-transparent focus:ring-0 focus:ring-offset-0"
on:click={() =>
onDownloadOfficialVersion(
release.version,
release.downloadUrl
)}
>
{#if release.isDownloaded}
<Icon
icon="ic:baseline-delete-forever"
@ -199,17 +226,22 @@
color="red"
/>
{:else}
<Icon icon="ic:baseline-download" color="#00d500" width="24" height="24" />
<Icon
icon="ic:baseline-download"
color="#00d500"
width="24"
height="24"
/>
{/if}
</Button>
</TableBodyCell>
<TableBodyCell tdClass="px-6 py-0 whitespace-nowrap font-medium"
<TableBodyCell tdClass="px-6 py-2 whitespace-nowrap font-medium"
>{release.version}</TableBodyCell
>
<TableBodyCell tdClass="px-6 py-0 whitespace-nowrap font-medium"
<TableBodyCell tdClass="px-6 py-2 whitespace-nowrap font-medium"
>{new Date(release.date).toLocaleDateString()}</TableBodyCell
>
<TableBodyCell tdClass="px-6 py-0 whitespace-nowrap font-medium"
<TableBodyCell tdClass="px-6 py-2 whitespace-nowrap font-medium"
><a href={release.githubLink} target="_blank" rel="noreferrer"
><Icon icon="mdi:github" width="24" height="24" /></a
>