Merge remote-tracking branch 'origin/develop' into develop

This commit is contained in:
sanasol
2026-01-25 01:27:29 +01:00
19 changed files with 1216 additions and 262 deletions

View File

@@ -0,0 +1,42 @@
name: Translation Request
description: Request translation for text or content
title: "[TRANSLATION] "
labels: ["translation"]
body:
- type: input
id: language
attributes:
label: Target Language
description: What language do you want to translate to?
placeholder: "e.g. Spanish (es-ES), French (fr-FR)"
validations:
required: true
- type: textarea
id: source_text
attributes:
label: Source Text
description: The original text that needs to be translated.
placeholder: "Paste the text here..."
validations:
required: true
- type: textarea
id: context
attributes:
label: Context
description: Provide context about where this text appears or how it's used.
placeholder: "This text appears in..., It's used for..."
- type: input
id: file_location
attributes:
label: File Location
description: Where is this text located in the codebase?
placeholder: "e.g. src/components/Button.js:15"
- type: textarea
id: notes
attributes:
label: Additional Notes
description: Any specific instructions or notes for the translator.

View File

@@ -25,6 +25,14 @@ jobs:
cache: 'npm'
- run: npm ci
- name: Create .env file
env:
CF_KEY: ${{ secrets.CURSEFORGE_API_KEY }}
DISCORD_ID: ${{ secrets.DISCORD_CLIENT_ID }}
run: |
echo "CURSEFORGE_API_KEY=$CF_KEY" > .env
echo "DISCORD_CLIENT_ID=$DISCORD_ID" >> .env
- name: Build Linux Packages
run: |
npx electron-builder --linux --x64 --arm64 --publish never
@@ -48,7 +56,17 @@ jobs:
node-version: '22'
cache: 'npm'
- run: npm ci
- run: npx electron-builder --win --publish never
- name: Create .env file
env:
CF_KEY: ${{ secrets.CURSEFORGE_API_KEY }}
DISCORD_ID: ${{ secrets.DISCORD_CLIENT_ID }}
run: |
echo "CURSEFORGE_API_KEY=$CF_KEY" > .env
echo "DISCORD_CLIENT_ID=$DISCORD_ID" >> .env
- name: Build Windows Packages
run: npx electron-builder --win --publish never
- uses: actions/upload-artifact@v4
with:
name: windows-builds
@@ -66,7 +84,17 @@ jobs:
node-version: '22'
cache: 'npm'
- run: npm ci
- run: npx electron-builder --mac --publish never
- name: Create .env file
env:
CF_KEY: ${{ secrets.CURSEFORGE_API_KEY }}
DISCORD_ID: ${{ secrets.DISCORD_CLIENT_ID }}
run: |
echo "CURSEFORGE_API_KEY=$CF_KEY" > .env
echo "DISCORD_CLIENT_ID=$DISCORD_ID" >> .env
- name: Build Windows Packages
run: npx electron-builder --mac --publish never
- uses: actions/upload-artifact@v4
with:
name: macos-builds

View File

@@ -619,9 +619,17 @@
<div id="progressErrorMessage" class="progress-error-message"></div>
<div class="progress-retry-section">
<span id="progressRetryInfo" class="progress-retry-info"></span>
<button id="progressRetryBtn" class="progress-retry-btn" style="display: none;">
🔄 Retry Download
<div class="progress-retry-buttons">
<button id="progressJRRetryBtn" class="progress-retry-btn" style="display: none;">
Retry Java Download
</button>
<button id="progressPWRRetryBtn" class="progress-retry-btn" style="display: none;">
Retry Game Download
</button>
<button id="progressRetryBtn" class="progress-retry-btn" style="display: none;">
Retry Download
</button>
</div>
</div>
</div>
</div>
@@ -777,7 +785,11 @@
<a href="https://github.com/ericiskoolbeans" target="_blank"
class="text-blue-400 hover:text-blue-300 transition-colors">@ericiskoolbeans</a>,
<a href="https://github.com/fazrigading" target="_blank"
class="text-blue-400 hover:text-blue-300 transition-colors">@fazrigading</a>
class="text-blue-400 hover:text-blue-300 transition-colors">@fazrigading</a>,
<a href="https://github.com/Rahul-Sahani04" target="_blank"
class="text-blue-400 hover:text-blue-300 transition-colors">@Rahul-Sahani04</a>,
<a href="https://github.com/xSamiVS" target="_blank"
class="text-blue-400 hover:text-blue-300 transition-colors">@xSamiVS</a>
</span>
</div>
</footer>
@@ -846,6 +858,7 @@
<script src="js/i18n.js"></script>
<script type="module" src="js/settings.js"></script>
<script type="module" src="js/update.js"></script>
<script src="js/updater.js"></script>
</body>

View File

