-
Downloading...
+
diff --git a/renderer.js b/renderer.js
index df621a9..5c2230c 100644
--- a/renderer.js
+++ b/renderer.js
@@ -1125,7 +1125,11 @@ async function fetchGitHubData() {
const offlineInd = document.getElementById('offline-indicator');
if (loader) loader.style.display = 'flex';
- if (loaderText) loaderText.textContent = "SYNCING: " + repo;
+ if (loaderText) {
+ const loaderLabel = document.getElementById('loader-text-label');
+ if (loaderLabel) loaderLabel.textContent = "SYNCING: " + repo;
+ else loaderText.textContent = "SYNCING: " + repo;
+ }
const hideLoader = () => {
if (loader) {
@@ -1416,8 +1420,29 @@ function setProcessingState(active) {
function updateProgress(percent, text) {
const bar = document.getElementById('progress-bar-fill');
if (bar) bar.style.width = percent + "%";
+
+ const label = document.getElementById('progress-text-label');
+ const dots = document.getElementById('progress-dots');
+ const suffix = document.getElementById('progress-text-suffix');
const txt = document.getElementById('progress-text');
- if (text && txt) txt.textContent = text;
+
+ if (!text) return;
+
+ if (label && dots && suffix) {
+ const match = text.match(/^(Downloading)(?:\.{0,3})?(.*)$/i);
+ if (match) {
+ label.textContent = match[1];
+ suffix.textContent = match[2] || '';
+ dots.classList.add('animate');
+ return;
+ }
+ label.textContent = text;
+ suffix.textContent = '';
+ dots.classList.remove('animate');
+ return;
+ }
+
+ if (txt) txt.textContent = text;
}
async function handleElectronFlow(url) {
diff --git a/skin_manager.js b/skin_manager.js
index 031a496..a3da23f 100644
--- a/skin_manager.js
+++ b/skin_manager.js
@@ -3,6 +3,7 @@
let mainMenuScene, mainMenuCamera, mainMenuRenderer, mainMenuPlayerGroup;
let isMainSkinDragging = false;
+let mainMenuSkinRenderMode = '3d';
let skinScene, skinCamera, skinRenderer, skinPlayerGroup;
let isSkinDragging = false;
@@ -75,12 +76,13 @@ function initMainMenuSkinViewer() {
// Interaction for Main Menu Viewer
let prevX = 0;
container.addEventListener('mousedown', (e) => {
+ if (mainMenuSkinRenderMode !== '3d') return;
isMainSkinDragging = true;
prevX = e.clientX;
});
window.addEventListener('mouseup', () => isMainSkinDragging = false);
window.addEventListener('mousemove', (e) => {
- if (isMainSkinDragging && mainMenuPlayerGroup) {
+ if (mainMenuSkinRenderMode === '3d' && isMainSkinDragging && mainMenuPlayerGroup) {
mainMenuPlayerGroup.rotation.y += (e.clientX - prevX) * 0.01;
prevX = e.clientX;
}
@@ -89,7 +91,9 @@ function initMainMenuSkinViewer() {
// Auto-rotate slowly
function animateMain() {
requestAnimationFrame(animateMain);
- if (!isMainSkinDragging && mainMenuPlayerGroup) mainMenuPlayerGroup.rotation.y += 0.005;
+ if (mainMenuSkinRenderMode === '3d' && !isMainSkinDragging && mainMenuPlayerGroup) {
+ mainMenuPlayerGroup.rotation.y += 0.005;
+ }
if (mainMenuRenderer && mainMenuScene && mainMenuCamera) mainMenuRenderer.render(mainMenuScene, mainMenuCamera);
}
animateMain();
@@ -123,7 +127,7 @@ async function loadMainMenuSkin() {
const img = new Image();
img.onload = () => {
const isLegacy = img.height === 32;
- updateSkinModel(img.src, isLegacy, mainMenuPlayerGroup);
+ updateSkinModel(img.src, isLegacy, mainMenuPlayerGroup, mainMenuSkinRenderMode);
};
img.src = url;
} else {
@@ -134,7 +138,25 @@ async function loadMainMenuSkin() {
}
}
-function updateSkinModel(dataUrl, isLegacy, targetGroup) {
+function toggleMainSkinRenderMode() {
+ mainMenuSkinRenderMode = mainMenuSkinRenderMode === '3d' ? '2d' : '3d';
+ const modeButton = document.getElementById('btn-skin-render-mode');
+ if (modeButton) modeButton.textContent = mainMenuSkinRenderMode.toUpperCase();
+
+ const container = document.getElementById('main-skin-viewer');
+ if (container) {
+ container.style.cursor = mainMenuSkinRenderMode === '3d' ? 'grab' : 'default';
+ }
+ isMainSkinDragging = false;
+
+ if (mainMenuSkinRenderMode === '2d' && mainMenuPlayerGroup) {
+ mainMenuPlayerGroup.rotation.y = 0;
+ }
+
+ loadMainMenuSkin();
+}
+
+function updateSkinModel(dataUrl, isLegacy, targetGroup, renderMode = '3d') {
if (!targetGroup) return;
new THREE.TextureLoader().load(dataUrl, (texture) => {
@@ -178,6 +200,50 @@ function updateSkinModel(dataUrl, isLegacy, targetGroup) {
left: [x+8, y+4, 4, 12], back: [x+12, y+4, 4, 12]
});
+ const create2DPart = (tex, uvFront, width, height, x, y, scale = 1) => {
+ const texWidth = tex.image.width;
+ const texHeight = tex.image.height;
+ const partTex = tex.clone();
+ partTex.magFilter = THREE.NearestFilter;
+ partTex.minFilter = THREE.NearestFilter;
+ partTex.repeat.set(uvFront[2] / texWidth, uvFront[3] / texHeight);
+ partTex.offset.set(uvFront[0] / texWidth, 1 - (uvFront[1] + uvFront[3]) / texHeight);
+ partTex.needsUpdate = true;
+
+ const geometry = new THREE.PlaneGeometry(width, height);
+ const material = new THREE.MeshBasicMaterial({ map: partTex, transparent: true, alphaTest: 0.5, side: THREE.DoubleSide });
+ const mesh = new THREE.Mesh(geometry, material);
+ mesh.position.set(x, y, 0);
+ mesh.scale.set(scale, scale, 1);
+ return mesh;
+ };
+
+ if (renderMode === '2d') {
+ const frontParts = [
+ create2DPart(texture, [8, 8, 8, 8], 8, 8, 0, 10),
+ create2DPart(texture, [40, 8, 8, 8], 8, 8, 0, 10, 1.12),
+ create2DPart(texture, [20, 20, 8, 12], 8, 12, 0, 0),
+ create2DPart(texture, [44, 20, 4, 12], 4, 12, -6, 0),
+ create2DPart(texture, isLegacy ? [44, 20, 4, 12] : [36, 52, 4, 12], 4, 12, 6, 0),
+ create2DPart(texture, [4, 20, 4, 12], 4, 12, -2, -12),
+ create2DPart(texture, isLegacy ? [4, 20, 4, 12] : [20, 52, 4, 12], 4, 12, 2, -12)
+ ];
+
+ if (!isLegacy) {
+ frontParts.push(
+ create2DPart(texture, [20, 36, 8, 12], 8, 12, 0, 0, 1.05),
+ create2DPart(texture, [44, 36, 4, 12], 4, 12, -6, 0, 1.05),
+ create2DPart(texture, [52, 52, 4, 12], 4, 12, 6, 0, 1.05),
+ create2DPart(texture, [4, 36, 4, 12], 4, 12, -2, -12, 1.05),
+ create2DPart(texture, [4, 52, 4, 12], 4, 12, 2, -12, 1.05)
+ );
+ }
+
+ frontParts.forEach((part) => targetGroup.add(part));
+ targetGroup.rotation.y = 0;
+ return;
+ }
+
// 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);
@@ -218,6 +284,8 @@ function updateSkinModel(dataUrl, isLegacy, targetGroup) {
targetGroup.add(layer);
}
});
+
+ targetGroup.rotation.y = 0;
});
}
@@ -318,7 +386,7 @@ function processSkinImage(img, srcUrl, isInitialLoad = false) {
}
if (!skinScene) initPreviewEngine();
- updateSkinModel(srcUrl, isLegacy, skinPlayerGroup);
+ updateSkinModel(srcUrl, isLegacy, skinPlayerGroup, '3d');
}
function initPreviewEngine() {
@@ -398,3 +466,4 @@ async function saveSkinToDisk() {
window.openSkinManager = openSkinManager;
window.initMainMenuSkinViewer = initMainMenuSkinViewer;
window.loadMainMenuSkin = loadMainMenuSkin;
+window.toggleMainSkinRenderMode = toggleMainSkinRenderMode;
diff --git a/style.css b/style.css
index bd8aabc..3ddc137 100644
--- a/style.css
+++ b/style.css
@@ -568,12 +568,71 @@ body.steamdeck-mode .sidebar {
transition: width 0.2s ease-out;
}
+.progress-text-wrap {
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ gap: 8px;
+ margin-bottom: 6px;
+}
+
+.progress-spinner {
+ width: 12px;
+ height: 12px;
+ border-width: 2px;
+ border-color: #333;
+ border-top-color: var(--mc-progress-fill);
+ margin: 0;
+ flex-shrink: 0;
+ animation-duration: 1s;
+}
+
.progress-text {
color: #fff;
font-size: 20px;
text-shadow: 2px 2px 0 #000;
- margin-bottom: 6px;
- text-align: center;
+ text-align: left;
+ display: inline-flex;
+ align-items: center;
+}
+
+.loading-dots {
+ display: inline-flex;
+ width: 1.8em;
+ justify-content: flex-start;
+}
+
+.loading-dots span {
+ visibility: hidden;
+}
+
+.loading-dots.animate span:nth-child(1) {
+ animation: dotCycle1 1.1s steps(1, end) infinite;
+}
+
+.loading-dots.animate span:nth-child(2) {
+ animation: dotCycle2 1.1s steps(1, end) infinite;
+}
+
+.loading-dots.animate span:nth-child(3) {
+ animation: dotCycle3 1.1s steps(1, end) infinite;
+}
+
+@keyframes dotCycle1 {
+ 0%, 49% { visibility: hidden; }
+ 50%, 100% { visibility: visible; }
+}
+
+@keyframes dotCycle2 {
+ 0%, 32% { visibility: hidden; }
+ 33%, 66% { visibility: visible; }
+ 67%, 100% { visibility: hidden; }
+}
+
+@keyframes dotCycle3 {
+ 0%, 16% { visibility: hidden; }
+ 17%, 82% { visibility: visible; }
+ 83%, 100% { visibility: hidden; }
}
.modal-overlay {
@@ -803,12 +862,18 @@ body.steamdeck-mode .sidebar {
background: rgba(0,0,0,0.8);
}
+
.loader-content {
position: relative;
z-index: 1;
text-align: center;
}
+#loader-text {
+ display: inline-flex;
+ align-items: center;
+}
+
.loader-spinner {
width: 80px;
height: 80px;
@@ -1333,3 +1398,27 @@ body.classic-theme .sidebar-title {
body.classic-theme .music-btn {
bottom: 92px;
}
+
+.skin-action-row {
+ display: flex;
+ align-items: stretch;
+ justify-content: center;
+}
+
+.skin-action-btn {
+ height: 48px !important;
+ margin-bottom: 0 !important;
+}
+
+.skin-action-btn-main {
+ width: 250px;
+ font-size: 20px;
+ border-right-width: 1px;
+}
+
+.skin-action-btn-mode {
+ width: 64px;
+ font-size: 20px;
+ border-left-width: 1px;
+ margin-left: -2px;
+}