diff --git a/src/MauiSherpa/Pages/XcodeManagement.razor b/src/MauiSherpa/Pages/XcodeManagement.razor index 830b1df..a5cec0f 100644 --- a/src/MauiSherpa/Pages/XcodeManagement.razor +++ b/src/MauiSherpa/Pages/XcodeManagement.razor @@ -443,129 +443,6 @@ else } } - @* ====== DOWNLOAD/INSTALL MODAL ====== *@ - @if (showInstallModal) - { - - } } @code { @@ -1131,17 +865,10 @@ else // Download/Install state private XcodeRelease? downloadingRelease; private XcodeDownloadProgress? downloadProgress; - private CancellationTokenSource? downloadCts; - private bool showInstallModal; - private InstallStep currentInstallStep = InstallStep.Download; - private string? installStepMessage; - private string? installError; // Runtime state private string? installingDownloadableRuntimeBuild; - private enum InstallStep { Download, Extract, FirstLaunch, Complete } - private double downloadProgressPercent => downloadProgress?.TotalBytes > 0 ? (double)downloadProgress.BytesReceived / downloadProgress.TotalBytes.Value * 100 @@ -1340,82 +1067,83 @@ else downloadingRelease = release; downloadProgress = null; - downloadCts = new CancellationTokenSource(); - showInstallModal = true; - currentInstallStep = InstallStep.Download; - installStepMessage = null; - installError = null; StateHasChanged(); try { - var tempPath = Path.Combine(Path.GetTempPath(), $"Xcode_{release.Version}_{release.BuildNumber}.xip"); - var progress = new Progress(p => - { - downloadProgress = p; - InvokeAsync(StateHasChanged); - }); + await OperationModal.RunAsync( + $"Install Xcode {release.Version}", + $"Downloading and installing Xcode {release.Version} (build {release.BuildNumber})...", + async ctx => + { + var tempPath = Path.Combine(Path.GetTempPath(), $"Xcode_{release.Version}_{release.BuildNumber}.xip"); + ctx.LogInfo($"Downloading Xcode {release.Version} ({release.BuildNumber})"); + ctx.SetStatus("Preparing download..."); - var downloadOk = await XcodeService.DownloadXcodeAsync(release, tempPath, progress, downloadCts.Token); - if (!downloadOk) - { - installError = "Download failed. You may need to sign in again."; - return; - } + var progress = new Progress(p => + { + downloadProgress = p; + var total = p.TotalBytes.HasValue ? FormatBytes(p.TotalBytes.Value) : "?"; + var status = $"{FormatBytes(p.BytesReceived)} / {total} — {FormatSpeed(p.BytesPerSecond)}"; + if (p.EstimatedTimeRemaining.HasValue) + status += $" — ETA: {FormatTimeSpan(p.EstimatedTimeRemaining.Value)}"; + + ctx.SetStatus(status); + ctx.SetProgress(p.TotalBytes > 0 + ? (int)Math.Clamp(Math.Round((double)p.BytesReceived / p.TotalBytes.Value * 100), 0, 100) + : null); + InvokeAsync(StateHasChanged); + }); + + var downloadOk = await XcodeService.DownloadXcodeAsync( + release, + tempPath, + progress, + ctx.CancellationToken); + if (!downloadOk) + { + ctx.LogError("Download failed. You may need to sign in again."); + return false; + } - currentInstallStep = InstallStep.Extract; - installStepMessage = "Extracting and moving to /Applications..."; - StateHasChanged(); + ctx.LogSuccess("Download complete"); + ctx.SetProgress(null); + ctx.SetStatus("Extracting and moving to /Applications..."); - var installProgress = new Progress(msg => - { - installStepMessage = msg; - if (msg.Contains("first launch", StringComparison.OrdinalIgnoreCase)) - currentInstallStep = InstallStep.FirstLaunch; - InvokeAsync(StateHasChanged); - }); - - var installOk = await XcodeService.InstallXcodeAsync(tempPath, null, installProgress, downloadCts.Token); - if (!installOk) - { - installError = "Installation failed. The Xcode .xip file has been preserved in your temp directory."; - return; - } + var installProgress = new Progress(message => + { + ctx.LogInfo(message); + ctx.SetStatus(message); + }); + + var installOk = await XcodeService.InstallXcodeAsync( + tempPath, + null, + installProgress, + ctx.CancellationToken); + if (!installOk) + { + ctx.LogError("Installation failed. The Xcode .xip file has been preserved in your temp directory."); + return false; + } - currentInstallStep = InstallStep.Complete; - installStepMessage = null; - await ViewModel.LoadInstalledAsync(); - } - catch (OperationCanceledException) - { - installError = "Installation cancelled."; - } - catch (Exception ex) - { - installError = $"Error: {ex.Message}"; + ctx.SetProgress(100); + ctx.SetStatus("Refreshing installed Xcode versions..."); + await ViewModel.LoadInstalledAsync(); + return true; + }); } finally { downloadingRelease = null; downloadProgress = null; - downloadCts?.Dispose(); - downloadCts = null; StateHasChanged(); } } private void CancelDownload() { - downloadCts?.Cancel(); - } - - private void CloseInstallModal() - { - showInstallModal = false; - downloadingRelease = null; - downloadProgress = null; - installError = null; - StateHasChanged(); + OperationModal.RequestCancellation(); } // ---- Runtimes ---- @@ -1548,13 +1276,6 @@ else catch { } } - private string GetStepClass(InstallStep step) - { - if (currentInstallStep == step) return "active"; - if (currentInstallStep > step) return "completed"; - return ""; - } - private static string GetRuntimeIcon(string? platformIdentifier) => FriendlyPlatform(platformIdentifier) switch { "iOS" => "fas fa-mobile-alt", @@ -1670,7 +1391,7 @@ else { if (bytes < 1024) return $"{bytes} B"; if (bytes < 1024 * 1024) return $"{bytes / 1024.0:F1} KB"; - if (bytes < 1024L * 1024 * 1024) return $"{bytes / (1024.0 * 1024):F1} MB"; + if (bytes < 1024L * 1024 * 1024) return $"{bytes / (1024.0 * 1024):F0} MB"; return $"{bytes / (1024.0 * 1024 * 1024):F2} GB"; }