This commit is contained in:
gardenGnostic 2026-03-08 22:03:45 +01:00 committed by GitHub
parent 32b02f11f8
commit 68f77fc77a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 964 additions and 576 deletions

View file

@ -27,39 +27,56 @@
</div>
<div class="main-wrapper">
<div class="sidebar">
<div class="sidebar-title">PATCH NOTES</div>
<div class="sidebar collapsed" id="main-sidebar">
<div class="sidebar-title" onclick="toggleSidebar()">
<span id="sidebar-title-text">PATCH NOTES</span>
<span id="sidebar-toggle-icon" title="Expand Patch Notes"></span>
</div>
<div id="updates-list">
<div class="update-item">Loading updates...</div>
</div>
</div>
<div class="content-area">
<div class="relative">
<img src="minecraftlogo.png" class="mc-logo-img" alt="Minecraft Logo">
<div id="splash-text" class="splash-text">Splash!</div>
</div>
<div id="btn-play-main" class="btn-mc btn-play nav-item" onclick="launchGame()" tabindex="0">PLAY</div>
<div class="version-row">
<span class="version-label">Version:</span>
<div id="version-select-box" class="version-select-box nav-item" tabindex="0">
<span id="current-version-display">Loading...</span>
<div class="select-arrow"></div>
<select id="version-select" class="hidden-select" onchange="updateSelectedRelease()">
<option>Loading...</option>
</select>
<div class="content-area" style="flex-direction: row; align-items: stretch; padding: 0;">
<!-- Left Side: Main Menu -->
<div class="menu-column" style="flex: 2; display: flex; flex-direction: column; align-items: center; justify-content: center; position: relative; z-index: 5;">
<div class="relative">
<img src="minecraftlogo.png" class="mc-logo-img" alt="Minecraft Logo">
<div id="splash-text" class="splash-text">Splash!</div>
</div>
<div id="btn-check-update" class="btn-mc btn-mini nav-item" onclick="checkForUpdatesManual()" title="Check for Updates" tabindex="0">
<img src="restart.png" alt="Update">
<div id="btn-play-main" class="btn-mc btn-play nav-item" onclick="launchGame()" tabindex="0">PLAY</div>
<div class="version-row">
<span class="version-label">Version:</span>
<div id="version-select-box" class="version-select-box nav-item" tabindex="0">
<span id="current-version-display">Loading...</span>
<div class="select-arrow"></div>
<select id="version-select" class="hidden-select" onchange="updateSelectedRelease()">
<option>Loading...</option>
</select>
</div>
<div id="btn-check-update" class="btn-mc btn-mini nav-item" onclick="checkForUpdatesManual()" title="Check for Updates" tabindex="0">
<img src="restart.png" alt="Update">
</div>
</div>
<div class="flex gap-4 w-[500px] max-w-[90%]">
<div class="btn-mc flex-grow nav-item" id="btn-instances" onclick="toggleInstances(true)" tabindex="0" style="font-size: 18px;">INSTANCES</div>
<div class="btn-mc flex-grow nav-item" id="btn-profile" onclick="toggleProfile(true)" tabindex="0" style="font-size: 18px;">PROFILE</div>
<div class="btn-mc flex-grow nav-item" id="btn-servers" onclick="toggleServers(true)" tabindex="0" style="font-size: 18px;">SERVERS</div>
<div class="btn-mc flex-grow nav-item" id="btn-options" onclick="toggleOptions(true)" tabindex="0" style="font-size: 18px;">OPTIONS</div>
</div>
</div>
<div class="flex gap-4 w-[500px] max-w-[90%]">
<div class="btn-mc flex-grow nav-item" id="btn-profile" onclick="toggleProfile(true)" tabindex="0">PROFILE</div>
<div class="btn-mc flex-grow nav-item" id="btn-servers" onclick="toggleServers(true)" tabindex="0">SERVERS</div>
<div class="btn-mc flex-grow nav-item" id="btn-options" onclick="toggleOptions(true)" tabindex="0">OPTIONS</div>
<!-- Right Side: Skin Viewer -->
<div class="skin-column" style="flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; position: relative; min-width: 300px; padding-right: 0px;">
<div id="main-skin-viewer" style="width: 100%; height: 500px; cursor: grab; display: flex; align-items: center; justify-content: center;"></div>
<div class="btn-mc nav-item" onclick="openSkinManager()" style="width: 250px; height: 48px; font-size: 20px; margin-top: -20px; z-index: 10; margin-left: auto; margin-right: auto;" tabindex="0">
CHANGE SKIN
</div>
</div>
<div class="progress-container" id="progress-container">
@ -103,6 +120,43 @@
</div>
</div>
<div class="modal-overlay" id="instances-modal">
<div class="modal-box" style="max-width: 900px;">
<div class="modal-title">GAME INSTANCES</div>
<div id="instances-list-container" class="w-full max-h-[400px] overflow-y-auto mb-6 border-2 border-[#555] bg-black p-2">
<!-- Instances will be listed here -->
<div class="text-center text-gray-400 py-4">No instances found.</div>
</div>
<div class="flex gap-4 w-full">
<div class="btn-mc flex-grow nav-item" onclick="createNewInstance()" tabindex="0">ADD INSTANCE</div>
<div class="btn-mc flex-grow nav-item" onclick="toggleInstances(false)" tabindex="0">DONE</div>
</div>
</div>
</div>
<div class="modal-overlay" id="add-instance-modal">
<div class="modal-box" style="max-width: 600px;">
<div class="modal-title" style="font-size: 32px;">NEW INSTANCE</div>
<div class="mc-input-group">
<label class="mc-label">Instance Name:</label>
<input type="text" id="new-instance-name" class="mc-input nav-item" placeholder="My Instance" tabindex="0">
</div>
<div class="mc-input-group">
<label class="mc-label">GitHub Repo (user/repo):</label>
<input type="text" id="new-instance-repo" class="mc-input nav-item" placeholder="smartcmd/MinecraftConsoles" tabindex="0">
</div>
<div class="flex gap-4 w-full mt-4">
<div class="btn-mc flex-grow nav-item" onclick="saveNewInstance()" tabindex="0">CREATE</div>
<div class="btn-mc flex-grow nav-item" onclick="toggleAddInstance(false)" tabindex="0">CANCEL</div>
</div>
</div>
</div>
<div class="modal-overlay" id="options-modal">
<div class="modal-box">
<div class="modal-title">LAUNCHER OPTIONS</div>
@ -168,7 +222,7 @@
<div class="mc-input-group">
<label class="mc-label">In-Game Username:</label>
<input type="text" id="username-input" class="mc-input nav-item" placeholder="Steve" tabindex="0">
<input type="text" id="username-input" class="mc-input nav-item" placeholder="Steve" tabindex="0" maxlength="16">
</div>
<div class="mc-input-group mt-4">
@ -194,12 +248,66 @@
</div>
</div>
<div class="modal-overlay" id="skin-modal" style="display: none;">
<div class="modal-box" style="max-width: 900px; background: var(--mc-gui-bg); border: 4px solid #000; padding: 30px; box-shadow: inset 4px 4px 0 #fff;">
<div class="modal-title" style="margin-bottom: 25px;">SKIN UPLOADER</div>
<div id="drop-zone" class="nav-item" style="width: 100%; height: 120px; display: flex; align-items: center; justify-content: center; background: #000; border: 2px solid #555; cursor: pointer; margin-bottom: 20px;" tabindex="0">
<input type="file" id="skin-input" accept=".png" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer" />
<div id="upload-prompt" class="text-center">
<p class="text-xl text-white" style="text-shadow: 2px 2px 0 #000;">CLICK OR DRAG SKIN HERE</p>
<p class="text-xs text-gray-400 mt-1 uppercase font-bold" style="letter-spacing: 2px;">64x32 or 64x64 PNG</p>
</div>
</div>
<div id="preview-container" class="hidden flex flex-col items-center w-full">
<div class="flex flex-row gap-6 items-stretch w-full justify-center mb-6" style="height: 400px;">
<!-- 3D Preview Panel -->
<div class="relative bg-black rounded-none border-2 border-[#555] flex flex-col items-center overflow-hidden w-1/2">
<div id="skin-viewer-container" class="w-full h-full"></div>
<div style="position: absolute; bottom: 10px; left: 10px; color: #55ff55; font-size: 14px; text-shadow: 1px 1px 0 #000; z-index: 5;">3D PREVIEW</div>
</div>
<!-- info/2D Panel -->
<div class="flex-1 w-1/2 flex flex-col gap-4">
<div class="bg-black p-4 border-2 border-[#555] flex flex-col items-center flex-1 justify-center">
<div class="relative" style="margin-bottom: 15px;">
<canvas id="skin-canvas" width="64" height="32" class="pixelated" style="width: 256px; height: 128px; image-rendering: pixelated; border: 2px solid #555;"></canvas>
</div>
<div style="color: #aaa; font-size: 14px; text-align: center; text-transform: uppercase;">LEGACY OUTPUT (64x32)</div>
<div id="status-message" style="margin-top: 10px; font-size: 18px;"></div>
</div>
<div style="background: rgba(0,0,0,0.3); padding: 15px; border: 2px solid #555; color: #eee; font-size: 16px;">
<div style="display: flex; justify-content: space-between; border-bottom: 2px solid #555; padding-bottom: 8px; margin-bottom: 8px;">
<span>DETECTED:</span>
<span id="format-label" style="color: #fff;">--</span>
</div>
<div style="display: flex; justify-content: space-between;">
<span>TARGET:</span>
<span style="color: #888; font-size: 14px;">Common/res/mob/char.png</span>
</div>
</div>
</div>
</div>
<div id="save-skin-btn" class="btn-mc w-full nav-item" tabindex="0">SAVE SKIN</div>
</div>
<div class="flex justify-center w-full mt-6">
<div id="btn-close-skin" class="btn-mc nav-item !mb-0" onclick="closeSkinManager()" tabindex="0">DONE</div>
</div>
</div>
</div>
<div id="toast">NOTIFICATION</div>
<div id="music-toggle" class="music-btn nav-item" onclick="toggleMusic()" title="Toggle Music" tabindex="0">
<span id="music-icon"></span>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="skin_manager.js"></script>
<script src="renderer.js"></script>
</body>
</html>

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "legacylauncher",
"version": "2.0.2",
"version": "2.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "legacylauncher",
"version": "2.0.2",
"version": "2.2.0",
"license": "ISC",
"dependencies": {
"electron-store": "^6.0.1",

View file

@ -1,6 +1,6 @@
{
"name": "legacylauncher",
"version": "2.2.0",
"version": "2.9.0",
"description": "",
"main": "main.js",
"scripts": {

File diff suppressed because it is too large Load diff

390
skin_manager.js Normal file
View file

@ -0,0 +1,390 @@
// skin_manager.js
// Handles skin uploading, conversion, 3D preview, and saving to char.png
let mainMenuScene, mainMenuCamera, mainMenuRenderer, mainMenuPlayerGroup;
let isMainSkinDragging = false;
let skinScene, skinCamera, skinRenderer, skinPlayerGroup;
let isSkinDragging = false;
let previousSkinMousePosition = { x: 0, y: 0 };
let processedSkinDataUrl = null;
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
const skinInput = document.getElementById('skin-input');
const dropZone = document.getElementById('drop-zone');
const saveSkinBtn = document.getElementById('save-skin-btn');
const closeSkinBtn = document.getElementById('btn-close-skin');
if (skinInput) skinInput.addEventListener('change', (e) => handleSkinFile(e.target.files[0]));
if (dropZone) {
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('border-green-500');
});
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('border-green-500'));
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('border-green-500');
handleSkinFile(e.dataTransfer.files[0]);
});
}
if (saveSkinBtn) {
saveSkinBtn.addEventListener('click', saveSkinToDisk);
}
if (closeSkinBtn) {
closeSkinBtn.addEventListener('click', closeSkinManager);
}
// Initialize Main Menu Viewer
initMainMenuSkinViewer();
});
function initMainMenuSkinViewer() {
const container = document.getElementById('main-skin-viewer');
if (!container) return;
mainMenuScene = new THREE.Scene();
mainMenuCamera = new THREE.PerspectiveCamera(45, container.offsetWidth / container.offsetHeight, 0.1, 1000);
mainMenuCamera.position.set(0, 5, 60);
mainMenuRenderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
mainMenuRenderer.setSize(container.offsetWidth, container.offsetHeight);
mainMenuRenderer.setPixelRatio(window.devicePixelRatio);
mainMenuRenderer.outputEncoding = THREE.sRGBEncoding;
container.appendChild(mainMenuRenderer.domElement);
mainMenuScene.add(new THREE.AmbientLight(0xffffff, 1.1));
const dl = new THREE.DirectionalLight(0xffffff, 0.5);
dl.position.set(10, 20, 15);
mainMenuScene.add(dl);
mainMenuPlayerGroup = new THREE.Group();
mainMenuScene.add(mainMenuPlayerGroup);
// Interaction for Main Menu Viewer
let prevX = 0;
container.addEventListener('mousedown', (e) => {
isMainSkinDragging = true;
prevX = e.clientX;
});
window.addEventListener('mouseup', () => isMainSkinDragging = false);
window.addEventListener('mousemove', (e) => {
if (isMainSkinDragging && mainMenuPlayerGroup) {
mainMenuPlayerGroup.rotation.y += (e.clientX - prevX) * 0.01;
prevX = e.clientX;
}
});
// Auto-rotate slowly
function animateMain() {
requestAnimationFrame(animateMain);
if (!isMainSkinDragging && mainMenuPlayerGroup) mainMenuPlayerGroup.rotation.y += 0.005;
if (mainMenuRenderer && mainMenuScene && mainMenuCamera) mainMenuRenderer.render(mainMenuScene, mainMenuCamera);
}
animateMain();
// Load current skin after short delay to ensure paths are ready
setTimeout(loadMainMenuSkin, 500);
// Handle window resize
window.addEventListener('resize', () => {
if (mainMenuCamera && mainMenuRenderer && container) {
mainMenuCamera.aspect = container.offsetWidth / container.offsetHeight;
mainMenuCamera.updateProjectionMatrix();
mainMenuRenderer.setSize(container.offsetWidth, container.offsetHeight);
}
});
}
async function loadMainMenuSkin() {
try {
const installDir = await window.getInstallDir();
const skinPath = path.join(installDir, 'Common', 'res', 'mob', 'char.png');
if (fs.existsSync(skinPath)) {
const skinData = fs.readFileSync(skinPath);
const blob = new Blob([skinData]);
const url = URL.createObjectURL(blob);
const img = new Image();
img.onload = () => {
const isLegacy = img.height === 32;
updateSkinModel(img.src, isLegacy, mainMenuPlayerGroup);
};
img.src = url;
} else {
console.log("No skin found at " + skinPath);
}
} catch (e) {
console.error("Failed to load main menu skin:", e);
}
}
function updateSkinModel(dataUrl, isLegacy, targetGroup) {
if (!targetGroup) return;
new THREE.TextureLoader().load(dataUrl, (texture) => {
texture.magFilter = THREE.NearestFilter;
texture.minFilter = THREE.NearestFilter;
texture.encoding = THREE.sRGBEncoding;
// Clear existing children
while(targetGroup.children.length > 0) targetGroup.remove(targetGroup.children[0]);
const createFaceMaterial = (tex, x, y, w, h) => {
const texWidth = tex.image.width;
const texHeight = tex.image.height;
const matTex = tex.clone();
matTex.magFilter = THREE.NearestFilter;
matTex.minFilter = THREE.NearestFilter;
matTex.repeat.set(w / texWidth, h / texHeight);
matTex.offset.set(x / texWidth, 1 - (y + h) / texHeight);
matTex.needsUpdate = true;
return new THREE.MeshLambertMaterial({ map: matTex, transparent: true, alphaTest: 0.5, side: THREE.FrontSide });
};
const createBodyPart = (w, h, d, tex, uv, offset = 0) => {
const geometry = new THREE.BoxGeometry(w, h, d);
const materials = [
createFaceMaterial(tex, uv.left[0], uv.left[1], uv.left[2], uv.left[3]), // Left (Standard MC "Right")
createFaceMaterial(tex, uv.right[0], uv.right[1], uv.right[2], uv.right[3]), // Right (Standard MC "Left")
createFaceMaterial(tex, uv.top[0], uv.top[1], uv.top[2], uv.top[3]),
createFaceMaterial(tex, uv.bottom[0], uv.bottom[1], uv.bottom[2], uv.bottom[3]),
createFaceMaterial(tex, uv.front[0], uv.front[1], uv.front[2], uv.front[3]),
createFaceMaterial(tex, uv.back[0], uv.back[1], uv.back[2], uv.back[3])
];
const mesh = new THREE.Mesh(geometry, materials);
if (offset !== 0) mesh.scale.set(1 + offset, 1 + offset, 1 + offset);
return mesh;
};
const limbUv = (x, y) => ({
top: [x+4, y, 4, 4], bottom: [x+8, y, 4, 4],
right: [x, y+4, 4, 12], front: [x+4, y+4, 4, 12],
left: [x+8, y+4, 4, 12], back: [x+12, y+4, 4, 12]
});
// Head
const headUvs = { top: [8, 0, 8, 8], bottom: [16, 0, 8, 8], right: [0, 8, 8, 8], left: [16, 8, 8, 8], front: [8, 8, 8, 8], back: [24, 8, 8, 8] };
const head = createBodyPart(8, 8, 8, texture, headUvs);
head.position.y = 10;
targetGroup.add(head);
// Hat
const hatUvs = { top: [40, 0, 8, 8], bottom: [48, 0, 8, 8], right: [32, 8, 8, 8], left: [48, 8, 8, 8], front: [40, 8, 8, 8], back: [56, 8, 8, 8] };
const hat = createBodyPart(8, 8, 8, texture, hatUvs, 0.12);
hat.position.y = 10;
targetGroup.add(hat);
// Torso
const torsoUvs = { top: [20, 16, 8, 4], bottom: [28, 16, 8, 4], right: [16, 20, 4, 12], left: [28, 20, 4, 12], front: [20, 20, 8, 12], back: [32, 20, 8, 12] };
targetGroup.add(createBodyPart(8, 12, 4, texture, torsoUvs));
// Jacket (non-legacy only)
if (!isLegacy) {
const jacketUvs = { top: [20, 32, 8, 4], bottom: [28, 32, 8, 4], right: [16, 36, 4, 12], left: [28, 36, 4, 12], front: [20, 36, 8, 12], back: [32, 36, 8, 12] };
targetGroup.add(createBodyPart(8, 12, 4, texture, jacketUvs, 0.05));
}
// Limbs
const limbs = [
{ pos: [-6, 0, 0], uv: limbUv(40, 16), layerUv: limbUv(40, 32) },
{ pos: [6, 0, 0], uv: isLegacy ? limbUv(40, 16) : limbUv(32, 48), layerUv: limbUv(48, 48) },
{ pos: [-2, -12, 0], uv: limbUv(0, 16), layerUv: limbUv(0, 32) },
{ pos: [2, -12, 0], uv: isLegacy ? limbUv(0, 16) : limbUv(16, 48), layerUv: limbUv(0, 48) }
];
limbs.forEach(l => {
const base = createBodyPart(4, 12, 4, texture, l.uv);
base.position.set(...l.pos);
targetGroup.add(base);
if (!isLegacy) {
const layer = createBodyPart(4, 12, 4, texture, l.layerUv, 0.05);
layer.position.set(...l.pos);
targetGroup.add(layer);
}
});
});
}
function openSkinManager() {
const modal = document.getElementById('skin-modal');
modal.style.display = 'flex';
modal.style.opacity = '1';
// Clear previous state in modal
const previewContainer = document.getElementById('preview-container');
if (previewContainer) previewContainer.classList.add('hidden');
const sysMsg = document.getElementById('sys-message');
if (sysMsg) sysMsg.classList.add('hidden');
// Load current skin into preview (optional, maybe we only want to see uploaded ones)
// loadCurrentSkinToPreview();
}
function closeSkinManager() {
const modal = document.getElementById('skin-modal');
modal.style.opacity = '0';
setTimeout(() => {
modal.style.display = 'none';
// Reset state
const prompt = document.getElementById('upload-prompt');
if (prompt) prompt.style.display = 'block';
const previewContainer = document.getElementById('preview-container');
if (previewContainer) previewContainer.classList.add('hidden');
processedSkinDataUrl = null;
}, 300);
}
function handleSkinFile(file) {
if (!file || !file.type.includes('png')) return window.showToast("Only PNG files are supported!");
const prompt = document.getElementById('upload-prompt');
if (prompt) prompt.style.display = 'none';
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => {
processSkinImage(img, e.target.result);
};
img.onerror = () => {
window.showToast("Failed to load image");
if(prompt) prompt.style.display = 'block';
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
}
function processSkinImage(img, srcUrl, isInitialLoad = false) {
const canvas = document.getElementById('skin-canvas');
const ctx = canvas.getContext('2d');
const formatLabel = document.getElementById('format-label');
const statusMessage = document.getElementById('status-message');
const previewContainer = document.getElementById('preview-container');
const saveBtn = document.getElementById('save-skin-btn');
const prompt = document.getElementById('upload-prompt');
if (img.width !== 64) {
if(prompt) prompt.style.display = 'block';
if(previewContainer) previewContainer.classList.add('hidden');
return window.showToast("Invalid skin width. Must be 64px.");
}
if(prompt) prompt.style.display = 'none';
const isLegacy = img.height === 32;
ctx.clearRect(0, 0, 64, 32);
ctx.drawImage(img, 0, 0, 64, 32, 0, 0, 64, 32);
processedSkinDataUrl = canvas.toDataURL('image/png');
previewContainer.classList.remove('hidden');
if (isInitialLoad) {
if(formatLabel) formatLabel.textContent = "Current Skin";
if(statusMessage) statusMessage.innerHTML = "<span class='text-blue-400 font-black' style='color: #60a5fa;'>LOADED FROM DISK</span>";
if(saveBtn) {
saveBtn.textContent = "SAVED";
saveBtn.classList.add('disabled');
}
} else {
if(formatLabel) formatLabel.textContent = isLegacy ? "64x32 (Legacy)" : "64x64 (Modern)";
if(statusMessage) statusMessage.innerHTML = isLegacy ? "<span class='text-green-400 font-black' style='color: #4ade80;'>LEGACY READY</span>" : "<span class='text-yellow-400 font-black' style='color: #facc15;'>CONVERTED TO 64x32</span>";
if(saveBtn) {
saveBtn.textContent = "SAVE SKIN";
saveBtn.classList.remove('disabled');
}
}
if (!skinScene) initPreviewEngine();
updateSkinModel(srcUrl, isLegacy, skinPlayerGroup);
}
function initPreviewEngine() {
const container = document.getElementById('skin-viewer-container');
if (!container) return;
skinScene = new THREE.Scene();
skinCamera = new THREE.PerspectiveCamera(35, container.offsetWidth / container.offsetHeight, 0.1, 1000);
skinCamera.position.set(0, 5, 70);
skinRenderer = new THREE.WebGLRenderer({ antialias: false, alpha: true });
skinRenderer.setSize(container.offsetWidth, container.offsetHeight);
skinRenderer.setPixelRatio(window.devicePixelRatio);
skinRenderer.outputEncoding = THREE.sRGBEncoding;
container.appendChild(skinRenderer.domElement);
skinScene.add(new THREE.AmbientLight(0xffffff, 0.9));
const dl = new THREE.DirectionalLight(0xffffff, 0.35);
dl.position.set(10, 20, 15);
skinScene.add(dl);
skinPlayerGroup = new THREE.Group();
skinScene.add(skinPlayerGroup);
container.addEventListener('mousedown', () => isSkinDragging = true);
window.addEventListener('mouseup', () => isSkinDragging = false);
window.addEventListener('mousemove', (e) => {
if (isSkinDragging && skinPlayerGroup) {
skinPlayerGroup.rotation.y += (e.movementX) * 0.01;
}
});
function animate() {
requestAnimationFrame(animate);
if (!isSkinDragging && skinPlayerGroup) skinPlayerGroup.rotation.y += 0.008;
if (skinRenderer && skinScene && skinCamera) skinRenderer.render(skinScene, skinCamera);
}
animate();
}
async function saveSkinToDisk() {
if (!processedSkinDataUrl) return;
try {
const installDir = await window.getInstallDir();
const savePath = path.join(installDir, 'Common', 'res', 'mob', 'char.png');
const dir = path.dirname(savePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const base64Data = processedSkinDataUrl.replace(/^data:image\/png;base64,/, "");
fs.writeFileSync(savePath, base64Data, 'base64');
window.showToast("Skin Saved Successfully!");
const saveBtn = document.getElementById('save-skin-btn');
if(saveBtn) {
saveBtn.textContent = "SAVED!";
saveBtn.classList.add('disabled');
}
// Refresh main menu skin
loadMainMenuSkin();
// Close modal after short delay?
setTimeout(closeSkinManager, 1000);
} catch (e) {
window.showToast("Error Saving Skin: " + e.message);
console.error(e);
}
}
// Global Export
window.openSkinManager = openSkinManager;
window.initMainMenuSkinViewer = initMainMenuSkinViewer;
window.loadMainMenuSkin = loadMainMenuSkin;

View file

@ -476,3 +476,8 @@ JAKE: Pip pip!
JAKE: By jove!
JAKE: Egads!
JAKE: Jolly good show
Jade Harley!!!
Jade Harley!!!
Jade Harley!!!
Jade Harley!!!
Jade Harley!!!

View file

@ -112,6 +112,43 @@ body {
overflow-y: auto;
z-index: 10;
backdrop-filter: blur(8px);
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1), padding 0.3s ease;
}
.sidebar.collapsed {
width: 80px;
padding: 30px 10px;
}
.sidebar.collapsed #updates-list {
display: none;
}
.sidebar.collapsed .sidebar-title {
border-bottom: none;
margin-bottom: 0;
justify-content: center;
padding-bottom: 0;
}
.sidebar.collapsed #sidebar-title-text {
display: none;
}
.sidebar.collapsed #sidebar-toggle-icon {
font-size: 32px;
}
#sidebar-toggle-icon {
font-size: 24px;
font-weight: bold;
cursor: pointer;
transition: transform 0.2s, color 0.2s;
}
#sidebar-toggle-icon:hover {
color: #55ff55;
transform: scale(1.1);
}
.sidebar-title {
@ -119,9 +156,12 @@ body {
color: #fff;
text-shadow: 2px 2px 0 #000;
margin-bottom: 25px;
text-align: center;
border-bottom: 2px solid #555;
padding-bottom: 15px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
}
.commit-author {