feat: auto-resume download process & auto-retry if disconnected (#143)

This commit is contained in:
Fazri Gading
2026-01-25 01:36:20 +08:00
committed by GitHub
parent a6f716c61b
commit a7d0523186
7 changed files with 1503 additions and 247 deletions

View File

@@ -602,21 +602,30 @@
</div>
</div>
<div id="progressOverlay" class="progress-overlay" style="display: none;">
<div class="progress-content">
<div class="progress-info">
<span id="progressText" data-i18n="progress.initializing">Initializing...</span>
<span id="progressPercent">0%</span>
</div>
<div class="progress-bar-container">
<div id="progressBarFill" class="progress-bar-fill"></div>
</div>
<div class="progress-details">
<span id="progressSpeed"></span>
<span id="progressSize"></span>
</div>
</div>
</div>
<div id="progressOverlay" class="progress-overlay" style="display: none;">
<div class="progress-content">
<div class="progress-info">
<span id="progressText" data-i18n="progress.initializing">Initializing...</span>
<span id="progressPercent">0%</span>
</div>
<div class="progress-bar-container">
<div id="progressBarFill" class="progress-bar-fill"></div>
</div>
<div class="progress-details">
<span id="progressSpeed"></span>
<span id="progressSize"></span>
</div>
<div id="progressErrorContainer" class="progress-error-container" style="display: none;">
<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
</button>
</div>
</div>
</div>
</div>
<div id="chatUsernameModal" class="chat-username-modal" style="display: none;">
<div class="chat-username-modal-content">

View File

@@ -1,11 +1,27 @@
let progressOverlay;
let progressBar;
let progressBarFill;
let progressText;
let progressPercent;
let progressSpeed;
let progressSize;
let progressOverlay;
let progressBar;
let progressBarFill;
let progressText;
let progressPercent;
let progressSpeed;
let progressSize;
let progressErrorContainer;
let progressErrorMessage;
let progressRetryInfo;
let progressRetryBtn;
// Download retry state
let currentDownloadState = {
isDownloading: false,
canRetry: false,
retryData: null,
lastError: null,
errorType: null,
branch: null,
fileName: null,
cacheDir: null
};
function showPage(pageId) {
const pages = document.querySelectorAll('.page');
@@ -143,26 +159,108 @@ function hideProgress() {
}
}
function updateProgress(data) {
if (data.message && progressText) {
progressText.textContent = data.message;
}
if (data.percent !== null && data.percent !== undefined) {
const percent = Math.min(100, Math.max(0, Math.round(data.percent)));
if (progressPercent) progressPercent.textContent = `${percent}%`;
if (progressBarFill) progressBarFill.style.width = `${percent}%`;
if (progressBar) progressBar.style.width = `${percent}%`;
}
if (data.speed && data.downloaded && data.total) {
const speedMB = (data.speed / 1024 / 1024).toFixed(2);
const downloadedMB = (data.downloaded / 1024 / 1024).toFixed(2);
const totalMB = (data.total / 1024 / 1024).toFixed(2);
if (progressSpeed) progressSpeed.textContent = `${speedMB} MB/s`;
if (progressSize) progressSize.textContent = `${downloadedMB} / ${totalMB} MB`;
}
}
function updateProgress(data) {
// Handle retry state
if (data.retryState) {
currentDownloadState.retryData = data.retryState;
updateRetryState(data.retryState);
}
if (data.message && progressText) {
progressText.textContent = data.message;
}
if (data.percent !== null && data.percent !== undefined) {
const percent = Math.min(100, Math.max(0, Math.round(data.percent)));
if (progressPercent) progressPercent.textContent = `${percent}%`;
if (progressBarFill) progressBarFill.style.width = `${percent}%`;
if (progressBar) progressBar.style.width = `${percent}%`;
}
if (data.speed && data.downloaded && data.total) {
const speedMB = (data.speed / 1024 / 1024).toFixed(2);
const downloadedMB = (data.downloaded / 1024 / 1024).toFixed(2);
const totalMB = (data.total / 1024 / 1024).toFixed(2);
if (progressSpeed) progressSpeed.textContent = `${speedMB} MB/s`;
if (progressSize) progressSize.textContent = `${downloadedMB} / ${totalMB} MB`;
}
// Handle error states with enhanced categorization
// Don't show error during automatic retries - let the retry message display instead
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);
} else if (data.percent === 100) {
hideDownloadError();
} else if (data.retryState && data.retryState.isAutomaticRetry) {
// Hide any existing error during automatic retries
hideDownloadError();
}
}
function updateRetryState(retryState) {
if (!progressRetryInfo) return;
if (retryState.isAutomaticRetry && retryState.automaticStallRetries > 0) {
// Show automatic stall retry count
progressRetryInfo.textContent = `Auto-retry ${retryState.automaticStallRetries}/3`;
progressRetryInfo.style.display = 'block';
progressRetryInfo.style.background = 'rgba(255, 193, 7, 0.2)'; // Light orange background for auto-retries
progressRetryInfo.style.color = '#ff9800'; // Orange text for auto-retries
} else if (retryState.attempts > 1) {
// Show manual retry count
progressRetryInfo.textContent = `Attempt ${retryState.attempts}/${retryState.maxRetries}`;
progressRetryInfo.style.display = 'block';
progressRetryInfo.style.background = ''; // Reset background
progressRetryInfo.style.color = ''; // Reset color
} else {
progressRetryInfo.style.display = 'none';
progressRetryInfo.style.background = ''; // Reset background
progressRetryInfo.style.color = ''; // Reset color
}
}
function showDownloadError(errorMessage, canRetry = true, errorType = 'general') {
if (!progressErrorContainer || !progressErrorMessage || !progressRetryBtn) return;
currentDownloadState.lastError = errorMessage;
currentDownloadState.canRetry = canRetry;
currentDownloadState.errorType = errorType;
// Update retry context if available
if (data && data.retryData) {
currentDownloadState.branch = data.retryData.branch;
currentDownloadState.fileName = data.retryData.fileName;
currentDownloadState.cacheDir = data.retryData.cacheDir;
}
// User-friendly error messages
const userMessage = getErrorMessage(errorMessage, errorType);
progressErrorMessage.textContent = userMessage;
progressErrorContainer.style.display = 'block';
progressRetryBtn.style.display = canRetry ? 'block' : 'none';
// Add visual indicators based on error type
progressErrorContainer.className = `progress-error-container error-${errorType}`;
if (progressOverlay) {
progressOverlay.classList.add('error-state');
}
}
function hideDownloadError() {
if (!progressErrorContainer) return;
progressErrorContainer.style.display = 'none';
currentDownloadState.canRetry = false;
currentDownloadState.lastError = null;
currentDownloadState.errorType = null;
if (progressOverlay) {
progressOverlay.classList.remove('error-state');
}
}
function setupAnimations() {
document.body.style.opacity = '0';
@@ -470,17 +568,24 @@ function showNotification(message, type = 'info') {
}, 5000);
}
function setupUI() {
progressOverlay = document.getElementById('progressOverlay');
progressBar = document.getElementById('progressBar');
progressBarFill = document.getElementById('progressBarFill');
progressText = document.getElementById('progressText');
progressPercent = document.getElementById('progressPercent');
progressSpeed = document.getElementById('progressSpeed');
progressSize = document.getElementById('progressSize');
// Setup draggable progress bar
setupProgressDrag();
function setupUI() {
progressOverlay = document.getElementById('progressOverlay');
progressBar = document.getElementById('progressBar');
progressBarFill = document.getElementById('progressBarFill');
progressText = document.getElementById('progressText');
progressPercent = document.getElementById('progressPercent');
progressSpeed = document.getElementById('progressSpeed');
progressSize = document.getElementById('progressSize');
progressErrorContainer = document.getElementById('progressErrorContainer');
progressErrorMessage = document.getElementById('progressErrorMessage');
progressRetryInfo = document.getElementById('progressRetryInfo');
progressRetryBtn = document.getElementById('progressRetryBtn');
// Setup draggable progress bar
setupProgressDrag();
// Setup retry button
setupRetryButton();
lockPlayButton(true);
@@ -648,7 +753,175 @@ function toggleMaximize() {
}
}
// Make toggleMaximize globally available
window.toggleMaximize = toggleMaximize;
document.addEventListener('DOMContentLoaded', setupUI);
// Error categorization and user-friendly messages
function categorizeError(message) {
const msg = message.toLowerCase();
if (msg.includes('network') || msg.includes('connection') || msg.includes('offline')) {
return 'network';
} else if (msg.includes('stalled') || msg.includes('timeout')) {
return 'stall';
} else if (msg.includes('file') || msg.includes('disk')) {
return 'file';
} else if (msg.includes('permission') || msg.includes('access')) {
return 'permission';
} else if (msg.includes('server') || msg.includes('5')) {
return 'server';
} else if (msg.includes('corrupted') || msg.includes('pwr file') || msg.includes('unexpected eof')) {
return 'corruption';
} else if (msg.includes('butler') || msg.includes('patch installation')) {
return 'butler';
} else if (msg.includes('space') || msg.includes('full') || msg.includes('device full')) {
return 'space';
} else if (msg.includes('conflict') || msg.includes('already exists')) {
return 'conflict';
} else {
return 'general';
}
}
function getErrorMessage(technicalMessage, errorType) {
// Technical errors go to console, user gets friendly messages
console.error(`Download error [${errorType}]:`, technicalMessage);
switch (errorType) {
case 'network':
return 'Network connection lost. Please check your internet connection and retry.';
case 'stall':
return 'Download stalled due to slow connection. Please retry.';
case 'file':
return 'Unable to save file. Check disk space and permissions. Please retry.';
case 'permission':
return 'Permission denied. Check if launcher has write access. Please retry.';
case 'server':
return 'Server error. Please wait a moment and retry.';
case 'corruption':
return 'Corrupted PWR file detected. File deleted and will retry.';
case 'butler':
return 'Patch installation failed. Please retry.';
case 'space':
return 'Insufficient disk space. Free up space and retry.';
case 'conflict':
return 'Installation directory conflict. Please retry.';
default:
return 'Download failed. Please retry.';
}
}
// Connection quality indicator (simplified)
function updateConnectionQuality(quality) {
if (!progressSize) return;
const qualityColors = {
'Good': '#10b981',
'Fair': '#fbbf24',
'Poor': '#f87171'
};
const color = qualityColors[quality] || '#6b7280';
progressSize.style.color = color;
// Add subtle quality indicator
if (progressSize.dataset.quality !== quality) {
progressSize.dataset.quality = quality;
progressSize.style.transition = 'color 0.5s ease';
}
}
// Enhanced retry button setup
function setupRetryButton() {
if (!progressRetryBtn) return;
progressRetryBtn.addEventListener('click', async () => {
if (!currentDownloadState.canRetry || currentDownloadState.isDownloading) {
return;
}
// Disable retry button during retry
progressRetryBtn.disabled = true;
progressRetryBtn.textContent = '🔄 Retrying...';
progressRetryBtn.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) {
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');
}
} catch (error) {
console.error('Retry failed:', error);
const errorType = categorizeError(error.message);
showDownloadError(`Retry failed: ${error.message}`, true, errorType);
// Reset retry button
progressRetryBtn.disabled = false;
progressRetryBtn.textContent = '🔄 Retry Download';
progressRetryBtn.classList.remove('retrying');
currentDownloadState.isDownloading = false;
}
});
}
function getRetryContextMessage() {
const errorType = currentDownloadState.errorType;
switch (errorType) {
case 'network':
return 'Reconnecting and retrying download...';
case 'stall':
return 'Resuming stalled download...';
case 'server':
return 'Waiting for server and retrying...';
case 'corruption':
return 'Re-downloading corrupted PWR file...';
case 'butler':
return 'Re-attempting patch installation...';
case 'space':
return 'Retrying after clearing disk space...';
case 'permission':
return 'Retrying with corrected permissions...';
case 'conflict':
return 'Retrying after resolving conflicts...';
default:
return 'Initiating retry download...';
}
}
// Make toggleMaximize globally available
window.toggleMaximize = toggleMaximize;
document.addEventListener('DOMContentLoaded', setupUI);

