From d9c7b36690377ab1dc89d3354bf54342f4f59f8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C6=90=C9=94=C4=B1s3?= <8407711+w3spi5@users.noreply.github.com> Date: Tue, 6 Jan 2026 19:32:32 +0100 Subject: [PATCH 01/10] update 2.25 --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1b4b3a9..720ccd9 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# BigDump 2.24 - Staggered MySQL Dump Importer +# BigDump 2.25 - Staggered MySQL Dump Importer [![PHP Version](https://img.shields.io/badge/php-8.1+-yellow.svg)](https://php.net/) [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) -[![Package Version](https://img.shields.io/badge/version-2.24-blue.svg)](https://php.net/) +[![Package Version](https://img.shields.io/badge/version-2.25-blue.svg)](https://php.net/) [![Build Assets](https://img.shields.io/badge/build-GitHub_Actions-2088FF.svg)](https://github.com/w3spi5/bigdump/actions) [![PHAR](https://img.shields.io/badge/PHAR-single--file-purple.svg)](https://github.com/w3spi5/bigdump/releases) @@ -153,7 +153,7 @@ This can provide **10-50x speedup** when importing the optimized file. - `.sql.gz` - Gzip compressed SQL - `.sql.bz2` - Bzip2 compressed SQL -## PHAR Distribution (v2.24) — Easiest Install +## PHAR Distribution (v2.25) — Easiest Install Download `bigdump.phar` from [Releases](https://github.com/w3spi5/bigdump/releases) and upload to your server. That's it! From f1c7ca3b2b13fd275f3fa29cb3cf2f7a33e570ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C6=90=C9=94=C4=B1s3?= <8407711+w3spi5@users.noreply.github.com> Date: Tue, 13 Jan 2026 18:15:39 +0100 Subject: [PATCH 02/10] ui: replace unknown statistics ? marks with horizontal skeleton loader animation --- assets/css/bigdump.css | 35 +++++++++++++++++++++++++++++++++++ assets/src/css/tailwind.css | 35 +++++++++++++++++++++++++++++++++++ templates/import.php | 20 ++++++++++---------- 3 files changed, 80 insertions(+), 10 deletions(-) diff --git a/assets/css/bigdump.css b/assets/css/bigdump.css index b1a9d21..944ab86 100644 --- a/assets/css/bigdump.css +++ b/assets/css/bigdump.css @@ -773,6 +773,41 @@ display: flex; } +/* ======================================== + SKELETON LOADER + ======================================== */ + +.skeleton { + display: inline-block; + height: 1em; + min-width: 3em; + background: linear-gradient( + 90deg, + rgba(156, 163, 175, 0.3) 25%, + rgba(156, 163, 175, 0.5) 50%, + rgba(156, 163, 175, 0.3) 75% + ); + background-size: 200% 100%; + animation: skeleton-shimmer 1.5s ease-in-out infinite; + border-radius: 0.25rem; + vertical-align: middle; +} + +[data-theme="dark"] .skeleton { + background: linear-gradient( + 90deg, + rgba(75, 85, 99, 0.4) 25%, + rgba(75, 85, 99, 0.7) 50%, + rgba(75, 85, 99, 0.4) 75% + ); + background-size: 200% 100%; +} + +@keyframes skeleton-shimmer { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } +} + /* ======================================== FILE UPLOAD ENHANCEMENTS ======================================== */ diff --git a/assets/src/css/tailwind.css b/assets/src/css/tailwind.css index 3234c81..5c44939 100644 --- a/assets/src/css/tailwind.css +++ b/assets/src/css/tailwind.css @@ -817,6 +817,41 @@ display: flex; } +/* ======================================== + SKELETON LOADER + ======================================== */ + +.skeleton { + display: inline-block; + height: 1em; + min-width: 3em; + background: linear-gradient( + 90deg, + rgba(156, 163, 175, 0.3) 25%, + rgba(156, 163, 175, 0.5) 50%, + rgba(156, 163, 175, 0.3) 75% + ); + background-size: 200% 100%; + animation: skeleton-shimmer 1.5s ease-in-out infinite; + border-radius: 0.25rem; + vertical-align: middle; +} + +[data-theme="dark"] .skeleton { + background: linear-gradient( + 90deg, + rgba(75, 85, 99, 0.4) 25%, + rgba(75, 85, 99, 0.7) 50%, + rgba(75, 85, 99, 0.4) 75% + ); + background-size: 200% 100%; +} + +@keyframes skeleton-shimmer { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } +} + /* ======================================== FILE UPLOAD ENHANCEMENTS ======================================== */ diff --git a/templates/import.php b/templates/import.php index e1fa525..788c500 100644 --- a/templates/import.php +++ b/templates/import.php @@ -207,36 +207,36 @@ - - + ' ?> + ' ?> Queries - - + ' ?> + ' ?> Bytes - - + ' ?> + ' ?> KB - - + ' ?> + ' ?> MB - - + ' ?> + ' ?> From 9d174b047faac03c4925f26ed9c026d089fcc059 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C6=90=C9=94=C4=B1s3?= <8407711+w3spi5@users.noreply.github.com> Date: Tue, 13 Jan 2026 18:22:09 +0100 Subject: [PATCH 03/10] add animated bouncing arrow favicon during active import process --- assets/js/favicon-animator.js | 145 ++++++++++++++++++++++++++++++ assets/src/js/favicon-animator.js | 145 ++++++++++++++++++++++++++++++ src/Services/AjaxService.php | 16 ++++ templates/import.php | 1 + 4 files changed, 307 insertions(+) create mode 100644 assets/js/favicon-animator.js create mode 100644 assets/src/js/favicon-animator.js diff --git a/assets/js/favicon-animator.js b/assets/js/favicon-animator.js new file mode 100644 index 0000000..7a03b6c --- /dev/null +++ b/assets/js/favicon-animator.js @@ -0,0 +1,145 @@ +/** + * BigDump - Animated Favicon Module + * Creates a bouncing arrow animation during import process. + * Uses Canvas API to dynamically generate favicon frames. + */ +(function() { + 'use strict'; + + var canvas = null; + var ctx = null; + var animationId = null; + var originalFavicon = null; + var linkElement = null; + var frame = 0; + var isAnimating = false; + + // Animation settings + var SIZE = 32; // Favicon size + var ARROW_COLOR = '#3B82F6'; // Blue-500 + var BG_COLOR = '#1E3A5F'; // Dark blue background + var BOUNCE_HEIGHT = 6; // Pixels to bounce + var FRAME_RATE = 60; // ms between frames + + /** + * Initialize canvas and get original favicon + */ + function init() { + if (canvas) return; + + canvas = document.createElement('canvas'); + canvas.width = SIZE; + canvas.height = SIZE; + ctx = canvas.getContext('2d'); + + // Find and store original favicon + linkElement = document.querySelector('link[rel="icon"][type="image/png"]') || + document.querySelector('link[rel="icon"]'); + + if (linkElement) { + originalFavicon = linkElement.href; + } + } + + /** + * Draw a frame of the bouncing arrow animation + * @param {number} offset - Vertical bounce offset + */ + function drawFrame(offset) { + // Clear canvas + ctx.clearRect(0, 0, SIZE, SIZE); + + // Draw circular background + ctx.beginPath(); + ctx.arc(SIZE / 2, SIZE / 2, SIZE / 2 - 1, 0, Math.PI * 2); + ctx.fillStyle = BG_COLOR; + ctx.fill(); + + // Calculate arrow position with bounce + var centerX = SIZE / 2; + var centerY = SIZE / 2 + offset; + + // Draw downward arrow + ctx.beginPath(); + ctx.strokeStyle = ARROW_COLOR; + ctx.fillStyle = ARROW_COLOR; + ctx.lineWidth = 3; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + + // Arrow shaft + ctx.moveTo(centerX, centerY - 8); + ctx.lineTo(centerX, centerY + 4); + ctx.stroke(); + + // Arrow head (triangle) + ctx.beginPath(); + ctx.moveTo(centerX - 6, centerY); + ctx.lineTo(centerX, centerY + 8); + ctx.lineTo(centerX + 6, centerY); + ctx.closePath(); + ctx.fill(); + + // Update favicon + if (linkElement) { + linkElement.href = canvas.toDataURL('image/png'); + } + } + + /** + * Animation loop using eased bounce + */ + function animate() { + if (!isAnimating) return; + + // Eased bounce: sin wave for smooth up/down motion + var bounce = Math.sin(frame * 0.15) * BOUNCE_HEIGHT; + drawFrame(bounce); + + frame++; + animationId = setTimeout(animate, FRAME_RATE); + } + + /** + * Start the favicon animation + */ + function start() { + if (isAnimating) return; + + init(); + isAnimating = true; + frame = 0; + animate(); + console.log('Favicon animation started'); + } + + /** + * Stop the animation and restore original favicon + */ + function stop() { + if (!isAnimating) return; + + isAnimating = false; + + if (animationId) { + clearTimeout(animationId); + animationId = null; + } + + // Restore original favicon + if (linkElement && originalFavicon) { + linkElement.href = originalFavicon; + } + + console.log('Favicon animation stopped'); + } + + // Expose API + window.BigDump = window.BigDump || {}; + window.BigDump.faviconAnimator = { + start: start, + stop: stop, + isAnimating: function() { return isAnimating; } + }; + +})(); diff --git a/assets/src/js/favicon-animator.js b/assets/src/js/favicon-animator.js new file mode 100644 index 0000000..7a03b6c --- /dev/null +++ b/assets/src/js/favicon-animator.js @@ -0,0 +1,145 @@ +/** + * BigDump - Animated Favicon Module + * Creates a bouncing arrow animation during import process. + * Uses Canvas API to dynamically generate favicon frames. + */ +(function() { + 'use strict'; + + var canvas = null; + var ctx = null; + var animationId = null; + var originalFavicon = null; + var linkElement = null; + var frame = 0; + var isAnimating = false; + + // Animation settings + var SIZE = 32; // Favicon size + var ARROW_COLOR = '#3B82F6'; // Blue-500 + var BG_COLOR = '#1E3A5F'; // Dark blue background + var BOUNCE_HEIGHT = 6; // Pixels to bounce + var FRAME_RATE = 60; // ms between frames + + /** + * Initialize canvas and get original favicon + */ + function init() { + if (canvas) return; + + canvas = document.createElement('canvas'); + canvas.width = SIZE; + canvas.height = SIZE; + ctx = canvas.getContext('2d'); + + // Find and store original favicon + linkElement = document.querySelector('link[rel="icon"][type="image/png"]') || + document.querySelector('link[rel="icon"]'); + + if (linkElement) { + originalFavicon = linkElement.href; + } + } + + /** + * Draw a frame of the bouncing arrow animation + * @param {number} offset - Vertical bounce offset + */ + function drawFrame(offset) { + // Clear canvas + ctx.clearRect(0, 0, SIZE, SIZE); + + // Draw circular background + ctx.beginPath(); + ctx.arc(SIZE / 2, SIZE / 2, SIZE / 2 - 1, 0, Math.PI * 2); + ctx.fillStyle = BG_COLOR; + ctx.fill(); + + // Calculate arrow position with bounce + var centerX = SIZE / 2; + var centerY = SIZE / 2 + offset; + + // Draw downward arrow + ctx.beginPath(); + ctx.strokeStyle = ARROW_COLOR; + ctx.fillStyle = ARROW_COLOR; + ctx.lineWidth = 3; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + + // Arrow shaft + ctx.moveTo(centerX, centerY - 8); + ctx.lineTo(centerX, centerY + 4); + ctx.stroke(); + + // Arrow head (triangle) + ctx.beginPath(); + ctx.moveTo(centerX - 6, centerY); + ctx.lineTo(centerX, centerY + 8); + ctx.lineTo(centerX + 6, centerY); + ctx.closePath(); + ctx.fill(); + + // Update favicon + if (linkElement) { + linkElement.href = canvas.toDataURL('image/png'); + } + } + + /** + * Animation loop using eased bounce + */ + function animate() { + if (!isAnimating) return; + + // Eased bounce: sin wave for smooth up/down motion + var bounce = Math.sin(frame * 0.15) * BOUNCE_HEIGHT; + drawFrame(bounce); + + frame++; + animationId = setTimeout(animate, FRAME_RATE); + } + + /** + * Start the favicon animation + */ + function start() { + if (isAnimating) return; + + init(); + isAnimating = true; + frame = 0; + animate(); + console.log('Favicon animation started'); + } + + /** + * Stop the animation and restore original favicon + */ + function stop() { + if (!isAnimating) return; + + isAnimating = false; + + if (animationId) { + clearTimeout(animationId); + animationId = null; + } + + // Restore original favicon + if (linkElement && originalFavicon) { + linkElement.href = originalFavicon; + } + + console.log('Favicon animation stopped'); + } + + // Expose API + window.BigDump = window.BigDump || {}; + window.BigDump.faviconAnimator = { + start: start, + stop: stop, + isAnimating: function() { return isAnimating; } + }; + +})(); diff --git a/src/Services/AjaxService.php b/src/Services/AjaxService.php index 3d84edb..73bc39e 100644 --- a/src/Services/AjaxService.php +++ b/src/Services/AjaxService.php @@ -661,6 +661,10 @@ function createConnection() { } // Start elapsed timer startElapsedTimer(); + // Start favicon animation + if (window.BigDump && window.BigDump.faviconAnimator) { + window.BigDump.faviconAnimator.start(); + } }); // Handle progress events (real-time updates) @@ -697,6 +701,10 @@ function createConnection() { console.log('SSE: Import complete'); smoothing.stop(); stopElapsedTimer(); + // Stop favicon animation + if (window.BigDump && window.BigDump.faviconAnimator) { + window.BigDump.faviconAnimator.stop(); + } intentionalClose = true; source.close(); try { @@ -736,6 +744,10 @@ function createConnection() { smoothing.stop(); stopElapsedTimer(); + // Stop favicon animation + if (window.BigDump && window.BigDump.faviconAnimator) { + window.BigDump.faviconAnimator.stop(); + } // Display error in page with hasCreateTable info // Combine stats and hasCreateTable into stats object for displayErrorInPage var errorStats = data.stats || {}; @@ -744,6 +756,10 @@ function createConnection() { } catch (err) { smoothing.stop(); stopElapsedTimer(); + // Stop favicon animation + if (window.BigDump && window.BigDump.faviconAnimator) { + window.BigDump.faviconAnimator.stop(); + } displayErrorInPage('Import error occurred', null); } intentionalClose = true; diff --git a/templates/import.php b/templates/import.php index 788c500..c47c2ab 100644 --- a/templates/import.php +++ b/templates/import.php @@ -370,6 +370,7 @@ + From d0299e4f6bf07425bc30144036488b25cee31f86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C6=90=C9=94=C4=B1s3?= <8407711+w3spi5@users.noreply.github.com> Date: Tue, 13 Jan 2026 19:10:11 +0100 Subject: [PATCH 04/10] fix 99.5% progress bug + add celebration effects on import completion --- assets/css/bigdump.css | 51 +++++++ assets/js/celebration.js | 280 +++++++++++++++++++++++++++++++++++ assets/src/css/tailwind.css | 51 +++++++ assets/src/js/celebration.js | 280 +++++++++++++++++++++++++++++++++++ src/Services/AjaxService.php | 75 +++++++--- templates/import.php | 1 + 6 files changed, 719 insertions(+), 19 deletions(-) create mode 100644 assets/js/celebration.js create mode 100644 assets/src/js/celebration.js diff --git a/assets/css/bigdump.css b/assets/css/bigdump.css index 944ab86..8424456 100644 --- a/assets/css/bigdump.css +++ b/assets/css/bigdump.css @@ -808,6 +808,57 @@ 100% { background-position: -200% 0; } } +/* ======================================== + PROGRESS BAR COMPLETION ANIMATION + ======================================== */ + +.progress-bar-complete { + background: linear-gradient(90deg, #22c55e, #16a34a, #4ade80, #22c55e) !important; + background-size: 300% 100% !important; + animation: progress-complete-glow 2s ease-in-out infinite !important; + box-shadow: 0 0 20px rgba(34, 197, 94, 0.6), + 0 0 40px rgba(34, 197, 94, 0.4), + inset 0 0 10px rgba(255, 255, 255, 0.2) !important; +} + +@keyframes progress-complete-glow { + 0%, 100% { + background-position: 0% 50%; + box-shadow: 0 0 20px rgba(34, 197, 94, 0.6), + 0 0 40px rgba(34, 197, 94, 0.4), + inset 0 0 10px rgba(255, 255, 255, 0.2); + } + 50% { + background-position: 100% 50%; + box-shadow: 0 0 30px rgba(34, 197, 94, 0.8), + 0 0 60px rgba(34, 197, 94, 0.5), + inset 0 0 15px rgba(255, 255, 255, 0.3); + } +} + +/* Dark mode adjustment for completion glow */ +.dark .progress-bar-complete { + box-shadow: 0 0 25px rgba(74, 222, 128, 0.7), + 0 0 50px rgba(74, 222, 128, 0.5), + inset 0 0 10px rgba(255, 255, 255, 0.15) !important; +} + +/* Success message fade-in animation */ +.animate-fade-in { + animation: fade-in-up 0.5s ease-out forwards; +} + +@keyframes fade-in-up { + 0% { + opacity: 0; + transform: translateY(20px); + } + 100% { + opacity: 1; + transform: translateY(0); + } +} + /* ======================================== FILE UPLOAD ENHANCEMENTS ======================================== */ diff --git a/assets/js/celebration.js b/assets/js/celebration.js new file mode 100644 index 0000000..7343c46 --- /dev/null +++ b/assets/js/celebration.js @@ -0,0 +1,280 @@ +/** + * BigDump - Celebration Effects Module + * Creates fireworks and confetti animations on import completion. + * Uses Canvas API for particle-based visual effects. + */ +(function() { + 'use strict'; + + var canvas = null; + var ctx = null; + var animationId = null; + var particles = []; + var confetti = []; + var isRunning = false; + var startTime = 0; + var duration = 4000; // 4 seconds of celebration + + // Colors for fireworks and confetti + var fireworkColors = [ + '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', + '#DDA0DD', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E9' + ]; + + var confettiColors = [ + '#FF6B6B', '#4ECDC4', '#45B7D1', '#FFD93D', '#6BCB77', + '#FF8C00', '#DA70D6', '#20B2AA', '#FF69B4', '#87CEEB' + ]; + + /** + * Particle class for fireworks + */ + function Particle(x, y, color, velocity, gravity, decay, size) { + this.x = x; + this.y = y; + this.color = color; + this.velocity = velocity; + this.gravity = gravity || 0.05; + this.decay = decay || 0.015; + this.alpha = 1; + this.size = size || 3; + } + + Particle.prototype.update = function() { + this.velocity.x *= 0.99; + this.velocity.y *= 0.99; + this.velocity.y += this.gravity; + this.x += this.velocity.x; + this.y += this.velocity.y; + this.alpha -= this.decay; + return this.alpha > 0; + }; + + Particle.prototype.draw = function(ctx) { + ctx.save(); + ctx.globalAlpha = this.alpha; + ctx.beginPath(); + ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); + ctx.fillStyle = this.color; + ctx.fill(); + ctx.restore(); + }; + + /** + * Confetti piece class + */ + function ConfettiPiece(x, y, color) { + this.x = x; + this.y = y; + this.color = color; + this.width = Math.random() * 10 + 5; + this.height = Math.random() * 6 + 3; + this.velocity = { + x: (Math.random() - 0.5) * 6, + y: Math.random() * 3 + 2 + }; + this.rotation = Math.random() * 360; + this.rotationSpeed = (Math.random() - 0.5) * 10; + this.oscillationSpeed = Math.random() * 0.1 + 0.05; + this.oscillationDistance = Math.random() * 40 + 20; + this.initialX = x; + this.time = Math.random() * 100; + } + + ConfettiPiece.prototype.update = function(canvasHeight) { + this.time += this.oscillationSpeed; + this.x = this.initialX + Math.sin(this.time) * this.oscillationDistance; + this.y += this.velocity.y; + this.rotation += this.rotationSpeed; + this.velocity.y += 0.02; // Light gravity + return this.y < canvasHeight + 50; + }; + + ConfettiPiece.prototype.draw = function(ctx) { + ctx.save(); + ctx.translate(this.x, this.y); + ctx.rotate(this.rotation * Math.PI / 180); + ctx.fillStyle = this.color; + ctx.fillRect(-this.width / 2, -this.height / 2, this.width, this.height); + ctx.restore(); + }; + + /** + * Create a firework explosion at position + */ + function createFirework(x, y) { + var color = fireworkColors[Math.floor(Math.random() * fireworkColors.length)]; + var particleCount = 50 + Math.floor(Math.random() * 30); + + for (var i = 0; i < particleCount; i++) { + var angle = (Math.PI * 2 / particleCount) * i; + var speed = Math.random() * 5 + 3; + var velocity = { + x: Math.cos(angle) * speed, + y: Math.sin(angle) * speed + }; + particles.push(new Particle(x, y, color, velocity, 0.04, 0.012, Math.random() * 3 + 1)); + } + + // Add some random sparkle particles + for (var j = 0; j < 20; j++) { + var sparkAngle = Math.random() * Math.PI * 2; + var sparkSpeed = Math.random() * 8 + 2; + var sparkVelocity = { + x: Math.cos(sparkAngle) * sparkSpeed, + y: Math.sin(sparkAngle) * sparkSpeed + }; + particles.push(new Particle(x, y, '#FFFFFF', sparkVelocity, 0.06, 0.025, 2)); + } + } + + /** + * Create confetti burst + */ + function createConfettiBurst(count) { + if (!canvas) return; + + for (var i = 0; i < count; i++) { + var x = Math.random() * canvas.width; + var color = confettiColors[Math.floor(Math.random() * confettiColors.length)]; + confetti.push(new ConfettiPiece(x, -20, color)); + } + } + + /** + * Initialize canvas + */ + function init() { + if (canvas) return; + + canvas = document.createElement('canvas'); + canvas.id = 'celebration-canvas'; + canvas.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 9999;'; + document.body.appendChild(canvas); + + ctx = canvas.getContext('2d'); + resizeCanvas(); + + window.addEventListener('resize', resizeCanvas); + } + + /** + * Resize canvas to match window + */ + function resizeCanvas() { + if (!canvas) return; + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + } + + /** + * Animation loop + */ + function animate() { + if (!isRunning) return; + + var elapsed = Date.now() - startTime; + + // Clear canvas + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Update and draw particles + particles = particles.filter(function(p) { + var alive = p.update(); + if (alive) p.draw(ctx); + return alive; + }); + + // Update and draw confetti + confetti = confetti.filter(function(c) { + var alive = c.update(canvas.height); + if (alive) c.draw(ctx); + return alive; + }); + + // Create new fireworks periodically during first 2 seconds + if (elapsed < 2000 && Math.random() < 0.08) { + var x = Math.random() * canvas.width * 0.6 + canvas.width * 0.2; + var y = Math.random() * canvas.height * 0.4 + canvas.height * 0.1; + createFirework(x, y); + } + + // Add confetti continuously during first 3 seconds + if (elapsed < 3000 && Math.random() < 0.3) { + createConfettiBurst(3); + } + + // Continue animation until all particles are gone or duration exceeded + if (elapsed < duration || particles.length > 0 || confetti.length > 0) { + animationId = requestAnimationFrame(animate); + } else { + cleanup(); + } + } + + /** + * Clean up canvas and reset state + */ + function cleanup() { + isRunning = false; + if (animationId) { + cancelAnimationFrame(animationId); + animationId = null; + } + if (canvas && canvas.parentNode) { + canvas.parentNode.removeChild(canvas); + } + canvas = null; + ctx = null; + particles = []; + confetti = []; + window.removeEventListener('resize', resizeCanvas); + } + + /** + * Start the celebration effects + */ + function start() { + if (isRunning) return; + + init(); + isRunning = true; + startTime = Date.now(); + particles = []; + confetti = []; + + // Initial burst of fireworks + for (var i = 0; i < 5; i++) { + setTimeout(function() { + if (!isRunning) return; + var x = Math.random() * canvas.width * 0.6 + canvas.width * 0.2; + var y = Math.random() * canvas.height * 0.4 + canvas.height * 0.15; + createFirework(x, y); + }, i * 200); + } + + // Initial confetti burst + createConfettiBurst(100); + + animate(); + console.log('Celebration started!'); + } + + /** + * Stop the celebration effects immediately + */ + function stop() { + if (!isRunning) return; + cleanup(); + console.log('Celebration stopped'); + } + + // Expose API + window.BigDump = window.BigDump || {}; + window.BigDump.celebration = { + start: start, + stop: stop, + isRunning: function() { return isRunning; } + }; + +})(); diff --git a/assets/src/css/tailwind.css b/assets/src/css/tailwind.css index 5c44939..4c741fb 100644 --- a/assets/src/css/tailwind.css +++ b/assets/src/css/tailwind.css @@ -852,6 +852,57 @@ 100% { background-position: -200% 0; } } +/* ======================================== + PROGRESS BAR COMPLETION ANIMATION + ======================================== */ + +.progress-bar-complete { + background: linear-gradient(90deg, #22c55e, #16a34a, #4ade80, #22c55e) !important; + background-size: 300% 100% !important; + animation: progress-complete-glow 2s ease-in-out infinite !important; + box-shadow: 0 0 20px rgba(34, 197, 94, 0.6), + 0 0 40px rgba(34, 197, 94, 0.4), + inset 0 0 10px rgba(255, 255, 255, 0.2) !important; +} + +@keyframes progress-complete-glow { + 0%, 100% { + background-position: 0% 50%; + box-shadow: 0 0 20px rgba(34, 197, 94, 0.6), + 0 0 40px rgba(34, 197, 94, 0.4), + inset 0 0 10px rgba(255, 255, 255, 0.2); + } + 50% { + background-position: 100% 50%; + box-shadow: 0 0 30px rgba(34, 197, 94, 0.8), + 0 0 60px rgba(34, 197, 94, 0.5), + inset 0 0 15px rgba(255, 255, 255, 0.3); + } +} + +/* Dark mode adjustment for completion glow */ +.dark .progress-bar-complete { + box-shadow: 0 0 25px rgba(74, 222, 128, 0.7), + 0 0 50px rgba(74, 222, 128, 0.5), + inset 0 0 10px rgba(255, 255, 255, 0.15) !important; +} + +/* Success message fade-in animation */ +.animate-fade-in { + animation: fade-in-up 0.5s ease-out forwards; +} + +@keyframes fade-in-up { + 0% { + opacity: 0; + transform: translateY(20px); + } + 100% { + opacity: 1; + transform: translateY(0); + } +} + /* ======================================== FILE UPLOAD ENHANCEMENTS ======================================== */ diff --git a/assets/src/js/celebration.js b/assets/src/js/celebration.js new file mode 100644 index 0000000..7343c46 --- /dev/null +++ b/assets/src/js/celebration.js @@ -0,0 +1,280 @@ +/** + * BigDump - Celebration Effects Module + * Creates fireworks and confetti animations on import completion. + * Uses Canvas API for particle-based visual effects. + */ +(function() { + 'use strict'; + + var canvas = null; + var ctx = null; + var animationId = null; + var particles = []; + var confetti = []; + var isRunning = false; + var startTime = 0; + var duration = 4000; // 4 seconds of celebration + + // Colors for fireworks and confetti + var fireworkColors = [ + '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', + '#DDA0DD', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E9' + ]; + + var confettiColors = [ + '#FF6B6B', '#4ECDC4', '#45B7D1', '#FFD93D', '#6BCB77', + '#FF8C00', '#DA70D6', '#20B2AA', '#FF69B4', '#87CEEB' + ]; + + /** + * Particle class for fireworks + */ + function Particle(x, y, color, velocity, gravity, decay, size) { + this.x = x; + this.y = y; + this.color = color; + this.velocity = velocity; + this.gravity = gravity || 0.05; + this.decay = decay || 0.015; + this.alpha = 1; + this.size = size || 3; + } + + Particle.prototype.update = function() { + this.velocity.x *= 0.99; + this.velocity.y *= 0.99; + this.velocity.y += this.gravity; + this.x += this.velocity.x; + this.y += this.velocity.y; + this.alpha -= this.decay; + return this.alpha > 0; + }; + + Particle.prototype.draw = function(ctx) { + ctx.save(); + ctx.globalAlpha = this.alpha; + ctx.beginPath(); + ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); + ctx.fillStyle = this.color; + ctx.fill(); + ctx.restore(); + }; + + /** + * Confetti piece class + */ + function ConfettiPiece(x, y, color) { + this.x = x; + this.y = y; + this.color = color; + this.width = Math.random() * 10 + 5; + this.height = Math.random() * 6 + 3; + this.velocity = { + x: (Math.random() - 0.5) * 6, + y: Math.random() * 3 + 2 + }; + this.rotation = Math.random() * 360; + this.rotationSpeed = (Math.random() - 0.5) * 10; + this.oscillationSpeed = Math.random() * 0.1 + 0.05; + this.oscillationDistance = Math.random() * 40 + 20; + this.initialX = x; + this.time = Math.random() * 100; + } + + ConfettiPiece.prototype.update = function(canvasHeight) { + this.time += this.oscillationSpeed; + this.x = this.initialX + Math.sin(this.time) * this.oscillationDistance; + this.y += this.velocity.y; + this.rotation += this.rotationSpeed; + this.velocity.y += 0.02; // Light gravity + return this.y < canvasHeight + 50; + }; + + ConfettiPiece.prototype.draw = function(ctx) { + ctx.save(); + ctx.translate(this.x, this.y); + ctx.rotate(this.rotation * Math.PI / 180); + ctx.fillStyle = this.color; + ctx.fillRect(-this.width / 2, -this.height / 2, this.width, this.height); + ctx.restore(); + }; + + /** + * Create a firework explosion at position + */ + function createFirework(x, y) { + var color = fireworkColors[Math.floor(Math.random() * fireworkColors.length)]; + var particleCount = 50 + Math.floor(Math.random() * 30); + + for (var i = 0; i < particleCount; i++) { + var angle = (Math.PI * 2 / particleCount) * i; + var speed = Math.random() * 5 + 3; + var velocity = { + x: Math.cos(angle) * speed, + y: Math.sin(angle) * speed + }; + particles.push(new Particle(x, y, color, velocity, 0.04, 0.012, Math.random() * 3 + 1)); + } + + // Add some random sparkle particles + for (var j = 0; j < 20; j++) { + var sparkAngle = Math.random() * Math.PI * 2; + var sparkSpeed = Math.random() * 8 + 2; + var sparkVelocity = { + x: Math.cos(sparkAngle) * sparkSpeed, + y: Math.sin(sparkAngle) * sparkSpeed + }; + particles.push(new Particle(x, y, '#FFFFFF', sparkVelocity, 0.06, 0.025, 2)); + } + } + + /** + * Create confetti burst + */ + function createConfettiBurst(count) { + if (!canvas) return; + + for (var i = 0; i < count; i++) { + var x = Math.random() * canvas.width; + var color = confettiColors[Math.floor(Math.random() * confettiColors.length)]; + confetti.push(new ConfettiPiece(x, -20, color)); + } + } + + /** + * Initialize canvas + */ + function init() { + if (canvas) return; + + canvas = document.createElement('canvas'); + canvas.id = 'celebration-canvas'; + canvas.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 9999;'; + document.body.appendChild(canvas); + + ctx = canvas.getContext('2d'); + resizeCanvas(); + + window.addEventListener('resize', resizeCanvas); + } + + /** + * Resize canvas to match window + */ + function resizeCanvas() { + if (!canvas) return; + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + } + + /** + * Animation loop + */ + function animate() { + if (!isRunning) return; + + var elapsed = Date.now() - startTime; + + // Clear canvas + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Update and draw particles + particles = particles.filter(function(p) { + var alive = p.update(); + if (alive) p.draw(ctx); + return alive; + }); + + // Update and draw confetti + confetti = confetti.filter(function(c) { + var alive = c.update(canvas.height); + if (alive) c.draw(ctx); + return alive; + }); + + // Create new fireworks periodically during first 2 seconds + if (elapsed < 2000 && Math.random() < 0.08) { + var x = Math.random() * canvas.width * 0.6 + canvas.width * 0.2; + var y = Math.random() * canvas.height * 0.4 + canvas.height * 0.1; + createFirework(x, y); + } + + // Add confetti continuously during first 3 seconds + if (elapsed < 3000 && Math.random() < 0.3) { + createConfettiBurst(3); + } + + // Continue animation until all particles are gone or duration exceeded + if (elapsed < duration || particles.length > 0 || confetti.length > 0) { + animationId = requestAnimationFrame(animate); + } else { + cleanup(); + } + } + + /** + * Clean up canvas and reset state + */ + function cleanup() { + isRunning = false; + if (animationId) { + cancelAnimationFrame(animationId); + animationId = null; + } + if (canvas && canvas.parentNode) { + canvas.parentNode.removeChild(canvas); + } + canvas = null; + ctx = null; + particles = []; + confetti = []; + window.removeEventListener('resize', resizeCanvas); + } + + /** + * Start the celebration effects + */ + function start() { + if (isRunning) return; + + init(); + isRunning = true; + startTime = Date.now(); + particles = []; + confetti = []; + + // Initial burst of fireworks + for (var i = 0; i < 5; i++) { + setTimeout(function() { + if (!isRunning) return; + var x = Math.random() * canvas.width * 0.6 + canvas.width * 0.2; + var y = Math.random() * canvas.height * 0.4 + canvas.height * 0.15; + createFirework(x, y); + }, i * 200); + } + + // Initial confetti burst + createConfettiBurst(100); + + animate(); + console.log('Celebration started!'); + } + + /** + * Stop the celebration effects immediately + */ + function stop() { + if (!isRunning) return; + cleanup(); + console.log('Celebration stopped'); + } + + // Expose API + window.BigDump = window.BigDump || {}; + window.BigDump.celebration = { + start: start, + stop: stop, + isRunning: function() { return isRunning; } + }; + +})(); diff --git a/src/Services/AjaxService.php b/src/Services/AjaxService.php index 73bc39e..f968ca4 100644 --- a/src/Services/AjaxService.php +++ b/src/Services/AjaxService.php @@ -164,25 +164,32 @@ function displayErrorInPage(message, stats) { * @param {object} stats Statistics object */ function displaySuccessInPage(stats) { + // Start celebration effects (fireworks + confetti) + if (window.BigDump && window.BigDump.celebration) { + window.BigDump.celebration.start(); + } + var queriesDone = stats && stats.queries_done ? stats.queries_done.toLocaleString() : '0'; var linesDone = stats && stats.lines_done ? stats.lines_done.toLocaleString() : '0'; var bytesDone = stats && stats.bytes_done ? formatBytes(stats.bytes_done) : '0 B'; var elapsed = document.getElementById('elapsedTime'); var elapsedTime = elapsed ? elapsed.textContent : '00:00:00'; - // Create success HTML with Tailwind classes - var successHtml = '