diff --git a/TuneRipper/Pages/Home.razor b/TuneRipper/Pages/Home.razor index dfbdd75..9d4c6c4 100644 --- a/TuneRipper/Pages/Home.razor +++ b/TuneRipper/Pages/Home.razor @@ -41,12 +41,12 @@ disabled="@isProcessing" /> + @* Enhanced Progress Section - Show immediately when processing starts *@ + @if (isProcessing) + { +
+
+

+ @GetProgressIcon() + @GetProgressStageTitle() +

+
+ @progressPercentage% + ⏱️ @GetElapsedTime() + @if (estimatedTimeRemaining.HasValue && estimatedTimeRemaining.Value > TimeSpan.Zero) + { + ETA: @GetEstimatedTimeRemaining() + } +
+
+ +
+
+
+
+
+
+
+ @foreach (var stage in progressStages) + { +
+
@stage.Icon
+
@stage.Label
+
+ } +
+
+ +
+
+ @progressMessage + @if (progressPercentage > 0 && progressPercentage < 100) + { +
+ . + . + . +
+ } +
+ + @if (currentStageDetails != null) + { +
+ @currentStageDetails +
+ } +
+
+ } + @* Video Information Section - Show as soon as we have details *@ @if (currentVideoDetails != null) { @@ -69,6 +128,7 @@ {
🎵
+
@progressPercentage%
} } @@ -80,29 +140,10 @@ } - @if (isProcessing) - { -
-
-
-
-
- @if (currentVideoDetails != null && progressPercentage > 15) - { - @progressMessage - (@progressPercentage%) - } - else - { - @progressMessage (@progressPercentage%) - } -
-
- } - @if (!string.IsNullOrWhiteSpace(outputMessage) && !isProcessing) { -
+
+
✅

@outputMessage

} @@ -111,7 +152,8 @@ @if (!string.IsNullOrWhiteSpace(apiErrorMessage) && !isProcessing) {
-

❌ @apiErrorMessage

+
❌
+

@apiErrorMessage

}
@@ -132,6 +174,7 @@ @if (isLoadingZip) { Preparing ZIP... +
⏳
} else { @@ -198,10 +241,17 @@ private bool isLoadingZip = false; private int progressPercentage = 0; private string progressMessage = ""; + private string? currentStageDetails = null; private List downloadLinks = new(); private YouTubeVideoDetails? currentVideoDetails = null; private string currentSessionId = ""; private SessionZipInfo? sessionZipInfo = null; + + // Enhanced progress tracking + private DateTime processingStartTime; + private TimeSpan? estimatedTimeRemaining; + private List progressStages = new(); + private string currentProgressStage = "initializing"; public class DownloadItem { @@ -211,8 +261,21 @@ public DateTime CreatedAt { get; set; } } + public class ProgressStage + { + public string Id { get; set; } = ""; + public string Label { get; set; } = ""; + public string Icon { get; set; } = ""; + public bool IsCompleted { get; set; } + public bool IsActive { get; set; } + public int MinProgress { get; set; } + public int MaxProgress { get; set; } + } + protected override async Task OnInitializedAsync() { + InitializeProgressStages(); + try { // Initialize session on component load @@ -231,6 +294,109 @@ } } + private void InitializeProgressStages() + { + progressStages = new List + { + new() { Id = "initializing", Label = "Initializing", Icon = "🚀", MinProgress = 0, MaxProgress = 15 }, + new() { Id = "fetching", Label = "Fetching Video", Icon = "📡", MinProgress = 15, MaxProgress = 35 }, + new() { Id = "extracting", Label = "Extracting Audio", Icon = "🔄", MinProgress = 35, MaxProgress = 70 }, + new() { Id = "converting", Label = "Converting to MP3", Icon = "🎵", MinProgress = 70, MaxProgress = 90 }, + new() { Id = "finalizing", Label = "Finalizing", Icon = "✨", MinProgress = 90, MaxProgress = 100 } + }; + } + + private void UpdateProgressStage(int percentage) + { + foreach (var stage in progressStages) + { + stage.IsCompleted = percentage > stage.MaxProgress; + stage.IsActive = percentage >= stage.MinProgress && percentage <= stage.MaxProgress; + } + + var activeStage = progressStages.FirstOrDefault(s => s.IsActive); + if (activeStage != null && currentProgressStage != activeStage.Id) + { + currentProgressStage = activeStage.Id; + UpdateStageDetails(activeStage.Id, percentage); + } + } + + private void UpdateStageDetails(string stageId, int percentage) + { + currentStageDetails = stageId switch + { + "initializing" => "Preparing download request and validating URL...", + "fetching" => $"Analyzing video content and metadata... ({percentage}%)", + "extracting" => $"Extracting high-quality audio stream... ({percentage}%)", + "converting" => $"Converting to MP3 format with optimal settings... ({percentage}%)", + "finalizing" => $"Completing conversion and preparing download... ({percentage}%)", + _ => null + }; + } + + private string GetProgressStageTitle() + { + var activeStage = progressStages.FirstOrDefault(s => s.IsActive); + return activeStage?.Label ?? "Processing"; + } + + private string GetProgressIcon() + { + var activeStage = progressStages.FirstOrDefault(s => s.IsActive); + return activeStage?.Icon ?? "🎵"; + } + + private string GetProgressIconClass() + { + if (progressPercentage < 25) return "spinning"; + if (progressPercentage < 50) return "pulse"; + if (progressPercentage < 75) return "bounce"; + if (progressPercentage < 100) return "glow"; + return "success"; + } + + private string GetStageClass(ProgressStage stage) + { + if (stage.IsCompleted) return "completed"; + if (stage.IsActive) return "active"; + return "pending"; + } + + private string GetElapsedTime() + { + var elapsed = DateTime.Now - processingStartTime; + return elapsed.TotalMinutes >= 1 + ? $"{elapsed.Minutes:D2}:{elapsed.Seconds:D2}" + : $"{elapsed.Seconds}s"; + } + + private string GetEstimatedTimeRemaining() + { + if (!estimatedTimeRemaining.HasValue) return ""; + + var eta = estimatedTimeRemaining.Value; + return eta.TotalMinutes >= 1 + ? $"{eta.Minutes:D2}:{eta.Seconds:D2}" + : $"{eta.Seconds}s"; + } + + private void CalculateEstimatedTime(int currentProgress) + { + if (currentProgress > 5) // Only estimate after some progress + { + var elapsed = DateTime.Now - processingStartTime; + var progressRate = currentProgress / elapsed.TotalSeconds; + var remainingProgress = 100 - currentProgress; + var estimatedSeconds = remainingProgress / progressRate; + + if (estimatedSeconds > 0 && estimatedSeconds < 300) // Cap at 5 minutes + { + estimatedTimeRemaining = TimeSpan.FromSeconds(estimatedSeconds); + } + } + } + private async Task LoadSessionZipInfoAsync() { try @@ -292,6 +458,17 @@ outputMessage = ""; apiErrorMessage = ""; currentVideoDetails = null; + processingStartTime = DateTime.Now; + estimatedTimeRemaining = null; + currentStageDetails = null; + + // Reset progress stages + foreach (var stage in progressStages) + { + stage.IsCompleted = false; + stage.IsActive = false; + } + progressStages.First().IsActive = true; try { @@ -304,6 +481,7 @@ // Step 1: Start processing (which includes video details fetching) progressMessage = "Starting video processing..."; progressPercentage = 5; + UpdateProgressStage(progressPercentage); StateHasChanged(); var request = new ProcessVideoRequest @@ -357,6 +535,7 @@ bool videoDetailsReceived = false; int maxAttempts = 60; // Maximum polling attempts (about 2 minutes) int attemptCount = 0; + int lastProgress = 0; while (attemptCount < maxAttempts && isProcessing) { @@ -364,8 +543,15 @@ if (status != null) { - // Update progress - progressPercentage = status.PercentageComplete; + // Update progress only if it has increased (prevent UI flicker) + if (status.PercentageComplete > lastProgress) + { + progressPercentage = status.PercentageComplete; + lastProgress = progressPercentage; + UpdateProgressStage(progressPercentage); + CalculateEstimatedTime(progressPercentage); + } + progressMessage = status.Message ?? "Processing..."; // Check if we received video details @@ -428,6 +614,11 @@ { try { + // Ensure we reach 100% completion + progressPercentage = 100; + progressMessage = "Complete!"; + UpdateProgressStage(progressPercentage); + // Use the video details from the result if we don't have them yet if (currentVideoDetails == null && result.VideoDetails != null) { @@ -458,15 +649,15 @@ CreatedAt = DateTime.Now }); - outputMessage = $"✅ Successfully converted: {title}"; - progressPercentage = 100; - progressMessage = "Complete!"; + var processingTime = DateTime.Now - processingStartTime; + outputMessage = $"Successfully converted: {title} (took {processingTime.TotalSeconds:F1}s)"; // Reload session ZIP info after successful download await LoadSessionZipInfoAsync(); - // Small delay to show completion - await Task.Delay(500); + // Show completion for a moment + StateHasChanged(); + await Task.Delay(1000); isProcessing = false; youtubeUrl = ""; diff --git a/TuneRipper/Pages/Home.razor.css b/TuneRipper/Pages/Home.razor.css index fec3798..13c754a 100644 --- a/TuneRipper/Pages/Home.razor.css +++ b/TuneRipper/Pages/Home.razor.css @@ -585,6 +585,410 @@ } } +/* Enhanced Progress Section Styles */ +.enhanced-progress-section { + margin: 25px 0; + background: linear-gradient(135deg, #f8f9ff 0%, #f3f4ff 100%); + border: 1px solid #e1e5ff; + border-radius: 16px; + padding: 20px; + box-shadow: 0 4px 20px rgba(99, 102, 241, 0.08); + animation: slideIn 0.5s ease-out; + position: relative; + overflow: hidden; +} + +.enhanced-progress-section::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 2px; + background: linear-gradient(90deg, transparent, #6366f1, transparent); + animation: progressShimmer 2s infinite; +} + +.progress-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 20px; + gap: 15px; +} + +.progress-title { + font-size: 1.2rem; + font-weight: 600; + color: #374151; + margin: 0; + display: flex; + align-items: center; + gap: 10px; + flex: 1; +} + +.progress-icon { + font-size: 1.4rem; + transition: transform 0.3s ease; +} + +.progress-icon.spinning { + animation: spin 2s linear infinite; +} + +.progress-icon.pulse { + animation: pulse 1.5s ease-in-out infinite; +} + +.progress-icon.bounce { + animation: iconBounce 1s ease-in-out infinite; +} + +.progress-icon.glow { + animation: glow 2s ease-in-out infinite; +} + +.progress-icon.success { + color: #10b981; + animation: successPop 0.5s ease-out; +} + +.progress-stats { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 4px; + font-size: 0.85rem; + color: #6b7280; + white-space: nowrap; +} + +.progress-percentage { + font-size: 1.1rem; + font-weight: 700; + color: #6366f1; + background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.progress-elapsed, +.progress-eta { + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 0.8rem; + color: #9ca3af; +} + +.progress-bar-container { + position: relative; + margin-bottom: 20px; +} + +.progress-bar-background { + width: 100%; + height: 12px; + background: linear-gradient(90deg, #e5e7eb 0%, #f3f4f6 100%); + border-radius: 6px; + overflow: hidden; + box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1); + position: relative; +} + +.progress-bar-fill { + height: 100%; + background: linear-gradient(90deg, #6366f1 0%, #8b5cf6 50%, #a855f7 100%); + border-radius: 6px; + transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + overflow: hidden; + box-shadow: + 0 0 10px rgba(99, 102, 241, 0.4), + inset 0 1px 0 rgba(255, 255, 255, 0.2); +} + +.progress-bar-shimmer { + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient( + 90deg, + transparent, + rgba(255, 255, 255, 0.4), + transparent + ); + animation: shimmer 2s infinite; +} + +.progress-stages { + display: flex; + justify-content: space-between; + margin-top: 15px; + gap: 8px; +} + +.progress-stage { + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + flex: 1; + transition: all 0.3s ease; + min-width: 0; +} + +.stage-icon { + width: 32px; + height: 32px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + font-weight: 600; + transition: all 0.3s ease; + border: 2px solid transparent; +} + +.stage-label { + font-size: 0.75rem; + font-weight: 500; + text-align: center; + transition: all 0.3s ease; + word-wrap: break-word; + line-height: 1.2; +} + +.progress-stage.pending .stage-icon { + background: #f3f4f6; + color: #9ca3af; + border-color: #e5e7eb; +} + +.progress-stage.pending .stage-label { + color: #9ca3af; +} + +.progress-stage.active .stage-icon { + background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); + color: white; + border-color: #6366f1; + transform: scale(1.1); + box-shadow: 0 4px 12px rgba(99, 102,241, 0.3); + animation: stageActivePulse 2s infinite; +} + +.progress-stage.active .stage-label { + color: #6366f1; + font-weight: 600; +} + +.progress-stage.completed .stage-icon { + background: linear-gradient(135deg, #10b981 0%, #059669 100%); + color: white; + border-color: #10b981; + transform: scale(1.05); +} + +.progress-stage.completed .stage-label { + color: #10b981; + font-weight: 600; +} + +.progress-details { + text-align: center; +} + +.progress-message { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + margin-bottom: 8px; +} + +.message-text { + font-size: 0.95rem; + color: #4b5563; + font-weight: 500; +} + +.progress-dots { + display: flex; + gap: 2px; +} + +.progress-dots .dot { + width: 4px; + height: 4px; + background: #6b7280; + border-radius: 50%; + animation: dotPulse 1.5s infinite; +} + +.progress-dots .dot:nth-child(2) { + animation-delay: 0.3s; +} + +.progress-dots .dot:nth-child(3) { + animation-delay: 0.6s; +} + +.stage-details { + color: #6b7280; + font-size: 0.8rem; + font-style: italic; + line-height: 1.4; +} + +/* Enhanced download button processing state */ +.download-btn.processing { + background: linear-gradient(135deg, #6b7280 0%, #9ca3af 100%); + animation: buttonPulse 2s infinite; +} + +.download-btn.processing .btn-icon { + animation: spin 1s linear infinite; +} + +/* Enhanced success and error messages */ +.output-message.success { + background: linear-gradient(135deg, #ecfdf5 0%, #f0fdf4 100%); + border: 1px solid #bbf7d0; + color: #065f46; + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + animation: successSlide 0.6s ease-out; +} + +.success-icon { + font-size: 1.2rem; + animation: successPop 0.5s ease-out; +} + +.error-message { + background: linear-gradient(135deg, #fef2f2 0%, #fef7f7 100%); + border: 1px solid #fecaca; + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + animation: errorShake 0.5s ease-out; +} + +.error-icon { + font-size: 1.2rem; +} + +/* Enhanced loading states */ +.loading-spinner { + font-size: 1rem; + animation: spin 1s linear infinite; + margin-left: 8px; +} + +.processing-text { + position: absolute; + bottom: 10px; + right: 10px; + background: rgba(0, 0, 0, 0.8); + color: white; + padding: 4px 8px; + border-radius: 4px; + font-size: 0.8rem; + font-weight: 600; +} + +/* Video info enhancements */ +.processing-text { + position: absolute; + bottom: 10px; + right: 10px; + background: rgba(0, 0, 0, 0.8); + color: white; + padding: 4px 8px; + border-radius: 4px; + font-size: 0.8rem; + font-weight: 600; +} + +/* Animations */ +@keyframes progressShimmer { + 0% { left: -100%; } + 100% { left: 100%; } +} + +@keyframes stageActivePulse { + 0%, 100% { + box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3); + } + 50% { + box-shadow: 0 4px 20px rgba(99, 102, 241, 0.5); + } +} + +@keyframes iconBounce { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-4px); } +} + +@keyframes glow { + 0%, 100% { + text-shadow: 0 0 5px rgba(99, 102, 241, 0.5); + } + 50% { + text-shadow: 0 0 15px rgba(99, 102, 241, 0.8); + } +} + +@keyframes successPop { + 0% { transform: scale(0.8); opacity: 0; } + 50% { transform: scale(1.2); } + 100% { transform: scale(1); opacity: 1; } +} + +@keyframes dotPulse { + 0%, 80%, 100% { + opacity: 0.4; + transform: scale(0.8); + } + 40% { + opacity: 1; + transform: scale(1); + } +} + +@keyframes buttonPulse { + 0%, 100% { + box-shadow: 0 4px 12px rgba(107, 114, 128, 0.3); + } + 50% { + box-shadow: 0 4px 20px rgba(107, 114, 128, 0.5); + } +} + +@keyframes successSlide { + 0% { + opacity: 0; + transform: translateY(20px); + } + 100% { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes errorShake { + 0%, 100% { transform: translateX(0); } + 25% { transform: translateX(-5px); } + 75% { transform: translateX(5px); } +} + /* Responsive Design - Tablet */ @media (min-width: 640px) and (max-width: 1024px) { .hero-section { @@ -613,6 +1017,39 @@ padding: 16px 20px; } + /* Enhanced progress tablet styles */ + .enhanced-progress-section { + padding: 18px; + margin: 22px 0; + } + + .progress-header { + margin-bottom: 18px; + } + + .progress-title { + font-size: 1.15rem; + } + + .progress-stats { + font-size: 0.82rem; + } + + .progress-stages { + gap: 6px; + margin-top: 14px; + } + + .stage-icon { + width: 30px; + height: 30px; + font-size: 13px; + } + + .stage-label { + font-size: 0.7rem; + } + .features { gap: 35px; } @@ -680,6 +1117,62 @@ padding: 16px 24px; } + /* Enhanced progress mobile styles */ + .enhanced-progress-section { + padding: 15px; + margin: 20px 0; + } + + .progress-header { + flex-direction: column; + align-items: flex-start; + gap: 10px; + margin-bottom: 15px; + } + + .progress-title { + font-size: 1.1rem; + } + + .progress-stats { + align-self: stretch; + flex-direction: row; + justify-content: space-between; + font-size: 0.8rem; + } + + .progress-stages { + gap: 4px; + margin-top: 12px; + } + + .stage-icon { + width: 28px; + height: 28px; + font-size: 12px; + } + + .stage-label { + font-size: 0.65rem; + line-height: 1.1; + } + + .progress-bar-background { + height: 10px; + } + + .progress-bar-fill { + height: 10px; + } + + .message-text { + font-size: 0.9rem; + } + + .stage-details { + font-size: 0.75rem; + } + .features { gap: 25px; justify-content: space-around; @@ -792,6 +1285,67 @@ font-size: 0.9rem; } + /* Enhanced progress extra small mobile styles */ + .enhanced-progress-section { + padding: 12px; + margin: 15px 0; + } + + .progress-header { + gap: 8px; + margin-bottom: 12px; + } + + .progress-title { + font-size: 1rem; + } + + .progress-stats { + font-size: 0.75rem; + gap: 6px; + } + + .progress-percentage { + font-size: 1rem; + } + + .progress-elapsed, + .progress-eta { + font-size: 0.7rem; + } + + .progress-stages { + gap: 2px; + margin-top: 10px; + } + + .stage-icon { + width: 24px; + height: 24px; + font-size: 10px; + } + + .stage-label { + font-size: 0.6rem; + line-height: 1; + } + + .progress-bar-background { + height: 8px; + } + + .progress-bar-fill { + height: 8px; + } + + .message-text { + font-size: 0.85rem; + } + + .stage-details { + font-size: 0.7rem; + } + .features { gap: 20px; }