View File

@@ -1770,15 +1770,246 @@ body {
animation: shimmer 2s infinite;
}
@keyframes shimmer {
0% {
left: -100%;
}
100% {
left: 100%;
}
}
@keyframes shimmer {
0% {
left: -100%;
}
100% {
left: 100%;
}
}
/* Progress Error and Retry Styles */
.progress-error-container {
margin-top: 0.75rem;
padding-top: 0.75rem;
border-top: 1px solid rgba(239, 68, 68, 0.3);
animation: errorSlideIn 0.3s ease-out;
}
@keyframes errorSlideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.progress-error-message {
color: #f87171;
font-family: 'JetBrains Mono', monospace;
font-size: 0.75rem;
margin-bottom: 0.5rem;
text-shadow: 0 0 8px rgba(248, 113, 113, 0.4);
line-height: 1.4;
}
.progress-retry-section {
display: flex;
justify-content: space-between;
align-items: center;
gap: 0.75rem;
}
.progress-retry-info {
color: #fbbf24;
font-family: 'JetBrains Mono', monospace;
font-size: 0.7rem;
flex: 1;
}
.progress-retry-btn {
background: linear-gradient(135deg, #dc2626, #ef4444);
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 6px;
font-family: 'JetBrains Mono', monospace;
font-size: 0.75rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(220, 38, 38, 0.3);
white-space: nowrap;
min-width: 120px;
}
.progress-retry-btn:hover {
background: linear-gradient(135deg, #b91c1c, #dc2626);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(220, 38, 38, 0.4);
}
.progress-retry-btn:active {
transform: translateY(0);
box-shadow: 0 2px 6px rgba(220, 38, 38, 0.3);
}
.progress-retry-btn:disabled {
background: linear-gradient(135deg, #4b5563, #6b7280);
cursor: not-allowed;
transform: none;
box-shadow: none;
opacity: 0.6;
}
/* Progress overlay error state */
.progress-overlay.error-state {
border-color: rgba(239, 68, 68, 0.5);
box-shadow:
0 4px 16px rgba(0, 0, 0, 0.5),
0 0 30px rgba(239, 68, 68, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.05);
}
.progress-overlay.error-state #progressBarFill {
background: linear-gradient(90deg, #dc2626, #ef4444);
animation: errorPulse 1.5s ease-in-out infinite;
}
@keyframes errorPulse {
0%, 100% {
opacity: 0.8;
}
50% {
opacity: 1;
}
}
/* Error type specific styling */
.progress-error-container.error-network {
border-top-color: rgba(59, 130, 246, 0.5);
}
.progress-error-container.error-network .progress-error-message {
color: #60a5fa;
text-shadow: 0 0 8px rgba(96, 165, 250, 0.4);
}
.progress-error-container.error-stall {
border-top-color: rgba(245, 158, 11, 0.5);
}
.progress-error-container.error-stall .progress-error-message {
color: #fbbf24;
text-shadow: 0 0 8px rgba(251, 191, 36, 0.4);
}
.progress-error-container.error-file {
border-top-color: rgba(239, 68, 68, 0.5);
}
.progress-error-container.error-file .progress-error-message {
color: #f87171;
text-shadow: 0 0 8px rgba(248, 113, 113, 0.4);
}
.progress-error-container.error-permission {
border-top-color: rgba(168, 85, 247, 0.5);
}
.progress-error-container.error-permission .progress-error-message {
color: #a855f7;
text-shadow: 0 0 8px rgba(168, 85, 247, 0.4);
}
.progress-error-container.error-server {
border-top-color: rgba(236, 72, 153, 0.5);
}
.progress-error-container.error-server .progress-error-message {
color: #ec4899;
text-shadow: 0 0 8px rgba(236, 72, 153, 0.4);
}
.progress-error-container.error-corruption {
border-top-color: rgba(220, 38, 38, 0.8);
}
.progress-error-container.error-corruption .progress-error-message {
color: #dc2626;
text-shadow: 0 0 8px rgba(220, 38, 38, 0.6);
font-weight: 600;
}
.progress-error-container.error-butler {
border-top-color: rgba(245, 158, 11, 0.5);
}
.progress-error-container.error-butler .progress-error-message {
color: #f59e0b;
text-shadow: 0 0 8px rgba(245, 158, 11, 0.4);
}
.progress-error-container.error-space {
border-top-color: rgba(168, 85, 247, 0.5);
}
.progress-error-container.error-space .progress-error-message {
color: #a855f7;
text-shadow: 0 0 8px rgba(168, 85, 247, 0.4);
}
.progress-error-container.error-conflict {
border-top-color: rgba(6, 182, 212, 0.5);
}
.progress-error-container.error-conflict .progress-error-message {
color: #06b6d4;
text-shadow: 0 0 8px rgba(6, 182, 212, 0.4);
}
/* Connection quality indicators */
.progress-details {
transition: all 0.3s ease;
}
.progress-details #progressSize {
transition: color 0.5s ease;
}
/* Enhanced retry button states */
.progress-retry-btn.retrying {
background: linear-gradient(135deg, #059669, #10b981);
animation: retryingPulse 1s ease-in-out infinite;
}
@keyframes retryingPulse {
0%, 100% {
transform: scale(1);
box-shadow: 0 2px 8px rgba(5, 150, 105, 0.3);
}
50% {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(5, 150, 105, 0.4);
}
}
/* Network status indicator (optional future enhancement) */
.network-status {
position: absolute;
top: 5px;
right: 5px;
width: 8px;
height: 8px;
border-radius: 50%;
background: #10b981;
box-shadow: 0 0 6px rgba(16, 185, 129, 0.6);
}
.network-status.poor {
background: #f87171;
box-shadow: 0 0 6px rgba(248, 113, 113, 0.6);
}
.network-status.fair {
background: #fbbf24;
box-shadow: 0 0 6px rgba(251, 191, 36, 0.6);
}
.progress-bar-fill {
height: 100%;