149 Commits
0.0.1 ... dev

Author SHA1 Message Date
0f09d23e67 Setup NSIS installer 2025-05-16 13:33:43 -06:00
b858d50746 replaced list with buttons 2025-05-07 07:23:00 -06:00
4ecdd3be6a upload app.svelte 2025-05-06 22:14:23 -06:00
2825b461af working on modpack ui 2025-05-06 22:13:34 -06:00
0f14bc2de7 Admin Page Login 2025-05-06 19:14:16 -06:00
008761e94a starting work on admin backend 2025-05-05 13:36:29 -06:00
2be1341404 maybe fixed auth issue? 2025-05-04 13:10:10 -06:00
3df2030f1e version bump 2025-05-03 19:32:58 -06:00
9a7a317a11 Pointed at gitea.piwalker.net 2025-05-03 19:29:36 -06:00
1d7df8854f New Instance Page Layout 2025-03-14 14:44:38 +11:00
2b21faf626 Merge branch 'golang-dev' of https://gitea.piwalker.net/piwalker/FCLauncher into golang-dev 2025-03-13 20:21:22 -06:00
1acf7db41d updated gitignore 2025-03-13 20:20:41 -06:00
e45d32a4ad wording change 2025-03-12 13:01:06 -06:00
11bda753c6 version 0.0.4 2025-03-11 15:57:54 -06:00
5a4307dc94 added login prompt parameter to oauth login 2025-03-11 15:34:23 -06:00
231b789545 Finished oauth2 2025-03-11 10:20:43 -06:00
a4316fa8de Working on OAuth2 authentication. 2025-03-11 08:28:29 -06:00
ce24c0a55d Created delete instance button, and made minor ui modifications 2025-03-10 11:38:27 -06:00
3b6aa14082 updated gitignore 2024-11-30 08:32:11 -07:00
=
ee19529a42 version 0.0.3 2024-11-26 21:42:06 -07:00
ca4713eb51 Weird build changes 2024-11-27 15:41:43 +11:00
=
9d61ea470a implemented open instance folder button 2024-11-26 21:37:26 -07:00
=
fab46be020 added test link 2024-11-26 21:34:38 -07:00
e8c24ee540 Instances UI 2024-11-27 15:34:12 +11:00
=
86cc464f45 version 0.0.2 2024-11-26 21:02:56 -07:00
ab6ad50983 Merge branch 'golang-dev' of https://gitea.piwalker.net/piwalker/FCLauncher into golang-dev 2024-11-27 14:56:43 +11:00
ec0fdb87c6 Theme Buttons 2024-11-27 14:56:39 +11:00
=
035d59b0f6 Implemented auto updating 2024-11-26 20:54:30 -07:00
a412cbf9d6 Theming works 2024-11-27 14:20:21 +11:00
cf31acdbc0 Settings page baybee 2024-11-27 13:55:43 +11:00
45efbffe7c Fixed nav hover 2024-11-27 13:41:30 +11:00
=
29c2351b2f use unix path for https requests 2024-11-26 16:49:32 -07:00
=
c34652bc4a replace path separators again 2024-11-26 16:45:08 -07:00
=
d5f8881d7e more path issues 2024-11-26 16:29:58 -07:00
=
0c111235a6 all unix baby! 2024-11-26 16:23:54 -07:00
=
377e42a24c linux separator? 2024-11-26 16:21:26 -07:00
=
eef50d5f2c more path debugging 2024-11-26 16:19:46 -07:00
=
3389119d03 fixed path madness 2024-11-26 16:16:13 -07:00
=
cf24d351e3 more debug code 2024-11-26 16:14:02 -07:00
=
49147a4edd debugging lib paths 2024-11-26 16:12:17 -07:00
=
20b8433d34 path separator weridness 2024-11-26 16:09:54 -07:00
=
6b07b95146 debugging maven paths 2024-11-26 16:03:28 -07:00
=
cddb8bb478 fix path splitting issue 2024-11-26 16:00:05 -07:00
40311e1b12 Fix weird prompt to commit 2024-11-27 09:58:20 +11:00
=
aed69860de remove auth file before re-writting 2024-11-26 15:52:29 -07:00
=
dadf5f641b https request fix path separator 2024-11-26 15:49:44 -07:00
=
4a35078f90 more error checking on modpack downloads 2024-11-26 15:37:35 -07:00
=
e7f8de116e Added error checking 2024-11-26 15:31:01 -07:00
=
22c18915f5 Changed url to point to new server 2024-11-26 12:37:30 -07:00
=
69791e5188 open browser for auth 2024-11-26 11:50:33 -07:00
=
d4f9711699 some random ui polish changes 2024-11-26 10:22:37 -07:00
=
ad736787e9 Changed default RAM limit 2024-11-25 19:21:20 -07:00
=
8208434b8f fixed overrides extraction 2024-11-25 14:55:04 -07:00
=
2b372423eb Finalized Modpack installation. 2024-11-25 14:28:32 -07:00
=
f905d617a8 Able to install correct version of minecraft + loader from mrpack 2024-11-25 13:23:06 -07:00
=
ad84711646 Working on modpack installation page 2024-11-25 12:36:29 -07:00
24f32cdba5 Fixed navbar flicker on windows 2024-11-05 11:56:10 +11:00
3e667f004d package json 2024-11-05 11:11:40 +11:00
f3e6f5d925 Fixed icons growing on windows 2024-11-05 11:09:46 +11:00
7e5b9596a4 global theming works 2024-11-04 13:23:47 +11:00
d15158f79c Changed color handling, in testing right now 2024-11-03 14:12:07 +11:00
d9dbd2d29b Replacing old ui, list population needs moved to app 2024-11-02 19:06:56 +11:00
3fe2b3df6d Moving page elements out of the way when the navbar opens 2024-11-02 18:32:24 +11:00
6f6a00cb45 Merge branch 'golang-dev' of https://gitea.piwalker.net/piwalker/FCLauncher into golang-dev 2024-11-02 12:14:08 +11:00
0923913801 Trying to make pages adjust dynamically for navbar 2024-11-02 12:14:04 +11:00
b9d8b763a7 working on forge integration 2024-11-01 18:08:29 -06:00
4946b7e595 Merge branch 'golang-dev' of https://gitea.piwalker.net/piwalker/FCLauncher into golang-dev 2024-11-01 08:05:14 -06:00
620636fb36 fixed reauth 2024-11-01 08:05:12 -06:00
ca9e41e99e Merge branch 'golang-dev' of https://gitea.piwalker.net/piwalker/FCLauncher into golang-dev 2024-11-02 01:04:56 +11:00
129b1d1a19 Added launch text to launch button 2024-11-02 01:04:52 +11:00
59b836b40a change import extension 2024-11-01 07:51:38 -06:00
af6ff50cfd Instance switching in good ui 2024-11-02 00:44:39 +11:00
bd787bdfa2 More ui work 2024-11-02 00:12:40 +11:00
2d8ce26af9 Cool select UI 2024-11-01 23:41:52 +11:00
27c97010ae Made instances list global 2024-11-01 05:55:41 -06:00
9d23d503e2 array stuff in testpage 2024-11-01 22:43:18 +11:00
479ee2e6e0 removed logo because it was annoying to work around 2024-11-01 21:55:39 +11:00
7e290472fb Added a test page for constructing new ui 2024-11-01 21:50:35 +11:00
e5cf9f532e Pull forge versions 2024-10-31 21:20:20 -06:00
a614f71aa1 quilt implemented 2024-10-31 18:57:54 -06:00
46bfc92370 Merge branch 'golang-dev' of https://gitea.piwalker.net/piwalker/FCLauncher into golang-dev 2024-10-31 18:21:11 -06:00
f6b68b0b43 working on quilt 2024-10-31 18:21:09 -06:00
6709726709 Merge branch 'golang-dev' of https://gitea.piwalker.net/piwalker/FCLauncher into golang-dev 2024-11-01 11:20:10 +11:00
5745670c0a navbar controls things 2024-11-01 11:20:06 +11:00
cce23ec175 fixed work directory 2024-10-31 17:50:45 -06:00
d7cfdaf6f2 merge 2024-10-31 17:48:17 -06:00
bf4d4ac583 fixed fabric 2024-10-31 17:47:50 -06:00
525af0db42 background color 2024-11-01 10:38:55 +11:00
c1bab40aab Minor layout change 2024-11-01 10:03:42 +11:00
7e728ddff8 working on fabric issue 2024-10-31 16:46:12 -06:00
0f265ced92 Replaced stand-in items in navbar 2024-11-01 09:44:41 +11:00
859bf812cd fabric installing, sorta... 2024-10-31 16:23:38 -06:00
d0384b1778 merge 2024-10-31 14:57:12 -06:00
3951af01c9 fabric version selection 2024-10-31 14:55:58 -06:00
6179e669df logo color 2024-11-01 01:20:11 +11:00
a96dfa032d fixed text wrapping 2024-11-01 01:09:38 +11:00
58a7a472fc Change navbar text 2024-11-01 01:05:55 +11:00
189e3e438f implemented navbar 2024-11-01 01:00:46 +11:00
ca21acf6b1 Started Navbar 2024-10-31 13:38:56 +11:00
25426e749b updated gitignor 2024-10-30 19:35:29 -06:00
e36eabe5df merge 2024-10-30 19:34:21 -06:00
e71593bf3a updated gitignore 2024-10-30 19:33:50 -06:00
10d373fd4b Merge branch 'golang-dev' of https://gitea.piwalker.net/piwalker/FCLauncher into golang-dev 2024-10-31 12:32:31 +11:00
dec56ffcb2 added Navbar svelte 2024-10-31 12:29:26 +11:00
774b623f1d random tweaks 2024-10-30 19:11:24 -06:00
7c169ce4a7 finished auth 2024-10-30 17:16:10 -06:00
806f6e2dbf initial microsoft auth 2024-10-30 16:41:03 -06:00
3720f1e524 added logging config 2024-10-30 15:58:05 -06:00
2bbb4b8cf0 installation and launching of vanilla minecraft working 2024-10-30 15:39:11 -06:00
ab0bfebe87 Started work on downloading minecraft 2024-10-29 21:48:59 -06:00
98e03a3e60 added basic slide transition 2024-10-26 23:39:21 -06:00
4e6ea6f22b fixed windows issues 2024-10-26 22:33:33 -06:00
99a5934575 working on windows version 2024-10-26 19:27:39 -06:00
b6af1549a3 fixed application not closing 2024-10-26 19:10:26 -06:00
bce2b04d3e almost have launching working 2024-10-26 18:14:12 -06:00
4bcc2703d6 Working on division between modpack and instance 2024-10-26 14:00:53 -06:00
db9ba30442 Fixed output name of modpack files 2024-10-25 22:58:52 -06:00
3c8fb9a89f added basic support for downloading (not installing) modpack files 2024-10-25 22:56:22 -06:00
29bf715025 removed some frontend testing code 2024-10-25 21:43:19 -06:00
8efd3a5631 Added support for installing java versions 2024-10-25 20:46:06 -06:00
bca9bec6d6 install prism, and display status in ui 2024-10-24 19:48:19 -06:00
7ad769ff96 Started https download code 2024-10-24 17:05:47 -06:00
0a6b37ccab fixed formatting 2024-10-24 15:49:14 -06:00
3b89a26265 got svelte working 2024-10-24 15:46:17 -06:00
c646c36a06 built wails project 2024-10-24 13:31:01 -06:00
641691f66c Migrated to new version, still broken 2024-10-23 22:24:50 -06:00
a6411b6034 Cleared errors in admin.rs 2024-09-28 09:12:45 -06:00
7197fabd1e Working on better error handling. 2024-09-27 16:44:37 -06:00
3ccedf668f removed old launcher 2024-09-27 15:17:23 -06:00
092cb3d630 version bump 2024-09-27 12:28:10 -06:00
31df09f602 updated build script 2024-08-07 23:26:49 +00:00
0ca4ba2ded added confirmation dialog on modpack deletion 2024-07-15 11:08:31 -06:00
e8583d3a05 Finished Admin UI 2024-07-14 08:44:16 -06:00
c39fcfe66b Creation and deletion of modpacks implemented. 2024-07-13 22:26:03 -06:00
332acf65c8 Started work on administrative functions 2024-07-13 17:00:08 -06:00
a13366c413 updated app json file. 2024-07-13 11:06:11 -06:00
ad73603727 version bump 2024-07-13 09:53:13 -07:00
8374d39104 removed test message 2024-07-13 10:51:57 -06:00
8a55010c2f changed build script 2024-07-13 09:49:48 -07:00
4e4463997b modified build script 2024-07-13 10:40:18 -06:00
41dc315dfb Setup auto updating, and signed building 2024-07-13 10:36:34 -06:00
f88a5bffe1 Changed FTP access to HTTPS access 2024-07-13 09:37:19 -06:00
d5ce6e4fc8 Added Offline Functionality 2024-07-12 21:49:00 -06:00
8b88dffd84 Created Administrative Sign in screen 2024-07-12 21:11:52 -06:00
2c082627e2 removed update code 2024-06-27 17:49:11 -06:00
243e0eba82 change 2024-06-27 17:44:58 -06:00
204810303c version bump 2024-06-27 17:34:09 -06:00
e0d53c6dfc test update 2024-06-27 17:32:24 -06:00
418182ce08 udater changes 2024-06-27 17:23:30 -06:00
156 changed files with 9137 additions and 660 deletions

8
.gitignore vendored
View File

