diff --git a/webui/index.html b/webui/index.html index de36fe4e..bb8bd865 100644 --- a/webui/index.html +++ b/webui/index.html @@ -551,9 +551,18 @@ - - + + diff --git a/webui/static/script.js b/webui/static/script.js index 1bdfab33..1761338a 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -312,12 +312,20 @@ function handleServiceStatusUpdate(data) { } } +function _updateHeroBtnCount(buttonId, badgeId, count) { + const badge = document.getElementById(badgeId); + if (badge) { + badge.textContent = count; + badge.classList.toggle('has-items', count > 0); + } +} + function handleWatchlistCountUpdate(data) { if (data.success) { + _updateHeroBtnCount('watchlist-button', 'watchlist-badge', data.count); const watchlistButton = document.getElementById('watchlist-button'); if (watchlistButton) { const countdownText = data.next_run_in_seconds ? formatCountdownTime(data.next_run_in_seconds) : ''; - watchlistButton.textContent = `\u{1F441}\uFE0F Watchlist (${data.count})`; if (countdownText) { watchlistButton.title = `Next auto-scan in ${countdownText}`; } @@ -399,11 +407,10 @@ function handleDashboardDbStats(stats) { } function handleDashboardWishlistCount(data) { - // Same logic as updateWishlistCount response handler const count = data.count || 0; + _updateHeroBtnCount('wishlist-button', 'wishlist-badge', count); const wishlistButton = document.getElementById('wishlist-button'); if (wishlistButton) { - wishlistButton.textContent = `\uD83C\uDFB5 Wishlist (${count})`; if (count === 0) { wishlistButton.classList.remove('wishlist-active'); wishlistButton.classList.add('wishlist-inactive'); @@ -20816,11 +20823,9 @@ async function updateWishlistCount() { const data = await response.json(); const count = data.count || 0; + _updateHeroBtnCount('wishlist-button', 'wishlist-badge', count); const wishlistButton = document.getElementById('wishlist-button'); if (wishlistButton) { - wishlistButton.textContent = `🎵 Wishlist (${count})`; - - // Update button styling based on count (matching GUI behavior) if (count === 0) { wishlistButton.classList.remove('wishlist-active'); wishlistButton.classList.add('wishlist-inactive'); @@ -34060,11 +34065,10 @@ async function updateWatchlistButtonCount() { const data = await response.json(); if (data.success) { + _updateHeroBtnCount('watchlist-button', 'watchlist-badge', data.count); const watchlistButton = document.getElementById('watchlist-button'); if (watchlistButton) { - // Format countdown for button tooltip (optional enhancement) const countdownText = data.next_run_in_seconds ? formatCountdownTime(data.next_run_in_seconds) : ''; - watchlistButton.textContent = `👁️ Watchlist (${data.count})`; if (countdownText) { watchlistButton.title = `Next auto-scan in ${countdownText}`; } diff --git a/webui/static/style.css b/webui/static/style.css index 33192760..bfda0860 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -5727,40 +5727,236 @@ body { inset 0 1px 0 rgba(255, 255, 255, 0.2); } +/* ── Watchlist & Wishlist Hero Buttons ── */ +.watchlist-button, .wishlist-button { - background: linear-gradient(135deg, rgb(var(--accent-rgb)) 0%, rgb(var(--accent-light-rgb)) 100%); + position: relative; + display: inline-flex; + align-items: center; + gap: 9px; + padding: 12px 20px; + overflow: visible; + background: rgba(18, 18, 22, 0.85); + backdrop-filter: blur(16px) saturate(1.4); + color: #e0e0e0; + border: none; + border-radius: 22px; + z-index: 0; } -.wishlist-button:hover { - background: linear-gradient(135deg, rgb(var(--accent-light-rgb)) 0%, rgb(var(--accent-neon-rgb)) 100%); +/* Animated gradient border using border-image-like technique */ +.watchlist-button::before, +.wishlist-button::before { + content: ''; + position: absolute; + inset: 0; + border-radius: 22px; + padding: 1.5px; + -webkit-mask: + linear-gradient(#fff 0 0) content-box, + linear-gradient(#fff 0 0); + -webkit-mask-composite: xor; + mask: + linear-gradient(#fff 0 0) content-box, + linear-gradient(#fff 0 0); + mask-composite: exclude; + pointer-events: none; + z-index: 1; } -/* Wishlist button states based on count */ -.wishlist-button.wishlist-inactive { - background: linear-gradient(135deg, #404040 0%, #505050 100%); - color: #888888; +.watchlist-button::before { + background: linear-gradient( + 135deg, + #ff8f00, #ffc107, #ffd54f, #ffab00, #ffc107, #ff8f00 + ); + background-size: 300% 300%; + animation: heroBorderFlow 4s ease infinite; } -.wishlist-button.wishlist-inactive:hover { - background: linear-gradient(135deg, #505050 0%, #666666 100%); - color: #999999; +.wishlist-button::before { + background: linear-gradient( + 135deg, + rgb(var(--accent-rgb)), + rgb(var(--accent-light-rgb)), + rgb(var(--accent-neon-rgb)), + rgb(var(--accent-rgb)), + rgb(var(--accent-light-rgb)), + rgb(var(--accent-rgb)) + ); + background-size: 300% 300%; + animation: heroBorderFlow 4s ease infinite; + animation-delay: 2s; } -.wishlist-button.wishlist-active { - background: linear-gradient(135deg, rgb(var(--accent-rgb)) 0%, rgb(var(--accent-light-rgb)) 100%); - color: #000000; -} - -.wishlist-button.wishlist-active:hover { - background: linear-gradient(135deg, rgb(var(--accent-light-rgb)) 0%, rgb(var(--accent-neon-rgb)) 100%); +@keyframes heroBorderFlow { + 0% { background-position: 0% 50%; } + 50% { background-position: 100% 50%; } + 100% { background-position: 0% 50%; } } +/* Outer glow */ .watchlist-button { - background: linear-gradient(135deg, #ffc107 0%, #ffca28 100%); + box-shadow: 0 0 14px rgba(255, 193, 7, 0.12), 0 4px 16px rgba(0, 0, 0, 0.3); +} + +.wishlist-button { + box-shadow: 0 0 14px rgba(var(--accent-rgb), 0.12), 0 4px 16px rgba(0, 0, 0, 0.3); } .watchlist-button:hover { - background: linear-gradient(135deg, #ffca28 0%, #ffd54f 100%); + box-shadow: 0 0 24px rgba(255, 193, 7, 0.25), 0 4px 20px rgba(0, 0, 0, 0.4); + transform: translateY(-1px); +} + +.wishlist-button:hover { + box-shadow: 0 0 24px rgba(var(--accent-rgb), 0.25), 0 4px 20px rgba(0, 0, 0, 0.4); + transform: translateY(-1px); +} + +/* Icon */ +.hero-btn-icon { + font-size: 16px; + line-height: 1; +} + +/* Label */ +.hero-btn-label { + font-weight: 700; + letter-spacing: 0.3px; +} + +/* Watchlist text color tint */ +.watchlist-button .hero-btn-label { + color: #ffd54f; +} + +.wishlist-button .hero-btn-label { + color: rgb(var(--accent-light-rgb)); +} + +/* Count badge */ +.hero-btn-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 22px; + height: 22px; + padding: 0 6px; + border-radius: 11px; + font-size: 11px; + font-weight: 800; + line-height: 1; + transition: all 0.3s ease; +} + +.watchlist-button .hero-btn-badge { + background: rgba(255, 193, 7, 0.15); + color: #ffc107; + border: 1px solid rgba(255, 193, 7, 0.3); +} + +.wishlist-button .hero-btn-badge { + background: rgba(var(--accent-rgb), 0.15); + color: rgb(var(--accent-light-rgb)); + border: 1px solid rgba(var(--accent-rgb), 0.3); +} + +.hero-btn-badge.has-items { + animation: badgePulse 2s ease-in-out infinite; +} + +.watchlist-button .hero-btn-badge.has-items { + background: rgba(255, 193, 7, 0.25); + border-color: rgba(255, 193, 7, 0.5); +} + +.wishlist-button .hero-btn-badge.has-items { + background: rgba(var(--accent-rgb), 0.25); + border-color: rgba(var(--accent-rgb), 0.5); +} + +@keyframes badgePulse { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.12); } +} + +/* Shimmer sweep — clipped inside button */ +.hero-btn-shimmer { + position: absolute; + inset: 0; + border-radius: 22px; + overflow: hidden; + pointer-events: none; + z-index: 1; +} + +.hero-btn-shimmer::after { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + animation: shimmerSweep 5s ease-in-out infinite; +} + +.watchlist-button .hero-btn-shimmer::after { + background: linear-gradient( + 105deg, + transparent 30%, + rgba(255, 193, 7, 0.1) 50%, + transparent 70% + ); +} + +.wishlist-button .hero-btn-shimmer::after { + background: linear-gradient( + 105deg, + transparent 30%, + rgba(var(--accent-rgb), 0.1) 50%, + transparent 70% + ); + animation-delay: 2.5s; +} + +@keyframes shimmerSweep { + 0%, 100% { left: -100%; } + 40%, 60% { left: 100%; } +} + +/* ── Wishlist inactive state ── */ +.wishlist-button.wishlist-inactive { + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); +} + +.wishlist-button.wishlist-inactive::before { + background: linear-gradient(135deg, #444, #555, #444); + background-size: 100% 100%; + animation: none; +} + +.wishlist-button.wishlist-inactive .hero-btn-label { + color: #777; +} + +.wishlist-button.wishlist-inactive .hero-btn-badge { + background: rgba(255, 255, 255, 0.06); + color: #666; + border-color: rgba(255, 255, 255, 0.1); + animation: none; +} + +.wishlist-button.wishlist-inactive .hero-btn-shimmer { + display: none; +} + +.wishlist-button.wishlist-inactive:hover { + box-shadow: 0 0 12px rgba(255, 255, 255, 0.08), 0 4px 20px rgba(0, 0, 0, 0.4); +} + +/* Active wishlist just uses the normal accent styles above */ +.wishlist-button.wishlist-active { + /* already styled by default .wishlist-button rules */ } /* Service Status Grid */ diff --git a/webui/static/worker-orbs.js b/webui/static/worker-orbs.js index 72bc7358..4607e5d8 100644 --- a/webui/static/worker-orbs.js +++ b/webui/static/worker-orbs.js @@ -43,6 +43,8 @@ let onDashboard = false; let expandProgress = 0; let staggerTimers = []; + let collapseDelay = null; + const COLLAPSE_DELAY_MS = 7000; // ── Init ── @@ -251,6 +253,11 @@ function onMouseEnter() { if (!onDashboard) return; + // Cancel any pending collapse + if (collapseDelay) { + clearTimeout(collapseDelay); + collapseDelay = null; + } if (state === 'orbs' || state === 'collapsing') { state = 'expanding'; expandProgress = 0; @@ -260,7 +267,14 @@ function onMouseLeave() { if (!onDashboard) return; if (state === 'expanded' || state === 'expanding') { - enterCollapsingState(); + // Delay before collapsing back to orbs + if (collapseDelay) clearTimeout(collapseDelay); + collapseDelay = setTimeout(() => { + collapseDelay = null; + if (state === 'expanded' || state === 'expanding') { + enterCollapsingState(); + } + }, COLLAPSE_DELAY_MS); } } @@ -551,6 +565,7 @@ sparks = []; enterOrbState(); } else if (!onDashboard && wasDashboard) { + if (collapseDelay) { clearTimeout(collapseDelay); collapseDelay = null; } stopLoop(); state = 'idle'; sparks = [];