Compare commits

...

19 Commits

27 changed files with 877 additions and 82 deletions

14
FCLauncher/app.json Normal file
View File

@ -0,0 +1,14 @@
{
"version": "1.0.3",
"notes": "Removed test text",
"platforms": {
"linux-x86_64": {
"signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTZlMrWWVKdURJQTZvNXR1WTFXSSs4bm1KbkpTeUhDMG9aeThZVDZEVGRCTlJSbFRGMXpZeFlOV085ZThFL0xNTmZjUmk5MUNWSGVIcUxaZnM4bzk0RjJnRDhGMk82bXdNPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzIwODg5ODQ4CWZpbGU6ZmMtbGF1bmNoZXJfMS4wLjNfYW1kNjQuQXBwSW1hZ2UudGFyLmd6CkViSUNFWmNmZVJiQVR1MTBCUHptc3VxeVV4V3daK0tCUnBPc21GdlBUdlVFMXNLWmFSeUJ4V0ZPN2ZoUXdMRjBzZ3NUTjExSFFVV2QrOGxXQmZKSURRPT0K",
"url": "https://gitea.piwalker.net/fclauncher/app/linux/fc-launcher_1.0.3_amd64.AppImage.tar.gz"
},
"windows-x86_64": {
"signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTZlMrWWVKdURJQThTaEpZYjNicThPbVFzbXlwYlJQanBWZGhncEtCVEkvc21SLzcrS0NuZFozdDM5d01lOCtYUElyb0dYQmpPelF0WDF5R1IyN3RZVU1BaDFRWkFneFFJPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzIwODg5NjMwCWZpbGU6RkNMYXVuY2hlcl8xLjAuM194NjRfZW4tVVMubXNpLnppcApTOXQvQlZWV2VYS2ZjYmIwSWJobzIxYjh1TWhyNHlFS21BVU11RVVud3dUQmMzVldUSlh2VURob0s2T3dUM1ZtcHJrZ2VvYXhoZFlZWmJ2OEJmYlpDUT09Cg==",
"url": "https://gitea.piwalker.net/fclauncher/app/windows/FCLauncher_1.0.3_x64_en-US.msi.zip"
}
}
}

View File

