modified: src/launcher.rs

modified:   src/main.rs
	modified:   src/minecraft.rs
	modified:   src/util.rs
	modified:   src/www/portable.html
	deleted:    src/www/profile_customization.html
This commit is contained in:
Michael Wain 2025-03-23 18:16:01 +03:00
parent e90b025568
commit eebef351ad
6 changed files with 241 additions and 158 deletions

View File

@ -152,6 +152,36 @@ impl Launcher {
}
}
pub async fn login_user_server(&mut self, server: String, username: String, password: String) -> (bool, &str) {
let mut session_server_port: u16 = 8999;
let mut server_port: u16 = 25565;
let mut domain = server.clone();
if let Some(index) = server.find("#") {
let (a,b) = server.split_at(index+1);
session_server_port = b.parse().unwrap();
domain = a[..a.len()-1].to_string();
}
if let Some(index) = domain.find(":") {
let dmc = domain.clone();
let (a,b) = dmc.split_at(index+1);
domain = a[..a.len()-1].to_string();
server_port = b.parse().unwrap();
}
println!("Server information: {}:{} session={}", domain, server_port, session_server_port);
match minecraft::session::try_login(domain.clone(), session_server_port, username.clone(), password.clone(), self.config.allow_http).await {
Ok(status) => match status {
SignUpResponse::ServerError => (false, "Internal server error"),
SignUpResponse::BadCredentials => (false, "Username or password is not valid"),
SignUpResponse::UserAlreadyExists => (false, "User already exists"),
SignUpResponse::Registered(uuid) => self.save_server_info(uuid, username, password, domain, session_server_port, server_port)
}
Err(_e) => (false, "Internal server error")
}
}
pub async fn get_servers_list(&self) -> Vec<(String, String, Option<String>)> {
let mut v = Vec::new();
let servers = self.config.servers();

View File

@ -159,16 +159,41 @@ async fn main() {
responder.respond(Response::new(serde_json::to_vec(&UIMessage { params: v }).unwrap()));
}
"get_skin" => {
let params = params.unwrap().params;
let nickname = params[0].clone();
let domain = params[1].clone();
if let Some(server) = launcher.find_credentials(&nickname, &domain) {
let resp = util::get_image(&["http", if launcher.config.allow_http { "" } else { "s" }, "://", &domain, ":", &server.session_server_port.to_string(), "/api/skin/s", &server.credentials.uuid].concat()).await;
if let Ok(resp) = resp {
responder.respond(Response::new(serde_json::to_vec(&UIMessage { params: vec!["get_skin".to_string(), resp] }).unwrap()));
}
}
}
"get_cape" => {
let params = params.unwrap().params;
let nickname = params[0].clone();
let domain = params[1].clone();
if let Some(server) = launcher.find_credentials(&nickname, &domain) {
let resp = util::get_image(&["http", if launcher.config.allow_http { "" } else { "s" }, "://", &domain, ":", &server.session_server_port.to_string(), "/api/cape/a", &server.credentials.uuid].concat()).await;
if let Ok(resp) = resp {
responder.respond(Response::new(serde_json::to_vec(&UIMessage { params: vec!["get_cape".to_string(), resp] }).unwrap()));
}
}
}
"upload_skin" => {
let params = params.unwrap().params;
if let Some(server) = launcher.find_credentials(&params[0], &params[1]) {
if let Some(skin_path) = FileDialog::new().add_filter("Images", &["png"]).pick_file() {
let msg = launcher.upload_skin(skin_path, &server.credentials.uuid, &server.credentials.password, &[if launcher.config.allow_http {"http"} else {"https"}, "://", &server.domain, ":", &server.session_server_port.to_string(), "/api/upload"].concat()).await;
if let Ok(msg) = msg {
responder.respond(Response::new(serde_json::to_vec(&UIMessage { params: vec!["add_server_response".to_string(), String::new(), msg] }).unwrap()));
} else {
responder.respond(Response::new(serde_json::to_vec(&UIMessage { params: vec!["add_server_response".to_string(), String::new(), "Error uploading new skin".to_string()] }).unwrap()));
match msg {
Ok(msg ) => {
responder.respond(Response::new(serde_json::to_vec(&UIMessage { params: vec!["add_server_response".to_string(), String::new(), msg] }).unwrap()));
},
Err(_e) => {
responder.respond(Response::new(serde_json::to_vec(&UIMessage { params: vec!["add_server_response".to_string(), String::new(), "Error uploading new skin".to_string()] }).unwrap()));
}
}
}
}
@ -258,6 +283,11 @@ async fn main() {
// todo: implement error notifications
}
}
"add_server_login" => {
let params = &params.unwrap().params;
let (status, msg) = launcher.login_user_server(params[0].clone(), params[1].clone(), params[2].clone()).await;
responder.respond(Response::new(serde_json::to_vec(&UIMessage { params: vec!["add_server_response".to_string(), status.to_string(), msg.to_string()] }).unwrap()));
}
"add_server" => {
let params = &params.unwrap().params;
let (status, msg) = launcher.register_user_server(params[0].clone(), params[1].clone(), params[2].clone()).await;

View File

@ -206,6 +206,26 @@ pub mod session {
_ => Ok(SignUpResponse::ServerError)
}
}
pub async fn try_login(server_domain: String, port: u16, username: String, password: String, allow_http: bool) -> Result<SignUpResponse, Box<dyn Error + Send + Sync>> {
let request = SignUpRequest { username: username.clone(), password };
let mut r = surf::post([if allow_http { "http://".to_string() } else { "https://".to_string() }, server_domain, ":".to_string(), port.to_string(), "/api/login".to_string()].concat())
.body_json(&request)
.unwrap()
.await?;
let b= r.body_bytes().await.unwrap();
match r.status() {
surf::StatusCode::BadRequest => Ok(SignUpResponse::BadCredentials),
surf::StatusCode::Conflict => Ok(SignUpResponse::UserAlreadyExists),
surf::StatusCode::Ok => {
let response: ResponseUUID = serde_json::from_slice(&b).unwrap();
Ok(SignUpResponse::Registered(response.uuid))
},
_ => Ok(SignUpResponse::ServerError)
}
}
}
pub mod multimc {

View File

@ -1,3 +1,7 @@
use std::error::Error;
use base64::prelude::BASE64_STANDARD;
use base64::Engine;
use futures::AsyncReadExt;
use rand::{distr::Alphanumeric, Rng};
use tokio::{fs::File, io::AsyncWriteExt};
@ -11,6 +15,12 @@ pub fn random_string(len: usize) -> String {
.collect()
}
pub async fn get_image(url: &str) -> Result<String, Box<dyn Error + Send + Sync>> {
let bytes = surf::get(url).recv_bytes().await?;
let base64_string = BASE64_STANDARD.encode(&bytes);
Ok(format!("data:image/png;base64,{}", base64_string))
}
pub async fn download_file(url: &str, file_path: &str, sender: UnboundedSender<(usize, String)>, status: &str, join: bool) -> Result<(), Box<dyn std::error::Error>> {
let url = url.to_string();
let file_path = file_path.to_string();

View File

@ -3,6 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src=" https://cdn.jsdelivr.net/npm/skinview3d@3.1.0/bundles/skinview3d.bundle.min.js "></script>
<script src=" https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js "></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css" />
<script src="https://cdn.tailwindcss.com"></script>
@ -14,13 +15,13 @@
<body class="flex bg-gray-100 h-screen">
<div id="sidebar" class="flex h-screen bg-white w-16 flex-col justify-between border-e border-gray-100 hidden">
<div>
<div class="inline-flex size-16 items-center justify-center">
<!--<div class="inline-flex size-16 items-center justify-center">
<a href="#" onclick="showAppearance(this)">
<span class="grid size-10 place-content-center rounded-lg bg-green-500 text-xs text-white">
Me
</span>
</a>
</div>
</div>-->
<div class="border-t border-gray-100">
<div class="px-2">
@ -179,7 +180,7 @@
<p class="mt-4 text-sm text-gray-500">alterdekim</p>
</div>
<div id="installation-section" class="xsection bg-white shadow-lg rounded-xl p-6 w-96 text-center">
<div id="installation-section" class="xsection bg-white shadow-lg rounded-xl p-6 w-96 text-center hidden">
<h1 class="text-2xl font-semibold text-gray-700">Welcome to XCraft</h1>
<p class="mt-2 text-gray-600">Choose how you want to set up Minecraft.</p>
@ -317,45 +318,51 @@
<p class="mt-4 text-sm text-gray-500">alterdekim</p>
</div>
<div id="appearance-section" class="xsection bg-white shadow-lg rounded-xl p-6 w-96 text-center hidden">
<h2 class="text-2xl font-semibold text-gray-700">Account Settings</h2>
<div class="mt-6 text-left">
<span class="text-gray-600 font-medium">Username:</span>
<span id="account-name" class="text-gray-600 font-medium"></span>
</div>
<!-- Skin Upload -->
<div class="mt-6 text-left">
<label class="text-gray-600 font-medium">Minecraft Skin</label>
<div class="mt-2 flex items-center gap-4">
<button onclick="uploadSkin()"
class="bg-green-500 hover:bg-green-700 text-white px-4 py-2 rounded">
Upload
</button>
</div>
<div id="appearance-section" class="xsection flex">
<div>
<canvas id="skin_container"></canvas>
</div>
<div class="mt-6 text-left">
<label class="inline-flex items-center cursor-pointer">
<input id="slim-skin" type="checkbox" value="" class="sr-only peer">
<div class="relative w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-green-300 dark:peer-focus:ring-green-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-green-600 dark:peer-checked:bg-green-600"></div>
<span class="ms-3 text-sm font-medium text-gray-900 dark:text-gray-300">Slim skin</span>
</label>
</div>
<!-- Cape Upload -->
<div class="mt-6 text-left">
<label class="text-gray-600 font-medium">Minecraft Cape</label>
<div class="mt-2 flex items-center gap-4">
<button onclick="uploadCape()"
class="bg-green-500 hover:bg-green-700 text-white px-4 py-2 rounded">
Upload
</button>
</div>
<div class="bg-white shadow-lg rounded-xl p-6 w-96 text-center">
<h2 class="text-2xl font-semibold text-gray-700">Account Settings</h2>
<div class="mt-6 text-left">
<span class="text-gray-600 font-medium">Username:</span>
<span id="account-name" class="text-gray-600 font-medium"></span>
</div>
<!-- Skin Upload -->
<div class="mt-6 text-left">
<label class="text-gray-600 font-medium">Minecraft Skin</label>
<div class="mt-2 flex items-center gap-4">
<button onclick="uploadSkin()"
class="bg-green-500 hover:bg-green-700 text-white px-4 py-2 rounded">
Upload
</button>
</div>
</div>
<div class="mt-6 text-left">
<label class="inline-flex items-center cursor-pointer">
<input id="slim-skin" type="checkbox" value="" class="sr-only peer">
<div class="relative w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-green-300 dark:peer-focus:ring-green-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-green-600 dark:peer-checked:bg-green-600"></div>
<span class="ms-3 text-sm font-medium text-gray-900 dark:text-gray-300">Slim skin</span>
</label>
</div>
<!-- Cape Upload -->
<div class="mt-6 text-left">
<label class="text-gray-600 font-medium">Minecraft Cape</label>
<div class="mt-2 flex items-center gap-4">
<button onclick="uploadCape()"
class="bg-green-500 hover:bg-green-700 text-white px-4 py-2 rounded">
Upload
</button>
</div>
</div>
</div>
</div>
<div id="accounts-section" class="xsection grid grid-cols-3 gap-4 p-6 w-fill hidden">
</div>
@ -377,12 +384,19 @@
<input id="server_password" type="password" placeholder="Password" class="mt-4 w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-green-500">
<!-- Sign In Button -->
<button
onclick="addServerInstance()"
class="mt-4 w-full bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded transition">
Add
</button>
<div class="flex w-full">
<button
onclick="addServerInstanceLogin()"
class="mt-4 w-full bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded transition">
Login
</button>
<button
onclick="addServerInstance()"
class="ms-2 mt-4 w-full bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded transition">
Sign up
</button>
</div>
<!-- Footer -->
<p class="mt-4 text-sm text-gray-500">alterdekim</p>
</div>
@ -404,49 +418,62 @@
for( let i = 0; i < params.length; i++ ) {
if( params[i].startsWith("show_") ) {
showSection(undefined, params[i].substring(5));
} else if( params[i] == "sidebar_on" ) {
$("#sidebar").removeClass('hidden');
} else if( params[i] == "sidebar_off") {
$("#sidebar").addClass('hidden');
}
if( params[i] == "show_add" ) {
showAddSection();
} else if( params[i] == "set_downloadable_versions" ) {
setDownloadableVersions(params.slice(i+1));
break;
} else if( params[i] == "update_downloads" ) {
updateDownloads(params.slice(i+1));
switch(params[i]) {
case "sidebar_on":
$("#sidebar").removeClass('hidden');
break;
} else if( params[i] == "show_instances") {
showInstancesSection();
} else if( params[i] == "set_instances_list" ) {
setInstancesList(params.slice(i+1));
break;
} else if( params[i] == "fetch_servers_list" ) {
setServersList(params.slice(i+1));
break;
} else if( params[i] == "locate_java" ) {
setJavaPath(params[i+1]);
break;
} else if( params[i] == "add_server_response" ) {
addServerResponse(params[i+1], params[i+2]);
break;
} else if( params[i] == "fetch_settings_response" ) {
setSettings(params.slice(i+1));
break;
} else if( params[i] == "load_screenshots" ) {
setScreenshots(params.slice(i+1));
break;
} else if( params[i] == "fetch_bg") {
setBackground(params[i+1]);
i++;
} else if( params[i] == "update_logs" ) {
updateLogs(params[i+1]);
break;
} else if( params[i] == "fetch_credentials_list" ) {
setCredentialsList(params.slice(i+1));
break;
case "sidebar_off":
$("#sidebar").addClass('hidden');
break;
case "show_add":
showAddSection();
break;
case "set_downloadable_versions":
setDownloadableVersions(params.slice(i+1));
return;
case "update_downloads":
updateDownloads(params.slice(i+1));
return;
case "show_instances":
showInstancesSection();
break;
case "set_instances_list":
setInstancesList(params.slice(i+1));
return;
case "fetch_servers_list":
setServersList(params.slice(i+1));
return;
case "locate_java":
setJavaPath(params[i+1]);
return;
case "add_server_response":
addServerResponse(params[i+1], params[i+2]);
return;
case "fetch_settings_response":
setSettings(params.slice(i+1));
return;
case "load_screenshots":
setScreenshots(params.slice(i+1));
return;
case "fetch_bg":
setBackground(params[i+1]);
return;
case "update_logs":
updateLogs(params[i+1]);
return;
case "fetch_credentials_list":
setCredentialsList(params.slice(i+1));
return;
case "get_skin":
skinViewer.loadSkin(params[i+1]);
return;
case "get_cape":
skinViewer.loadCape(params[i+1]);
return;
default:
continue;
}
}
}
@ -547,6 +574,15 @@
function addServerResponse(status, msg) {
showPopup(msg);
updateSkinPreview();
}
function addServerInstanceLogin() {
let server = $("#server_address").val();
let username = $("#server_username").val();
let password = $("#server_password").val();
$.post({url: "add_server_login", data: JSON.stringify({ params: [server, username, password] }) }, processParams);
}
function addServerInstance() {
@ -574,9 +610,9 @@
for( let i = 0; i < params.length; i+=3 ) {
let instance = `<div onclick="runInstance('`+params[i]+`')" class="bg-white cursor-pointer hover:bg-green-500 hover:text-white shadow-lg rounded-xl w-48 h-24 flex justify-center items-center">
<img src="`+params[i+2]+`" class="w-12 h-12 rounded-full">
<div class="h-fill ms-2">
<h2 class="text-lg font-semibold">` + params[i+1] + `</h2>
<h2 class="text-sm font-semibold">` + params[i] + `</h2>
<div class="h-fill ms-2 w-32">
<h2 class="text-sm font-semibold truncate">` + params[i+1] + `</h2>
<h2 class="text-xs font-semibold truncate">` + params[i] + `</h2>
</div>
</div>`;
$("#instances-section").append(instance);
@ -591,9 +627,15 @@
accountDomain = domain;
}
function updateSkinPreview() {
$.post({url: "get_skin", data: JSON.stringify({ params: [accountNick, accountDomain] })}, processParams);
$.post({url: "get_cape", data: JSON.stringify({ params: [accountNick, accountDomain] })}, processParams);
}
function showAppearance(obj) {
if( accountNick != undefined && accountDomain != undefined ) {
$("#account-name").html(accountNick);
updateSkinPreview();
showSection(obj, 'appearance');
}
}
@ -616,10 +658,10 @@
if( i == 0 && params.length >= 2 ) {
setAccount(params[i+1], params[i]);
}
let instance = `<div onclick="setAccount('`+params[i+1]+`', '`+params[i]+`')" class="bg-white cursor-pointer hover:bg-green-500 hover:text-white shadow-lg rounded-xl w-48 h-24 flex justify-center items-center">
<div class="h-fill ms-2">
<h2 class="text-lg font-semibold">`+params[i+1]+`</h2>
<h2 class="text-sm font-semibold">`+params[i]+`</h2>
let instance = `<div onclick="setAccount('`+params[i+1]+`', '`+params[i]+`'); showAppearance()" class="bg-white cursor-pointer hover:bg-green-500 hover:text-white shadow-lg rounded-xl w-48 h-24 flex justify-center items-center">
<div class="h-fill ms-2 w-32">
<h2 class="text-sm font-semibold truncate">`+params[i+1]+`</h2>
<h2 class="text-xs font-semibold truncate">`+params[i]+`</h2>
</div>
</div>`;
$("#accounts-section").append(instance);
@ -631,9 +673,9 @@
for( let i = 0; i < params.length; i+=3) {
let instance = `<div onclick="runServerConnection('`+params[i]+`', '`+params[i+1]+`')" class="server-instance bg-white cursor-pointer hover:bg-green-500 hover:text-white shadow-lg rounded-xl w-48 h-24 flex justify-center items-center">
<img src="`+params[i+2]+`" class="w-12 h-12 rounded-full">
<div class="h-fill ms-2">
<h2 class="text-lg font-semibold">`+params[i]+`</h2>
<h2 class="text-sm font-semibold">`+params[i+1]+`</h2>
<div class="h-fill ms-2 w-32">
<h2 class="text-sm font-semibold truncate">`+params[i]+`</h2>
<h2 class="text-xs font-semibold truncate">`+params[i+1]+`</h2>
</div>
</div>`;
$("#servers-section").append(instance);
@ -732,6 +774,8 @@
$("#"+section+"-section").removeClass('hidden');
}
var skinViewer = undefined;
$( document ).ready(async function() {
$.get("check_installation", processParams);
$.get("fetch_settings", processParams);
@ -742,6 +786,15 @@
$.post({ url: "set_skin_model", data: JSON.stringify({ params: [accountNick, accountDomain, $(this).is(':checked')+""] }) }, processParams);
});
skinViewer = new skinview3d.SkinViewer({
canvas: document.getElementById("skin_container"),
width: 300,
height: 400,
skin: "img/skin.png"
});
skinViewer.autoRotate = true;
setInterval(function() {
if( !$("#loading-section").hasClass("hidden") ) {
$.get("check_download_status", processParams);

View File

@ -1,60 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Profile Customization</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100 flex justify-center items-center h-screen">
<div class="bg-white shadow-lg rounded-xl p-6 w-96 text-center">
<h2 class="text-2xl font-semibold text-gray-700">Customize Profile</h2>
<!-- Avatar Preview -->
<div class="flex flex-col items-center mt-4">
<img id="avatar-preview" src="https://www.minecraft.net/etc.clientlibs/minecraft/clientlibs/main/resources/img/minecraft-creeper-face.jpg"
alt="User Avatar" class="w-16 h-16 rounded-full border-4 border-gray-300">
</div>
<!-- Upload Avatar -->
<label class="mt-4 cursor-pointer text-green-500 underline block">
Change Avatar
<input type="file" id="avatar-upload" class="hidden" accept="image/*">
</label>
<!-- Username Input -->
<input type="text" id="username" placeholder="Enter username"
class="mt-4 w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-green-500">
<!-- Skin Upload -->
<label class="mt-4 cursor-pointer text-green-500 underline block">
Upload Skin (.png)
<input type="file" id="skin-upload" class="hidden" accept="image/png">
</label>
<!-- Save Button -->
<button onclick="saveProfile()"
class="mt-4 w-full bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded transition">
Save Changes
</button>
<p class="mt-4 text-sm text-gray-500">Minecraft Launcher v1.0</p>
</div>
<script>
document.getElementById('avatar-upload').addEventListener('change', function(event) {
const file = event.target.files[0];
if (file) {
document.getElementById('avatar-preview').src = URL.createObjectURL(file);
}
});
function saveProfile() {
const username = document.getElementById('username').value;
alert(Profile Updated: ${username});
}
</script>
</body>
</html>