@@ -4,8 +4,9 @@ const i18n = (() => {
let translations = {};
const availableLanguages = [
{ code: 'en', name: 'English' },
{ code: 'es', name: 'Español' },
{ code: 'pt-BR', name: 'Português (Brasil)' }
{ code: 'es-ES', name: 'Español (España)' },
{ code: 'pt-BR', name: 'Portuguese (Brazil)' },
{ code: 'tr-TR', name: 'Turkish (Turkey)' }
];
// Load single language file

View File

@@ -200,10 +200,15 @@ async function loadBrowseMods() {
browseContainer.innerHTML = `
<div class=\"empty-browse-mods\">
<i class=\"fas fa-key\"></i>
<h4>API Key Required</h4>
<p>CurseForge API key is needed to browse mods</p>
<h4 data-i18n="mods.apiKeyRequired">API Key Required</h4>
<p data-i18n="mods.apiKeyRequiredDesc">CurseForge API key is needed to browse mods</p>
</div>
`;
if (window.i18n) {
const container = modsContainer.querySelector('.empty-browse-mods');
container.querySelector('h4').textContent = window.i18n.t('mods.apiKeyRequired');
container.querySelector('p').textContent = window.i18n.t('mods.apiKeyRequiredDesc');
}
return;
}

View File

@@ -10,6 +10,8 @@ let progressErrorContainer;
let progressErrorMessage;
let progressRetryInfo;
let progressRetryBtn;
let progressJRRetryBtn;
let progressPWRRetryBtn;
// Download retry state
let currentDownloadState = {
@@ -199,7 +201,8 @@ function updateProgress(data) {
if ((data.error || (data.message && data.message.includes('failed'))) &&
!(data.retryState && data.retryState.isAutomaticRetry)) {
const errorType = categorizeError(data.message);
showDownloadError(data.message, data.canRetry, errorType);
console.log('[UI] Showing download error:', { message: data.message, canRetry: data.canRetry, errorType });
showDownloadError(data.message, data.canRetry, errorType, data);
} else if (data.percent === 100) {
hideDownloadError();
} else if (data.retryState && data.retryState.isAutomaticRetry) {
@@ -230,8 +233,16 @@ function updateRetryState(retryState) {
}
}
function showDownloadError(errorMessage, canRetry = true, errorType = 'general') {
if (!progressErrorContainer || !progressErrorMessage || !progressRetryBtn) return;
function showDownloadError(errorMessage, canRetry = true, errorType = 'general', data = null) {
if (!progressErrorContainer || !progressErrorMessage) return;
console.log('[UI] showDownloadError called with:', { errorMessage, canRetry, errorType, data });
console.log('[UI] Data properties:', {
hasData: !!data,
hasRetryData: !!(data && data.retryData),
dataErrorType: data && data.errorType,
dataIsJREError: data && data.retryData && data.retryData.isJREError
});
currentDownloadState.lastError = errorMessage;
currentDownloadState.canRetry = canRetry;
@@ -242,13 +253,37 @@ function showDownloadError(errorMessage, canRetry = true, errorType = 'general')
currentDownloadState.branch = data.retryData.branch;
currentDownloadState.fileName = data.retryData.fileName;
currentDownloadState.cacheDir = data.retryData.cacheDir;
// Override errorType if specified in data
if (data.errorType) {
currentDownloadState.errorType = data.errorType;
}
}
// Hide all retry buttons first
if (progressRetryBtn) progressRetryBtn.style.display = 'none';
if (progressJRRetryBtn) progressJRRetryBtn.style.display = 'none';
if (progressPWRRetryBtn) progressPWRRetryBtn.style.display = 'none';
// User-friendly error messages
const userMessage = getErrorMessage(errorMessage, errorType);
progressErrorMessage.textContent = userMessage;
progressErrorContainer.style.display = 'block';
progressRetryBtn.style.display = canRetry ? 'block' : 'none';
// Show appropriate retry button based on error type
if (canRetry) {
if (errorType === 'jre') {
if (progressJRRetryBtn) {
console.log('[UI] Showing JRE retry button');
progressJRRetryBtn.style.display = 'block';
}
} else {
// All other errors use PWR retry button (game download, butler, etc.)
if (progressPWRRetryBtn) {
console.log('[UI] Showing PWR retry button');
progressPWRRetryBtn.style.display = 'block';
}
}
}
// Add visual indicators based on error type
progressErrorContainer.className = `progress-error-container error-${errorType}`;
@@ -261,6 +296,11 @@ function showDownloadError(errorMessage, canRetry = true, errorType = 'general')
function hideDownloadError() {
if (!progressErrorContainer) return;
// Hide all retry buttons
if (progressRetryBtn) progressRetryBtn.style.display = 'none';
if (progressJRRetryBtn) progressJRRetryBtn.style.display = 'none';
if (progressPWRRetryBtn) progressPWRRetryBtn.style.display = 'none';
progressErrorContainer.style.display = 'none';
currentDownloadState.canRetry = false;
currentDownloadState.lastError = null;
@@ -589,6 +629,8 @@ function setupUI() {
progressErrorMessage = document.getElementById('progressErrorMessage');
progressRetryInfo = document.getElementById('progressRetryInfo');
progressRetryBtn = document.getElementById('progressRetryBtn');
progressJRRetryBtn = document.getElementById('progressJRRetryBtn');
progressPWRRetryBtn = document.getElementById('progressPWRRetryBtn');
// Setup draggable progress bar
setupProgressDrag();
@@ -784,6 +826,8 @@ function categorizeError(message) {
return 'space';
} else if (msg.includes('conflict') || msg.includes('already exists')) {
return 'conflict';
} else if (msg.includes('jre') || msg.includes('java runtime')) {
return 'jre';
} else {
return 'general';
}
@@ -812,6 +856,8 @@ function getErrorMessage(technicalMessage, errorType) {
return 'Insufficient disk space. Free up space and retry.';
case 'conflict':
return 'Installation directory conflict. Please retry.';
case 'jre':
return 'Java runtime download failed. Please retry.';
default:
return 'Download failed. Please retry.';
}
@@ -839,53 +885,173 @@ function updateConnectionQuality(quality) {
// Enhanced retry button setup
function setupRetryButton() {
if (!progressRetryBtn) return;
progressRetryBtn.addEventListener('click', async () => {
// Setup JRE retry button
if (progressJRRetryBtn) {
progressJRRetryBtn.addEventListener('click', async () => {
if (!currentDownloadState.canRetry || currentDownloadState.isDownloading) {
return;
}
// Disable retry button during retry
progressRetryBtn.disabled = true;
progressRetryBtn.textContent = '🔄 Retrying...';
progressRetryBtn.classList.add('retrying');
progressJRRetryBtn.disabled = true;
progressJRRetryBtn.textContent = 'Retrying...';
progressJRRetryBtn.classList.add('retrying');
currentDownloadState.isDownloading = true;
try {
hideDownloadError();
if (progressRetryInfo) {
progressRetryInfo.style.background = '';
progressRetryInfo.style.color = '';
}
if (progressText) {
progressText.textContent = 'Re-downloading Java runtime...';
}
if (!currentDownloadState.retryData || currentDownloadState.errorType !== 'jre') {
currentDownloadState.retryData = {
isJREError: true,
jreUrl: '',
fileName: 'jre.tar.gz',
cacheDir: '',
osName: 'linux',
arch: 'amd64'
};
console.log('[UI] Created default JRE retry data:', currentDownloadState.retryData);
}
if (window.electronAPI && window.electronAPI.retryDownload) {
const result = await window.electronAPI.retryDownload(currentDownloadState.retryData);
if (!result.success) {
throw new Error(result.error || 'JRE retry failed');
}
} else {
console.warn('electronAPI.retryDownload not available, simulating JRE retry...');
await new Promise(resolve => setTimeout(resolve, 2000));
throw new Error('JRE retry API not available');
}
} catch (error) {
console.error('JRE retry failed:', error);
showDownloadError(`JRE retry failed: ${error.message}`, true, 'jre');
} finally {
if (progressJRRetryBtn) {
progressJRRetryBtn.disabled = false;
progressJRRetryBtn.textContent = 'Retry Java Download';
progressJRRetryBtn.classList.remove('retrying');
}
currentDownloadState.isDownloading = false;
}
});
}
// Setup PWR retry button
if (progressPWRRetryBtn) {
progressPWRRetryBtn.addEventListener('click', async () => {
if (!currentDownloadState.canRetry || currentDownloadState.isDownloading) {
return;
}
progressPWRRetryBtn.disabled = true;
progressPWRRetryBtn.textContent = 'Retrying...';
progressPWRRetryBtn.classList.add('retrying');
currentDownloadState.isDownloading = true;
try {
// Hide error state during retry
hideDownloadError();
// Reset retry info styling for manual retries
if (progressRetryInfo) {
progressRetryInfo.style.background = '';
progressRetryInfo.style.color = '';
}
// Update progress text with context-aware message
if (progressText) {
const contextMessage = getRetryContextMessage();
progressText.textContent = contextMessage;
}
// Ensure retry data exists, create defaults if null
if (!currentDownloadState.retryData) {
if (!currentDownloadState.retryData || currentDownloadState.errorType === 'jre') {
currentDownloadState.retryData = {
branch: 'release',
fileName: '4.pwr'
};
console.log('[UI] Created default PWR retry data:', currentDownloadState.retryData);
}
if (window.electronAPI && window.electronAPI.retryDownload) {
const result = await window.electronAPI.retryDownload(currentDownloadState.retryData);
if (!result.success) {
throw new Error(result.error || 'Game retry failed');
}
} else {
console.warn('electronAPI.retryDownload not available, simulating PWR retry...');
await new Promise(resolve => setTimeout(resolve, 2000));
throw new Error('Game retry API not available');
}
} catch (error) {
console.error('PWR retry failed:', error);
const errorType = categorizeError(error.message);
showDownloadError(`Game retry failed: ${error.message}`, true, errorType, error);
} finally {
if (progressPWRRetryBtn) {
progressPWRRetryBtn.disabled = false;
progressPWRRetryBtn.textContent = error && error.isJREError ? 'Retry Java Download' : 'Retry Game Download';
progressPWRRetryBtn.classList.remove('retrying');
}
currentDownloadState.isDownloading = false;
}
});
}
// Setup generic retry button (fallback)
if (progressRetryBtn) {
progressRetryBtn.addEventListener('click', async () => {
if (!currentDownloadState.canRetry || currentDownloadState.isDownloading) {
return;
}
progressRetryBtn.disabled = true;
progressRetryBtn.textContent = 'Retrying...';
progressRetryBtn.classList.add('retrying');
currentDownloadState.isDownloading = true;
try {
hideDownloadError();
if (progressRetryInfo) {
progressRetryInfo.style.background = '';
progressRetryInfo.style.color = '';
}
if (progressText) {
const contextMessage = getRetryContextMessage();
progressText.textContent = contextMessage;
}
if (!currentDownloadState.retryData) {
if (currentDownloadState.errorType === 'jre') {
currentDownloadState.retryData = {
isJREError: true,
jreUrl: '',
fileName: 'jre.tar.gz',
cacheDir: '',
osName: 'linux',
arch: 'amd64'
};
} else {
currentDownloadState.retryData = {
branch: 'release',
fileName: '4.pwr'
};
}
console.log('[UI] Created default retry data:', currentDownloadState.retryData);
}
// Send retry request to backend
if (window.electronAPI && window.electronAPI.retryDownload) {
const result = await window.electronAPI.retryDownload(currentDownloadState.retryData);
if (!result.success) {
throw new Error(result.error || 'Retry failed');
}
} else {
// Fallback for development/testing
console.warn('electronAPI.retryDownload not available, simulating retry...');
await new Promise(resolve => setTimeout(resolve, 2000));
throw new Error('Retry API not available');
@@ -895,15 +1061,17 @@ function setupRetryButton() {
console.error('Retry failed:', error);
const errorType = categorizeError(error.message);
showDownloadError(`Retry failed: ${error.message}`, true, errorType);
// Reset retry button
} finally {
if (progressRetryBtn) {
progressRetryBtn.disabled = false;
progressRetryBtn.textContent = '🔄 Retry Download';
progressRetryBtn.textContent = 'Retry Download';
progressRetryBtn.classList.remove('retrying');
}
currentDownloadState.isDownloading = false;
}
});
}
}
function getRetryContextMessage() {
const errorType = currentDownloadState.errorType;
@@ -925,6 +1093,8 @@ function getRetryContextMessage() {
return 'Retrying with corrected permissions...';
case 'conflict':
return 'Retrying after resolving conflicts...';
case 'jre':
return 'Re-downloading Java runtime...';
default:
return 'Initiating retry download...';
}

149
GUI/js/updater.js Normal file
View File

@@ -0,0 +1,149 @@
// Launcher Update Manager UI
let updateModal = null;
let downloadProgressBar = null;
function initUpdater() {
// Listen for update events from main process
if (window.electronAPI && window.electronAPI.onUpdateAvailable) {
window.electronAPI.onUpdateAvailable((updateInfo) => {
showUpdateModal(updateInfo);
});
}
if (window.electronAPI && window.electronAPI.onUpdateDownloadProgress) {
window.electronAPI.onUpdateDownloadProgress((progress) => {
updateDownloadProgress(progress);
});
}
if (window.electronAPI && window.electronAPI.onUpdateDownloaded) {
window.electronAPI.onUpdateDownloaded((info) => {
showInstallUpdatePrompt(info);
});
}
}
function showUpdateModal(updateInfo) {
if (updateModal) {
updateModal.remove();
}
updateModal = document.createElement('div');
updateModal.className = 'update-modal-overlay';
updateModal.innerHTML = `
<div class="update-modal">
<div class="update-header">
<i class="fas fa-download"></i>
<h2>Launcher Update Available</h2>
</div>
<div class="update-content">
<p class="update-version">Version ${updateInfo.newVersion} is available!</p>
<p class="current-version">Current version: ${updateInfo.currentVersion}</p>
${updateInfo.releaseNotes ? `<div class="release-notes">${updateInfo.releaseNotes}</div>` : ''}
</div>
<div class="update-progress" style="display: none;">
<div class="progress-bar-container">
<div class="progress-bar" id="updateProgressBar"></div>
</div>
<p class="progress-text" id="updateProgressText">Downloading...</p>
</div>
<div class="update-actions">
<button class="btn-primary" onclick="downloadUpdate()">
<i class="fas fa-download"></i> Download Update
</button>
</div>
</div>
`;
document.body.appendChild(updateModal);
}
async function downloadUpdate() {
const downloadBtn = updateModal.querySelector('.btn-primary');
const progressDiv = updateModal.querySelector('.update-progress');
// Disable button and show progress
downloadBtn.disabled = true;
progressDiv.style.display = 'block';
try {
await window.electronAPI.downloadUpdate();
} catch (error) {
console.error('Failed to download update:', error);
alert('Failed to download update. Please try again later.');
dismissUpdateModal();
}
}
function updateDownloadProgress(progress) {
if (!updateModal) return;
const progressBar = document.getElementById('updateProgressBar');
const progressText = document.getElementById('updateProgressText');
if (progressBar) {
progressBar.style.width = `${progress.percent}%`;
}
if (progressText) {
const mbTransferred = (progress.transferred / 1024 / 1024).toFixed(2);
const mbTotal = (progress.total / 1024 / 1024).toFixed(2);
const speed = (progress.bytesPerSecond / 1024 / 1024).toFixed(2);
progressText.textContent = `Downloading... ${mbTransferred}MB / ${mbTotal}MB (${speed} MB/s)`;
}
}
function showInstallUpdatePrompt(info) {
if (updateModal) {
updateModal.remove();
}
updateModal = document.createElement('div');
updateModal.className = 'update-modal-overlay';
updateModal.innerHTML = `
<div class="update-modal">
<div class="update-header">
<i class="fas fa-check-circle"></i>
<h2>Update Downloaded</h2>
</div>
<div class="update-content">
<p>Version ${info.version} has been downloaded and is ready to install.</p>
<p class="update-note">The launcher will restart to complete the installation.</p>
</div>
<div class="update-actions">
<button class="btn-primary" onclick="installUpdate()">
<i class="fas fa-sync-alt"></i> Restart & Install
</button>
</div>
</div>
`;
document.body.appendChild(updateModal);
}
async function installUpdate() {
try {
await window.electronAPI.installUpdate();
} catch (error) {
console.error('Failed to install update:', error);
}
}
function dismissUpdateModal() {
if (updateModal) {
updateModal.remove();
updateModal = null;
}
}
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', initUpdater);
// Export functions
window.UpdaterUI = {
showUpdateModal,
dismissUpdateModal,
downloadUpdate,
installUpdate
};

View File

@@ -57,7 +57,9 @@
"noDescription": "No description available",
"confirmDelete": "Are you sure you want to delete \"{name}\"?",
"confirmDeleteDesc": "This action cannot be undone.",
"confirmDeletion": "Confirm Deletion"
"confirmDeletion": "Confirm Deletion",
"apiKeyRequired": "API Key Required",
"apiKeyRequiredDesc": "CurseForge API key is needed to browse mods"
},
"news": {
"title": "ALL NEWS",

View File

@@ -14,9 +14,11 @@
"install": {
"title": "LAUNCHER GRATUITO",
"playerName": "Nombre del Jugador",
"playerNamePlaceholder": "Ingresa tu nombre", "gameBranch": "Versión del Juego",
"playerNamePlaceholder": "Ingresa tu nombre",
"gameBranch": "Versión del Juego",
"releaseVersion": "Lanzamiento (Estable)",
"preReleaseVersion": "Pre-Lanzamiento (Experimental)", "customInstallation": "Instalación Personalizada",
"preReleaseVersion": "Pre-Lanzamiento (Experimental)",
"customInstallation": "Instalación Personalizada",
"installationFolder": "Carpeta de Instalación",
"pathPlaceholder": "Ubicación predeterminada",
"browse": "Examinar",
@@ -55,7 +57,9 @@
"noDescription": "Sin descripción disponible",
"confirmDelete": "¿Estás seguro de que quieres eliminar \"{name}\"?",
"confirmDeleteDesc": "Esta acción no se puede deshacer.",
"confirmDeletion": "Confirmar eliminación"
"confirmDeletion": "Confirmar eliminación",
"apiKeyRequired": "Clave API Requerida",
"apiKeyRequiredDesc": "Se necesita una clave API de CurseForge para explorar mods"
},
"news": {
"title": "TODAS LAS NOTICIAS",

View File

@@ -55,7 +55,9 @@
"noDescription": "Nenhuma descrição disponível",
"confirmDelete": "Tem certeza de que deseja excluir \"{name}\"?",
"confirmDeleteDesc": "Esta ação não pode ser desfeita.",
"confirmDeletion": "Confirmar exclusão"
"confirmDeletion": "Confirmar exclusão",
"apiKeyRequired": "Chave de API Necessária",
"apiKeyRequiredDesc": "Chave de API do CurseForge é necessária para procurar mods"
},
"news": {
"title": "TODAS AS NOTÍCIAS",

246
GUI/locales/tr-TR.json Normal file
View File

@@ -0,0 +1,246 @@
{
"nav": {
"play": "Oyna",
"mods": "Modlar",
"news": "Haberler",
"chat": "Oyuncu Sohbeti",
"settings": "Ayarlar"
},
"header": {
"playersLabel": "Oyuncular:",
"manageProfiles": "Profilleri Yönet",
"defaultProfile": "Varsayılan"
},
"install": {
"title": "ÜCRETSİZ OYNA BAŞLATICI",
"playerName": "Oyuncu Adı",
"playerNamePlaceholder": "Adınızı girin",
"gameBranch": "Oyun Sürümü",
"releaseVersion": "Yayın (Stabil)",
"preReleaseVersion": "Ön-Yayın (Deneysel)",
"customInstallation": "Özel Kurulum",
"installationFolder": "Kurulum Klasörü",
"pathPlaceholder": "Varsayılan konum",
"browse": "Gözat",
"installButton": "HYTALE KURU",
"installing": "KURULUYOR..."
},
"play": {
"ready": "OYNAMAYA HAZIR",
"subtitle": "Hytale'i başlat ve maceraya başla",
"playButton": "HYTALE'YI OYNA",
"latestNews": "SON HABERLER",
"viewAll": "HEPSINI GÖR",
"checking": "KONTROL EDİLİYOR...",
"play": "OYNA"
},
"mods": {
"searchPlaceholder": "Modları ara...",
"myMods": "BENİM MODLARIM",
"previous": "ÖNCEKİ",
"next": "SONRAKİ",
"page": "Sayfa",
"of": "nın",
"modalTitle": "BENİM MODLARIM",
"noModsFound": "Mod Bulunamadı",
"noModsFoundDesc": "Aramanızı ayarlamayı deneyin",
"noModsInstalled": "Hiçbir Mod Kurulu Değil",
"noModsInstalledDesc": "CurseForge'dan modlar ekleyin veya yerel dosyalar içe aktarın",
"view": "GÖR",
"install": "KURU",
"installed": "KURULU",
"enable": "ETKİNLEŞTİR",
"disable": "DEĞİ",
"active": "AKTİF",
"disabled": "DEĞİ",
"delete": "Modı sil",
"noDescription": "Açıklama yok",
"confirmDelete": "\"{name}\" öğesini silmek istediğinizden emin misiniz?",
"confirmDeleteDesc": "Bu işlem geri alınamaz.",
"confirmDeletion": "Silmeyi Onayla",
"apiKeyRequired": "API Anahtarı Gerekli",
"apiKeyRequiredDesc": "Modlara göz atmak için CurseForge API anahtarı gereklidir"
},
"news": {
"title": "TÜM HABERLER",
"readMore": "Daha Fazla Oku"
},
"chat": {
"title": "OYUNCU SOHBETI",
"pickColor": "Renk",
"inputPlaceholder": "Mesajınızı yazın...",
"send": "Gönder",
"online": "çevrimiçi",
"charCounter": "{current}/{max}",
"secureChat": "Güvenli sohbet - Bağlantılar sansürlenir",
"joinChat": "Sohbete Katıl",
"chooseUsername": "Oyuncu Sohbetine katılmak için bir kullanıcı adı seçin",
"username": "Kullanıcı Adı",
"usernamePlaceholder": "Kullanıcı adınızı girin...",
"usernameHint": "3-20 karakter, yalnızca harfler, sayılar, - ve _",
"joinButton": "Sohbete Katıl",
"colorModal": {
"title": "Kullanıcı Adı Rengini Özelleştir",
"chooseSolid": "Düz bir renk seçin:",
"customColor": "Özel renk:",
"preview": "Ön izleme:",
"previewUsername": "Kullanıcı Adı",
"apply": "Rengi Uygula"
}
},
"settings": {
"title": "AYARLAR",
"java": "Java Çalışma Zamanı",
"useCustomJava": "Özel Java Yolunu Kullan",
"javaDescription": "Yüklü Java çalışma zamanını kendi kurulumunuzla geçersiz kılın",
"javaPath": "Java Çalıştırılabilir Yolu",
"javaPathPlaceholder": "Java yolunu seçin...",
"javaBrowse": "Gözat",
"javaHint": "Java kurulum klasörünü seçin (Windows, Mac, Linux destekler)",
"discord": "Discord Entegrasyonu",
"enableRPC": "Discord Rich Presence'ı Etkinleştir",
"discordDescription": "Başlatıcı etkinliğinizi Discord'da gösterin",
"game": "Oyun Seçenekleri",
"playerName": "Oyuncu Adı",
"playerNamePlaceholder": "Oyuncu adınızı girin",
"playerNameHint": "Bu ad oyun içinde kullanılacak (1-16 karakter)",
"openGameLocation": "Oyun Konumunu Aç",
"openGameLocationDesc": "Oyun kurulum klasörünü açın",
"account": "Oyuncu UUID Yönetimi",
"currentUUID": "Geçerli UUID",
"uuidPlaceholder": "UUID yükleniyor...",
"copyUUID": "UUID'yi Kopyala",
"regenerateUUID": "UUID'yi Yeniden Oluştur",
"uuidHint": "Bu kullanıcı adı için benzersiz oyuncu tanımlayıcınız",
"manageUUIDs": "Tüm UUID'leri Yönet",
"manageUUIDsDesc": "Tüm oyuncu UUID'lerini görüntüleyin ve yönetin",
"language": "Dil",
"selectLanguage": "Dil Seçin",
"repairGame": "Oyunu Onarı",
"reinstallGame": "Oyun dosyalarını yeniden kur (veri korur)",
"gpuPreference": "GPU Tercihi",
"gpuHint": "Tercih ettiğiniz GPU'yu seçin (Linux: DRI_PRIME'ı etkiler)",
"gpuAuto": "Otomatik",
"gpuIntegrated": "Entegre",
"gpuDedicated": "Ayrılmış",
"logs": "SİSTEM KAYITLARI",
"logsCopy": "Kopyala",
"logsRefresh": "Yenile",
"logsFolder": "Klasörü Aç",
"logsLoading": "Loglar yükleniyor...",
"closeLauncher": "Başlatıcı Davranışı",
"closeOnStart": "Oyun başlatıldığında Başlatıcıyı Kapat",
"closeOnStartDescription": "Hytale başlatıldıktan sonra başlatıcıyı otomatik olarak kapatın",
"gameBranch": "Oyun Dalı",
"branchRelease": "Yayın",
"branchPreRelease": "Ön-Yayın",
"branchHint": "Stabil yayın ve deneysel ön-yayın sürümleri arasında geçiş yapın",
"branchWarning": "Dalı değiştirmek farklı bir oyun sürümünü indirecek ve kuracaktır",
"branchSwitching": "{branch} sürümüne geçiliyor...",
"branchSwitched": "{branch} sürümüne başarıyla geçildi!",
"installRequired": "Kurulum Gerekli",
"branchInstallConfirm": "Oyun {branch} dalı için kurulacak. Devam et?"
},
"uuid": {
"modalTitle": "UUID Yönetimi",
"currentUserUUID": "Geçerli Kullanıcı UUID",
"allPlayerUUIDs": "Tüm Oyuncu UUID'leri",
"generateNew": "Yeni UUID Oluştur",
"loadingUUIDs": "UUID'ler yükleniyor...",
"setCustomUUID": "Özel UUID Ayarla",
"customPlaceholder": "Özel UUID girin (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)",
"setUUID": "UUID Ayarla",
"warning": "Uyarı: Özel bir UUID ayarlamak geçerli oyuncu kimliğinizi değiştirecektir",
"copyTooltip": "UUID'yi Kopyala",
"regenerateTooltip": "Yeni UUID Oluştur"
},
"profiles": {
"modalTitle": "Profilleri Yönet",
"newProfilePlaceholder": "Yeni Profil Adı",
"createProfile": "Profil Oluştur"
},
"discord": {
"notificationText": "Discord topluluğumuza katılın!",
"joinButton": "Discord'a Katıl"
},
"common": {
"confirm": "Onayla",
"cancel": "İptal",
"save": "Kaydet",
"close": "Kapat",
"delete": "Sil",
"edit": "Düzenle",
"loading": "Yükleniyor...",
"apply": "Uygula",
"install": "Kur"
},
"notifications": {
"gameDataNotFound": "Hata: Oyun verileri bulunamadı",
"gameUpdatedSuccess": "Oyun başarıyla güncellendi! 🎉",
"updateFailed": "Güncelleme başarısız: {error}",
"updateError": "Güncelleme hatası: {error}",
"discordEnabled": "Discord Rich Presence etkinleştirildi",
"discordDisabled": "Discord Rich Presence devre dışı bırakıldı",
"discordSaveFailed": "Discord ayarı kaydedilemedi",
"playerNameRequired": "Lütfen geçerli bir oyuncu adı girin",
"playerNameSaved": "Oyuncu adı başarıyla kaydedildi",
"playerNameSaveFailed": "Oyuncu adı kaydedilemedi",
"uuidCopied": "UUID panoya kopyalandı!",
"uuidCopyFailed": "UUID kopyalanamadı",
"uuidRegenNotAvailable": "UUID yeniden oluşturma kullanılamıyor",
"uuidRegenFailed": "UUID yeniden oluşturulamadı",
"uuidGenerated": "Yeni UUID başarıyla oluşturuldu!",
"uuidGeneratedShort": "Yeni UUID oluşturuldu!",
"uuidGenerateFailed": "Yeni UUID oluşturulamadı",
"uuidRequired": "Lütfen bir UUID girin",
"uuidInvalidFormat": "Geçersiz UUID formatı",
"uuidSetFailed": "Özel UUID ayarlanamadı",
"uuidSetSuccess": "Özel UUID başarıyla ayarlandı!",
"uuidDeleteFailed": "UUID silinemedi",
"uuidDeleteSuccess": "UUID başarıyla silindi!",
"modsDownloading": "{name} indiriliyor...",
"modsTogglingMod": "Mod değiştiriliyor...",
"modsDeletingMod": "Mod siliniyor...",
"modsLoadingMods": "CurseForge'dan modlar yükleniyor...",
"modsInstalledSuccess": "{name} başarıyla kuruldu! 🎉",
"modsDeletedSuccess": "{name} başarıyla silindi",
"modsDownloadFailed": "Mod indirilemedi: {error}",
"modsToggleFailed": "Mod değiştirilemedi: {error}",
"modsDeleteFailed": "Mod silinemedi: {error}",
"modsModNotFound": "Mod bilgileri bulunamadı"
},
"confirm": {
"defaultTitle": "Eylemi onayla",
"regenerateUuidTitle": "Yeni UUID oluştur",
"regenerateUuidMessage": "Yeni bir UUID oluşturmak istediğinizden emin misiniz? Bu oyuncu kimliğinizi değiştirecektir.",
"regenerateUuidButton": "Oluştur",
"setCustomUuidTitle": "Özel UUID ayarla",
"setCustomUuidMessage": "Bu özel UUID'yi ayarlamak istediğinizden emin misiniz? Bu oyuncu kimliğinizi değiştirecektir.",
"setCustomUuidButton": "UUID Ayarla",
"deleteUuidTitle": "UUID'yi sil",
"deleteUuidMessage": "\"{username}\" için UUID'yi silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.",
"deleteUuidButton": "Sil",
"uninstallGameTitle": "Oyunu kaldır",
"uninstallGameMessage": "Hytale'yi kaldırmak istediğinizden emin misiniz? Tüm oyun dosyaları silinecektir.",
"uninstallGameButton": "Kaldır"
},
"progress": {
"initializing": "Başlatılıyor...",
"downloading": "İndiriliyor...",
"installing": "Kuruluyur...",
"extracting": "Ayıklanıyor...",
"verifying": "Doğrulanıyor...",
"switchingProfile": "Profil değiştiriliyor...",
"profileSwitched": "Profil değiştirildi!",
"startingGame": "Oyun başlatılıyor...",
"launching": "BAŞLATILIYOR...",
"uninstallingGame": "Oyun kaldırılıyor...",
"gameUninstalled": "Oyun başarıyla kaldırıldı!",
"uninstallFailed": "Kaldırma başarısız: {error}",
"startingUpdate": "Zorunlu oyun güncellemesi başlatılıyor...",
"installationComplete": "Kurulum başarıyla tamamlandı!",
"installationFailed": "Kurulum başarısız: {error}",
"installingGameFiles": "Oyun dosyaları kuruluyor...",
"installComplete": "Kurulum tamamlandı!"
}
}

View File

@@ -1815,6 +1815,12 @@ body {
gap: 0.75rem;
}
.progress-retry-buttons {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.progress-retry-info {
color: #fbbf24;
font-family: 'JetBrains Mono', monospace;
@@ -5985,3 +5991,166 @@ select.settings-input option {
opacity: 1;
}
}
/* Launcher Update Modal Styles */
.update-modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(10px);
display: flex;
align-items: center;
justify-content: center;
z-index: 100000;
animation: fadeIn 0.3s ease;
}
.update-modal {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
border: 2px solid rgba(147, 51, 234, 0.3);
border-radius: 16px;
padding: 2rem;
max-width: 500px;
width: 90%;
box-shadow: 0 20px 60px rgba(147, 51, 234, 0.3);
animation: slideUp 0.3s ease;
}
@keyframes slideUp {
from {
transform: translateY(30px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.update-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1.5rem;
color: #9333ea;
}
.update-header i {
font-size: 2rem;
}
.update-header h2 {
margin: 0;
font-size: 1.5rem;
color: #fff;
}
.update-content {
color: #e0e0e0;
margin-bottom: 1.5rem;
}
.update-version {
font-size: 1.2rem;
font-weight: 600;
color: #9333ea;
margin-bottom: 0.5rem;
}
.current-version {
color: #888;
font-size: 0.9rem;
margin-bottom: 1rem;
}
.release-notes {
background: rgba(0, 0, 0, 0.3);
border-left: 3px solid #9333ea;
padding: 1rem;
border-radius: 8px;
max-height: 200px;
overflow-y: auto;
font-size: 0.9rem;
line-height: 1.6;
}
.update-progress {
margin-bottom: 1.5rem;
}
.progress-bar-container {
background: rgba(0, 0, 0, 0.3);
border-radius: 10px;
height: 20px;
overflow: hidden;
margin-bottom: 0.5rem;
}
.progress-bar {
height: 100%;
background: linear-gradient(90deg, #9333ea 0%, #7c3aed 100%);
width: 0%;
transition: width 0.3s ease;
border-radius: 10px;
}
.progress-text {
color: #aaa;
font-size: 0.9rem;
text-align: center;
margin: 0;
}
.update-note {
background: rgba(147, 51, 234, 0.1);
border: 1px solid rgba(147, 51, 234, 0.3);
padding: 0.75rem;
border-radius: 8px;
font-size: 0.9rem;
margin-top: 1rem;
}
.update-actions {
display: flex;
gap: 1rem;
justify-content: flex-end;
}
.update-actions button {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 0.5rem;
}
.update-actions .btn-primary {
background: linear-gradient(135deg, #9333ea 0%, #7c3aed 100%);
color: white;
}
.update-actions .btn-primary:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(147, 51, 234, 0.4);
}
.update-actions .btn-secondary {
background: rgba(255, 255, 255, 0.1);
color: #e0e0e0;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.update-actions .btn-secondary:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.15);
}
.update-actions button:disabled {
opacity: 0.5;
cursor: not-allowed;
}

View File

@@ -15,12 +15,6 @@ class AppUpdater {
}
setupAutoUpdater() {
// Enable dev mode for testing (reads dev-app-update.yml)
// Only enable in development, not in production builds
if (process.env.NODE_ENV === 'development' || !app.isPackaged) {
autoUpdater.forceDevUpdateConfig = true;
console.log('Dev update mode enabled - using dev-app-update.yml');
}
// Configure logger for electron-updater
// Create a compatible logger interface
@@ -176,7 +170,7 @@ class AppUpdater {
console.warn('macOS update error: App may not be code-signed. Auto-update requires code signing.');
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
this.mainWindow.webContents.send('update-error', {
message: 'Auto-update requires code signing. Please download manually from GitHub.',
message: 'Please download manually from GitHub.',
code: err.code,
isMacSigningError: true,
requiresManualDownload: true,

View File

@@ -520,9 +520,17 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver
try {
await downloadJRE(progressCallback, customCacheDir, customJreDir);
} catch (error) {
// Don't immediately fall back to system Java for JRE download errors - let user retry
if (error.isJREError) {
console.error('[Install] JRE download failed, allowing user retry:', error.message);
throw error; // Re-throw JRE errors to trigger retry UI
}
// For non-download JRE errors, fall back to system Java
const fallback = await detectSystemJava();
if (fallback) {
javaBin = fallback;
console.log('[Install] Using system Java as fallback');
} else {
throw error;
}

View File

@@ -9,7 +9,7 @@ const tar = require('tar');
const { expandHome, JRE_DIR } = require('../core/paths');
const { getOS, getArch } = require('../utils/platformUtils');
const { loadConfig } = require('../core/config');
const { downloadFile } = require('../utils/fileManager');
const { downloadFile, retryDownload } = require('../utils/fileManager');
const execFileAsync = promisify(execFile);
const JAVA_EXECUTABLE = 'java' + (process.platform === 'win32' ? '.exe' : '');
@@ -188,6 +188,20 @@ async function getJavaDetection() {
};
}
// Manual retry function for JRE downloads
async function retryJREDownload(url, cacheFile, progressCallback) {
console.log('Initiating manual JRE retry...');
// Ensure cache directory exists before retrying
const cacheDir = path.dirname(cacheFile);
if (!fs.existsSync(cacheDir)) {
console.log('Creating JRE cache directory:', cacheDir);
fs.mkdirSync(cacheDir, { recursive: true });
}
return await retryDownload(url, cacheFile, progressCallback);
}
async function downloadJRE(progressCallback, cacheDir, jreDir = JRE_DIR) {
if (!fs.existsSync(cacheDir)) {
fs.mkdirSync(cacheDir, { recursive: true });
@@ -230,7 +244,40 @@ async function downloadJRE(progressCallback, cacheDir, jreDir = JRE_DIR) {
progressCallback('Fetching Java runtime...', null, null, null, null);
}
console.log('Fetching Java runtime...');
await downloadFile(platform.url, cacheFile, progressCallback);
let jreFile;
try {
jreFile = await downloadFile(platform.url, cacheFile, progressCallback);
// If downloadFile returns false or undefined, it means the download failed
// We should retry the download with a manual retry
if (!jreFile || typeof jreFile !== 'string') {
console.log('[JRE Download] JRE file download failed or incomplete, attempting retry...');
jreFile = await retryJREDownload(platform.url, cacheFile, progressCallback);
}
// Double-check we have a valid file
if (!jreFile || typeof jreFile !== 'string') {
throw new Error(`JRE download failed: received invalid path ${jreFile}. Please retry download.`);
}
} catch (downloadError) {
console.error('[JRE Download] JRE download failed:', downloadError.message);
// Enhance error with retry information for the UI
const enhancedError = new Error(`JRE download failed: ${downloadError.message}`);
enhancedError.originalError = downloadError;
enhancedError.canRetry = downloadError.isConnectionLost ? false : (downloadError.canRetry !== false);
enhancedError.jreUrl = platform.url;
enhancedError.jreDest = cacheFile;
enhancedError.osName = osName;
enhancedError.arch = arch;
enhancedError.fileName = fileName;
enhancedError.cacheDir = cacheDir;
enhancedError.isJREError = true; // Flag to identify JRE errors
enhancedError.isConnectionLost = downloadError.isConnectionLost || false;
throw enhancedError;
}
console.log('Download finished');
}

View File

@@ -423,7 +423,8 @@ async function downloadFile(url, dest, progressCallback, maxRetries = 5) {
const canRetry = (error.canRetry === false) ? false : isRetryable;
if (!canRetry || attempt === maxRetries - 1) {
retryState.canRetry = false;
// Don't set retryState.canRetry to false for max retries - user should still be able to retry manually
retryState.canRetry = error.canRetry === false ? false : true;
console.error(`Non-retryable error or max retries reached: ${error.code || error.message}`);
break;
}
@@ -439,6 +440,9 @@ async function downloadFile(url, dest, progressCallback, maxRetries = 5) {
enhancedError.retryState = retryState;
enhancedError.lastError = lastError;
enhancedError.detailedError = detailedError;
// Allow manual retry unless it's a connection lost error
enhancedError.canRetry = !lastError?.isConnectionLost && lastError?.canRetry !== false;
throw enhancedError;
}
@@ -543,6 +547,13 @@ async function retryDownload(url, dest, progressCallback, previousError = null)
additionalRetries = Math.max(2, 5 - previousError.retryState.attempts);
}
// Ensure cache directory exists before retrying
const destDir = path.dirname(dest);
if (!fs.existsSync(destDir)) {
console.log('Creating cache directory:', destDir);
fs.mkdirSync(destDir, { recursive: true });
}
try {
await downloadFile(url, dest, progressCallback, additionalRetries);
console.log('Manual retry successful');

192
main.js
View File

@@ -1,8 +1,10 @@
const path = require('path');
require('dotenv').config({ path: path.join(__dirname, '.env') });
const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron');
const { autoUpdater } = require('electron-updater');
const fs = require('fs');
const { launchGame, launchGameWithVersionCheck, installGame, saveUsername, loadUsername, saveChatUsername, loadChatUsername, saveChatColor, loadChatColor, saveJavaPath, loadJavaPath, saveInstallPath, loadInstallPath, saveDiscordRPC, loadDiscordRPC, saveLanguage, loadLanguage, saveCloseLauncherOnStart, loadCloseLauncherOnStart, isGameInstalled, uninstallGame, repairGame, getHytaleNews, handleFirstLaunchCheck, proposeGameUpdate, markAsLaunched } = require('./backend/launcher');
const { retryPWRDownload } = require('./backend/managers/gameManager');
const logger = require('./backend/logger');
const profileManager = require('./backend/managers/profileManager');
@@ -160,7 +162,60 @@ function createWindow() {
// Initialize Discord Rich Presence
initDiscordRPC();
// Auto-updates handled by electron-updater
// Configure and initialize electron-updater
autoUpdater.autoDownload = false;
autoUpdater.autoInstallOnAppQuit = true;
autoUpdater.on('checking-for-update', () => {
console.log('Checking for launcher updates...');
});
autoUpdater.on('update-available', (info) => {
console.log('Update available:', info.version);
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('update-available', {
currentVersion: app.getVersion(),
newVersion: info.version,
releaseNotes: info.releaseNotes,
releaseDate: info.releaseDate
});
}
});
autoUpdater.on('update-not-available', (info) => {
console.log('Launcher is up to date:', info.version);
});
autoUpdater.on('error', (err) => {
console.error('Error in auto-updater:', err);
});
autoUpdater.on('download-progress', (progressObj) => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('update-download-progress', {
percent: progressObj.percent,
transferred: progressObj.transferred,
total: progressObj.total,
bytesPerSecond: progressObj.bytesPerSecond
});
}
});
autoUpdater.on('update-downloaded', (info) => {
console.log('Update downloaded:', info.version);
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('update-downloaded', {
version: info.version
});
}
});
// Check for updates after 3 seconds
setTimeout(() => {
autoUpdater.checkForUpdates().catch(err => {
console.log('Failed to check for updates:', err.message);
});
}, 3000);
mainWindow.webContents.on('devtools-opened', () => {
mainWindow.webContents.closeDevTools();
@@ -430,37 +485,41 @@ ipcMain.handle('install-game', async (event, playerName, javaPath, installPath,
console.log('[Main] Returning success response for install-game:', successResponse);
return successResponse;
} catch (error) {
console.error('Install error:', error);
// console.error('Install error:', error);
const errorMessage = error.message || error.toString();
// Enhanced error data extraction for both download and Butler errors
let errorData = {
message: errorMessage,
error: true,
canRetry: true,
canRetry: true, // Default to true, will be overridden by specific error props
retryData: null
};
// Prioritize JRE errors first
if (error.isJREError) {
console.log('[Main] Processing JRE download error with retry context');
errorData.retryData = {
isJREError: true,
jreUrl: error.jreUrl,
fileName: error.fileName,
cacheDir: error.cacheDir,
osName: error.osName,
arch: error.arch
};
// For JRE errors, allow manual retry unless explicitly disabled
errorData.canRetry = error.canRetry !== false;
errorData.errorType = 'jre';
}
// Handle Butler-specific errors
if (error.butlerError) {
else if (error.butlerError) {
console.log('[Main] Processing Butler error with retry context');
errorData.retryData = {
branch: error.branch || 'release',
fileName: error.fileName || '4.pwr',
cacheDir: error.cacheDir
};
errorData.canRetry = error.canRetry !== undefined ? error.canRetry : true;
// Add Butler-specific error details
if (error.stderr) {
console.error('[Main] Butler stderr:', error.stderr);
}
if (error.stdout) {
console.log('[Main] Butler stdout:', error.stdout);
}
if (error.errorCode) {
console.log('[Main] Butler error code:', error.errorCode);
}
errorData.canRetry = error.canRetry !== false;
}
// Handle PWR download errors
else if (error.branch && error.fileName) {
@@ -470,7 +529,7 @@ ipcMain.handle('install-game', async (event, playerName, javaPath, installPath,
fileName: error.fileName,
cacheDir: error.cacheDir
};
errorData.canRetry = error.canRetry !== undefined ? error.canRetry : true;
errorData.canRetry = error.canRetry !== false;
}
// Default fallback for other errors
else {
@@ -479,6 +538,8 @@ ipcMain.handle('install-game', async (event, playerName, javaPath, installPath,
branch: 'release',
fileName: '4.pwr'
};
// For generic errors, assume it's retryable unless specified
errorData.canRetry = error.canRetry !== false;
}
// Send enhanced error info for retry UI
@@ -636,7 +697,7 @@ ipcMain.handle('uninstall-game', async () => {
try {
await uninstallGame();
} catch (error) {
console.error('Uninstall error:', error);
// console.error('Uninstall error:', error);
return { success: false, error: error.message };
}
});
@@ -670,15 +731,6 @@ ipcMain.handle('retry-download', async (event, retryData) => {
try {
console.log('[IPC] retry-download called with data:', retryData);
// Handle null retry data gracefully
if (!retryData || !retryData.branch || !retryData.fileName) {
console.log('[IPC] Invalid retry data, using defaults');
retryData = {
branch: 'release',
fileName: '4.pwr'
};
}
const progressCallback = (message, percent, speed, downloaded, total, retryState) => {
if (mainWindow && !mainWindow.isDestroyed()) {
const data = {
@@ -693,14 +745,36 @@ ipcMain.handle('retry-download', async (event, retryData) => {
}
};
// Handle JRE download retries
if (retryData && retryData.isJREError) {
console.log(`[IPC] Retrying JRE download: jreUrl=${retryData.jreUrl}, fileName=${retryData.fileName}`);
console.log('[IPC] Full JRE retry data:', JSON.stringify(retryData, null, 2));
const { retryJREDownload } = require('./backend/managers/javaManager');
await retryJREDownload(retryData.jreUrl, jreCacheFile, progressCallback);
const jreCacheFile = path.join(retryData.cacheDir, retryData.fileName);
return { success: true };
}
// Handle PWR download retries (default)
if (!retryData || !retryData.branch || !retryData.fileName) {
console.log('[IPC] Invalid retry data, using PWR defaults');
retryData = {
branch: 'release',
fileName: '4.pwr'
};
}
// Extract PWR download info from retryData
const branch = retryData.branch;
const fileName = retryData.fileName;
const cacheDir = retryData.cacheDir;
console.log(`[IPC] Retrying PWR download: branch=${branch}, fileName=${fileName}`);
console.log('[IPC] Full PWR retry data:', JSON.stringify(retryData, null, 2));
// Perform the retry with enhanced context
// Perform retry with enhanced context
await retryPWRDownload(branch, fileName, progressCallback, cacheDir);
return { success: true };
@@ -710,15 +784,28 @@ ipcMain.handle('retry-download', async (event, retryData) => {
// Send error update to frontend with context
if (mainWindow && !mainWindow.isDestroyed()) {
const data = {
message: errorMessage,
error: true,
canRetry: true,
retryData: {
const isJreError = retryData?.isJREError;
const errorRetryData = isJreError ?
{
isJREError: true,
jreUrl: retryData?.jreUrl,
fileName: retryData?.fileName,
cacheDir: retryData?.cacheDir,
osName: retryData?.osName,
arch: retryData?.arch
} :
{
branch: retryData?.branch || 'release',
fileName: retryData?.fileName || '4.pwr',
cacheDir: retryData?.cacheDir
}
};
const data = {
message: errorMessage,
error: true,
canRetry: error.canRetry !== false, // Respect canRetry from the thrown error
retryData: errorRetryData,
errorType: isJreError ? 'jre' : 'general' // Add errorType for the UI
};
mainWindow.webContents.send('progress-update', data);
}
@@ -846,7 +933,6 @@ ipcMain.handle('load-settings', async () => {
});
const { getModsPath, loadInstalledMods, downloadMod, uninstallMod, toggleMod, getCurrentUuid, getAllUuidMappings, setUuidForUser, generateNewUuid, deleteUuidForUser, resetCurrentUserUuid } = require('./backend/launcher');
const { retryPWRDownload } = require('./backend/managers/gameManager');
const os = require('os');
ipcMain.handle('get-local-app-data', async () => {
@@ -953,14 +1039,38 @@ ipcMain.handle('copy-mod-file', async (event, sourcePath, modsPath) => {
}
});
// Auto-updates handled by electron-updater
// ipcMain.handle('check-for-updates', ...) - removed
// Electron-updater IPC handlers
ipcMain.handle('check-for-updates', async () => {
try {
const result = await autoUpdater.checkForUpdates();
return {
updateAvailable: result && result.updateInfo,
currentVersion: app.getVersion(),
updateInfo: result ? result.updateInfo : null
};
} catch (error) {
console.error('Error checking for updates:', error);
return { updateAvailable: false, error: error.message };
}
});
// Auto-updates handled by electron-updater
// ipcMain.handle('open-download-page', ...) - removed
ipcMain.handle('download-update', async () => {
try {
await autoUpdater.downloadUpdate();
return { success: true };
} catch (error) {
console.error('Error downloading update:', error);
return { success: false, error: error.message };
}
});
// Auto-updates handled by electron-updater
// ipcMain.handle('get-update-info', ...) - removed
ipcMain.handle('install-update', () => {
autoUpdater.quitAndInstall(false, true);
});
ipcMain.handle('get-launcher-version', () => {
return app.getVersion();
});
ipcMain.handle('get-gpu-info', () => {
try {

156
package-lock.json generated
View File

@@ -14,6 +14,7 @@
"discord-rpc": "^4.0.1",
"dotenv": "^17.2.3",
"electron-updater": "^6.7.3",
"fs-extra": "^11.3.3",
"tar": "^6.2.1",
"uuid": "^9.0.1"
},
@@ -147,6 +148,21 @@
"global-agent": "^3.0.0"
}
},
"node_modules/@electron/get/node_modules/fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
},
"engines": {
"node": ">=6 <7 || >=8"
}
},
"node_modules/@electron/notarize": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz",
@@ -345,34 +361,6 @@
"balanced-match": "^1.0.0"
}
},
"node_modules/@electron/universal/node_modules/fs-extra": {
"version": "11.3.3",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz",
"integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==",
"dev": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=14.14"
}
},
"node_modules/@electron/universal/node_modules/jsonfile": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
"dev": true,
"license": "MIT",
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/@electron/universal/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
@@ -389,16 +377,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@electron/universal/node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/@electron/windows-sign": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz",
@@ -406,7 +384,6 @@
"dev": true,
"license": "BSD-2-Clause",
"optional": true,
"peer": true,
"dependencies": {
"cross-dirname": "^0.1.0",
"debug": "^4.3.4",
@@ -421,50 +398,6 @@
"node": ">=14.14"
}
},
"node_modules/@electron/windows-sign/node_modules/fs-extra": {
"version": "11.3.3",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz",
"integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=14.14"
}
},
"node_modules/@electron/windows-sign/node_modules/jsonfile": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/@electron/windows-sign/node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/@isaacs/balanced-match": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
@@ -916,6 +849,7 @@
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -1679,8 +1613,7 @@
"integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true
"optional": true
},
"node_modules/cross-spawn": {
"version": "7.0.6",
@@ -1897,6 +1830,7 @@
"integrity": "sha512-ce4Ogns4VMeisIuCSK0C62umG0lFy012jd8LMZ6w/veHUeX4fqfDrGe+HTWALAEwK6JwKP+dhPvizhArSOsFbg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"app-builder-lib": "26.4.0",
"builder-util": "26.3.4",
@@ -2272,7 +2206,6 @@
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@electron/asar": "^3.2.1",
"debug": "^4.1.1",
@@ -2293,7 +2226,6 @@
"integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"graceful-fs": "^4.1.2",
"jsonfile": "^4.0.0",
@@ -2310,16 +2242,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/encoding": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
"license": "MIT",
"optional": true,
"dependencies": {
"iconv-lite": "^0.6.2"
}
},
"node_modules/end-of-stream": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
@@ -2612,18 +2534,38 @@
}
},
"node_modules/fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
"dev": true,
"version": "11.3.3",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz",
"integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==",
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=6 <7 || >=8"
"node": ">=14.14"
}
},
"node_modules/fs-extra/node_modules/jsonfile": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
"license": "MIT",
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/fs-extra/node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"license": "MIT",
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/fs-minipass": {
@@ -3561,7 +3503,6 @@
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"minimist": "^1.2.6"
},
@@ -3914,6 +3855,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -3943,7 +3885,6 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"commander": "^9.4.0"
},
@@ -3961,7 +3902,6 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": "^12.20.0 || >=14"
}
@@ -4158,7 +4098,6 @@
"deprecated": "Rimraf versions prior to v4 are no longer supported",
"dev": true,
"license": "ISC",
"peer": true,
"dependencies": {
"glob": "^7.1.3"
},
@@ -4619,7 +4558,6 @@
"integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"mkdirp": "^0.5.1",
"rimraf": "~2.6.2"

View File

@@ -115,5 +115,20 @@ contextBridge.exposeInMainWorld('electronAPI', {
activate: (id) => ipcRenderer.invoke('profile-activate', id),
delete: (id) => ipcRenderer.invoke('profile-delete', id),
update: (id, updates) => ipcRenderer.invoke('profile-update', id, updates)
},
// Launcher Update API
checkForUpdates: () => ipcRenderer.invoke('check-for-updates'),
downloadUpdate: () => ipcRenderer.invoke('download-update'),
installUpdate: () => ipcRenderer.invoke('install-update'),
getLauncherVersion: () => ipcRenderer.invoke('get-launcher-version'),
onUpdateAvailable: (callback) => {
ipcRenderer.on('update-available', (event, data) => callback(data));
},
onUpdateDownloadProgress: (callback) => {
ipcRenderer.on('update-download-progress', (event, data) => callback(data));
},
onUpdateDownloaded: (callback) => {
ipcRenderer.on('update-downloaded', (event, data) => callback(data));
}
});