@ -1,6 +1,6 @@
[package]
name = "fclauncher"
version = "0.0.1"
version = "0.0.5"
description = "Launcher for Familycraft"
authors = ["Samuel Walker", "Benjamin Walker"]
edition = "2021"
@ -11,7 +11,7 @@ edition = "2021"
tauri-build = { version = "1", features = [] }
[dependencies]
tauri = { version = "1", features = [ "dialog-ask", "shell-open"] }
tauri = { version = "1", features = [ "process-all", "dialog-open", "updater", "dialog-ask", "shell-open"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
suppaftp = { version = "6.0.1", features = ["native-tls"] }
@ -21,11 +21,13 @@ zip-extract = "0.1.3"
dirs = "5.0.1"
gethostname = "0.4.3"
self_update = "0.40.0"
parking_lot = "0.12.3"
reqwest = { version = "0.12.5", features = ["stream"] }
futures-util = "0.3.30"
ssh2 = "0.9.4"
chrono = "0.4.38"
zip = "2.1.3"
[features]
# This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!!
custom-protocol = ["tauri/custom-protocol"]
[[bin]]
name = "FCLauncher"
path = "src/main.rs"

4
FCLauncher/src-tauri/build.sh Executable file
View File

@ -0,0 +1,4 @@
export TAURI_PRIVATE_KEY=$(cat ~/.tauri/fclauncher.key)
read -s PASSWORD
export TAURI_KEY_PASSWORD=$PASSWORD
NO_STRIP=true cargo tauri build

View File

@ -0,0 +1,185 @@
use std::io::Cursor;
use std::io::Read;
use std::fs::File;
use std::io::Seek;
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;
use crate::modpack::get_modpacks;
use crate::modpack::get_versions;
use crate::modpack::VersionEntry;
use crate::sftp;
use crate::modpack;
use crate::ModpackEntry;
use ssh2::Sftp;
use ssh2::Session;
use chrono;
use zip::unstable::write::FileOptionsExt;
use zip::write::FileOptions;
use zip::write::SimpleFileOptions;
use zip::ZipArchive;
use zip::ZipWriter;
//static USERNAME: parking_lot::Mutex<String> = parking_lot::const_mutex(String::new());
//static PASSWORD: parking_lot::Mutex<String> = parking_lot::const_mutex(String::new());
static SESSION: tauri::async_runtime::Mutex<Option<Session>> = tauri::async_runtime::Mutex::const_new(None);
#[tauri::command]
pub async fn login(username: String, password: String, window: tauri::Window) {
let res = sftp::connect(username, password);
if(res.is_ok()){
//*USERNAME.lock() = username;
//*PASSWORD.lock() = password;
*SESSION.lock().await = Some(res.unwrap());
window.emit("Login_Success", {});
}else{
window.emit("Login_Failed", {});
}
}
#[tauri::command]
pub async fn drop_session(){
let ref mut session = *SESSION.lock().await;
if let Some(session) = session {
session.disconnect(None, "disconnecting", None);
}
*session = None;
}
async fn update_modpacks(modpacks: Vec<modpack::ModpackEntry>) -> Result<(), String>{
let data = serde_json::to_string(&modpacks).or(Err("Unable to serialize json"))?;
let reader = Cursor::new(data.as_bytes());
let ref mut session = *SESSION.lock().await;
if let Some(session) = session {
sftp::uplaod(None, session.clone(), PathBuf::from("/ftp/modpacks.json"), reader, format!("modpacks.json"), data.as_bytes().len()).await;
}
Ok(())
}
async fn update_versions(id: String, versions: Vec<modpack::VersionEntry>) -> Result<(), String>{
let data = serde_json::to_string(&versions).or(Err("Unable to serialize json"))?;
let reader = Cursor::new(data.as_bytes());
let ref mut session = *SESSION.lock().await;
if let Some(session) = session {
sftp::uplaod(None, session.clone(), PathBuf::from(format!("/ftp/{}/versions.json", id)), reader, format!("modpacks.json"), data.as_bytes().len()).await;
}
Ok(())
}
#[tauri::command]
pub async fn shift_up(id: String){
let mut modpacks = modpack::get_modpacks().await;
let mut index = 0;
for pack in modpacks.as_slice() {
if(pack.id == id){
break;
}
index += 1;
}
if index != 0 {
modpacks.swap(index, index-1);
}
update_modpacks(modpacks).await;
}
#[tauri::command]
pub async fn shift_down(id: String){
let mut modpacks = modpack::get_modpacks().await;
let mut index = 0;
for pack in modpacks.as_slice() {
if(pack.id == id){
break;
}
index += 1;
}
if index != modpacks.len()-1 {
modpacks.swap(index, index+1);
}
update_modpacks(modpacks).await;
}
#[tauri::command]
pub async fn add_pack(id: String, name: String){
{
let ref mut session = *SESSION.lock().await;
if let Some(session) = session{
sftp::mkdir(session.clone(), PathBuf::from(format!("/ftp/{}", id))).await;
sftp::mkdir(session.clone(), PathBuf::from(format!("/ftp/{}/Versions", id))).await;
}
}
let versions: Vec<VersionEntry> = Vec::new();
update_versions(id.clone(), versions).await;
let mut modpacks = get_modpacks().await;
modpacks.push(modpack::ModpackEntry{id: id, name: name, last_updated: format!("{:?}", chrono::offset::Utc::now())});
update_modpacks(modpacks).await;
}
#[tauri::command]
pub async fn remove_pack(id: String){
let mut modpacks = get_modpacks().await;
let mut index = 0;
for pack in modpacks.clone() {
if pack.id == id {
modpacks.remove(index);
break;
}
index += 1;
}
update_modpacks(modpacks).await;
{
let ref mut session = *SESSION.lock().await;
if let Some(session) = session{
sftp::rmdir(session.clone(), PathBuf::from(format!("/ftp/{}", id))).await;
}
}
}
#[tauri::command]
pub async fn update_pack(window: tauri::Window, id: String, path: String, version: String) -> Result<(), String>{
println!("Update modpack {}, to version {}, from file {}", id, version, path);
let file = File::open(Path::new(path.as_str())).or(Err(format!("Unable to open file")))?;
let mut archive = ZipArchive::new(file).or(Err(format!("File not a zip archive!")))?;
let mut buf = Cursor::new(vec![]);
let mut out_archive = ZipWriter::new(&mut buf);
for i in 0..archive.len() {
let mut file = archive.by_index(i).or(Err(format!("error reading archive")))?;
if(file.name() == "overrides/version.txt"){
continue;
}
out_archive.start_file(file.name(), SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated));
std::io::copy(&mut file, &mut out_archive);
}
out_archive.start_file("overrides/version.txt", SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated));
out_archive.write_all(version.as_bytes());
out_archive.finish();
buf.rewind().unwrap();
let timestamp = format!("{:?}", chrono::offset::Utc::now());
let path = format!("Versions/{}-{}.mrpack", id, timestamp);
{
let ref mut session = *SESSION.lock().await;
if let Some(session) = session{
let size = buf.clone().bytes().count();
let upload_path = format!("/ftp/{}/{}", id, path.clone());
println!("Uploading to {}", upload_path);
sftp::uplaod(Some(window), session.clone(), PathBuf::from(upload_path), &mut buf, path.clone(), size).await;
}
}
let mut versions = get_versions(id.clone()).await?;
versions.push(VersionEntry{Version: version, Date: timestamp.clone(), File: path});
update_versions(id.clone(), versions).await;
let mut modpacks = get_modpacks().await;
let mut index = 0;
for mut pack in modpacks.as_slice(){
if pack.id == id {
modpacks[index].last_updated = timestamp;
break;
}
index += 1;
}
update_modpacks(modpacks).await;
Ok(())
}

View File

