show progress

This commit is contained in:
Michal Vrba
2025-12-14 16:33:34 +01:00
parent bf85ef29f8
commit dec69eb937
2 changed files with 776 additions and 31 deletions
+222 -31
View File
@@ -41,12 +41,12 @@
disabled="@isProcessing" /> disabled="@isProcessing" />
<button @onclick="DownloadMp3" <button @onclick="DownloadMp3"
@onmousedown="PreventDefault" @onmousedown="PreventDefault"
class="download-btn" class="download-btn @(isProcessing ? "processing" : "")"
disabled="@(isProcessing)"> disabled="@(isProcessing)">
<span class="btn-icon">🎵</span> <span class="btn-icon">🎵</span>
@if (isProcessing) @if (isProcessing)
{ {
<span>Processing...</span> <span>Processing... (@progressPercentage%)</span>
} }
else else
{ {
@@ -55,6 +55,65 @@
</button> </button>
</div> </div>
@* Enhanced Progress Section - Show immediately when processing starts *@
@if (isProcessing)
{
<div class="enhanced-progress-section">
<div class="progress-header">
<h3 class="progress-title">
<span class="progress-icon @GetProgressIconClass()">@GetProgressIcon()</span>
@GetProgressStageTitle()
</h3>
<div class="progress-stats">
<span class="progress-percentage">@progressPercentage%</span>
<span class="progress-elapsed">⏱️ @GetElapsedTime()</span>
@if (estimatedTimeRemaining.HasValue && estimatedTimeRemaining.Value > TimeSpan.Zero)
{
<span class="progress-eta">ETA: @GetEstimatedTimeRemaining()</span>
}
</div>
</div>
<div class="progress-bar-container">
<div class="progress-bar-background">
<div class="progress-bar-fill" style="width: @(progressPercentage)%">
<div class="progress-bar-shimmer"></div>
</div>
</div>
<div class="progress-stages">
@foreach (var stage in progressStages)
{
<div class="progress-stage @GetStageClass(stage)">
<div class="stage-icon">@stage.Icon</div>
<div class="stage-label">@stage.Label</div>
</div>
}
</div>
</div>
<div class="progress-details">
<div class="progress-message">
<span class="message-text">@progressMessage</span>
@if (progressPercentage > 0 && progressPercentage < 100)
{
<div class="progress-dots">
<span class="dot">.</span>
<span class="dot">.</span>
<span class="dot">.</span>
</div>
}
</div>
@if (currentStageDetails != null)
{
<div class="stage-details">
<small>@currentStageDetails</small>
</div>
}
</div>
</div>
}
@* Video Information Section - Show as soon as we have details *@ @* Video Information Section - Show as soon as we have details *@
@if (currentVideoDetails != null) @if (currentVideoDetails != null)
{ {
@@ -69,6 +128,7 @@
{ {
<div class="processing-overlay"> <div class="processing-overlay">
<div class="processing-spinner">🎵</div> <div class="processing-spinner">🎵</div>
<div class="processing-text">@progressPercentage%</div>
</div> </div>
} }
} }
@@ -80,29 +140,10 @@
</div> </div>
} }
@if (isProcessing)
{
<div class="progress-container @(currentVideoDetails != null ? "with-video-details" : "")">
<div class="progress-bar">
<div class="progress-fill" style="width: @(progressPercentage)%"></div>
</div>
<div class="progress-text">
@if (currentVideoDetails != null && progressPercentage > 15)
{
<span class="progress-highlight">@progressMessage</span>
<span class="progress-percentage">(@progressPercentage%)</span>
}
else
{
<span>@progressMessage (@progressPercentage%)</span>
}
</div>
</div>
}
@if (!string.IsNullOrWhiteSpace(outputMessage) && !isProcessing) @if (!string.IsNullOrWhiteSpace(outputMessage) && !isProcessing)
{ {
<div class="output-message"> <div class="output-message success">
<div class="success-icon">✅</div>
<p>@outputMessage</p> <p>@outputMessage</p>
</div> </div>
} }
@@ -111,7 +152,8 @@
@if (!string.IsNullOrWhiteSpace(apiErrorMessage) && !isProcessing) @if (!string.IsNullOrWhiteSpace(apiErrorMessage) && !isProcessing)
{ {
<div class="error-message"> <div class="error-message">
<p>❌ @apiErrorMessage</p> <div class="error-icon">❌</div>
<p>@apiErrorMessage</p>
</div> </div>
} }
</div> </div>
@@ -132,6 +174,7 @@
@if (isLoadingZip) @if (isLoadingZip)
{ {
<span>Preparing ZIP...</span> <span>Preparing ZIP...</span>
<div class="loading-spinner">⏳</div>
} }
else else
{ {
@@ -198,10 +241,17 @@
private bool isLoadingZip = false; private bool isLoadingZip = false;
private int progressPercentage = 0; private int progressPercentage = 0;
private string progressMessage = ""; private string progressMessage = "";
private string? currentStageDetails = null;
private List<DownloadItem> downloadLinks = new(); private List<DownloadItem> downloadLinks = new();
private YouTubeVideoDetails? currentVideoDetails = null; private YouTubeVideoDetails? currentVideoDetails = null;
private string currentSessionId = ""; private string currentSessionId = "";
private SessionZipInfo? sessionZipInfo = null; private SessionZipInfo? sessionZipInfo = null;
// Enhanced progress tracking
private DateTime processingStartTime;
private TimeSpan? estimatedTimeRemaining;
private List<ProgressStage> progressStages = new();
private string currentProgressStage = "initializing";
public class DownloadItem public class DownloadItem
{ {
@@ -211,8 +261,21 @@
public DateTime CreatedAt { get; set; } 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() protected override async Task OnInitializedAsync()
{ {
InitializeProgressStages();
try try
{ {
// Initialize session on component load // Initialize session on component load
@@ -231,6 +294,109 @@
} }
} }
private void InitializeProgressStages()
{
progressStages = new List<ProgressStage>
{
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() private async Task LoadSessionZipInfoAsync()
{ {
try try
@@ -292,6 +458,17 @@
outputMessage = ""; outputMessage = "";
apiErrorMessage = ""; apiErrorMessage = "";
currentVideoDetails = null; 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 try
{ {
@@ -304,6 +481,7 @@
// Step 1: Start processing (which includes video details fetching) // Step 1: Start processing (which includes video details fetching)
progressMessage = "Starting video processing..."; progressMessage = "Starting video processing...";
progressPercentage = 5; progressPercentage = 5;
UpdateProgressStage(progressPercentage);
StateHasChanged(); StateHasChanged();
var request = new ProcessVideoRequest var request = new ProcessVideoRequest
@@ -357,6 +535,7 @@
bool videoDetailsReceived = false; bool videoDetailsReceived = false;
int maxAttempts = 60; // Maximum polling attempts (about 2 minutes) int maxAttempts = 60; // Maximum polling attempts (about 2 minutes)
int attemptCount = 0; int attemptCount = 0;
int lastProgress = 0;
while (attemptCount < maxAttempts && isProcessing) while (attemptCount < maxAttempts && isProcessing)
{ {
@@ -364,8 +543,15 @@
if (status != null) if (status != null)
{ {
// Update progress // Update progress only if it has increased (prevent UI flicker)
progressPercentage = status.PercentageComplete; if (status.PercentageComplete > lastProgress)
{
progressPercentage = status.PercentageComplete;
lastProgress = progressPercentage;
UpdateProgressStage(progressPercentage);
CalculateEstimatedTime(progressPercentage);
}
progressMessage = status.Message ?? "Processing..."; progressMessage = status.Message ?? "Processing...";
// Check if we received video details // Check if we received video details
@@ -428,6 +614,11 @@
{ {
try 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 // Use the video details from the result if we don't have them yet
if (currentVideoDetails == null && result.VideoDetails != null) if (currentVideoDetails == null && result.VideoDetails != null)
{ {
@@ -458,15 +649,15 @@
CreatedAt = DateTime.Now CreatedAt = DateTime.Now
}); });
outputMessage = $"✅ Successfully converted: {title}"; var processingTime = DateTime.Now - processingStartTime;
progressPercentage = 100; outputMessage = $"Successfully converted: {title} (took {processingTime.TotalSeconds:F1}s)";
progressMessage = "Complete!";
// Reload session ZIP info after successful download // Reload session ZIP info after successful download
await LoadSessionZipInfoAsync(); await LoadSessionZipInfoAsync();
// Small delay to show completion // Show completion for a moment
await Task.Delay(500); StateHasChanged();
await Task.Delay(1000);
isProcessing = false; isProcessing = false;
youtubeUrl = ""; youtubeUrl = "";
+554
View File
@@ -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 */ /* Responsive Design - Tablet */
@media (min-width: 640px) and (max-width: 1024px) { @media (min-width: 640px) and (max-width: 1024px) {
.hero-section { .hero-section {
@@ -613,6 +1017,39 @@
padding: 16px 20px; 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 { .features {
gap: 35px; gap: 35px;
} }
@@ -680,6 +1117,62 @@
padding: 16px 24px; 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 { .features {
gap: 25px; gap: 25px;
justify-content: space-around; justify-content: space-around;
@@ -792,6 +1285,67 @@
font-size: 0.9rem; 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 { .features {
gap: 20px; gap: 20px;
} }