Replace Blazor loader with custom music-themed splash

Replaces default Blazor loading spinner with a custom, animated music-themed loader featuring a gradient background, floating musical notes, logo, equalizer animation, and dynamic progress bar/messages. Removes old loader CSS from app.css and adds new styles to index.html. Loader progress and messages are now animated via JavaScript, with interactive note effects on click. Loader and background fade out smoothly when the app is ready.
This commit is contained in:
Michal Vrba
2025-12-11 13:37:44 +01:00
parent 6e325e007e
commit c6a8055ece
2 changed files with 409 additions and 48 deletions
+5 -43
View File
@@ -15,6 +15,11 @@ html, body {
width: 100vw;
}
/* Smooth transition when app loads */
.app-loaded #app {
background: none !important;
}
h1:focus {
outline: none;
}
@@ -76,49 +81,6 @@ a, .btn-link {
content: "An error has occurred."
}
.loading-progress {
position: absolute;
display: block;
width: 8rem;
height: 8rem;
inset: 20vh 0 auto 0;
margin: 0 auto 0 auto;
}
.loading-progress circle {
fill: none;
stroke: #e0e0e0;
stroke-width: 0.6rem;
transform-origin: 50% 50%;
transform: rotate(-90deg);
}
.loading-progress circle:last-child {
stroke: #1b6ec2;
stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
transition: stroke-dasharray 0.05s ease-in-out;
}
.loading-progress-text {
position: absolute;
text-align: center;
font-weight: bold;
inset: calc(20vh + 3.25rem) 0 auto 0.2rem;
}
.loading-progress-text:after {
content: var(--blazor-load-percentage-text, "Loading");
}
code {
color: #c02d76;
}
.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
color: var(--bs-secondary-color);
text-align: end;
}
.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
text-align: start;
}
+404 -5
View File
@@ -18,15 +18,291 @@
<link href="TuneRipper.styles.css" rel="stylesheet" />
<script type="importmap"></script>
<style>
/* Custom WASM Loader Styles */
#app {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
}
.custom-loader {
text-align: center;
color: white;
z-index: 10;
position: relative;
}
.loader-logo {
width: 120px;
height: 120px;
margin: 0 auto 30px;
border-radius: 20px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
animation: pulse 2s ease-in-out infinite;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
position: relative;
}
.loader-logo img {
width: 80px;
height: 80px;
object-fit: contain;
}
.loader-logo::before {
content: '';
position: absolute;
top: -2px;
left: -2px;
right: -2px;
bottom: -2px;
background: linear-gradient(45deg, #ff6b6b, #feca57, #48dbfb, #ff9ff3);
border-radius: 22px;
z-index: -1;
animation: border-glow 3s ease-in-out infinite;
}
.loader-title {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 10px;
background: linear-gradient(45deg, #fff, #f0f8ff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
animation: title-glow 3s ease-in-out infinite;
}
.loader-subtitle {
font-size: 1.2rem;
margin-bottom: 40px;
opacity: 0.9;
font-weight: 300;
}
/* Progress Bar */
.progress-container {
width: 300px;
height: 8px;
background: rgba(255, 255, 255, 0.2);
border-radius: 10px;
overflow: hidden;
margin: 0 auto 20px;
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.2);
}
.progress-bar {
height: 100%;
background: linear-gradient(90deg, #ff6b6b, #feca57, #48dbfb, #ff9ff3);
background-size: 300% 100%;
border-radius: 10px;
width: 0%;
transition: width 0.3s ease;
animation: gradient-shift 2s ease-in-out infinite;
box-shadow: 0 0 15px rgba(255, 255, 255, 0.5);
}
.loading-text {
font-size: 0.9rem;
opacity: 0.8;
margin-top: 15px;
transition: opacity 0.3s ease;
}
/* Floating Musical Notes */
.music-notes {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1;
}
.note {
position: absolute;
font-size: 2rem;
color: rgba(255, 255, 255, 0.4);
animation: float-note 6s linear infinite;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
}
.note:nth-child(1) { left: 10%; animation-delay: 0s; animation-duration: 8s; }
.note:nth-child(2) { left: 20%; animation-delay: 1s; animation-duration: 7s; }
.note:nth-child(3) { left: 30%; animation-delay: 2s; animation-duration: 9s; }
.note:nth-child(4) { left: 40%; animation-delay: 0.5s; animation-duration: 6s; }
.note:nth-child(5) { left: 50%; animation-delay: 3s; animation-duration: 8s; }
.note:nth-child(6) { left: 60%; animation-delay: 1.5s; animation-duration: 7s; }
.note:nth-child(7) { left: 70%; animation-delay: 2.5s; animation-duration: 9s; }
.note:nth-child(8) { left: 80%; animation-delay: 4s; animation-duration: 6s; }
.note:nth-child(9) { left: 90%; animation-delay: 0.8s; animation-duration: 8s; }
/* Equalizer bars */
.equalizer {
display: flex;
align-items: center;
justify-content: center;
gap: 3px;
margin: 20px 0;
}
.eq-bar {
width: 4px;
background: rgba(255, 255, 255, 0.8);
border-radius: 2px;
animation: equalizer 1.5s ease-in-out infinite;
}
.eq-bar:nth-child(1) { height: 20px; animation-delay: 0s; }
.eq-bar:nth-child(2) { height: 35px; animation-delay: 0.1s; }
.eq-bar:nth-child(3) { height: 25px; animation-delay: 0.2s; }
.eq-bar:nth-child(4) { height: 40px; animation-delay: 0.3s; }
.eq-bar:nth-child(5) { height: 30px; animation-delay: 0.4s; }
.eq-bar:nth-child(6) { height: 45px; animation-delay: 0.5s; }
.eq-bar:nth-child(7) { height: 25px; animation-delay: 0.6s; }
/* Animations */
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
@keyframes border-glow {
0%, 100% { opacity: 0.8; }
50% { opacity: 1; }
}
@keyframes title-glow {
0%, 100% { text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3); }
50% { text-shadow: 0 2px 20px rgba(255, 255, 255, 0.5); }
}
@keyframes float-note {
0% {
transform: translateY(100vh) rotate(0deg);
opacity: 0;
}
10% {
opacity: 1;
}
90% {
opacity: 1;
}
100% {
transform: translateY(-100px) rotate(360deg);
opacity: 0;
}
}
@keyframes gradient-shift {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
@keyframes equalizer {
0%, 100% { transform: scaleY(1); }
50% { transform: scaleY(0.3); }
}
/* Hide when loaded */
.app-loaded #app .custom-loader {
opacity: 0;
transform: translateY(-20px);
transition: all 0.8s ease-out;
pointer-events: none;
}
.app-loaded #app .music-notes {
opacity: 0;
transition: opacity 0.8s ease-out;
}
.app-loaded #app {
background: none !important;
transition: background 0.8s ease-out;
}
/* Responsive adjustments */
@media (max-width: 480px) {
.loader-logo {
width: 100px;
height: 100px;
}
.loader-logo img {
width: 70px;
height: 70px;
}
.loader-title {
font-size: 2rem;
}
.progress-container {
width: 250px;
}
.note {
font-size: 1.5rem;
}
}
</style>
</head>
<body>
<div id="app">
<svg class="loading-progress">
<circle r="40%" cx="50%" cy="50%" />
<circle r="40%" cx="50%" cy="50%" />
</svg>
<div class="loading-progress-text"></div>
<!-- Floating Musical Notes Background -->
<div class="music-notes">
<div class="note">♪</div>
<div class="note">♫</div>
<div class="note">♪</div>
<div class="note">♬</div>
<div class="note">♫</div>
<div class="note">♪</div>
<div class="note">♬</div>
<div class="note">♫</div>
<div class="note">♪</div>
</div>
<!-- Custom Loader -->
<div class="custom-loader">
<div class="loader-logo">
<img src="images/logo.png" alt="TuneRipper Logo" />
</div>
<h1 class="loader-title">TuneRipper</h1>
<p class="loader-subtitle">YouTube to MP3 Converter</p>
<!-- Equalizer Animation -->
<div class="equalizer">
<div class="eq-bar"></div>
<div class="eq-bar"></div>
<div class="eq-bar"></div>
<div class="eq-bar"></div>
<div class="eq-bar"></div>
<div class="eq-bar"></div>
<div class="eq-bar"></div>
</div>
<!-- Progress Bar -->
<div class="progress-container">
<div class="progress-bar" id="loadingProgress"></div>
</div>
<div class="loading-text" id="loadingText">Initializing audio engine...</div>
</div>
</div>
<div id="blazor-error-ui">
@@ -34,6 +310,129 @@
<a href="." class="reload">Reload</a>
<span class="dismiss">🗙</span>
</div>
<script>
// Enhanced progress tracking with music-themed messages
let progress = 0;
let messageIndex = 0;
const progressBar = document.getElementById('loadingProgress');
const loadingTextEl = document.getElementById('loadingText');
const loadingMessages = [
'Initializing audio engine...',
'Loading sound codecs...',
'Tuning the digital instruments...',
'Preparing the recording studio...',
'Setting up the mixing board...',
'Calibrating audio levels...',
'Loading music libraries...',
'Warming up the amplifiers...',
'Almost ready to rock! 🎸'
];
function updateProgress() {
// Simulate realistic loading progress
const increment = Math.random() * 8 + 3;
progress += increment;
if (progress > 95) progress = 95; // Stop at 95% until Blazor is actually loaded
progressBar.style.width = progress + '%';
// Update message based on progress
const newMessageIndex = Math.min(
Math.floor((progress / 95) * loadingMessages.length),
loadingMessages.length - 1
);
if (newMessageIndex !== messageIndex) {
messageIndex = newMessageIndex;
loadingTextEl.style.opacity = '0';
setTimeout(() => {
loadingTextEl.textContent = loadingMessages[messageIndex];
loadingTextEl.style.opacity = '0.8';
}, 150);
}
if (progress < 95) {
setTimeout(updateProgress, Math.random() * 400 + 300);
}
}
function completeLoading() {
progress = 100;
progressBar.style.width = '100%';
loadingTextEl.style.opacity = '0';
setTimeout(() => {
loadingTextEl.textContent = 'Ready to convert! 🎵';
loadingTextEl.style.opacity = '0.8';
}, 150);
setTimeout(() => {
document.body.classList.add('app-loaded');
}, 800);
}
// Start progress simulation
setTimeout(updateProgress, 500);
// Listen for Blazor events
let blazorLoaded = false;
// Check if Blazor is loaded via multiple methods
function checkBlazorLoaded() {
if (blazorLoaded) return;
if (window.Blazor || document.querySelector('[data-blazor-loaded]')) {
blazorLoaded = true;
completeLoading();
}
}
// Method 1: Standard load event
window.addEventListener('load', () => {
setTimeout(checkBlazorLoaded, 200);
});
// Method 2: Blazor specific events
document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => {
if (window.Blazor) {
blazorLoaded = true;
completeLoading();
}
}, 1000);
});
// Method 3: Fallback timer (ensure it loads eventually)
setTimeout(() => {
if (!blazorLoaded) {
blazorLoaded = true;
completeLoading();
}
}, 5000);
// Add some interactive notes on click/touch
document.addEventListener('click', function(e) {
if (!blazorLoaded && Math.random() > 0.7) {
const note = document.createElement('div');
note.className = 'note';
note.textContent = ['♪', '♫', '♬'][Math.floor(Math.random() * 3)];
note.style.left = e.clientX + 'px';
note.style.top = e.clientY + 'px';
note.style.position = 'fixed';
note.style.zIndex = '20';
note.style.animation = 'float-note 2s linear forwards';
document.body.appendChild(note);
setTimeout(() => {
note.remove();
}, 2000);
}
});
</script>
<script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
</body>