@ -12,20 +12,25 @@ fn ftp_connection_anonymous() -> Result<NativeTlsFtpStream, FtpError>{
ftp_connection("anonymous", "anonymous@")
}
fn ftp_connection(username: &str, password: &str) -> Result<NativeTlsFtpStream, FtpError>{
let ftp_stream = NativeTlsFtpStream::connect("gitea.piwalker.net:21").unwrap_or_else(|err|
panic!("{}", err)
);
let cert = include_bytes!("../res/vsftpd.crt");
let cert = Certificate::from_pem(cert).unwrap();
let mut ftp_stream = ftp_stream.into_secure(NativeTlsConnector::from(TlsConnector::builder().add_root_certificate(cert).build().unwrap()), "gitea.piwalker.net").unwrap();
ftp_stream.login("anonymous", "anonymous@").map(|_| Ok(ftp_stream)).unwrap()
pub fn test_cred(username: &str, password: &str) -> bool{
return ftp_connection(username, password).is_ok();
}
fn ftp_connection(username: &str, password: &str) -> Result<NativeTlsFtpStream, FtpError>{
let ftp_stream = NativeTlsFtpStream::connect("gitea.piwalker.net:21")?;
let cert = include_bytes!("../res/vsftpd.crt");
let cert = Certificate::from_pem(cert).unwrap();
let mut ftp_stream = ftp_stream.into_secure(NativeTlsConnector::from(TlsConnector::builder().add_root_certificate(cert).build().unwrap()), "gitea.piwalker.net").unwrap();
let result = ftp_stream.login(username, password);
if result.is_ok() {
return Ok(ftp_stream);
}
Err(result.unwrap_err())
}
pub fn ftp_retr(window: Option<tauri::Window>, file: PathBuf , mut writer: impl Write, mut callback: impl FnMut(Option<tauri::Window>, usize, usize)) -> Result<bool, FtpError> {
let mut ftp_stream = ftp_connection_anonymous().unwrap();
let mut ftp_stream = ftp_connection_anonymous()?;
let file = file.to_str().unwrap().replace("\\", "/");
let size = ftp_stream.size(&file)?;
let mut total = 0;

View File

@ -0,0 +1,39 @@
use std::io::Write;
use std::path::PathBuf;
use futures_util::StreamExt;
use std::cmp::min;
use serde::Serialize;
#[derive(Clone, Serialize)]
pub struct DownloadStatus {
pub downloaded: usize,
pub total: usize,
pub time_elapsed: usize,
pub download_name: String
}
pub async fn download(window: Option<tauri::Window>, url: String , mut writer: impl Write, downloadName: String) -> Result<(), String> {
let client = reqwest::Client::new();
let res = client.get(url).send().await.or(Err(format!("Failed to fetch from URL!")))?;
let total_size = res.content_length().ok_or(format!("Failed to get content length"))?;
let mut downloaded: u64 = 0;
let mut stream = res.bytes_stream();
while let Some(item) = stream.next().await {
let chunk = item.or(Err(format!("Error while downloading file!")))?;
writer.write_all(&chunk).or(Err("Error writing to stream!"))?;
let new = min(downloaded + (chunk.len() as u64), total_size);
downloaded = new;
println!("Downloading {}: {}MB / {}MB", downloadName.clone(), downloaded/(1024*1024), total_size/(1024*1024));
if let Some(window) = window.clone() {
if downloaded != total_size {
window.emit("download_progress", DownloadStatus{downloaded: downloaded as usize, total: total_size as usize, time_elapsed: 0, download_name: downloadName.clone()}).or(Err(format!("Unable to signal window")))?;
} else {
window.emit("download_finished", true).or(Err(format!("Unable to signal window!")))?;
}
}
}
return Ok(());
}

View File

@ -6,7 +6,7 @@ use tar::Archive;
use crate::system_dirs::get_local_data_directory;
use crate::ftp::{self, ftp_get_size};
use crate::https;
use crate::util;
fn check_java(version: u8) -> bool {
@ -14,14 +14,14 @@ fn check_java(version: u8) -> bool {
dir.exists()
}
pub fn install_java(version: u8, window: tauri::Window) {
pub async fn install_java(version: u8, window: tauri::Window) {
if check_java(version) {
return;
}
let ftp_dir = PathBuf::new().join("java").join(format!("java-{}-{}", version, if env::consts::OS == "windows" { "win.zip" } else {"lin.tar.gz"}));
//let ftp_dir = PathBuf::new().join("java").join(format!("java-{}-{}", version, if env::consts::OS == "windows" { "win.zip" } else {"lin.tar.gz"}));
let mut buff = Cursor::new(vec![]);
ftp::ftp_retr(Some(window), ftp_dir, &mut buff, |window, data, size| util::download_callback(format!("Java {}", version), window,data, size)).unwrap();
//ftp::ftp_retr(Some(window), ftp_dir, &mut buff, |window, data, size| util::download_callback(format!("Java {}", version), window,data, size)).unwrap();
https::download(Some(window.clone()), format!("https://gitea.piwalker.net/fclauncher/java/java-{}-{}", version, if env::consts::OS == "windows" { "win.zip" } else {"lin.tar.gz"}), &mut buff, format!("Java {}", version)).await;
std::fs::create_dir_all(get_local_data_directory().join("java").join(format!("java-{}-{}", version, if env::consts::OS == "windows" { "win" } else {"lin"}))).unwrap();
buff.rewind().unwrap();
if env::consts::OS != "windows" {

View File

@ -9,12 +9,15 @@ use serde_json::{Map, Result, Value};
use serde::Serialize;
use serde::Deserialize;
mod ftp;
//mod ftp;
mod java;
mod prism;
mod system_dirs;
mod util;
mod modpack;
mod admin;
mod https;
mod sftp;
#[derive(Serialize, Deserialize)]
struct ModpackEntry{
@ -30,12 +33,10 @@ fn greet(name: &str) -> String {
fn main() {
let status = self_update::backends::gitea::Update::configure().with_host("https://gitea.piwalker.net").repo_owner("piwalker").repo_name("FCLauncher").bin_name("FCLauncher").show_download_progress(true).current_version(cargo_crate_version!()).build().unwrap().update().unwrap();
println!("update status: `{}`", status.version());
modpack::get_modpacks();
//modpack::get_modpacks();
//prism::install_prism();
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![greet, modpack::get_modpacks, modpack::launch_modpack, prism::launch_prism, prism::install_prism])
.invoke_handler(tauri::generate_handler![greet, modpack::get_modpacks, modpack::launch_modpack, modpack::get_versions, modpack::get_latest_version, prism::launch_prism, prism::install_prism, admin::login, admin::drop_session, admin::shift_up, admin::shift_down, admin::add_pack, admin::remove_pack, admin::update_pack])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View File

@ -1,5 +1,5 @@
use crate::{ftp, java};
use crate::{java};
use crate::system_dirs::{get_data_directory, get_java_executable, get_local_data_directory, get_prism_executable};
use std::time::Duration;
use std::{env, thread};
@ -7,28 +7,33 @@ use std::fs::File;
use std::process::Command;
use std::{io::Cursor, path::PathBuf};
use std::io::{Read, Seek, Write};
use reqwest::IntoUrl;
use serde::de::value::Error;
use serde_json::Value;
use serde::Serialize;
use serde::Deserialize;
use crate::util;
use std::fs;
use crate::https;
#[derive(Serialize, Deserialize, Clone)]
pub struct ModpackEntry{
name: String,
id: String
pub name: String,
pub id: String,
pub last_updated: String
}
#[derive(Clone)]
#[derive(Serialize, Deserialize, Clone)]
pub struct VersionEntry{
version: String,
file: String,
date: String
pub Version: String,
pub File: String,
pub Date: String
}
fn get_modpack_name(id: String) -> String {
let modpacks = get_modpacks();
async fn get_modpack_name(id: String) -> String {
let modpacks = get_modpacks().await;
let mut instance_name = String::new();
for pack in modpacks {
if pack.id == id {
@ -40,14 +45,18 @@ fn get_modpack_name(id: String) -> String {
fn check_modpack_needs_update(id: String) -> bool{
let mut instance_name = get_modpack_name(id.clone());
async fn check_modpack_needs_update(id: String) -> bool{
let mut instance_name = get_modpack_name(id.clone()).await;
if !get_local_data_directory().join("prism").join("instances").join(&mut instance_name).exists() {
return true;
}
let versions = get_versions(id);
let latest = versions[versions.len()-1].version.clone();
let versions = get_versions(id).await;
if !versions.is_ok() {
return false;
}
let versions = versions.unwrap();
let latest = versions[versions.len()-1].Version.clone();
let mut file = File::open(get_local_data_directory().join("prism").join("instances").join(instance_name).join(".minecraft").join("version.txt")).unwrap();
let mut current = String::new();
@ -64,28 +73,29 @@ fn check_modpack_needs_update(id: String) -> bool{
#[tauri::command]
pub async fn launch_modpack(window: tauri::Window, id: String){
if check_modpack_needs_update(id.clone()) {
install_modpack(window, id.clone());
if check_modpack_needs_update(id.clone()).await {
install_modpack(window, id.clone()).await;
}
// Launch
let mut child = Command::new(get_local_data_directory().join("prism").join(get_prism_executable())).arg("-l").arg(get_modpack_name(id)).spawn().unwrap();
let mut child = Command::new(get_local_data_directory().join("prism").join(get_prism_executable())).arg("-l").arg(get_modpack_name(id).await).spawn().unwrap();
}
fn install_modpack(window: tauri::Window, id: String){
let versions = get_versions(id.clone());
let path = env::temp_dir().join(format!("{}.mrpack", get_modpack_name(id.clone())));
async fn install_modpack(window: tauri::Window, id: String){
let versions = get_versions(id.clone()).await.unwrap();
let path = env::temp_dir().join(format!("{}.mrpack", get_modpack_name(id.clone()).await));
let mut file = File::create(path.clone()).unwrap();
let ftp_path = PathBuf::new().join(id.clone()).join(versions[versions.len()-1].file.clone().as_str());
let ftp_path = PathBuf::new().join(id.clone()).join(versions[versions.len()-1].File.clone().as_str());
println!("Downloading file {}", ftp_path.to_str().unwrap());
ftp::ftp_retr(Some(window.clone()), ftp_path, &mut file, |window, data, size| util::download_callback(get_modpack_name(id.clone()), window, data, size)).unwrap();
//ftp::ftp_retr(Some(window.clone()), ftp_path, &mut file, |window, data, size| util::download_callback(name.clone(), window, data, size)).unwrap();
https::download(Some(window.clone()), format!("https://gitea.piwalker.net/fclauncher/{}/{}", id.clone(), versions[versions.len()-1].File.clone()), &mut file, get_modpack_name(id.clone()).await).await;
let mut child = Command::new(get_local_data_directory().join("prism").join(get_prism_executable())).arg("-I").arg(path).spawn().unwrap();
loop {
let version_path = get_local_data_directory().join("prism").join("instances").join(get_modpack_name(id.clone())).join(".minecraft").join("version.txt");
let version_path = get_local_data_directory().join("prism").join("instances").join(get_modpack_name(id.clone()).await).join(".minecraft").join("version.txt");
if version_path.clone().exists() {
let mut ver_file = File::open(version_path).unwrap();
let mut buf = String::new();
ver_file.read_to_string(&mut buf).unwrap();
if buf == versions[versions.len()-1].version.clone().as_str() {
if buf == versions[versions.len()-1].Version.clone().as_str() {
break;
}
}
@ -93,7 +103,7 @@ fn install_modpack(window: tauri::Window, id: String){
}
thread::sleep(Duration::from_secs(1));
child.kill();
let info_path = get_local_data_directory().join("prism").join("instances").join(get_modpack_name(id.clone())).join("mmc-pack.json");
let info_path = get_local_data_directory().join("prism").join("instances").join(get_modpack_name(id.clone()).await).join("mmc-pack.json");
let mut info_file = File::open(info_path.clone()).unwrap();
let info_json: Value = serde_json::from_reader(info_file).unwrap();
let mut mc_version = "0.0";
@ -103,9 +113,9 @@ fn install_modpack(window: tauri::Window, id: String){
}
}
let java = get_java_version(mc_version);
java::install_java(java, window.clone());
java::install_java(java, window.clone()).await;
let option_path = get_local_data_directory().join("prism").join("instances").join(get_modpack_name(id.clone())).join("instance.cfg");
let option_path = get_local_data_directory().join("prism").join("instances").join(get_modpack_name(id.clone()).await).join("instance.cfg");
let mut option_file = File::open(option_path.clone()).unwrap();
let mut buf = String::new();
option_file.read_to_string(&mut buf);
@ -128,33 +138,54 @@ fn install_modpack(window: tauri::Window, id: String){
}
#[tauri::command]
pub fn get_modpacks() -> Vec<ModpackEntry> {
unsafe{
static mut modpacks: Vec<ModpackEntry> = Vec::new();
if modpacks.is_empty() {
let mut buf = Cursor::new(vec![]);
ftp::ftp_retr(None, PathBuf::new().join("modpacks.json"), &mut buf, |_, _, _| return);
buf.rewind();
let v: Value = serde_json::from_reader(buf).unwrap();
println!("{}", v[0]["name"]);
for pack in v.as_array().unwrap() {
modpacks.push(ModpackEntry{name: pack["name"].as_str().unwrap().to_string(), id: pack["id"].as_str().unwrap().to_string()});
pub async fn get_modpacks() -> Vec<ModpackEntry> {
//unsafe{
let mut modpacks: Vec<ModpackEntry> = Vec::new();
//if modpacks.is_empty() {
let mut buf = Cursor::new(vec![]);
//ftp::ftp_retr(None, PathBuf::new().join("modpacks.json"), &mut buf, |_, _, _| return);
https::download(None, format!("https://gitea.piwalker.net/fclauncher/modpacks.json"), &mut buf, format!("modpacks.json")).await;
buf.rewind();
let res = serde_json::from_reader(buf);
if !res.is_ok() {
println!("Result not ok!");
let paths = fs::read_dir(get_local_data_directory().join("prism").join("instances")).unwrap();
for path in paths {
let path = path.unwrap();
if fs::metadata(path.path()).unwrap().is_file() {
continue;
}
let name = path.file_name().into_string().unwrap();
if name.starts_with(".") {
continue;
}
modpacks.push(ModpackEntry{name: name.clone(), id: name, last_updated: format!("")})
}
return modpacks.clone();
return modpacks.clone()
}
let modpacks: Vec<ModpackEntry> = res.unwrap();
//println!("{}", v[0].name);
//for pack in v.as_array().unwrap() {
//modpacks.push(ModpackEntry{name: pack["name"].as_str().unwrap().to_string(), id: pack["id"].as_str().unwrap().to_string(), last_updated: pack["last-updated"].as_str().unwrap().to_string()});
//}
//}
return modpacks.clone();
//}
}
fn get_versions(id: String) -> Vec<VersionEntry> {
#[tauri::command]
pub async fn get_versions(id: String) -> Result<Vec<VersionEntry>,String> {
let mut versions: Vec<VersionEntry> = Vec::new();
let mut buf = Cursor::new(vec![]);
ftp::ftp_retr(None, PathBuf::new().join(id).join("versions.json"), &mut buf, |_, _, _| return);
//ftp::ftp_retr(None, PathBuf::new().join(id).join("versions.json"), &mut buf, |_, _, _| return);
https::download(None, format!("https://gitea.piwalker.net/fclauncher/{}/versions.json", id.clone()), &mut buf, format!("{}/versions.json", id.clone())).await;
buf.rewind();
let v: Value = serde_json::from_reader(buf).unwrap();
for version in v.as_array().unwrap() {
versions.push(VersionEntry{version: version["Version"].as_str().unwrap().to_string(), file: version["File"].as_str().unwrap().to_string(), date: version["Date"].as_str().unwrap().to_string()});
}
return versions.clone();
let versions: Vec<VersionEntry> = serde_json::from_reader(buf).or(Err(format!("Unable to parse json")))?;
//for version in v.as_array().unwrap() {
//versions.push(VersionEntry{version: version["Version"].as_str().unwrap().to_string(), file: version["File"].as_str().unwrap().to_string(), date: version["Date"].as_str().unwrap().to_string()});
//}
return Ok(versions.clone());
}
fn get_java_version(mc_version: &str) -> u8{
@ -173,3 +204,17 @@ fn get_java_version(mc_version: &str) -> u8{
}
return java;
}
#[tauri::command]
pub async fn get_latest_version(id: String) -> Result<String, String>{
let versions = get_versions(id).await.unwrap();
if(versions.len() == 0){
return Ok(format!(""));
}
Ok(versions[versions.len()-1].Version.clone())
}
//pub fn create_json(modpacks: Vec<ModpackEntry>) -> Result<serde_json::Value, String> {
//}

View File

@ -3,7 +3,7 @@ use flate2::read::GzDecoder;
use tar::Archive;
use tauri::api::file;
use crate::{ftp, java, system_dirs::{get_local_data_directory, get_prism_executable}, util};
use crate::{https, java, system_dirs::{get_local_data_directory, get_prism_executable}, util};
pub fn check_prism() -> bool {
@ -16,12 +16,11 @@ pub async fn install_prism(window: tauri::Window){
if check_prism() {
return;
}
java::install_java(21, window.clone());
let path = PathBuf::new().join("prism").join(format!("prism-{}",if env::consts::OS == "windows" {"win.zip"} else {"lin.tar.gz"}));
let size = ftp::ftp_get_size(path.clone()).unwrap();
java::install_java(21, window.clone()).await;
let mut buff = Cursor::new(vec![]);
let mut total = 0;
ftp::ftp_retr(Some(window.clone()), path, &mut buff, |window: Option<tauri::Window>, data, size| util::download_callback("Prism Launcher".to_string(),window, data, size)).unwrap();
//ftp::ftp_retr(Some(window.clone()), path, &mut buff, |window: Option<tauri::Window>, data, size| util::download_callback("Prism Launcher".to_string(),window, data, size)).unwrap();
https::download(Some(window.clone()), format!("https://gitea.piwalker.net/fclauncher/prism/prism-{}", if env::consts::OS == "windows" {"win.zip"} else {"lin.tar.gz"}), &mut buff, format!("Prism Launcher")).await;
std::fs::create_dir_all(get_local_data_directory().join("prism")).unwrap();
buff.rewind().unwrap();
if env::consts::OS != "windows" {
@ -37,7 +36,8 @@ pub async fn install_prism(window: tauri::Window){
}
let mut buff = Cursor::new(vec![]);
ftp::ftp_retr(Some(window.clone()), PathBuf::new().join("prism").join("prismlauncher.cfg"), &mut buff, |_, _, _| return).unwrap();
//ftp::ftp_retr(Some(window.clone()), PathBuf::new().join("prism").join("prismlauncher.cfg"), &mut buff, |_, _, _| return).unwrap();
https::download(None, format!("https://gitea.piwalker.net/fclauncher/prism/prismlauncher.cfg"), &mut buff, format!("prismlauncher.cfg")).await;
buff.rewind();
let mut file = File::create(get_local_data_directory().join("prism").join("prismlauncher.cfg")).unwrap();
loop {

View File

@ -0,0 +1,66 @@
use std::io::prelude::*;
use std::net::TcpStream;
use std::path::Path;
use std::path::PathBuf;
use futures_util::future::BoxFuture;
use futures_util::io::BufReader;
use futures_util::io::Cursor;
use futures_util::FutureExt;
use ssh2::OpenFlags;
use ssh2::Session;
use ssh2::Sftp;
use crate::https;
pub fn connect(username: String, password: String) -> Result<Session, String> {
let tcp = TcpStream::connect("gitea.piwalker.net:22").or(Err(format!("Unable to connect to host")))?;
let mut sess = Session::new().or(Err(format!("Unable to creat stream")))?;
sess.set_tcp_stream(tcp);
sess.handshake().unwrap();
sess.userauth_password(username.as_str(), password.as_str()).or(Err(format!("Invalid username or password")))?;
Ok(sess)
}
pub async fn uplaod(window: Option<tauri::Window>, sess: Session, path: PathBuf, mut reader: impl Read, upload_name: String, total_size: usize) -> Result<(), String>{
let sftp = sess.sftp().or(Err("unable to open sftp session"))?;
let mut file = sftp.create(path.as_path()).or(Err("Unable to open file"))?;
let mut uploaded = 0;
while true {
let mut buf = vec![0u8; 1024*32];
let res = reader.read(&mut buf).unwrap();
if res <= 0 {
if let Some(window) = window.clone() {
window.emit("download_finished", true).or(Err(format!("Unable to signal window!")))?;
}
break;
}
file.write_all(buf.split_at(res).0).unwrap();
uploaded += res;
println!("Uploading {} {}MB / {}MB", upload_name, uploaded/(1024*1024), total_size/(1024*1024));
if let Some(window) = window.clone() {
window.emit("download_progress", https::DownloadStatus{downloaded: uploaded as usize, total: total_size as usize, time_elapsed: 0, download_name: upload_name.clone()}).or(Err(format!("Unable to signal window")))?;
}
}
Ok(())
}
pub async fn mkdir(sess: Session, path: PathBuf) -> Result<(), String>{
let sftp = sess.sftp().or(Err("Unable to open sftp session"))?;
sftp.mkdir(path.as_path(), 0o775).or(Err(format!("Unable to create directory")))?;
Ok(())
}
pub fn rmdir(sess: Session, path: PathBuf) -> BoxFuture<'static, ()>{
async move {
let sftp = sess.sftp().or(Err("Unable to open sftp session")).unwrap();
let dirs = sftp.readdir(path.as_path()).or(Err("unable to stat directory")).unwrap();
for dir in dirs {
if dir.1.is_dir() {
rmdir(sess.clone(), dir.0).await;
}else {
sftp.unlink(dir.0.as_path()).or(Err(format!("Unable to delete file"))).unwrap();
}
}
sftp.rmdir(path.as_path()).or(Err(format!("Unable to delete directory"))).unwrap();
}.boxed()
}

View File

@ -6,9 +6,17 @@
},
"package": {
"productName": "FCLauncher",
"version": "0.0.0"
"version": "1.0.4"
},
"tauri": {
"updater": {
"active": true,
"endpoints": [
"https://gitea.piwalker.net/fclauncher/app.json"
],
"dialog": true,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDNDOEUwMjYxRUU2NEI5RgpSV1NmUytZZUp1RElBN3dEaGhpWG9JZVNQcFlnNFFzaXN0UnBsVmxNeVdWWnJoQmh4cGJRbjN3Ygo="
},
"allowlist": {
"all": false,
"shell": {
@ -17,7 +25,11 @@
},
"dialog": {
"all": false,
"ask": true
"ask": true,
"open": true
},
"process": {
"all": true
}
},
"windows": [

46
FCLauncher/src/Admin.html Normal file
View File

@ -0,0 +1,46 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="stylesheet" href="styles.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-width=cover" />
<title>Tauri App</title>
<script type="module" src="/admin.js" defer></script>
</head>
<body>
<div class="Logo">
<button id="back"></button>
<img src="assets/Title.png" alt="Title" id="Title">
</div>
<div class="progress">
<div class="progressFinished"></div>
</div>
<div class="container-horizontal" data-bs-theme="dark">
<div id="modpacks" >
</div>
<div class="vertical-buttons" >
<button id="up" class="square-button"></button>
<button id="down" class="square-button"></button>
<button id="remove" class="square-button"></button>
</div>
<div class="vertical" id="update">
<input placeholder="Version" id="pack_version" />
<input placeholder="File" id="file_path" />
<button id="browse">Browse</button>
<button id="update_pack">Update Pack</button>
</div>
<div class="vertical" id="create">
<input placeholder="Name" id="pack_name" />
<input placeholder="ID" id="pack_id" />
<button id="add">Create Pack</button>
</div>
</div>
</body>
</html>

33
FCLauncher/src/Login.html Normal file
View File

@ -0,0 +1,33 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="stylesheet" href="styles.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-width=cover" />
<title>Tauri App</title>
<script type="module" src="/login.js" defer></script>
</head>
<body>
<div class="Logo">
<button id="back"></button>
<img src="assets/Title.png" alt="Title" id="Title">
</div>
<div class="container" data-bs-theme="dark">
<p class="Error" id="Incorrect">Username or Password is incorrect!</p>
<input id="Username" placeholder="Username" />
<input id="Password" placeholder="Password" type="password" />
<div class="loginButtons">
<button id="Cancel">Cancel</button>
<button id="Login">Login</button>
</div>
</div>
</body>
</html>

137
FCLauncher/src/admin.js Normal file
View File

@ -0,0 +1,137 @@
const { invoke } = window.__TAURI__.tauri;
const { listen } = window.__TAURI__.event;
const { ask, open } = window.__TAURI__.dialog;
const downBar = document.querySelector(".progressFinished");
//import { listen } from '@tauri-apps/api';
var selected_pack = "";
var update_menu = document.getElementById("update");
var create_menu = document.getElementById("create");
const download_progress = listen("download_progress", (progress) => {
console.log("Downloading");
//console.log("Downloaded "+progress.payload.downloaded/(1024*1024) +"MB / " + progress.payload.total/(1024*1024) + "MB");
let downProgress = (progress.payload.downloaded/(1024*1024)).toFixed(2);
let downTotal = (progress.payload.total/(1024*1024)).toFixed(2);
downBar.style.width = `${(progress.payload.downloaded / progress.payload.total) * 100}%`;
document.querySelector(".progress").style.visibility = "visible";
});
const download_finished = listen("download_finished", (event) => {
downBar.style.width = 0;
document.querySelector(".progress").style.visibility = "hidden";
});
window.addEventListener("DOMContentLoaded", () => {
document.getElementById("browse").addEventListener("click", browse);
document.getElementById("update_pack").addEventListener("click", update_pack);
document.getElementById("up").addEventListener("click", up);
document.getElementById("down").addEventListener("click", down);
document.getElementById("add").addEventListener("click", add);
document.getElementById("remove").addEventListener("click", remove);
document.getElementById("back").addEventListener("click", back);
});
window.onload = async function() {
refresh();
}
function up(){
invoke("shift_up", { id: selected_pack }).then(refresh);
}
function down(){
invoke("shift_down", { id: selected_pack }).then(refresh);
}
function back(){
invoke("drop_session");
window.location.href = "index.html";
}
function refresh(){
update_menu.style.display = "none";
create_menu.style.display = "none";
invoke("get_modpacks").then(addModpacks);
}
function addModpacks(modpacks) {
var modpacks_list = document.getElementById("modpacks");
while (modpacks_list.firstChild) {
modpacks_list.removeChild(modpacks_list.lastChild);
}
for (let i = 0; i < modpacks.length; i++){
var div = document.createElement("div");
div.textContent = modpacks[i].name;
div.className = "modpack";
div.id = modpacks[i].id;
if(modpacks[i].id == selected_pack){
div.classList.add("modpack-selected");
update_menu.style.display = "flex";
invoke("get_latest_version", {id: selected_pack}).then(update_version);
}
div.addEventListener("click", function() { modpackClick(modpacks[i].id) });
modpacks_list.appendChild(div);
}
var div = document.createElement("div");
div.textContent = "<Create New Pack>";
div.className = "modpack";
div.id = "*new*";
div.addEventListener("click", function() { modpackClick("*new*") });
modpacks_list.appendChild(div);
}
function modpackClick(id){
var old = selected_pack;
document.getElementById(id).classList.add("modpack-selected");
selected_pack = id;
if (old == id){
selected_pack = "";
update_menu.style.display = "none";
create_menu.style.display = "none";
}else if (id == "*new*"){
update_menu.style.display = "none";
create_menu.style.display = "flex";
}else{
update_menu.style.display = "flex";
create_menu.style.display = "none";
invoke("get_latest_version", {id: selected_pack}).then(update_version);
}
document.getElementById(old).classList.remove("modpack-selected");
}
function add(){
var id = document.getElementById("pack_id").value;
var name = document.getElementById("pack_name").value;
selected_pack = id;
invoke("add_pack", {id: id, name: name}).then(refresh);
}
function remove(){
ask("Are you sure you want to remove " + document.getElementById(selected_pack).textContent + "?", {title: "Are you sure?", type: "Message"}).then((value) => { if (value) { invoke("remove_pack", {id: selected_pack}).then(refresh); } });
}
async function browse(){
const selected = await open ({
multiple: false,
filters: [{
name: 'Modrinth Modpack',
extensions: ['mrpack']
}]
});
if (selected != null){
document.getElementById("file_path").value = selected;
}
}
function update_version(version){
document.getElementById("pack_version").value = version;
}
function update_pack(){
var version = document.getElementById("pack_version").value;
var path = document.getElementById("file_path").value;
invoke("update_pack", {id: selected_pack, path: path, version: version}).then(refresh);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

@ -0,0 +1,11 @@
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" height="800px" width="800px" version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" xmlns:xlink="http://www.w3.org/1999/xlink" enable-background="new 0 0 512 512">
<g>
<g>
<path d="M256,11C120.9,11,11,120.9,11,256s109.9,245,245,245s245-109.9,245-245S391.1,11,256,11z M256,460.2 c-112.6,0-204.2-91.6-204.2-204.2S143.4,51.8,256,51.8S460.2,143.4,460.2,256S368.6,460.2,256,460.2z"/>
<path d="m357.6,235.6h-81.2v-81.2c0-11.3-9.1-20.4-20.4-20.4-11.3,0-20.4,9.1-20.4,20.4v81.2h-81.2c-11.3,0-20.4,9.1-20.4,20.4s9.1,20.4 20.4,20.4h81.2v81.2c0,11.3 9.1,20.4 20.4,20.4 11.3,0 20.4-9.1 20.4-20.4v-81.2h81.2c11.3,0 20.4-9.1 20.4-20.4s-9.1-20.4-20.4-20.4z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 943 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -15,6 +15,7 @@
<body>
<div class="Logo">
<button id="settings"></button>
<img src="assets/Title.png" alt="Title" id="Title">
</div>
<div class="progress">

38
FCLauncher/src/login.js Normal file
View File

@ -0,0 +1,38 @@
const { invoke } = window.__TAURI__.tauri;
const { listen } = window.__TAURI__.event;
const { ask } = window.__TAURI__.dialog;
const downBar = document.querySelector(".progressFinished");
//import { listen } from '@tauri-apps/api';
window.addEventListener("DOMContentLoaded", () => {
document.getElementById("back").addEventListener("click", back);
document.getElementById("Cancel").addEventListener("click", back);
document.getElementById("Login").addEventListener("click", login);
document.getElementById("Password").addEventListener("keypress", keypress);
});
function back(){
invoke("drop_session");
window.location.href = "index.html";
}
function login(){
invoke("login", { username: document.getElementById("Username").value, password: document.getElementById("Password").value});
}
function keypress(e){
if(e.keyCode === 13){
e.preventDefault();
login();
}
}
const failed = listen("Login_Failed", (event) => {
document.getElementById("Incorrect").style.visibility = "visible";
})
const success = listen("Login_Success", (event) => {
window.location.href = "Admin.html";
})

View File

@ -1,6 +1,7 @@
const { invoke } = window.__TAURI__.tauri;
const { listen } = window.__TAURI__.event;
const { ask } = window.__TAURI__.dialog;
const { exit } = window.__TAURI__.process;
const downBar = document.querySelector(".progressFinished");
//import { listen } from '@tauri-apps/api';
@ -33,6 +34,8 @@ window.addEventListener("DOMContentLoaded", () => {
document.getElementById("launchGame").addEventListener("click", gameLaunch);
document.getElementById("prism").addEventListener("click", prism);
document.getElementById("settings").addEventListener("click", login);
document.getElementById("back").addEventListener("click", back);
});
function packSelect() {
@ -40,6 +43,15 @@ function packSelect() {
}
function login(){
window.location.href = "Login.html";
}
function back(){
console.log("test");
window.location.href = "index.html";
}
function load() {
console.log("loading");
var dropdown = document.getElementById("Modpacks");
@ -58,8 +70,8 @@ window.onload = async function() {
function addModpacks(modpacks) {
var dropdown = document.getElementById("Modpacks");
modpacks.sort((a, b) => a.name.localeCompare(b.name));
modpacks.reverse();
//modpacks.sort((a, b) => a.name.localeCompare(b.name));
//modpacks.reverse();
for (let i = 0; i < modpacks.length; i++){
var opt = document.createElement("option");
opt.text = modpacks[i].name;
@ -74,7 +86,7 @@ function gameLaunch() {
document.getElementById("launchGame").disabled = true;
document.getElementById("launchGame").textContent ="Launching...";
//TODO Launch Game
invoke("launch_modpack", { id: selectedId}).then(window.close);
invoke("launch_modpack", { id: selectedId}).then(() => { exit(1); });
}

View File

@ -27,6 +27,39 @@
text-align: center;
}
.vertical {
margin: 0;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
gap: 1em;
}
.container-horizontal {
margin: 0;
padding-top: 30px;
display: flex;
flex-direction: row;
justify-content: center;
text-align: center;
align-items: left;
justify-content: left;
gap: 20px;
}
.horizontal-input {
margin: 0;
margin-left: 5px;
display: flex;
flex-direction: row;
justify-content: center;
text-align: center;
align-items: left;
justify-content: left;
gap: 20px;
}
body {
margin: 0px;
padding: 0px;
@ -228,3 +261,114 @@ button {
display: block;
margin: 0.5em;
}
#settings{
width: 2em;
height: 2.5em;
top: 5px;
left: 5px;
float: right;
position: absolute;
background-image: url('assets/settings.png');
background-size:cover;
background-repeat: no-repeat;
}
#back{
width: 2em;
height: 2.5em;
top: 5px;
left: 5px;
float: right;
position: absolute;
background-image: url('assets/back.png');
background-size:cover;
background-repeat: no-repeat;
}
.container input{
margin: 0.5em;
width: 45%
}
.loginButtons{
display: flex;
margin: 0.5em;
flex-direction: row;
margin-left: auto;
margin-right: auto;
width: 50%;
gap: .5em;
}
.Error{
color: black;
background-color: red;
margin: 0;
visibility: hidden;
}
#modpacks{
width: 30%;
height: 13em;
background-color: #666565;
overflow: auto;
margin-left: 10px;
border: 1px solid;
}
.modpack {
width: 100%;
}
.modpack:hover {
background-color: lightgray;
color: black;
}
.modpack-selected {
background-color: blue;
color: white;
}
.vertical-buttons {
display: flex;
margin-left: 0.5em;
margin-top: 0;
flex-direction: column;
height: 15em;
gap: 1.5em;
}
.square-button {
width: 2em;
height: 2.5em;
top: 5px;
left: 5px;
}
#up{
background-image: url('assets/up.png');
background-size:cover;
background-repeat: no-repeat;
}
#down{
background-image: url('assets/down.png');
background-size:cover;
background-repeat: no-repeat;
}
#create{
display: none;
}
#update{
display: none;
}
#remove{
background-image: url('assets/remove.png');
background-size:cover;
background-repeat: no-repeat;
}