@ -3,6 +3,8 @@
# will have compiled files and executables # will have compiled files and executables
debug/ debug/
target/ target/
logs/
**/logs/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
@ -10,4 +12,10 @@ Cargo.lock
# These are backup files generated by rustfmt # These are backup files generated by rustfmt
**/*.rs.bk **/*.rs.bk
**/*.log
FCLauncher/build/bin
FCLauncher/node_modules
FCLauncher/frontend/dist
FCLauncher/frontend/wailsjs/go/

14
FCLauncher.old/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

Before

Width:  |  Height:  |  Size: 367 KiB

After

Width:  |  Height:  |  Size: 367 KiB

View File

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 109 KiB

View File

@ -1,6 +1,6 @@
[package] [package]
name = "fclauncher" name = "fclauncher"
version = "0.0.1" version = "0.0.5"
description = "Launcher for Familycraft" description = "Launcher for Familycraft"
authors = ["Samuel Walker", "Benjamin Walker"] authors = ["Samuel Walker", "Benjamin Walker"]
edition = "2021" edition = "2021"
@ -8,10 +8,10 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies] [build-dependencies]
tauri-build = { version = "1", features = [] } tauri-build = { version = "2", features = [] }
[dependencies] [dependencies]
tauri = { version = "1", features = [ "dialog-ask", "shell-open"] } tauri = { version = "2", features = [] }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
suppaftp = { version = "6.0.1", features = ["native-tls"] } suppaftp = { version = "6.0.1", features = ["native-tls"] }
@ -21,11 +21,19 @@ zip-extract = "0.1.3"
dirs = "5.0.1" dirs = "5.0.1"
gethostname = "0.4.3" gethostname = "0.4.3"
self_update = "0.40.0" 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"
tauri-plugin-dialog = "2"
tauri-plugin-shell = "2"
tauri-plugin-process = "2"
[features] [features]
# This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!! # This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!!
custom-protocol = ["tauri/custom-protocol"] custom-protocol = ["tauri/custom-protocol"]
[[bin]] [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
name = "FCLauncher" tauri-plugin-updater = "2"
path = "src/main.rs"

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,11 @@
{
"identifier": "desktop-capability",
"platforms": [
"macOS",
"windows",
"linux"
],
"permissions": [
"updater:default"
]
}

View File

@ -0,0 +1,20 @@
{
"identifier": "migrated",
"description": "permissions that were migrated from v1",
"local": true,
"windows": [
"main"
],
"permissions": [
"core:default",
"shell:allow-open",
"dialog:allow-open",
"dialog:allow-message",
"dialog:allow-ask",
"process:allow-restart",
"process:allow-exit",
"dialog:default",
"shell:default",
"process:default"
]
}

View File

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View File

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

@ -0,0 +1,295 @@
use std::fs::File;
use std::io::Cursor;
use std::io::Read;
use std::io::Seek;
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;
use crate::modpack;
use crate::modpack::get_modpacks;
use crate::modpack::get_versions;
use crate::modpack::VersionEntry;
use crate::sftp;
use chrono;
use serde::Serialize;
use ssh2::Session;
use zip::write::SimpleFileOptions;
use zip::ZipArchive;
use zip::ZipWriter;
use tauri::Emitter;
//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);
pub fn emit(event: &str, payload: impl Serialize + Clone, window: tauri::AppHandle) {
if !window.emit(event, payload).is_ok() {
println!("Failed to emit to window!");
}
}
#[tauri::command]
pub async fn login(username: String, password: String, window: tauri::AppHandle) {
let res = sftp::connect(username, password);
if res.is_ok() {
//*USERNAME.lock() = username;
//*PASSWORD.lock() = password;
*SESSION.lock().await = Some(res.unwrap());
emit("Login_Success", {}, window);
} else {
emit("Login_Failed", {}, window);
}
}
#[tauri::command]
pub async fn drop_session() {
let ref mut session = *SESSION.lock().await;
if let Some(session) = session {
session.disconnect(None, "disconnecting", None).unwrap();
}
*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?;
}
Err(format!("Session doesnt exist?"))
}
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?;
}
Err(format!("Session doesnt exist?"))
}
#[tauri::command]
pub async fn shift_up(id: String, window: tauri::AppHandle) {
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);
}
let res = update_modpacks(modpacks).await;
if !res.is_ok() {
emit("Error", res.unwrap_err(), window);
}
}
#[tauri::command]
pub async fn shift_down(id: String, window: tauri::AppHandle) {
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);
}
let res = update_modpacks(modpacks).await;
if !res.is_ok() {
emit("Error", res.unwrap_err(), window);
}
}
#[tauri::command]
pub async fn add_pack(id: String, name: String, window: tauri::AppHandle) {
{
let ref mut session = *SESSION.lock().await;
if let Some(session) = session {
let res = sftp::mkdir(session.clone(), PathBuf::from(format!("/ftp/{}", id))).await;
if !res.is_ok() {
emit("Error", res.unwrap_err(), window.clone());
}
let res = sftp::mkdir(
session.clone(),
PathBuf::from(format!("/ftp/{}/Versions", id)),
)
.await;
if !res.is_ok() {
emit("Error", res.unwrap_err(), window.clone());
}
}
}
let versions: Vec<VersionEntry> = Vec::new();
let res = update_versions(id.clone(), versions).await;
if !res.is_ok() {
emit("Error", res.unwrap_err(), window.clone());
}
let mut modpacks = get_modpacks().await;
modpacks.push(modpack::ModpackEntry {
id: id,
name: name,
last_updated: format!("{:?}", chrono::offset::Utc::now()),
});
let res = update_modpacks(modpacks).await;
if !res.is_ok() {
emit("Error", res.unwrap_err(), window);
}
}
#[tauri::command]
pub async fn remove_pack(id: String, window: tauri::AppHandle) {
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;
}
let res = update_modpacks(modpacks).await;
if !res.is_ok() {
emit("Error", res.unwrap_err(), window);
}
{
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::AppHandle,
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;
}
let res = out_archive.start_file(
file.name(),
SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated),
);
if !res.is_ok() {
emit("Error", format!("Unable to start zip archive"), window);
return Err(format!("Unable to start zip archive"));
}
let res = std::io::copy(&mut file, &mut out_archive);
if !res.is_ok() {
emit("Error", format!("Unable to copy archive to ram"), window);
return Err(format!("Unable to copy archive to ram"));
}
}
let res = out_archive.start_file(
"overrides/version.txt",
SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated),
);
if !res.is_ok() {
emit("Error", format!("Unable to create version file"), window);
return Err(format!("Unable to create version file"));
}
let res = out_archive.write_all(version.as_bytes());
if !res.is_ok() {
emit("Error", format!("Unable to write to zip"), window);
return Err(format!("Unable to write to zip"));
}
let res = out_archive.finish();
if !res.is_ok() {
emit("Error", format!("Unable to finish zip"), window);
return Err(format!("Unable to finish zip"));
}
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);
let res = sftp::uplaod(
Some(window.clone()),
session.clone(),
PathBuf::from(upload_path),
&mut buf,
path.clone(),
size,
)
.await;
if !res.is_ok() {
emit("Error", res.clone().unwrap_err(), window.clone());
return res;
}
}
}
let mut versions = get_versions(id.clone()).await?;
versions.push(VersionEntry {
Version: version,
Date: timestamp.clone(),
File: path,
});
let res = update_versions(id.clone(), versions).await;
if !res.is_ok() {
emit("Error", res.clone().unwrap_err(), window.clone());
return res;
}
let mut modpacks = get_modpacks().await;
let mut index = 0;
for pack in modpacks.as_slice() {
if pack.id == id {
modpacks[index].last_updated = timestamp;
break;
}
index += 1;
}
let res = update_modpacks(modpacks).await;
if !res.is_ok() {
emit("Error", res.clone().unwrap_err(), window.clone());
return res;
}
Ok(())
}

View File

@ -12,20 +12,25 @@ fn ftp_connection_anonymous() -> Result<NativeTlsFtpStream, FtpError>{
ftp_connection("anonymous", "anonymous@") ftp_connection("anonymous", "anonymous@")
} }
pub fn test_cred(username: &str, password: &str) -> bool{
fn ftp_connection(username: &str, password: &str) -> Result<NativeTlsFtpStream, FtpError>{ return ftp_connection(username, password).is_ok();
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()
} }
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> { 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 file = file.to_str().unwrap().replace("\\", "/");
let size = ftp_stream.size(&file)?; let size = ftp_stream.size(&file)?;
let mut total = 0; let mut total = 0;

View File

@ -0,0 +1,67 @@
use futures_util::StreamExt;
use serde::Serialize;
use std::cmp::min;
use std::io::Write;
use std::path::PathBuf;
use tauri::Emitter;
#[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::AppHandle>,
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

@ -0,0 +1,110 @@
use flate2::read::GzDecoder;
use std::env;
use std::io::{Cursor, Read, Seek};
use std::path::{Components, Path, PathBuf};
use tar::Archive;
use crate::https;
use crate::system_dirs::get_local_data_directory;
use crate::util;
fn check_java(version: u8) -> bool {
let dir = get_local_data_directory().join("java").join(format!(
"java-{}-{}",
version,
if env::consts::OS == "windows" {
"win"
} else {
"lin"
}
));
dir.exists()
}
pub async fn install_java(version: u8, window: tauri::AppHandle) {
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 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();
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" {
let tar = GzDecoder::new(buff);
let mut archive = Archive::new(tar);
if !unpack_archive(
archive,
get_local_data_directory()
.join("java")
.join(format!("java-{}-lin", version)),
)
.is_ok()
{
std::fs::remove_dir_all(
get_local_data_directory()
.join("java")
.join(format!("java-{}-lin", version)),
)
.unwrap();
}
} else {
if !zip_extract::extract(
buff,
get_local_data_directory()
.join("java")
.join(format!("java-{}-win", version))
.as_path(),
true,
)
.is_ok()
{
std::fs::remove_dir_all(
get_local_data_directory()
.join("java")
.join(format!("java-{}-win", version)),
)
.unwrap();
}
}
}
fn unpack_archive<T: Read>(
mut archive: Archive<T>,
dst: PathBuf,
) -> Result<(), Box<dyn std::error::Error>> {
for file in archive.entries()? {
let path = PathBuf::new().join(dst.clone());
let mut file = file?;
let file_path = file.path()?;
let mut file_path = file_path.components();
let _ = file_path.next();
let file_path = file_path.as_path();
file.unpack(path.join(file_path))?;
}
Ok(())
}

View File

@ -1,25 +1,28 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!! // Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use std::collections::{HashMap, HashSet};
use std::{io::Cursor, path::PathBuf};
use std::io::Seek;
use self_update::cargo_crate_version; use self_update::cargo_crate_version;
use serde_json::{Map, Result, Value};
use serde::Serialize;
use serde::Deserialize; use serde::Deserialize;
use serde::Serialize;
use serde_json::{Map, Result, Value};
use std::collections::{HashMap, HashSet};
use std::io::Seek;
use std::{io::Cursor, path::PathBuf};
mod ftp; //mod ftp;
mod admin;
mod https;
mod java; mod java;
mod modpack;
mod prism; mod prism;
mod sftp;
mod system_dirs; mod system_dirs;
mod util; mod util;
mod modpack;
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
struct ModpackEntry { struct ModpackEntry {
name: String, name: String,
id: String id: String,
} }
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command // Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
@ -28,14 +31,30 @@ fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name) format!("Hello, {}! You've been greeted from Rust!", name)
} }
fn main() { 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(); //modpack::get_modpacks();
println!("update status: `{}`", status.version());
modpack::get_modpacks();
//prism::install_prism(); //prism::install_prism();
tauri::Builder::default() tauri::Builder::default()
.invoke_handler(tauri::generate_handler![greet, modpack::get_modpacks, modpack::launch_modpack, prism::launch_prism, prism::install_prism]) .plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_dialog::init())
.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!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");
} }

View File

@ -0,0 +1,324 @@
use crate::https;
use crate::java;
use crate::system_dirs::{
get_data_directory, get_java_executable, get_local_data_directory, get_prism_executable,
};
use crate::util;
use reqwest::IntoUrl;
use serde::de::value::Error;
use serde::Deserialize;
use serde::Serialize;
use serde_json::Value;
use std::fs;
use std::fs::File;
use std::io::{Read, Seek, Write};
use std::process::Command;
use std::time::Duration;
use std::{env, thread};
use std::{io::Cursor, path::PathBuf};
#[derive(Serialize, Deserialize, Clone)]
pub struct ModpackEntry {
pub name: String,
pub id: String,
pub last_updated: String,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct VersionEntry {
pub Version: String,
pub File: String,
pub Date: String,
}
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 {
instance_name = pack.name;
}
}
return instance_name;
}
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).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();
file.read_to_string(&mut current);
if latest != current {
return true;
}
return false;
}
#[tauri::command]
pub async fn launch_modpack(window: tauri::AppHandle, id: String) {
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).await)
.spawn()
.unwrap();
}
async fn install_modpack(window: tauri::AppHandle, 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());
println!("Downloading file {}", ftp_path.to_str().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()).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() {
break;
}
}
thread::sleep(Duration::from_secs(3));
}
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()).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";
for component in info_json["components"].as_array().unwrap() {
if component["uid"] == "net.minecraft" {
mc_version = component["version"].as_str().unwrap();
}
}
let java = get_java_version(mc_version);
java::install_java(java, window.clone()).await;
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);
let mut option_file = File::create(option_path).unwrap();
let mut set = false;
for line in buf.lines() {
if line.starts_with("JavaPath=") {
option_file.write_all(
format!(
"JavaPath={}/java-{}-{}/bin/{}\n",
get_local_data_directory()
.join("java")
.into_os_string()
.to_str()
.unwrap()
.replace("\\", "/"),
java,
if env::consts::OS == "windows" {
"win"
} else {
"lin"
},
get_java_executable()
)
.as_bytes(),
);
set = true;
} else {
option_file.write_all(format!("{}\n", line).as_bytes());
}
}
if !set {
option_file.write_all(
format!(
"JavaPath={}/java-{}-{}/bin/{}\n",
get_local_data_directory()
.join("java")
.into_os_string()
.to_str()
.unwrap()
.replace("\\", "/"),
java,
if env::consts::OS == "windows" {
"win"
} else {
"lin"
},
get_java_executable()
)
.as_bytes(),
);
option_file.write_all("OverrideJavaLocation=true\n".as_bytes());
option_file.write_all("OverrideJava=true\n".as_bytes());
}
}
#[tauri::command]
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();
}
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();
//}
}
#[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);
https::download(
None,
format!(
"https://gitea.piwalker.net/fclauncher/{}/versions.json",
id.clone()
),
&mut buf,
format!("{}/versions.json", id.clone()),
)
.await;
buf.rewind();
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 {
let components: Vec<&str> = mc_version.split(".").collect();
let mut java = 8;
if components[1] == "17" {
java = 17
} else if components[1] == "18" || components[1] == "19" {
java = 17
} else if components[1].parse::<i32>().unwrap() > 19 {
if components[1] == "20" && components[1].parse::<i32>().unwrap() < 5 {
java = 17
} else {
java = 21
}
}
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

@ -0,0 +1,124 @@
use flate2::read::GzDecoder;
use std::{
env,
fs::File,
io::{BufRead, Cursor, Seek, Write},
path::PathBuf,
process::Command,
str::FromStr,
};
use tar::Archive;
//use tauri::file;
use crate::{
https, java,
system_dirs::{get_local_data_directory, get_prism_executable},
util,
};
pub fn check_prism() -> bool {
let path = get_local_data_directory().join("prism");
path.exists()
}
#[tauri::command]
pub async fn install_prism(window: tauri::AppHandle) {
if check_prism() {
return;
}
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();
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" {
let tar = GzDecoder::new(buff);
let mut archive = Archive::new(tar);
if !archive
.unpack(get_local_data_directory().join("prism"))
.is_ok()
{
std::fs::remove_dir_all(get_local_data_directory().join("prism"));
}
} else {
if !zip_extract::extract(
buff,
get_local_data_directory().join("prism").as_path(),
true,
)
.is_ok()
{
std::fs::remove_dir_all(get_local_data_directory().join("prism"));
}
}
let mut buff = Cursor::new(vec![]);
//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 {
let mut buf = String::new();
let count = buff.read_line(&mut buf).unwrap();
if count == 0 {
break;
}
if buf.starts_with("JavaPath") {
buf = format!(
"JavaPath={}/java/java-21-{}\n",
get_local_data_directory()
.to_str()
.unwrap()
.replace("\\", "/"),
if env::consts::OS == "windows" {
"win"
} else {
"lin"
}
);
} else if buf.starts_with("LastHostname") {
buf = format!(
"LastHostname={}\n",
gethostname::gethostname().to_str().unwrap()
);
}
file.write_all(buf.as_bytes());
}
}
#[tauri::command]
pub fn launch_prism() {
let mut child = Command::new(
get_local_data_directory()
.join("prism")
.join(get_prism_executable()),
)
.spawn()
.unwrap();
}

View File

@ -0,0 +1,102 @@
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 std::io::prelude::*;
use std::net::TcpStream;
use std::path::Path;
use std::path::PathBuf;
use tauri::Emitter;
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::AppHandle>,
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

@ -0,0 +1,35 @@
use dirs::home_dir;
use std::{
env,
path::{Path, PathBuf},
};
pub fn get_local_data_directory() -> PathBuf {
dirs::data_local_dir().unwrap().join("FCLauncher")
}
pub fn get_data_directory() -> PathBuf {
dirs::data_dir().unwrap().join("FCLauncher")
}
pub fn get_java_executable() -> String {
return format!(
"java{}",
if env::consts::OS == "windows" {
".exe"
} else {
""
}
);
}
pub fn get_prism_executable() -> String {
return format!(
"{}",
if env::consts::OS == "windows" {
"prismlauncher.exe"
} else {
"PrismLauncher"
}
);
}

View File

@ -0,0 +1,46 @@
use std::clone;
use tauri::Emitter;
use serde::Serialize;
#[derive(Clone, Serialize)]
pub struct DownloadStatus {
downloaded: usize,
total: usize,
time_elapsed: usize,
download_name: String,
}
pub fn download_callback(
download_name: String,
window: Option<tauri::AppHandle>,
count: usize,
size: usize,
) {
unsafe {
static mut total: usize = 0;
total += count;
if let Some(window1) = window.clone() {
window1.emit(
"download_progress",
DownloadStatus {
downloaded: total,
total: size,
time_elapsed: 0,
download_name: download_name,
},
);
}
println!(
"Downloading {}MB / {}MB",
total / (1024 * 1024),
size / (1024 * 1024)
);
if count == 0 {
if let Some(window2) = window {
window2.emit("download_finished", true);
}
total = 0;
}
}
}

View File

@ -0,0 +1,43 @@
{
"build": {
"frontendDist": "../src"
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"createUpdaterArtifacts": "v1Compatible"
},
"productName": "FCLauncher",
"mainBinaryName": "FCLauncher",
"version": "1.0.5",
"identifier": "net.piwalker",
"plugins": {
"updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDNDOEUwMjYxRUU2NEI5RgpSV1NmUytZZUp1RElBN3dEaGhpWG9JZVNQcFlnNFFzaXN0UnBsVmxNeVdWWnJoQmh4cGJRbjN3Ygo=",
"endpoints": [
"https://gitea.piwalker.net/fclauncher/app.json"
]
}
},
"app": {
"security": {
"csp": null
},
"withGlobalTauri": true,
"windows": [
{
"title": "FCLauncher",
"width": 800,
"height": 600,
"resizable": false
}
]
}
}

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>

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>

141
FCLauncher.old/src/admin.js Normal file
View File

@ -0,0 +1,141 @@
const { invoke } = window.__TAURI__.tauri;
const { listen } = window.__TAURI__.event;
const { ask, open, message } = window.__TAURI__.dialog;
const downBar = document.querySelector(".progressFinished");
//import { listen } from '@tauri-apps/api';
const error = listen("Error", (error) => {
message(error.payload, {title: "Error", type: "error"});
});
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);
}

View File

Before

Width:  |  Height:  |  Size: 367 KiB

After

Width:  |  Height:  |  Size: 367 KiB

View File

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 109 KiB

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

View File

Before

Width:  |  Height:  |  Size: 995 B

After

Width:  |  Height:  |  Size: 995 B

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

View File

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

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

View File

@ -0,0 +1,42 @@
const { invoke } = window.__TAURI__.tauri;
const { listen } = window.__TAURI__.event;
const { ask, message } = window.__TAURI__.dialog;
const downBar = document.querySelector(".progressFinished");
//import { listen } from '@tauri-apps/api';
const error = listen("Error", (error) => {
message(error.payload, {title: "Error", type: "error"});
});
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,9 +1,14 @@
const { invoke } = window.__TAURI__.tauri; const { invoke } = window.__TAURI__.tauri;
const { listen } = window.__TAURI__.event; const { listen } = window.__TAURI__.event;
const { ask } = window.__TAURI__.dialog; const { ask, message } = window.__TAURI__.dialog;
const { exit } = window.__TAURI__.process;
const downBar = document.querySelector(".progressFinished"); const downBar = document.querySelector(".progressFinished");
//import { listen } from '@tauri-apps/api'; //import { listen } from '@tauri-apps/api';
const error = listen("Error", (error) => {
message(error.payload, {title: "Error", type: "error"});
});
const download_progress = listen("download_progress", (progress) => { const download_progress = listen("download_progress", (progress) => {
console.log("Downloading"); console.log("Downloading");
//console.log("Downloaded "+progress.payload.downloaded/(1024*1024) +"MB / " + progress.payload.total/(1024*1024) + "MB"); //console.log("Downloaded "+progress.payload.downloaded/(1024*1024) +"MB / " + progress.payload.total/(1024*1024) + "MB");
@ -33,6 +38,8 @@ window.addEventListener("DOMContentLoaded", () => {
document.getElementById("launchGame").addEventListener("click", gameLaunch); document.getElementById("launchGame").addEventListener("click", gameLaunch);
document.getElementById("prism").addEventListener("click", prism); document.getElementById("prism").addEventListener("click", prism);
document.getElementById("settings").addEventListener("click", login);
document.getElementById("back").addEventListener("click", back);
}); });
function packSelect() { function packSelect() {
@ -40,6 +47,15 @@ function packSelect() {
} }
function login(){
window.location.href = "Login.html";
}
function back(){
console.log("test");
window.location.href = "index.html";
}
function load() { function load() {
console.log("loading"); console.log("loading");
var dropdown = document.getElementById("Modpacks"); var dropdown = document.getElementById("Modpacks");
@ -58,8 +74,8 @@ window.onload = async function() {
function addModpacks(modpacks) { function addModpacks(modpacks) {
var dropdown = document.getElementById("Modpacks"); var dropdown = document.getElementById("Modpacks");
modpacks.sort((a, b) => a.name.localeCompare(b.name)); //modpacks.sort((a, b) => a.name.localeCompare(b.name));
modpacks.reverse(); //modpacks.reverse();
for (let i = 0; i < modpacks.length; i++){ for (let i = 0; i < modpacks.length; i++){
var opt = document.createElement("option"); var opt = document.createElement("option");
opt.text = modpacks[i].name; opt.text = modpacks[i].name;
@ -74,7 +90,7 @@ function gameLaunch() {
document.getElementById("launchGame").disabled = true; document.getElementById("launchGame").disabled = true;
document.getElementById("launchGame").textContent ="Launching..."; document.getElementById("launchGame").textContent ="Launching...";
//TODO Launch Game //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; 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 { body {
margin: 0px; margin: 0px;
padding: 0px; padding: 0px;
@ -228,3 +261,114 @@ button {
display: block; display: block;
margin: 0.5em; 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;
}

View File

@ -1,52 +0,0 @@
use std::env;
use std::io::{Cursor, Seek, Read};
use std::path::{Components, Path, PathBuf};
use flate2::read::GzDecoder;
use tar::Archive;
use crate::system_dirs::get_local_data_directory;
use crate::ftp::{self, ftp_get_size};
use crate::util;
fn check_java(version: u8) -> bool {
let dir = get_local_data_directory().join("java").join(format!("java-{}-{}", version, if env::consts::OS == "windows" { "win" } else {"lin"}));
dir.exists()
}
pub 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 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();
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" {
let tar = GzDecoder::new(buff);
let mut archive = Archive::new(tar);
if !unpack_archive(archive, get_local_data_directory().join("java").join(format!("java-{}-lin", version))).is_ok() {
std::fs::remove_dir_all(get_local_data_directory().join("java").join(format!("java-{}-lin", version))).unwrap();
}
} else {
if !zip_extract::extract(buff, get_local_data_directory().join("java").join(format!("java-{}-win", version)).as_path(), true).is_ok() {
std::fs::remove_dir_all(get_local_data_directory().join("java").join(format!("java-{}-win", version))).unwrap();
}
}
}
fn unpack_archive<T: Read>(mut archive: Archive<T>, dst: PathBuf) -> Result<(), Box<dyn std::error::Error>> {
for file in archive.entries()? {
let path = PathBuf::new().join(dst.clone());
let mut file = file?;
let file_path = file.path()?;
let mut file_path = file_path.components();
let _ = file_path.next();
let file_path = file_path.as_path();
file.unpack(path.join(file_path))?;
}
Ok(())
}

View File

@ -1,175 +0,0 @@
use crate::{ftp, 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};
use std::fs::File;
use std::process::Command;
use std::{io::Cursor, path::PathBuf};
use std::io::{Read, Seek, Write};
use serde_json::Value;
use serde::Serialize;
use serde::Deserialize;
use crate::util;
#[derive(Serialize, Deserialize, Clone)]
pub struct ModpackEntry{
name: String,
id: String
}
#[derive(Clone)]
pub struct VersionEntry{
version: String,
file: String,
date: String
}
fn get_modpack_name(id: String) -> String {
let modpacks = get_modpacks();
let mut instance_name = String::new();
for pack in modpacks {
if pack.id == id {
instance_name = pack.name;
}
}
return instance_name;
}
fn check_modpack_needs_update(id: String) -> bool{
let mut instance_name = get_modpack_name(id.clone());
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 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();
file.read_to_string(&mut current);
if latest != current {
return true;
}
return false;
}
#[tauri::command]
pub async fn launch_modpack(window: tauri::Window, id: String){
if check_modpack_needs_update(id.clone()) {
install_modpack(window, id.clone());
}
// 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();
}
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())));
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());
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();
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");
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() {
break;
}
}
thread::sleep(Duration::from_secs(3));
}
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 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";
for component in info_json["components"].as_array().unwrap() {
if component["uid"] == "net.minecraft" {
mc_version = component["version"].as_str().unwrap();
}
}
let java = get_java_version(mc_version);
java::install_java(java, window.clone());
let option_path = get_local_data_directory().join("prism").join("instances").join(get_modpack_name(id.clone())).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);
let mut option_file = File::create(option_path).unwrap();
let mut set = false;
for line in buf.lines() {
if line.starts_with("JavaPath=") {
option_file.write_all(format!("JavaPath={}/java-{}-{}/bin/{}\n", get_local_data_directory().join("java").into_os_string().to_str().unwrap().replace("\\", "/"), java, if env::consts::OS == "windows" {"win"} else {"lin"}, get_java_executable()).as_bytes());
set = true;
} else {
option_file.write_all(format!("{}\n",line).as_bytes());
}
}
if !set {
option_file.write_all(format!("JavaPath={}/java-{}-{}/bin/{}\n", get_local_data_directory().join("java").into_os_string().to_str().unwrap().replace("\\", "/"), java, if env::consts::OS == "windows" {"win"} else {"lin"}, get_java_executable()).as_bytes());
option_file.write_all("OverrideJavaLocation=true\n".as_bytes());
option_file.write_all("OverrideJava=true\n".as_bytes());
}
}
#[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()});
}
}
return modpacks.clone();
}
}
fn get_versions(id: String) -> Vec<VersionEntry> {
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);
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();
}
fn get_java_version(mc_version: &str) -> u8{
let components: Vec<&str> = mc_version.split(".").collect();
let mut java = 8;
if components[1] == "17" {
java = 17
} else if components[1] == "18" || components[1] == "19" {
java = 17
} else if components[1].parse::<i32>().unwrap() > 19 {
if components[1] == "20" && components[1].parse::<i32>().unwrap() < 5 {
java = 17
} else {
java = 21
}
}
return java;
}

View File

@ -1,65 +0,0 @@
use std::{env, fs::File, io::{BufRead, Cursor, Seek, Write}, path::PathBuf, process::Command, str::FromStr};
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};
pub fn check_prism() -> bool {
let path = get_local_data_directory().join("prism");
path.exists()
}
#[tauri::command]
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();
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();
std::fs::create_dir_all(get_local_data_directory().join("prism")).unwrap();
buff.rewind().unwrap();
if env::consts::OS != "windows" {
let tar = GzDecoder::new(buff);
let mut archive = Archive::new(tar);
if !archive.unpack(get_local_data_directory().join("prism")).is_ok() {
std::fs::remove_dir_all(get_local_data_directory().join("prism"));
}
} else {
if !zip_extract::extract(buff, get_local_data_directory().join("prism").as_path(), true).is_ok() {
std::fs::remove_dir_all(get_local_data_directory().join("prism"));
}
}
let mut buff = Cursor::new(vec![]);
ftp::ftp_retr(Some(window.clone()), PathBuf::new().join("prism").join("prismlauncher.cfg"), &mut buff, |_, _, _| return).unwrap();
buff.rewind();
let mut file = File::create(get_local_data_directory().join("prism").join("prismlauncher.cfg")).unwrap();
loop {
let mut buf = String::new();
let count = buff.read_line(&mut buf).unwrap();
if count == 0 {
break;
}
if buf.starts_with("JavaPath") {
buf = format!("JavaPath={}/java/java-21-{}\n", get_local_data_directory().to_str().unwrap().replace("\\", "/"), if env::consts::OS == "windows" { "win" } else { "lin" });
}else if buf.starts_with("LastHostname") {
buf = format!("LastHostname={}\n", gethostname::gethostname().to_str().unwrap());
}
file.write_all(buf.as_bytes());
}
}
#[tauri::command]
pub fn launch_prism() {
let mut child = Command::new(get_local_data_directory().join("prism").join(get_prism_executable())).spawn().unwrap();
}

View File

@ -1,20 +0,0 @@
use std::{env, path::{Path, PathBuf}};
use dirs::home_dir;
pub fn get_local_data_directory() -> PathBuf {
dirs::data_local_dir().unwrap().join("FCLauncher")
}
pub fn get_data_directory() -> PathBuf {
dirs::data_dir().unwrap().join("FCLauncher")
}
pub fn get_java_executable() -> String {
return format!("java{}", if env::consts::OS == "windows" { ".exe" } else { "" })
}
pub fn get_prism_executable() -> String {
return format!("{}", if env::consts::OS == "windows" { "prismlauncher.exe" } else { "PrismLauncher" })
}

View File

@ -1,30 +0,0 @@
use std::clone;
use serde::Serialize;
#[derive(Clone, Serialize)]
pub struct DownloadStatus {
downloaded: usize,
total: usize,
time_elapsed: usize,
download_name: String
}
pub fn download_callback(download_name: String, window: Option<tauri::Window>, count: usize, size: usize){
unsafe{
static mut total: usize = 0;
total += count;
if let Some(window1) = window.clone() {
window1.emit("download_progress", DownloadStatus{downloaded: total, total: size, time_elapsed: 0, download_name: download_name});
}
println!("Downloading {}MB / {}MB", total/(1024*1024), size/(1024*1024));
if count == 0 {
if let Some(window2) = window {
window2.emit("download_finished", true);
}
total = 0;
}
}
}

View File

@ -1,47 +0,0 @@
{
"build": {
"devPath": "../src",
"distDir": "../src",
"withGlobalTauri": true
},
"package": {
"productName": "FCLauncher",
"version": "0.0.0"
},
"tauri": {
"allowlist": {
"all": false,
"shell": {
"all": false,
"open": true
},
"dialog": {
"all": false,
"ask": true
}
},
"windows": [
{
"title": "FCLauncher",
"width": 800,
"height": 600,
"resizable": false
}
],
"security": {
"csp": null
},
"bundle": {
"active": true,
"targets": "all",
"identifier": "net.piwalker",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}
}

4
fclauncher/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
build/bin
node_modules
frontend/dist
frontend/wailsjs/go/

34
fclauncher/2 Normal file
View File

@ -0,0 +1,34 @@
package main
import (
"context"
"os"
"path/filepath"
)
type Instance struct {
InstanceName string
ModpackId string
ModpackVersion string
}
type InstanceManager struct {
instances []Instance
PrismLauncher Prism
ctx context.Context
}
func (i *InstanceManager)SearchInstances() {
i.instances = []Instance{}
dir := i.PrismLauncher.GetInstanceDir()
subdirs, _ := os.ReadDir(dir)
for _, d := range subdirs {
if !d.IsDir() {
continue
}
if _, err := os.Stat(filepath.Join(dir, d.Name(), "instance.json")); err != nil {
continue
}
f, _ = os.OpenFile()
}
}

44
fclauncher/Admin.go Normal file
View File

@ -0,0 +1,44 @@
package main
import (
"fmt"
"golang.org/x/crypto/ssh"
)
type Admin struct {
conf *ssh.ClientConfig
}
func sftpConnect(conf *ssh.ClientConfig) (ssh.Conn, error) {
return ssh.Dial("tcp", "gitea-svr.piwalker.net:22", conf)
}
func sftpAuth(username string, password string) (*ssh.ClientConfig, error) {
key, _, _, _, err := ssh.ParseAuthorizedKey([]byte("gitea-svr.piwalker.net ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILkyh7MDvubWw4OzTFbvsUz7gOmOzBq77i5Q86STKqja"));
config := &ssh.ClientConfig{
User: username,
Auth: []ssh.AuthMethod{
ssh.Password(password),
},
HostKeyCallback: ssh.FixedHostKey(key),
}
conn, err := sftpConnect(config)
if err != nil {
return nil, err
}
conn.Close()
return config, nil
}
func (a *Admin) AdminAuth(username string, password string) bool {
conf, err := sftpAuth(username, password)
if err != nil {
fmt.Println("SFTP error: ", err)
a.conf = nil
return false
}
a.conf = conf
return true
}

View File

@ -0,0 +1,624 @@
package main
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
wruntime "github.com/wailsapp/wails/v2/pkg/runtime"
"github.com/zhyee/zipstream"
)
type MrData struct {
FormatVersion int
Game string
VersionId string
Name string
Summary string
Files []MrFile
Dependencies map[string]string
}
type MrFile struct {
Path string
Hashes map[string]string
Env map[string]string
Downloads []string
FileSize int
}
type Instance struct {
InstanceName string
ModpackId string
ModpackVersion string
MinecraftVersion string
ForgeVersion string
NeoForgeVersion string
FabricVersion string
QuiltVersion string
JavaVersion int
Libraries []string
MainClass string
}
type InstanceManager struct {
instances []Instance
app *App
}
type mmcpack struct {
Components []component
}
type component struct {
Uid string
Version string
}
func (i *InstanceManager) SearchInstances() {
i.instances = []Instance{}
dir, _ := os.UserConfigDir()
dir = filepath.Join(dir, "FCLauncher", "instances")
if _, err := os.Stat(dir); err != nil {
return
}
subdirs, _ := os.ReadDir(dir)
for _, d := range subdirs {
if !d.IsDir() {
continue
}
if _, err := os.Stat(filepath.Join(dir, d.Name(), "instance.json")); err != nil {
continue
}
f, _ := os.OpenFile(filepath.Join(dir, d.Name(), "instance.json"), os.O_RDONLY, 0755)
defer f.Close()
buff := new(bytes.Buffer)
io.Copy(buff, f)
instance := Instance{}
json.Unmarshal(buff.Bytes(), &instance)
i.instances = append(i.instances, instance)
}
}
func (i *InstanceManager) checkJavaVersion(instance Instance) {
infoPath := filepath.Join(i.app.PrismLauncher.GetInstanceDir(), instance.InstanceName, "mmc-pack.json")
f, _ := os.OpenFile(infoPath, os.O_RDONLY, 0755)
defer f.Close()
dataStr, _ := io.ReadAll(f)
var data mmcpack
json.Unmarshal(dataStr, &data)
mc_version := "0.0"
for _, comp := range data.Components {
if comp.Uid == "net.minecraft" {
mc_version = comp.Version
break
}
}
fmt.Printf("MC Version: %s", mc_version)
tokensStr := strings.Split(mc_version, ".")
tokens := []int{0, 0, 0}
tokens[0], _ = strconv.Atoi(tokensStr[0])
tokens[1], _ = strconv.Atoi(tokensStr[1])
if len(tokensStr) > 2 {
tokens[2], _ = strconv.Atoi(tokensStr[2])
}
javaVer := 8
if tokens[1] == 17 {
javaVer = 17
} else if tokens[1] == 18 || tokens[1] == 19 {
javaVer = 17
} else if tokens[1] > 19 {
if tokens[1] == 20 && tokens[2] < 5 {
javaVer = 17
} else {
javaVer = 21
}
}
fmt.Printf("Req Java Version: %d", javaVer)
if !i.app.Java.CheckJavaVer(javaVer) {
i.app.Java.InstallJavaVer(javaVer)
}
confPath := filepath.Join(i.app.PrismLauncher.GetInstanceDir(), instance.InstanceName, "instance.cfg")
f, _ = os.OpenFile(confPath, os.O_RDONLY, 0755)
defer f.Close()
buff := new(bytes.Buffer)
io.Copy(buff, f)
sc := bufio.NewScanner(buff)
f, _ = os.OpenFile(confPath, os.O_CREATE|os.O_RDWR, 0755)
plat := "lin"
exe := "java"
if runtime.GOOS == "windows" {
plat = "win"
exe = "Java.exe"
}
confDir, _ := os.UserConfigDir()
found := false
for sc.Scan() {
line := sc.Text()
if strings.HasPrefix(line, "JavaPath=") {
line = fmt.Sprintf("JavaPath=%s/FCLauncher/java/java-%d-%s/bin/%s", strings.ReplaceAll(confDir, "\\", "/"), javaVer, plat, exe)
found = true
}
f.WriteString(line + "\n")
}
if !found {
line := fmt.Sprintf("JavaPath=%s/FCLauncher/java/java-%d-%s/bin/%s", strings.ReplaceAll(confDir, "\\", "/"), javaVer, plat, exe)
f.WriteString(line + "\n")
f.WriteString("OverrideJavaLocation=true\nOverrideJava=true\n")
}
f.Close()
}
func (i *InstanceManager) InstallModpack(modpack Modpack, instanceName string) {
i.app.Status(fmt.Sprintf("Installing %s", modpack.Name))
version := modpack.Versions[len(modpack.Versions)-1]
dname, _ := os.MkdirTemp("", "fclauncher-*")
f, _ := os.OpenFile(filepath.Join(dname, instanceName+".mrpack"), os.O_CREATE|os.O_RDWR, 0755)
defer f.Close()
HttpDownload(modpack.Id+"/"+version.File, f, i.app.Ctx)
i.app.PrismLauncher.ImportModpack(f.Name())
instance := Instance{InstanceName: instanceName, ModpackVersion: version.Version, ModpackId: modpack.Id}
i.instances = append(i.instances, instance)
f, _ = os.OpenFile(filepath.Join(i.app.PrismLauncher.GetInstanceDir(), instanceName, "instance.json"), os.O_CREATE|os.O_RDWR, 0755)
defer f.Close()
data, _ := json.Marshal(instance)
f.Write(data)
i.checkJavaVersion(instance)
i.SearchInstances()
}
func (i *InstanceManager) InstallVanilla(version string, instanceName string) {
dir, _ := os.UserConfigDir()
err := DownloadAssets(version, filepath.Join(dir, "FCLauncher", "assets"), *i.app)
if err != nil {
fmt.Printf("Unable to download assets: %s\n", err)
} else {
fmt.Printf("Assets Downloaded")
}
err = DownloadLibraries(version, filepath.Join(dir, "FCLauncher", "lib"), *i.app)
if err != nil {
fmt.Printf("Unable to download libs: %s\n", err)
} else {
fmt.Printf("Libs Downloaded")
}
InstallNatives(version, filepath.Join(dir, "FCLauncher", "instances", instanceName, "minecraft", "natives"))
DownloadLoggingConfig(version, filepath.Join(dir, "FCLauncher", "instances", instanceName, "minecraft"))
err = DownloadExecutable(version, filepath.Join(dir, "FCLauncher", "bin"), *i.app)
if err != nil {
fmt.Printf("Unable to download binaries: %s\n", err)
} else {
fmt.Printf("Binaries Downloaded")
}
metadata, err := GetVersionMetadata(version)
if err != nil {
fmt.Printf("unable to pull metadata: %s\n", err)
}
err = os.MkdirAll(filepath.Join(dir, "FCLauncher", "instances", instanceName, "minecraft"), 0755)
if err != nil {
fmt.Printf("unable to create directory: %s\n", err)
}
instance := Instance{InstanceName: instanceName, MinecraftVersion: version, JavaVersion: metadata.JavaVersion.MajorVersion, MainClass: metadata.MainClass}
for _, lib := range metadata.Libraries {
instance.Libraries = append(instance.Libraries, lib.Downloads.Artifact.Path)
}
data, err := json.Marshal(instance)
if err != nil {
fmt.Printf("unable to marshal json data: %s\n", err)
}
f, err := os.OpenFile(filepath.Join(dir, "FCLauncher", "instances", instanceName, "instance.json"), os.O_CREATE|os.O_RDWR, 0755)
if err != nil {
fmt.Printf("unable to open file: %s\n", err)
}
defer f.Close()
_, err = f.Write(data)
if err != nil {
fmt.Printf("unable to write data: %s\n", err)
}
i.instances = append(i.instances, instance)
if !i.app.Java.CheckJavaVer(instance.JavaVersion) {
i.app.Status(fmt.Sprintf("Installing Java Version %d", instance.JavaVersion))
i.app.Java.InstallJavaVer(instance.JavaVersion)
}
i.SearchInstances()
}
func (i *InstanceManager) GetInstances() []string {
names := []string{}
for _, inst := range i.instances {
names = append(names, inst.InstanceName)
}
return names
}
func (i *InstanceManager) CheckUpdate(instance Instance) {
return
i.app.Status("Checking for Updates")
i.app.Modpacks.QuerryModpacks()
pack := i.app.Modpacks.GetModpack(instance.ModpackId)
if pack.Versions[len(pack.Versions)-1].Version == instance.ModpackVersion {
return
}
i.app.Status(fmt.Sprintf("Updating %s", instance.InstanceName))
version := pack.Versions[len(pack.Versions)-1]
dname, _ := os.MkdirTemp("", "fclauncher-*")
f, _ := os.OpenFile(filepath.Join(dname, instance.InstanceName+".mrpack"), os.O_CREATE|os.O_RDWR, 0755)
defer f.Close()
HttpDownload(pack.Id+"/"+version.File, f, i.app.Ctx)
i.app.PrismLauncher.ImportModpack(f.Name())
instance.ModpackVersion = version.Version
f, _ = os.OpenFile(filepath.Join(i.app.PrismLauncher.GetInstanceDir(), instance.InstanceName, "instance.json"), os.O_CREATE|os.O_RDWR, 0755)
defer f.Close()
data, _ := json.Marshal(instance)
f.Write(data)
i.checkJavaVersion(instance)
i.SearchInstances()
}
func (i *InstanceManager) GetInstance(instance string) (Instance, error) {
instanceObject := Instance{}
found := false
for _, inst := range i.instances {
if inst.InstanceName == instance {
instanceObject = inst
found = true
break
}
}
if !found {
return Instance{}, fmt.Errorf("unable to find instance %s\n", instance)
}
return instanceObject, nil
}
func (i *InstanceManager) LaunchInstance(instance string) {
i.app.Status(fmt.Sprintf("Launching %s", instance))
dir, err := os.UserConfigDir()
if err != nil {
fmt.Printf("unable to get config directory\n")
}
instanceObject, err := i.GetInstance(instance)
if err != nil {
fmt.Printf("Unable to find instance\n")
}
execName := "java"
suffix := "lin"
if runtime.GOOS == "windows" {
execName = "Javaw.exe"
suffix = "win"
}
dir = filepath.Join(dir, "FCLauncher")
auth, err := MicrosoftAuth(i.app.Auth)
if err != nil {
fmt.Printf("unable to authenticate: %s\n", err)
return
}
args, err := GetOnlineLaunchArgs(instanceObject.MinecraftVersion, instanceObject, filepath.Join(dir, "lib"), filepath.Join(dir, "bin"), filepath.Join(dir, "assets"), filepath.Join(dir, "instances", instance, "minecraft"), auth)
if err != nil {
fmt.Printf("unable to get launch args: %s\n", err)
}
if instanceObject.ForgeVersion != "" {
args = append(args, "--launchTarget")
args = append(args, "forge_client")
}
fmt.Printf("Args: %+v", args)
child := exec.Command(filepath.Join(dir, "java", fmt.Sprintf("java-%d-%s", instanceObject.JavaVersion, suffix), "bin", execName), args...)
child.Dir = filepath.Join(dir, "instances", instance, "minecraft")
wruntime.WindowHide(i.app.Ctx)
data, err := child.CombinedOutput()
wruntime.WindowShow(i.app.Ctx)
fmt.Printf("Command Output: %s\n", data)
}
func (i *InstanceManager) InstallFabric(instance string, fabricVersion string) {
i.app.Status("Installing Fabric")
instanceObject, err := i.GetInstance(instance)
if err != nil {
fmt.Printf("Instance does not exist\n")
}
metadata, err := GetFabricMetadata(instanceObject.MinecraftVersion, fabricVersion)
if err != nil {
fmt.Printf("unable to get version metadata\n")
}
client:
for _, lib := range metadata.LauncherMeta.Libraries.Client {
tokens := strings.Split(ProcessMavenPath(lib.Name), string(os.PathSeparator))
pkg := tokens[len(tokens)-2]
instanceObject.Libraries = append(instanceObject.Libraries, filepath.Join(ProcessMavenPath(lib.Name), ProcessMavenFilename(lib.Name)))
for ind, path := range instanceObject.Libraries {
path = strings.ReplaceAll(path, "/", string(os.PathSeparator))
tokens := strings.Split(path, string(os.PathSeparator))
if pkg == tokens[len(tokens)-3] {
instanceObject.Libraries[ind] = filepath.Join(ProcessMavenPath(lib.Name), ProcessMavenFilename(lib.Name))
fmt.Printf("duplicate library %s\n", pkg)
continue client
}
}
}
common:
for _, lib := range metadata.LauncherMeta.Libraries.Common {
tokens := strings.Split(ProcessMavenPath(lib.Name), string(os.PathSeparator))
pkg := tokens[len(tokens)-2]
instanceObject.Libraries = append(instanceObject.Libraries, filepath.Join(ProcessMavenPath(lib.Name), ProcessMavenFilename(lib.Name)))
for ind, path := range instanceObject.Libraries {
path = strings.ReplaceAll(path, "/", string(os.PathSeparator))
tokens := strings.Split(path, string(os.PathSeparator))
fmt.Printf("Inspecing path %s with %d tokens\n", path, len(tokens))
if pkg == tokens[len(tokens)-3] {
instanceObject.Libraries[ind] = filepath.Join(ProcessMavenPath(lib.Name), ProcessMavenFilename(lib.Name))
fmt.Printf("duplicate library %s\n", pkg)
continue common
}
}
}
instanceObject.Libraries = append(instanceObject.Libraries, filepath.Join(ProcessMavenPath(metadata.Loader.Maven), ProcessMavenFilename(metadata.Loader.Maven)))
instanceObject.Libraries = append(instanceObject.Libraries, filepath.Join(ProcessMavenPath(metadata.Intermediary.Maven), ProcessMavenFilename(metadata.Intermediary.Maven)))
instanceObject.MainClass = metadata.LauncherMeta.MainClass["client"]
instanceObject.FabricVersion = fabricVersion
dir, _ := os.UserConfigDir()
InstallFabricLibs(instanceObject.MinecraftVersion, fabricVersion, filepath.Join(dir, "FCLauncher", "lib"), i.app)
f, _ := os.OpenFile(filepath.Join(dir, "FCLauncher", "instances", instance, "instance.json"), os.O_CREATE|os.O_RDWR, 0755)
data, _ := json.Marshal(instanceObject)
defer f.Close()
f.Write(data)
for ind, inst := range i.instances {
if inst.InstanceName == instance {
i.instances[ind] = instanceObject
break
}
}
}
func (i *InstanceManager) InstallQuilt(instance string, quiltVersion string) {
i.app.Status("Installing Quilt")
instanceObject, err := i.GetInstance(instance)
if err != nil {
fmt.Printf("Instance does not exist\n")
}
metadata, err := GetQuiltMetadata(instanceObject.MinecraftVersion, quiltVersion)
if err != nil {
fmt.Printf("unable to get version metadata\n")
}
client:
for _, lib := range metadata.LauncherMeta.Libraries.Client {
tokens := strings.Split(ProcessMavenPath(lib.Name), string(os.PathSeparator))
pkg := tokens[len(tokens)-2]
instanceObject.Libraries = append(instanceObject.Libraries, filepath.Join(ProcessMavenPath(lib.Name), ProcessMavenFilename(lib.Name)))
for ind, path := range instanceObject.Libraries {
path = strings.ReplaceAll(path, "/", string(os.PathSeparator))
tokens := strings.Split(path, string(os.PathSeparator))
if pkg == tokens[len(tokens)-3] {
instanceObject.Libraries[ind] = filepath.Join(ProcessMavenPath(lib.Name), ProcessMavenFilename(lib.Name))
fmt.Printf("duplicate library %s\n", pkg)
continue client
}
}
}
common:
for _, lib := range metadata.LauncherMeta.Libraries.Common {
tokens := strings.Split(ProcessMavenPath(lib.Name), string(os.PathSeparator))
pkg := tokens[len(tokens)-2]
instanceObject.Libraries = append(instanceObject.Libraries, filepath.Join(ProcessMavenPath(lib.Name), ProcessMavenFilename(lib.Name)))
for ind, path := range instanceObject.Libraries {
path = strings.ReplaceAll(path, "/", string(os.PathSeparator))
tokens := strings.Split(path, string(os.PathSeparator))
if pkg == tokens[len(tokens)-3] {
instanceObject.Libraries[ind] = filepath.Join(ProcessMavenPath(lib.Name), ProcessMavenFilename(lib.Name))
fmt.Printf("duplicate library %s\n", pkg)
continue common
}
}
}
instanceObject.Libraries = append(instanceObject.Libraries, filepath.Join(ProcessMavenPath(metadata.Loader.Maven), ProcessMavenFilename(metadata.Loader.Maven)))
instanceObject.Libraries = append(instanceObject.Libraries, filepath.Join(ProcessMavenPath(metadata.Intermediary.Maven), ProcessMavenFilename(metadata.Intermediary.Maven)))
instanceObject.Libraries = append(instanceObject.Libraries, filepath.Join(ProcessMavenPath(metadata.Hashed.Maven), ProcessMavenFilename(metadata.Hashed.Maven)))
instanceObject.MainClass = metadata.LauncherMeta.MainClass["client"]
instanceObject.QuiltVersion = quiltVersion
dir, _ := os.UserConfigDir()
InstallQuiltLibs(instanceObject.MinecraftVersion, quiltVersion, filepath.Join(dir, "FCLauncher", "lib"), i.app)
f, _ := os.OpenFile(filepath.Join(dir, "FCLauncher", "instances", instance, "instance.json"), os.O_CREATE|os.O_RDWR, 0755)
data, _ := json.Marshal(instanceObject)
defer f.Close()
f.Write(data)
for ind, inst := range i.instances {
if inst.InstanceName == instance {
i.instances[ind] = instanceObject
break
}
}
}
func (i *InstanceManager) InstallForge(instance string, forgeVersion string) {
instanceObject, err := i.GetInstance(instance)
if err != nil {
fmt.Printf("Unable to find instance: %s\n", err)
}
installData, err := GetForgeInstallData(instanceObject.MinecraftVersion, forgeVersion)
if err != nil {
fmt.Printf("Unable to get install data: %s\n", err)
}
dir, _ := os.UserConfigDir()
InstallForgeLibs(instanceObject.MinecraftVersion, forgeVersion, filepath.Join(dir, "FCLauncher", "lib"))
instanceObject.ForgeVersion = forgeVersion
outer:
for _, lib := range installData.Libraries {
tokens := strings.Split(lib.Downloads.Artifact.Path, string(os.PathSeparator))
pkg := tokens[len(tokens)-2]
instanceObject.Libraries = append(instanceObject.Libraries, lib.Downloads.Artifact.Path)
for ind, path := range instanceObject.Libraries {
tokens := strings.Split(path, string(os.PathSeparator))
if pkg == tokens[len(tokens)-3] {
instanceObject.Libraries[ind] = filepath.Join(ProcessMavenPath(lib.Name), ProcessMavenFilename(lib.Name))
fmt.Printf("duplicate library %s\n", pkg)
continue outer
}
}
}
instanceObject.MainClass = installData.MainClass
f, _ := os.OpenFile(filepath.Join(dir, "FCLauncher", "instances", instance, "instance.json"), os.O_CREATE|os.O_RDWR, 0755)
data, _ := json.Marshal(instanceObject)
defer f.Close()
f.Write(data)
for ind, inst := range i.instances {
if inst.InstanceName == instance {
i.instances[ind] = instanceObject
break
}
}
}
func (i *InstanceManager) ImportModpack(modpack Modpack, name string) {
i.app.Status(fmt.Sprintf("Downloading %s", modpack.Name))
buff := new(bytes.Buffer)
err := HttpDownload(filepath.Join(modpack.Id, modpack.Versions[len(modpack.Versions)-1].File), buff, i.app.Ctx)
if err != nil {
fmt.Printf("Unable to download modpack file: %s\n", err)
return
}
i.ImportMrpack(buff, name)
}
func (i *InstanceManager) ImportMrpack(data io.Reader, name string) {
dir, _ := os.UserConfigDir()
InstancePath := filepath.Join(dir, "FCLauncher", "instances", name, "minecraft")
zr := zipstream.NewReader(data)
mrdata := MrData{}
i.app.Status("Unpacking modpack File")
for {
entry, err := zr.GetNextEntry()
if err == io.EOF {
break
}
if err != nil {
fmt.Printf("Error unpacking modpack file: %s\n", err)
break
}
if entry.Name == "modrinth.index.json" {
i.app.Status("Loading metadata")
file, _ := entry.Open()
data, _ := io.ReadAll(file)
json.Unmarshal(data, &mrdata)
} else {
i.app.Status(fmt.Sprintf("Unpacking %s", entry.Name))
prefix := strings.Split(entry.Name, "/")[0]
if prefix == "overrides" || prefix == "client-overrides" {
path := strings.SplitN(entry.Name, "/", 2)[1]
if entry.IsDir() {
fmt.Printf("creating directory %s\n", filepath.Join(InstancePath, path))
if _, err := os.Stat(filepath.Join(InstancePath, path)); err != nil {
os.MkdirAll(filepath.Join(InstancePath, path), 0755)
}
} else {
zf, _ := entry.Open()
defer zf.Close()
fileDir := ""
tokens := strings.Split(path, "/")
for ind, token := range tokens {
if ind != len(tokens)-1 {
fileDir = filepath.Join(fileDir, token)
}
}
fmt.Printf("creating directory %s\n", filepath.Join(InstancePath, fileDir))
if _, err := os.Stat(filepath.Join(InstancePath, fileDir)); err != nil {
os.MkdirAll(filepath.Join(InstancePath, fileDir), 0755)
}
file, _ := os.OpenFile(filepath.Join(InstancePath, path), os.O_CREATE|os.O_RDWR, 0755)
defer file.Close()
io.Copy(file, zf)
}
}
}
}
i.InstallVanilla(mrdata.Dependencies["minecraft"], name)
if mrdata.Dependencies["forge"] != "" {
fmt.Printf("Forge not implemented!")
//implement forge
} else if mrdata.Dependencies["neoforge"] != "" {
fmt.Printf("Neoforge not implemented!")
//implement neoforge
} else if mrdata.Dependencies["fabric-loader"] != "" {
i.InstallFabric(name, mrdata.Dependencies["fabric-loader"])
} else if mrdata.Dependencies["quilt-loader"] != "" {
i.InstallQuilt(name, mrdata.Dependencies["quilt-loader"])
}
i.app.Status("Downloading Mods")
for _, f := range mrdata.Files {
fmt.Printf("Downloading %s\n", f.Path)
i.app.Status(fmt.Sprintf("Downloading %s", f.Path))
resp, err := http.Get(f.Downloads[0])
if err != nil {
fmt.Printf("Unable to download file %s\n", err)
continue
}
defer resp.Body.Close()
buff := new(bytes.Buffer)
downloaded := 0
for {
count, err := io.CopyN(buff, resp.Body, BlockSize)
if err == io.EOF {
downloaded += int(count)
break
}
if err != nil {
fmt.Printf("Error Downloading libs: %e\n", err)
return
}
downloaded += int(count)
wruntime.EventsEmit(i.app.Ctx, "download", downloaded, f.FileSize)
}
fileDir := ""
tokens := strings.Split(f.Path, "/")
for ind, token := range tokens {
if ind != len(tokens)-1 {
fileDir = filepath.Join(fileDir, token)
}
}
if _, err := os.Stat(filepath.Join(InstancePath, fileDir)); err != nil {
os.MkdirAll(filepath.Join(InstancePath, fileDir), 0755)
}
file, _ := os.OpenFile(filepath.Join(InstancePath, f.Path), os.O_CREATE|os.O_RDWR, 0755)
defer file.Close()
io.Copy(file, buff)
wruntime.EventsEmit(i.app.Ctx, "download_complete")
}
}
func (i *InstanceManager) OpenInstanceFolder(instance string) {
_, err := i.GetInstance(instance)
if err != nil {
fmt.Printf("Instance does not exist\n")
}
dir, _ := os.UserConfigDir()
openbrowser(filepath.Join(dir, "FCLauncher", "instances", instance, "minecraft"))
}
func (i *InstanceManager) DeleteInstance(instance string) {
_, err := i.GetInstance(instance)
if err != nil {
fmt.Printf("Instance does not exist\n")
return
}
dir, _ := os.UserConfigDir()
os.RemoveAll(filepath.Join(dir, "FCLauncher", "instances", instance))
i.SearchInstances()
}

120
fclauncher/Java.go Normal file
View File

@ -0,0 +1,120 @@
package main
import (
"archive/tar"
"bytes"
"compress/gzip"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/zhyee/zipstream"
)
type JavaManager struct {
app *App
}
func (JavaManager)CheckJavaVer(version int) bool{
suffix := "lin"
if runtime.GOOS == "windows" {
suffix = "win"
}
path, _ := os.UserConfigDir()
path = filepath.Join(path, "FCLauncher", "java", fmt.Sprintf("java-%d-%s", version, suffix))
_, err := os.Stat(path)
if err == nil {
return true
}
return false
}
func (j *JavaManager)InstallJavaVer(version int) {
suffix := "lin.tar.gz"
if runtime.GOOS == "windows" {
suffix = "win.zip"
}
buff := new(bytes.Buffer)
HttpDownload("java/"+fmt.Sprintf("java-%d-%s", version, suffix), buff, j.app.Ctx)
path, _ := os.UserConfigDir()
suffix = "lin"
if runtime.GOOS == "windows" {
suffix = "win"
}
path = filepath.Join(path, "FCLauncher", "java", fmt.Sprintf("java-%d-%s", version, suffix))
os.MkdirAll(path, 0755)
if runtime.GOOS == "windows" {
zr := zipstream.NewReader(buff)
for {
entry, err := zr.GetNextEntry()
if err == io.EOF {
break
}
if err != nil {
return
}
target := filepath.Join(path, strings.SplitN(entry.Name, "/", 2)[1])
if !entry.IsDir() {
rc, err := entry.Open()
if err != nil {
return
}
f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, entry.FileInfo().Mode())
if err != nil {
return
}
if _, err := io.Copy(f, rc); err != nil {
return
}
f.Close()
rc.Close()
} else {
if _, err := os.Stat(target); err != nil {
if err := os.MkdirAll(target, 0755); err != nil {
return
}
}
}
}
} else {
gzip, _ := gzip.NewReader(buff)
defer gzip.Close()
tr := tar.NewReader(gzip)
out:
for {
header, err := tr.Next()
switch {
case err == io.EOF:
break out
case err != nil:
return
case header == nil:
continue
}
target := filepath.Join(path, strings.SplitN(header.Name, string(os.PathSeparator), 2)[1])
switch header.Typeflag {
case tar.TypeDir:
if _, err := os.Stat(target); err != nil {
if err := os.MkdirAll(target, 0755); err != nil {
return
}
}
case tar.TypeReg:
f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
if err != nil {
return
}
if _, err := io.Copy(f, tr); err != nil {
return
}
f.Close()
}
}
}
}

63
fclauncher/Modpack.go Normal file
View File

@ -0,0 +1,63 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"time"
)
type Modpack struct {
Name string
Id string
Last_updated string
Versions []Version
}
type Version struct {
Version string
Data time.Time
File string
}
type ModpackManager struct {
app *App
Modpacks []Modpack
}
func (m *ModpackManager) QuerryModpacks() {
m.Modpacks = []Modpack{}
buff := new(bytes.Buffer)
err := HttpDownload("modpacks.json", buff, nil)
if err != nil {
fmt.Printf("HTTP error: %s\n", err)
return
}
err = json.Unmarshal(buff.Bytes(), &m.Modpacks)
if err != nil {
return
}
for ind, pack := range m.Modpacks {
buff = new(bytes.Buffer)
err = HttpDownload(pack.Id+"/versions.json", buff, nil)
if err != nil {
continue
}
json.Unmarshal(buff.Bytes(), &pack.Versions)
m.Modpacks[ind] = pack
}
}
func (m *ModpackManager) GetModpacks() []Modpack {
return m.Modpacks
}
func (m *ModpackManager) GetModpack(id string) Modpack {
for _, pack := range m.Modpacks {
if pack.Id == id {
return pack
}
}
return Modpack{}
}

170
fclauncher/Prism.go Normal file
View File

@ -0,0 +1,170 @@
package main
import (
"archive/tar"
"bufio"
"bytes"
"compress/gzip"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/zhyee/zipstream"
)
type Prism struct {
app *App
}
func (Prism) CheckInstalled() bool {
path, _ := os.UserConfigDir()
_, err := os.Stat(filepath.Join(path, "FCLauncher", "prism"))
if err == nil {
return true
} else {
return false
}
}
func (p *Prism) Install() {
suffix := "lin.tar.gz"
shortSuffix := "lin"
if runtime.GOOS == "windows" {
suffix = "win.zip"
shortSuffix = "win"
}
buff := new(bytes.Buffer)
HttpDownload("prism/prism-"+suffix, buff, p.app.Ctx)
path, _ := os.UserConfigDir()
os.MkdirAll(filepath.Join(path, "FCLauncher", "prism"), 0755)
if runtime.GOOS == "windows" {
zr := zipstream.NewReader(buff)
for {
entry, err := zr.GetNextEntry()
if err == io.EOF {
break
}
if err != nil {
return
}
target := filepath.Join(path, "FCLauncher", "prism", entry.Name)
if !entry.IsDir() {
rc, err := entry.Open()
if err != nil {
return
}
f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, entry.FileInfo().Mode())
if err != nil {
return
}
if _, err := io.Copy(f, rc); err != nil {
return
}
f.Close()
rc.Close()
} else {
if _, err := os.Stat(target); err != nil {
if err := os.MkdirAll(target, 0755); err != nil {
return
}
}
}
}
} else {
gzip, _ := gzip.NewReader(buff)
defer gzip.Close()
tr := tar.NewReader(gzip)
out:
for {
header, err := tr.Next()
switch {
case err == io.EOF:
break out
case err != nil:
return
case header == nil:
continue
}
target := filepath.Join(path, "FCLauncher", "prism", header.Name)
switch header.Typeflag {
case tar.TypeDir:
if _, err := os.Stat(target); err != nil {
if err := os.MkdirAll(target, 0755); err != nil {
return
}
}
case tar.TypeReg:
f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
if err != nil {
return
}
if _, err := io.Copy(f, tr); err != nil {
return
}
f.Close()
}
}
}
dir, _ := os.UserConfigDir()
buff = new(bytes.Buffer)
HttpDownload("prism/prismlauncher.cfg", buff, nil)
scanner := bufio.NewScanner(buff)
f, _ := os.OpenFile(filepath.Join(dir, "FCLauncher", "prism", "prismlauncher.cfg"), os.O_CREATE|os.O_RDWR, 0755)
defer f.Close()
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "JavaPath") {
line = fmt.Sprintf("JavaPath=%s/FCLauncher/java/java-21-%s", strings.ReplaceAll(dir, "\\", "/"), shortSuffix)
}
if strings.HasPrefix(line, "LastHostname") {
host, _ := os.Hostname()
line = fmt.Sprintf("LastHostname=%s", host)
}
f.WriteString(line + "\n")
}
}
func (Prism)GetInstanceDir() string{
dir, _ := os.UserConfigDir()
return filepath.Join(dir, "FCLauncher", "prism", "instances")
}
func (Prism)getExecutableName() string {
execName := "PrismLauncher"
if runtime.GOOS == "windows" {
execName = "prismlauncher.exe"
}
return execName
}
func (p *Prism)ImportModpack(path string) {
dir, _ := os.UserConfigDir()
child := exec.Command(filepath.Join(dir, "FCLauncher", "prism", p.getExecutableName()), "-I", path)
child.Start()
tokens := strings.Split(path, string(os.PathSeparator))
versionPath := filepath.Join(dir, "FCLauncher", "prism", "instances",strings.Split(tokens[len(tokens)-1], ".")[0], ".minecraft", "version.txt")
for {
time.Sleep(time.Second * 3)
if _, err := os.Stat(versionPath); err == nil {
break
}
}
child.Process.Kill()
}
func (p *Prism)LaunchInstance(instance Instance) {
p.app.Status(fmt.Sprintf("Launching %s", instance.InstanceName))
dir, _ := os.UserConfigDir()
child := exec.Command(filepath.Join(dir, "FCLauncher", "prism", p.getExecutableName()), "-l", instance.InstanceName)
child.Start()
}

16
fclauncher/README.md Normal file
View File

@ -0,0 +1,16 @@
# README
## About
This is the official Wails Svelte-TS template.
## Live Development
To run in live development mode, run `wails dev` in the project directory. This will run a Vite development
server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser
and have access to your Go methods, there is also a dev server that runs on http://localhost:34115. Connect
to this in your browser, and you can call your Go code from devtools.
## Building
To build a redistributable, production mode package, use `wails build`.

158
fclauncher/app.go Normal file
View File

@ -0,0 +1,158 @@
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"os"
"os/exec"
"path/filepath"
"runtime"
"github.com/inconshreveable/go-update"
wruntime "github.com/wailsapp/wails/v2/pkg/runtime"
)
const client_id string = "9305aeb8-5ecb-4e7a-b28f-c33aefcfbd8d"
const client_version string = "0.0.7"
type LauncherMetadata struct {
Schema_Version string
Version string
Desc string
Downloads map[string]string
}
// App struct
type App struct {
Ctx context.Context
PrismLauncher Prism
Java JavaManager
Instance InstanceManager
Modpacks ModpackManager
Auth authenticationResp
}
// NewApp creates a new App application struct
func NewApp() *App {
a := &App{}
a.Java = JavaManager{app: a}
a.Instance = InstanceManager{app: a}
a.Modpacks = ModpackManager{app: a}
return a
}
// startup is called when the app starts. The context is saved
// so we can call the runtime methods
func (a *App) startup(ctx context.Context) {
a.Ctx = ctx
}
// Greet returns a greeting for the given name
func openbrowser(url string) {
var err error
switch runtime.GOOS {
case "linux":
err = exec.Command("xdg-open", url).Start()
case "windows":
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
case "darwin":
err = exec.Command("open", url).Start()
default:
err = fmt.Errorf("unsupported platform")
}
if err != nil {
log.Fatal(err)
}
}
func (a *App) CheckPrerequisites() {
buff := new(bytes.Buffer)
err := HttpDownload("launcher.json", buff, nil)
fmt.Printf("Starting\n")
if err == nil {
data, _ := io.ReadAll(buff)
meta := LauncherMetadata{}
json.Unmarshal(data, &meta)
if client_version != meta.Version {
//Update available!
val, _ := wruntime.MessageDialog(a.Ctx, wruntime.MessageDialogOptions{Type: wruntime.QuestionDialog, Title: "Update!", Message: fmt.Sprintf("There is an update available:\n\n%s -> %s\n\nUpdate Description:\n\n%s\n\nWould you like to update?\n", client_version, meta.Version, meta.Desc)})
if val == "Yes" {
//run the update
fmt.Printf("Updating\n")
buff := new(bytes.Buffer)
HttpDownload(meta.Downloads[runtime.GOOS], buff, nil)
executable, _ := os.Executable()
err := update.Apply(buff, update.Options{})
if err != nil {
fmt.Printf("Error!")
}
child := exec.Command(executable)
err = child.Start()
if err != nil {
fmt.Printf("Unable to launch: %s\n", err)
}
wruntime.Quit(a.Ctx)
}
}
}
a.Status("Querrying Existing Instances")
a.Instance.SearchInstances()
a.Status("Pulling Modpacks")
a.Modpacks.QuerryModpacks()
a.Status("Logging in with Microsoft")
dir, _ := os.UserConfigDir()
authenticated := false
if _, err := os.Stat(filepath.Join(dir, "FCLauncher", "authentication.json")); err == nil {
f, _ := os.OpenFile(filepath.Join(dir, "FCLauncher", "authentication.json"), os.O_RDONLY, 0755)
defer f.Close()
data, _ := io.ReadAll(f)
json.Unmarshal(data, &a.Auth)
a.Auth, err = TokenRefresh(*a, a.Auth)
if err == nil {
authenticated = true
} else {
fmt.Printf("token reauth failed, requesting device code: %s\n", err)
}
}
if !authenticated {
var err error
//a.Auth, err = AuthCode(*a)
a.Auth, err = OAuth2(*a)
if err != nil {
fmt.Printf("Authentication Error: %s\n", err)
return
}
}
os.MkdirAll(filepath.Join(dir, "FCLauncher"), 0755)
f, _ := os.OpenFile(filepath.Join(dir, "FCLauncher", "authentication.json"), os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0755)
defer f.Close()
data, _ := json.Marshal(a.Auth)
f.Write(data)
}
func (App) GetVersions() ([]string, error) {
manifest, err := GetVersionManifest()
if err != nil {
fmt.Printf("Manifest Error: %s\n", err)
return []string{}, err
}
versions := []string{}
for _, version := range manifest.Versions {
versions = append(versions, version.Id)
}
return versions, nil
}
func (a *App) Status(status string) {
fmt.Printf("LOG: %s\n", status)
wruntime.EventsEmit(a.Ctx, "status", status)
}

268
fclauncher/auth.go Normal file
View File

@ -0,0 +1,268 @@
package main
import (
"bytes"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"
wruntime "github.com/wailsapp/wails/v2/pkg/runtime"
)
type LauncherAuth struct {
Id string
Name string
Token string
}
type McProfile struct {
Id string
Name string
}
type devcodeResp struct {
User_code string
Device_code string
Verification_uri string
Expires_in string
Interval int
Message string
}
type authenticationResp struct {
Access_token string
Token_type string
Refresh_token string
Expires_in string
Error string
Error_description string
}
type xboxAuthProperties struct {
AuthMethod string
SiteName string
RpsTicket string
}
type xboxAuthRequest struct {
Properties xboxAuthProperties
RelyingParty string
TokenType string
}
type xboxDisplayClaim struct {
Uhs string
Gtg string
Xid string
Agg string
Usr string
Utr string
Prv string
}
type xboxDisplayClaims struct {
Xui []xboxDisplayClaim
}
type xboxAuthResponse struct {
IssueInstant time.Time
NotAfter time.Time
Token string
DisplayClaims xboxDisplayClaims
}
type XSTSProperties struct {
SandboxId string
UserTokens []string
}
type XSTSRequest struct {
Properties XSTSProperties
RelyingParty string
TokenType string
}
type McAuthRequest struct {
Xtoken string `json:"xtoken"`
Platform string `json:"platform"`
}
type McAuthResponse struct {
Username string
Access_token string
Expires_in string
Token_type string
}
func getHTTPRedirect(w http.ResponseWriter, r *http.Request, srv *http.Server, code *string) {
r.ParseForm()
fmt.Printf("Response Code: %s\n", r.Form.Get("code"))
if r.Form.Get("code") != "" {
*code = r.Form.Get("code")
io.WriteString(w, "You can now close this window and return to the application.")
} else {
srv.Shutdown(r.Context())
}
}
func AuthCode(a App) (authenticationResp, error) {
authentication := authenticationResp{}
resp, err := http.PostForm("https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode", url.Values{
"client_id": {client_id},
"scope": {"XboxLive.SignIn XboxLive.offline_access"},
})
if err != nil {
return authentication, fmt.Errorf("Unable to request device code: %e\n", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return authentication, fmt.Errorf("Unable to request device code: %s\n", resp.Status)
}
data, _ := io.ReadAll(resp.Body)
codeResp := devcodeResp{}
json.Unmarshal(data, &codeResp)
//display message
fmt.Printf("resp: %s\n", data)
openbrowser(codeResp.Verification_uri)
wruntime.MessageDialog(a.Ctx, wruntime.MessageDialogOptions{Type: wruntime.InfoDialog, Title: "Authentication", Message: codeResp.Message + ". The code has been automatically coppied to the clipboard."})
wruntime.ClipboardSetText(a.Ctx, codeResp.Device_code)
ticker := time.NewTicker(time.Second * time.Duration(codeResp.Interval))
for range ticker.C {
resp, err := http.PostForm("https://login.microsoftonline.com/consumers/oauth2/v2.0/token", url.Values{
"client_id": {client_id},
"grant_type": {"urn:ietf:params:oauth:grant-type:device_code"},
"device_code": {codeResp.Device_code},
})
if err != nil {
return authentication, fmt.Errorf("Authentication request error %e\n", err)
}
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
json.Unmarshal(data, &authentication)
if authentication.Error == "" {
return authentication, nil
}
}
return authentication, fmt.Errorf("Unknown error")
}
func OAuth2(a App) (authenticationResp, error) {
code := "code"
srv := http.Server{Addr: ":5000"}
authentication := authenticationResp{}
verifier := make([]byte, 128)
rand.Read(verifier)
verifier_string := base64.RawURLEncoding.EncodeToString(verifier)
challenge := sha256.Sum256([]byte(verifier_string))
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { getHTTPRedirect(w, r, &srv, &code) })
openbrowser("https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?client_id=" + client_id + "&response_type=code&redirect_uri=http%3A%2F%2F127.0.0.1%3A5000&response_mode=query&scope=XboxLive.signin&state=12345&code_challenge=" + base64.RawURLEncoding.EncodeToString(challenge[:]) + "&code_challenge_method=S256&prompt=login")
srv.ListenAndServe()
fmt.Printf("continuing auth\n")
resp, err := http.PostForm("https://login.microsoftonline.com/consumers/oauth2/v2.0/token", url.Values{
"grant_type": {"authorization_code"},
"code": {code},
"redirect_uri": {"http://127.0.0.1:5000"},
"code_verifier": {verifier_string},
"client_id": {client_id},
})
if err != nil {
return authenticationResp{}, fmt.Errorf("unable to request token: %e", err)
}
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
json.Unmarshal(data, &authentication)
//fmt.Printf("auth data: %s\n", data)
return authentication, nil
}
func TokenRefresh(app App, auth authenticationResp) (authenticationResp, error) {
resp, err := http.PostForm("https://login.microsoftonline.com/consumers/oauth2/v2.0/token", url.Values{
"client_id": {client_id},
"grant_type": {"refresh_token"},
"refresh_token": {auth.Refresh_token},
"scope": {"XboxLive.SignIn XboxLive.offline_access"},
})
if err != nil {
return authenticationResp{}, fmt.Errorf("unable to refresh token: %e\n", err)
}
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
authResp := authenticationResp{}
json.Unmarshal(data, &authResp)
if authResp.Error != "" {
return authResp, fmt.Errorf("unable to request new token: %s", authResp.Error_description)
}
return authResp, nil
}
func MicrosoftAuth(auth authenticationResp) (LauncherAuth, error) {
//Xbox Live Auth
req, _ := json.Marshal(xboxAuthRequest{Properties: xboxAuthProperties{AuthMethod: "RPS", SiteName: "user.auth.xboxlive.com", RpsTicket: "d=" + auth.Access_token}, RelyingParty: "http://auth.xboxlive.com", TokenType: "JWT"})
client := http.Client{}
httpreq, _ := http.NewRequest("POST", "https://user.auth.xboxlive.com/user/authenticate", bytes.NewBuffer(req))
httpreq.Header.Add("x-xbl-contract-version", "1")
httpreq.Header.Add("Content-Type", "application/json")
httpreq.Header.Add("Accept", "application/json")
httpResp, err := client.Do(httpreq)
if err != nil {
return LauncherAuth{}, fmt.Errorf("unable to obtain xbox live token: %e\n", err)
}
defer httpResp.Body.Close()
if httpResp.StatusCode != 200 {
return LauncherAuth{}, fmt.Errorf("unable to obtain xbox live token: %s\n", httpResp.Status)
}
d, _ := io.ReadAll(httpResp.Body)
xblAuth := xboxAuthResponse{}
json.Unmarshal(d, &xblAuth)
xstsData, _ := json.Marshal(XSTSRequest{Properties: XSTSProperties{SandboxId: "RETAIL", UserTokens: []string{xblAuth.Token}}, RelyingParty: "rp://api.minecraftservices.com/", TokenType: "JWT"})
httpXstsReq, _ := http.NewRequest("POST", "https://xsts.auth.xboxlive.com/xsts/authorize", bytes.NewBuffer(xstsData))
httpXstsReq.Header.Add("Content-Type", "application/json")
httpResp, err = client.Do(httpXstsReq)
if err != nil {
return LauncherAuth{}, fmt.Errorf("unable to obtain minecraft sts token: %e\n", err)
}
defer httpResp.Body.Close()
if httpResp.StatusCode != 200 {
return LauncherAuth{}, fmt.Errorf("unable to obtain minecraft sts token: %s\n", httpResp.Status)
}
d, _ = io.ReadAll(httpResp.Body)
mcApi := xboxAuthResponse{}
json.Unmarshal(d, &mcApi)
mcAuthData, _ := json.Marshal(McAuthRequest{Xtoken: "XBL 3.0 x=" + mcApi.DisplayClaims.Xui[0].Uhs + ";" + mcApi.Token, Platform: "PC_LAUNCHER"})
httpReqMC, _ := http.NewRequest("POST", "https://api.minecraftservices.com/launcher/login", bytes.NewBuffer(mcAuthData))
httpReqMC.Header.Add("Content-Type", "application/json")
httpReqMC.Header.Add("Accept", "application/json")
resp, err := client.Do(httpReqMC)
if err != nil {
return LauncherAuth{}, fmt.Errorf("unable to obtain mojang auth token: %e\n", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return LauncherAuth{}, fmt.Errorf("unable to obtain mojang auth token: %s\n", resp.Status)
}
d, _ = io.ReadAll(resp.Body)
mcAuth := McAuthResponse{}
json.Unmarshal(d, &mcAuth)
httpreq, err = http.NewRequest("GET", "https://api.minecraftservices.com/minecraft/profile", new(bytes.Buffer))
httpreq.Header.Add("Content-Type", "application/json")
httpreq.Header.Add("Accept", "application/json")
httpreq.Header.Add("Authorization", "Bearer "+mcAuth.Access_token)
resp, _ = client.Do(httpreq)
if err != nil {
return LauncherAuth{}, fmt.Errorf("unable to get profile data: %e\n", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return LauncherAuth{}, fmt.Errorf("unable to get profile data: %s\n", resp.Status)
}
data, _ := io.ReadAll(resp.Body)
profile := McProfile{}
json.Unmarshal(data, &profile)
return LauncherAuth{Id: profile.Id, Name: profile.Name, Token: mcAuth.Access_token}, nil
}

View File

@ -0,0 +1,35 @@
# Build Directory
The build directory is used to house all the build files and assets for your application.
The structure is:
* bin - Output directory
* darwin - macOS specific files
* windows - Windows specific files
## Mac
The `darwin` directory holds files specific to Mac builds.
These may be customised and used as part of the build. To return these files to the default state, simply delete them
and
build with `wails build`.
The directory contains the following files:
- `Info.plist` - the main plist file used for Mac builds. It is used when building using `wails build`.
- `Info.dev.plist` - same as the main plist file but used when building using `wails dev`.
## Windows
The `windows` directory contains the manifest and rc files used when building with `wails build`.
These may be customised for your application. To return these files to the default state, simply delete them and
build with `wails build`.
- `icon.ico` - The icon used for the application. This is used when building using `wails build`. If you wish to
use a different icon, simply replace this file with your own. If it is missing, a new `icon.ico` file
will be created using the `appicon.png` file in the build directory.
- `installer/*` - The files used to create the Windows installer. These are used when building using `wails build`.
- `info.json` - Application details used for Windows builds. The data here will be used by the Windows installer,
as well as the application itself (right click the exe -> properties -> details)
- `wails.exe.manifest` - The main application manifest file.

View File

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 123 KiB

View File

@ -0,0 +1,68 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleName</key>
<string>{{.Info.ProductName}}</string>
<key>CFBundleExecutable</key>
<string>{{.Name}}</string>
<key>CFBundleIdentifier</key>
<string>com.wails.{{.Name}}</string>
<key>CFBundleVersion</key>
<string>{{.Info.ProductVersion}}</string>
<key>CFBundleGetInfoString</key>
<string>{{.Info.Comments}}</string>
<key>CFBundleShortVersionString</key>
<string>{{.Info.ProductVersion}}</string>
<key>CFBundleIconFile</key>
<string>iconfile</string>
<key>LSMinimumSystemVersion</key>
<string>10.13.0</string>
<key>NSHighResolutionCapable</key>
<string>true</string>
<key>NSHumanReadableCopyright</key>
<string>{{.Info.Copyright}}</string>
{{if .Info.FileAssociations}}
<key>CFBundleDocumentTypes</key>
<array>
{{range .Info.FileAssociations}}
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>{{.Ext}}</string>
</array>
<key>CFBundleTypeName</key>
<string>{{.Name}}</string>
<key>CFBundleTypeRole</key>
<string>{{.Role}}</string>
<key>CFBundleTypeIconFile</key>
<string>{{.IconName}}</string>
</dict>
{{end}}
</array>
{{end}}
{{if .Info.Protocols}}
<key>CFBundleURLTypes</key>
<array>
{{range .Info.Protocols}}
<dict>
<key>CFBundleURLName</key>
<string>com.wails.{{.Scheme}}</string>
<key>CFBundleURLSchemes</key>
<array>
<string>{{.Scheme}}</string>
</array>
<key>CFBundleTypeRole</key>
<string>{{.Role}}</string>
</dict>
{{end}}
</array>
{{end}}
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
</dict>
</plist>

View File

@ -0,0 +1,63 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleName</key>
<string>{{.Info.ProductName}}</string>
<key>CFBundleExecutable</key>
<string>{{.Name}}</string>
<key>CFBundleIdentifier</key>
<string>com.wails.{{.Name}}</string>
<key>CFBundleVersion</key>
<string>{{.Info.ProductVersion}}</string>
<key>CFBundleGetInfoString</key>
<string>{{.Info.Comments}}</string>
<key>CFBundleShortVersionString</key>
<string>{{.Info.ProductVersion}}</string>
<key>CFBundleIconFile</key>
<string>iconfile</string>
<key>LSMinimumSystemVersion</key>
<string>10.13.0</string>
<key>NSHighResolutionCapable</key>
<string>true</string>
<key>NSHumanReadableCopyright</key>
<string>{{.Info.Copyright}}</string>
{{if .Info.FileAssociations}}
<key>CFBundleDocumentTypes</key>
<array>
{{range .Info.FileAssociations}}
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>{{.Ext}}</string>
</array>
<key>CFBundleTypeName</key>
<string>{{.Name}}</string>
<key>CFBundleTypeRole</key>
<string>{{.Role}}</string>
<key>CFBundleTypeIconFile</key>
<string>{{.IconName}}</string>
</dict>
{{end}}
</array>
{{end}}
{{if .Info.Protocols}}
<key>CFBundleURLTypes</key>
<array>
{{range .Info.Protocols}}
<dict>
<key>CFBundleURLName</key>
<string>com.wails.{{.Scheme}}</string>
<key>CFBundleURLSchemes</key>
<array>
<string>{{.Scheme}}</string>
</array>
<key>CFBundleTypeRole</key>
<string>{{.Role}}</string>
</dict>
{{end}}
</array>
{{end}}
</dict>
</plist>

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@ -0,0 +1,15 @@
{
"fixed": {
"file_version": "{{.Info.ProductVersion}}"
},
"info": {
"0000": {
"ProductVersion": "{{.Info.ProductVersion}}",
"CompanyName": "{{.Info.CompanyName}}",
"FileDescription": "{{.Info.ProductName}}",
"LegalCopyright": "{{.Info.Copyright}}",
"ProductName": "{{.Info.ProductName}}",
"Comments": "{{.Info.Comments}}"
}
}
}

View File

@ -0,0 +1,115 @@
Unicode true
####
## Please note: Template replacements don't work in this file. They are provided with default defines like
## mentioned underneath.
## If the keyword is not defined, "wails_tools.nsh" will populate them with the values from ProjectInfo.
## If they are defined here, "wails_tools.nsh" will not touch them. This allows to use this project.nsi manually
## from outside of Wails for debugging and development of the installer.
##
## For development first make a wails nsis build to populate the "wails_tools.nsh":
## > wails build --target windows/amd64 --nsis
## Then you can call makensis on this file with specifying the path to your binary:
## For a AMD64 only installer:
## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe
## For a ARM64 only installer:
## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe
## For a installer with both architectures:
## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe
####
## The following information is taken from the ProjectInfo file, but they can be overwritten here.
####
## !define INFO_PROJECTNAME "MyProject" # Default "{{.Name}}"
## !define INFO_COMPANYNAME "MyCompany" # Default "{{.Info.CompanyName}}"
## !define INFO_PRODUCTNAME "MyProduct" # Default "{{.Info.ProductName}}"
## !define INFO_PRODUCTVERSION "1.0.0" # Default "{{.Info.ProductVersion}}"
## !define INFO_COPYRIGHT "Copyright" # Default "{{.Info.Copyright}}"
###
## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe"
## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
####
## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html
####
## Include the wails tools
####
!include "wails_tools.nsh"
# The version information for this two must consist of 4 parts
VIProductVersion "${INFO_PRODUCTVERSION}.0"
VIFileVersion "${INFO_PRODUCTVERSION}.0"
VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}"
VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer"
VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}"
VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}"
VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}"
VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}"
# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware
ManifestDPIAware true
!include "MUI.nsh"
!define MUI_ICON "..\icon.ico"
!define MUI_UNICON "..\icon.ico"
# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314
!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps
!define MUI_ABORTWARNING # This will warn the user if they exit from the installer.
!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page.
# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer
!insertmacro MUI_PAGE_DIRECTORY # In which folder install page.
!insertmacro MUI_PAGE_INSTFILES # Installing page.
!insertmacro MUI_PAGE_FINISH # Finished installation page.
!insertmacro MUI_UNPAGE_INSTFILES # Uinstalling page
!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer
## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1
#!uninstfinalize 'signtool --file "%1"'
#!finalize 'signtool --file "%1"'
Name "${INFO_PRODUCTNAME}"
OutFile "..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file.
InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder).
ShowInstDetails show # This will always show the installation details.
Function .onInit
!insertmacro wails.checkArchitecture
FunctionEnd
Section
!insertmacro wails.setShellContext
!insertmacro wails.webview2runtime
SetOutPath $INSTDIR
!insertmacro wails.files
CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
!insertmacro wails.associateFiles
!insertmacro wails.associateCustomProtocols
!insertmacro wails.writeUninstaller
AccessControl::GrantOnFile "$INSTDIR" "(BU)" "FullAccess"
SectionEnd
Section "uninstall"
!insertmacro wails.setShellContext
RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath
RMDir /r $INSTDIR
Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk"
Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk"
!insertmacro wails.unassociateFiles
!insertmacro wails.unassociateCustomProtocols
!insertmacro wails.deleteUninstaller
SectionEnd

View File

@ -0,0 +1,236 @@
# DO NOT EDIT - Generated automatically by `wails build`
!include "x64.nsh"
!include "WinVer.nsh"
!include "FileFunc.nsh"
!ifndef INFO_PROJECTNAME
!define INFO_PROJECTNAME "fclauncher"
!endif
!ifndef INFO_COMPANYNAME
!define INFO_COMPANYNAME "fclauncher"
!endif
!ifndef INFO_PRODUCTNAME
!define INFO_PRODUCTNAME "fclauncher"
!endif
!ifndef INFO_PRODUCTVERSION
!define INFO_PRODUCTVERSION "1.0.0"
!endif
!ifndef INFO_COPYRIGHT
!define INFO_COPYRIGHT "Copyright........."
!endif
!ifndef PRODUCT_EXECUTABLE
!define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
!endif
!ifndef UNINST_KEY_NAME
!define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
!endif
!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}"
!ifndef REQUEST_EXECUTION_LEVEL
!define REQUEST_EXECUTION_LEVEL "admin"
!endif
RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
!ifdef ARG_WAILS_AMD64_BINARY
!define SUPPORTS_AMD64
!endif
!ifdef ARG_WAILS_ARM64_BINARY
!define SUPPORTS_ARM64
!endif
!ifdef SUPPORTS_AMD64
!ifdef SUPPORTS_ARM64
!define ARCH "amd64_arm64"
!else
!define ARCH "amd64"
!endif
!else
!ifdef SUPPORTS_ARM64
!define ARCH "arm64"
!else
!error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY"
!endif
!endif
!macro wails.checkArchitecture
!ifndef WAILS_WIN10_REQUIRED
!define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later."
!endif
!ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED
!define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}"
!endif
${If} ${AtLeastWin10}
!ifdef SUPPORTS_AMD64
${if} ${IsNativeAMD64}
Goto ok
${EndIf}
!endif
!ifdef SUPPORTS_ARM64
${if} ${IsNativeARM64}
Goto ok
${EndIf}
!endif
IfSilent silentArch notSilentArch
silentArch:
SetErrorLevel 65
Abort
notSilentArch:
MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}"
Quit
${else}
IfSilent silentWin notSilentWin
silentWin:
SetErrorLevel 64
Abort
notSilentWin:
MessageBox MB_OK "${WAILS_WIN10_REQUIRED}"
Quit
${EndIf}
ok:
!macroend
!macro wails.files
!ifdef SUPPORTS_AMD64
${if} ${IsNativeAMD64}
File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}"
${EndIf}
!endif
!ifdef SUPPORTS_ARM64
${if} ${IsNativeARM64}
File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}"
${EndIf}
!endif
!macroend
!macro wails.writeUninstaller
WriteUninstaller "$INSTDIR\uninstall.exe"
SetRegView 64
WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}"
WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}"
WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}"
WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}"
WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
IntFmt $0 "0x%08X" $0
WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0"
!macroend
!macro wails.deleteUninstaller
Delete "$INSTDIR\uninstall.exe"
SetRegView 64
DeleteRegKey HKLM "${UNINST_KEY}"
!macroend
!macro wails.setShellContext
${If} ${REQUEST_EXECUTION_LEVEL} == "admin"
SetShellVarContext all
${else}
SetShellVarContext current
${EndIf}
!macroend
# Install webview2 by launching the bootstrapper
# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment
!macro wails.webview2runtime
!ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT
!define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime"
!endif
SetRegView 64
# If the admin key exists and is not empty then webview2 is already installed
ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
${If} $0 != ""
Goto ok
${EndIf}
${If} ${REQUEST_EXECUTION_LEVEL} == "user"
# If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed
ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
${If} $0 != ""
Goto ok
${EndIf}
${EndIf}
SetDetailsPrint both
DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}"
SetDetailsPrint listonly
InitPluginsDir
CreateDirectory "$pluginsdir\webview2bootstrapper"
SetOutPath "$pluginsdir\webview2bootstrapper"
File "tmp\MicrosoftEdgeWebview2Setup.exe"
ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install'
SetDetailsPrint both
ok:
!macroend
# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b
!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND
; Backup the previously associated file class
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" ""
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0"
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}"
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}`
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}`
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open"
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}`
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}`
!macroend
!macro APP_UNASSOCIATE EXT FILECLASS
; Backup the previously associated file class
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup`
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0"
DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}`
!macroend
!macro wails.associateFiles
; Create file associations
!macroend
!macro wails.unassociateFiles
; Delete app associations
!macroend
!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND
DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}"
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" ""
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}"
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" ""
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" ""
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}"
!macroend
!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL
DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
!macroend
!macro wails.associateCustomProtocols
; Create custom protocols associations
!macroend
!macro wails.unassociateCustomProtocols
; Delete app custom protocol associations
!macroend

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<assemblyIdentity type="win32" name="com.wails.{{.Name}}" version="{{.Info.ProductVersion}}.0" processorArchitecture="*"/>
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
</dependentAssembly>
</dependency>
<asmv3:application>
<asmv3:windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware> <!-- fallback for Windows 7 and 8 -->
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness> <!-- falls back to per-monitor if per-monitor v2 is not supported -->
</asmv3:windowsSettings>
</asmv3:application>
</assembly>

145
fclauncher/fabric.go Normal file
View File

@ -0,0 +1,145 @@
package main
import (
"bytes"
"crypto/sha1"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
wruntime "github.com/wailsapp/wails/v2/pkg/runtime"
)
type Fabric struct {
}
type FabricDefinition struct {
Separator string
Build int
Maven string
Version string
Stable bool
}
type FabricLibrary struct {
Name string
Url string
Sha1 string
}
type FabricLibraries struct {
Client []FabricLibrary
Common []FabricLibrary
Server []FabricLibrary
}
type FabricMeta struct {
Version int
Libraries FabricLibraries
MainClass map[string]string
}
type FabricVersion struct {
Loader FabricDefinition
Intermediary FabricDefinition
LauncherMeta FabricMeta
}
func (Fabric) GetFabricVersions(mcVersion string) ([]FabricVersion, error) {
resp, err := http.Get("https://meta.fabricmc.net/v2/versions/loader/" + mcVersion)
if err != nil {
return []FabricVersion{}, fmt.Errorf("Unable to pull fabric version manifest: %s\n", err)
}
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
versions := []FabricVersion{}
json.Unmarshal(data, &versions)
return versions, nil
}
func GetFabricMetadata(mcVersion string, fabricVersion string) (FabricVersion, error) {
versions, err := Fabric{}.GetFabricVersions(mcVersion)
if err != nil {
return FabricVersion{}, fmt.Errorf("unable to download versions manifest: %e\n", err)
}
for _, version := range versions {
if version.Loader.Version == fabricVersion {
return version, nil
}
}
return FabricVersion{}, fmt.Errorf("Unable to find requested version.\n")
}
func InstallLib(lib FabricLibrary, libDir string, a *App) {
a.Status(fmt.Sprintf("Checking %s\n", lib.Name))
path := filepath.Join(ProcessMavenPath(lib.Name), ProcessMavenFilename(lib.Name))
if _, err := os.Stat(filepath.Join(libDir, path)); err == nil {
f, _ := os.OpenFile(filepath.Join(libDir, path), os.O_RDONLY, 0755)
defer f.Close()
data, _ := io.ReadAll(f)
sha := sha1.Sum(data)
if hex.EncodeToString(sha[:20]) == lib.Sha1 {
return
}
}
a.Status(fmt.Sprintf("Downloading %s\n", lib.Name))
unixPath := strings.ReplaceAll(path, "\\", "/")
resp, err := http.Get(lib.Url + unixPath)
if err != nil {
return
}
defer resp.Body.Close()
buff := new(bytes.Buffer)
downloaded := 0
for {
count, err := io.CopyN(buff, resp.Body, BlockSize)
if err == io.EOF {
downloaded += int(count)
break
}
if err != nil {
fmt.Printf("Error Downloading libs: %e\n", err)
return
}
downloaded += int(count)
wruntime.EventsEmit(a.Ctx, "download", downloaded, resp.ContentLength)
}
os.MkdirAll(filepath.Join(libDir, ProcessMavenPath(lib.Name)), 0755)
f, _ := os.OpenFile(filepath.Join(libDir, path), os.O_CREATE|os.O_RDWR, 0755)
defer f.Close()
io.Copy(f, buff)
wruntime.EventsEmit(a.Ctx, "download_complete")
}
func InstallFabricLibs(mcVersion string, fabricVersion string, libDir string, a *App) {
metadata, _ := GetFabricMetadata(mcVersion, fabricVersion)
for _, lib := range metadata.LauncherMeta.Libraries.Client {
InstallLib(lib, libDir, a)
}
for _, lib := range metadata.LauncherMeta.Libraries.Common {
InstallLib(lib, libDir, a)
}
InstallLib(FabricLibrary{Name: metadata.Loader.Maven, Sha1: "", Url: "https://maven.fabricmc.net/"}, libDir, a)
InstallLib(FabricLibrary{Name: metadata.Intermediary.Maven, Sha1: "", Url: "https://maven.fabricmc.net/"}, libDir, a)
}
func ProcessMavenPath(maven string) string {
tokens := strings.Split(maven, ":")
path := filepath.Join(strings.Split(tokens[0], ".")...)
pack := tokens[1]
version := tokens[2]
path = filepath.Join(path, pack, version)
return path
}
func ProcessMavenFilename(maven string) string {
tokens := strings.Split(maven, ":")
pack := tokens[1]
version := tokens[2]
return pack + "-" + version + ".jar"
}

185
fclauncher/forge.go Normal file
View File

@ -0,0 +1,185 @@
package main
import (
"crypto/sha1"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/zhyee/zipstream"
)
type Forge struct{}
type ForgeVersion struct {
Version string
Time string
Url string
}
type ForgeLibraryArtifact struct {
Path string
Url string
Sha1 string
}
type ForgeLibraryDownload struct {
Artifact ForgeLibraryArtifact
}
type ForgeLibrary struct {
Name string
Downloads ForgeLibraryDownload
}
type ForgeInstallData struct {
Id string
Time time.Time
ReleaseTime time.Time
InheritsFrom string
Type string
MainClass string
Libraries []ForgeLibrary
}
func parseForgeVersions(html string) []ForgeVersion {
lines := strings.Split(html, "\n")
parsing := false
foundTR := false
buff := ""
versions := []ForgeVersion{}
for _, line := range lines {
if strings.Contains(line, "<tbody>") {
parsing = true
} else if strings.Contains(line, "</tbody>") {
parsing = false
} else if parsing {
if strings.Contains(line, "<tr>") {
buff = ""
foundTR = true
} else if strings.Contains(line, "</tr>") {
foundTR = false
versions = append(versions, parseForgeVersion(buff))
} else if foundTR {
buff += line + "\n"
}
}
}
return versions
}
func parseForgeVersion(html string) ForgeVersion {
lines := strings.Split(html, "\n")
version := ForgeVersion{}
for ind, line := range lines {
if strings.Contains(line, "<td class=\"download-version\">") {
version.Version = strings.TrimSpace(lines[ind+1])
} else if strings.Contains(line, "<td class=\"download-time\"") {
version.Time = strings.Split(strings.Split(line, "<td class=\"download-time\" title=\"")[1], "\">")[0]
} else if strings.Contains(line, "https://adfoc.us") && strings.Contains(line, "installer.jar") {
version.Url = strings.Split(strings.Split(line, "&url=")[1], "\">")[0]
}
}
return version
}
func (Forge) GetForgeVersions(mcVersion string) ([]ForgeVersion, error) {
resp, err := http.Get(fmt.Sprintf("https://files.minecraftforge.net/net/minecraftforge/forge/index_%s.html", mcVersion))
if err != nil {
return []ForgeVersion{}, fmt.Errorf("unable to access minecraft forge index: %e", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return []ForgeVersion{}, fmt.Errorf("unable to access minecraft forge index: %s", err)
}
data, _ := io.ReadAll(resp.Body)
return parseForgeVersions(string(data)), nil
}
func GetForgeInstallDataFromVersion(version ForgeVersion) (ForgeInstallData, error) {
resp, err := http.Get(version.Url)
if err != nil {
return ForgeInstallData{}, fmt.Errorf("unable to pull jar file: %e", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return ForgeInstallData{}, fmt.Errorf("unable to pull jar file: %s", resp.Status)
}
zs := zipstream.NewReader(resp.Body)
for {
entry, err := zs.GetNextEntry()
if err == io.EOF {
break
}
if entry.Name != "version.json" {
continue
}
f, _ := entry.Open()
defer f.Close()
data, _ := io.ReadAll(f)
installData := ForgeInstallData{}
json.Unmarshal(data, &installData)
return installData, nil
}
return ForgeInstallData{}, fmt.Errorf("unable to find version.json")
}
func GetForgeInstallData(mcVersion string, forgeVersion string) (ForgeInstallData, error) {
versions, err := Forge{}.GetForgeVersions(mcVersion)
if err != nil {
return ForgeInstallData{}, fmt.Errorf("failed to pull forge versions: %e", err)
}
for _, version := range versions {
if version.Version == forgeVersion {
return GetForgeInstallDataFromVersion(version)
}
}
return ForgeInstallData{}, fmt.Errorf("unable to find the requested version")
}
func InstallForgeLibs(mcVersion string, forgeVersion string, libDir string) {
installData, err := GetForgeInstallData(mcVersion, forgeVersion)
if err != nil {
fmt.Printf("Unable to get install data: %s\n", err)
}
for _, lib := range installData.Libraries {
if _, err := os.Stat(filepath.Join(libDir, lib.Downloads.Artifact.Path)); err == nil {
if f, err := os.OpenFile(filepath.Join(libDir, lib.Downloads.Artifact.Path), os.O_RDONLY, 0755); err == nil {
defer f.Close()
data, _ := io.ReadAll(f)
sha := sha1.Sum(data)
if hex.EncodeToString(sha[:20]) == lib.Downloads.Artifact.Sha1 {
continue
}
}
}
resp, err := http.Get(lib.Downloads.Artifact.Url)
if err != nil {
fmt.Printf("Unable to download library %s: %s\n", lib.Name, err)
continue
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
fmt.Printf("Unable to download library %s: %s\n", lib.Name, resp.Status)
continue
}
tokens := strings.Split(lib.Downloads.Artifact.Path, "/")
path := ""
for ind, token := range tokens {
if ind == len(tokens)-1 {
break
}
path = filepath.Join(path, token)
}
os.MkdirAll(filepath.Join(libDir, path), 0755)
f, _ := os.OpenFile(filepath.Join(libDir, lib.Downloads.Artifact.Path), os.O_CREATE|os.O_RDWR, 0755)
defer f.Close()
io.Copy(f, resp.Body)
}
}

View File

@ -0,0 +1,5 @@
{
"recommendations": [
"svelte.svelte-vscode"
]
}

View File

@ -0,0 +1,65 @@
# Svelte + TS + Vite
This template should help get you started developing with Svelte and TypeScript in Vite.
## Recommended IDE Setup
[VS Code](https://code.visualstudio.com/)
+ [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode).
## Need an official Svelte framework?
Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its
serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less,
and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more.
## Technical considerations
**Why use this over SvelteKit?**
- It brings its own routing solution which might not be preferable for some users.
- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app.
`vite dev` and `vite build` wouldn't work in a SvelteKit environment, for example.
This template contains as little as possible to get started with Vite + TypeScript + Svelte, while taking into account
the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the
other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte
project.
Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been
structured similarly to SvelteKit so that it is easy to migrate.
**Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?**
Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash
references keeps the default TypeScript setting of accepting type information from the entire workspace, while also
adding `svelte` and `vite/client` type information.
**Why include `.vscode/extensions.json`?**
Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to
install the recommended extension upon opening the project.
**Why enable `allowJs` in the TS template?**
While `allowJs: false` would indeed prevent the use of `.js` files in the project, it does not prevent the use of
JavaScript syntax in `.svelte` files. In addition, it would force `checkJs: false`, bringing the worst of both worlds:
not being able to guarantee the entire codebase is TypeScript, and also having worse typechecking for the existing
JavaScript. In addition, there are valid use cases in which a mixed codebase may be relevant.
**Why is HMR not preserving my local component state?**
HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr`
and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the
details [here](https://github.com/rixo/svelte-hmr#svelte-hmr).
If you have state that's important to retain within a component, consider creating an external store which would not be
replaced by HMR.
```ts
// store.ts
// An extremely simple external store
import { writable } from 'svelte/store'
export default writable(0)
```

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>fclauncher</title>
</head>
<body>
<div id="app"></div>
<script src="./src/main.ts" type="module"></script>
</body>
</html>

1613
fclauncher/frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,22 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.json"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^1.0.1",
"@tsconfig/svelte": "^3.0.0",
"svelte": "^3.49.0",
"svelte-check": "^2.8.0",
"svelte-preprocess": "^4.10.7",
"tslib": "^2.4.0",
"typescript": "^4.6.4",
"vite": "^3.0.7"
}
}

View File

@ -0,0 +1 @@
48cb20b8d107dab0a7876a449352234a

View File

@ -0,0 +1,51 @@
<script lang="ts">
import { AdminAuth } from '../wailsjs/go/main/Admin.js'
import { adminLogin } from './global';
var username = "", password = ""
var loginFail = false
function login() {
AdminAuth(username, password).then((result) => {
if(result) {
$adminLogin = true
} else {
loginFail = true
}
})
}
</script>
<main>
<div class="container">
<input type="text" placeholder="Username" bind:value={username} />
<input type="password" placeholder="Password" bind:value={password} />
</div>
{#if loginFail}
<p>Login Failed!</p>
{/if}
<button on:click={login}>Login</button>
</main>
<style>
* {
box-sizing: border-box;
font-family: sans-serif;
--text-primary: #b6b6b6;
--text-secondary: #ececec;
--bg-primary: #23232e;
--bg-secondary: #141418;
}
p {
color: red;
font-weight: bold;
}
.container{
display: flex;
justify-content: center;
margin: 3rem;
}
</style>

View File

@ -0,0 +1,33 @@
<script lang="ts">
import AdminSettings from "./AdminSettings.svelte";
import AdminLogin from "./AdminLogin.svelte";
import { adminLogin } from "./global";
</script>
<main>
<h1>Admin</h1>
<div class="container">
{#if $adminLogin}
<AdminSettings />
{:else}
<AdminLogin />
{/if}
</div>
</main>
<style>
* {
box-sizing: border-box;
font-family: sans-serif;
--text-primary: #b6b6b6;
--text-secondary: #ececec;
--bg-primary: #23232e;
--bg-secondary: #141418;
}
.container{
display: flex;
justify-content: center;
margin: 3rem;
}
</style>

Some files were not shown because too many files have changed in this diff Show More