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)
+ {
+
+
+
+
+
+
+ @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 @@
{
}
}
@@ -80,29 +140,10 @@
}
- @if (isProcessing)
- {
-
-
-
- @if (currentVideoDetails != null && progressPercentage > 15)
- {
- @progressMessage
- (@progressPercentage%)
- }
- else
- {
- @progressMessage (@progressPercentage%)
- }
-
-
- }
-
@if (!string.IsNullOrWhiteSpace(outputMessage) && !isProcessing)
{
-
+
}
@@ -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;
}