diff --git a/frontend/app.js b/frontend/app.js index 914595a..65cf66c 100755 --- a/frontend/app.js +++ b/frontend/app.js @@ -10519,6 +10519,7 @@ document.querySelectorAll('.chip').forEach((c) => { // Mini now-playing bar $('miniPlayBtn').addEventListener('click', (e) => { e.stopPropagation(); Player.toggle(); }); + $('updateIconBtn').addEventListener('click', updateFromIcon); $('miniBar').addEventListener('click', () => { setHomeOverPlayer(false); hideMiniBar(); @@ -11102,16 +11103,37 @@ async function hardReloadUI() { let _updateBannerShown = false; let _updating = false; let _updateToast = null; + +// After "Later" (or a failed download) an update icon sits beside the search +// box until the user takes the update; it always targets the newest build. +function setUpdateIcon(on) { + const b = $('updateIconBtn'); + if (b) b.classList.toggle('hidden', !on); +} +async function updateFromIcon() { + if (_updating) return; + setUpdateIcon(false); + let target = null; + try { target = await fetchServerBuild(); } catch { /* offline */ } + if (!target || target === (RUNNING_BUILD || _knownBuildTag)) { + if (!target) { toast('Offline — try the update again when connected'); setUpdateIcon(true); } + return; + } + await applyUpdate(target); +} + function showUpdateBanner(target) { // Only show the dialog once per page load if (_updateBannerShown) return; _updateBannerShown = true; + // However the dialog is dismissed (Later, backdrop), the icon remains. + setUpdateIcon(true); // Show a modal dialog instead of a fleeting toast so the user can't miss it. const body = document.createElement('p'); body.textContent = 'A new version of YT Player is available. Click "Refresh UI" to reload the page with the latest build. All your playlists and history are stored locally and will be preserved.'; showModal('⬆ Update available', body, [ - { label: 'Later', onClick: closeModal }, + { label: 'Later', onClick: () => { closeModal(); setUpdateIcon(true); } }, { label: 'Refresh UI', primary: true, onClick: async () => { closeModal(); await applyUpdate(target); @@ -11185,7 +11207,8 @@ async function applyUpdate(target, { quiet = false } = {}) { _updating = false; writeUpdateState(null); if (_updateToast) { _updateToast.remove(); _updateToast = null; } - toast('⚠ Update didn’t download (' + (err && err.message ? err.message : 'connection problem') + '). It will be offered again.', { duration: 6000 }); + toast('⚠ Update didn’t download (' + (err && err.message ? err.message : 'connection problem') + '). Tap the update icon to try again.', { duration: 6000 }); + setUpdateIcon(true); // Let the next poll re-offer it. _updateBannerShown = false; } diff --git a/frontend/index.html b/frontend/index.html index de7f138..c2a00c5 100755 --- a/frontend/index.html +++ b/frontend/index.html @@ -100,6 +100,10 @@ placeholder="Search YouTube… (no login, no ads)" autocomplete="off" /> + +
diff --git a/frontend/styles.css b/frontend/styles.css index 85ed7ac..98d76c8 100755 --- a/frontend/styles.css +++ b/frontend/styles.css @@ -4287,3 +4287,19 @@ body.landscape-fs .player-stage { touch-action: none; } /* fullscreen (real or t .player-pane.home-over #artFallback { visibility: hidden; } .player-pane.home-over .related-panel { display: none; } .player-pane.home-over > :not(.player-stage):not(#serviceMode) { display: none !important; } + +/* Pending-update icon beside the search box (shown after the update dialog) */ +.update-icon-btn { + flex: none; width: 44px; height: 44px; padding: 0; align-self: center; + display: grid; place-items: center; cursor: pointer; + border-radius: 50%; border: 1px solid var(--accent); + background: color-mix(in srgb, var(--accent) 16%, var(--bg-2)); color: var(--accent-bright); + box-shadow: 0 0 0 0 var(--accent-glow); animation: updPulse 2.4s ease-in-out infinite; +} +.update-icon-btn svg { width: 22px; height: 22px; fill: none; stroke: currentColor; stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; } +.update-icon-btn.hidden { display: none; } +@keyframes updPulse { 0%, 100% { box-shadow: 0 0 0 0 var(--accent-glow); } 50% { box-shadow: 0 0 0 6px transparent; } } +@media (prefers-reduced-motion: reduce) { .update-icon-btn { animation: none; } } +/* The search box gives up width for the update icon instead of pushing Search off screen */ +.search-form { min-width: 0; } +.search-form #searchInput { flex: 1 1 auto; min-width: 0; }