From 859e88753f043a39e8830767cfb6f9e01d17ed8d Mon Sep 17 00:00:00 2001 From: jelveh Date: Sat, 11 Jul 2026 15:16:04 -0700 Subject: [PATCH] Add delayed centered spinner for apps tab Show a subtle centered loading spinner for the Apps dashboard when fetching apps if the request is slow. Implements a 1.5s delay before showing the spinner and guarantees a minimum 1.5s visible time to avoid blinking; the spinner is only shown when the container is empty (so background refreshes keep existing tiles). Adds a finish helper to clear the spinner timer and await the minimum visible time before rendering results or errors. Also adds CSS for the spinner element and its animations. --- src/gui/src/UI/Dashboard/TabApps.js | 32 +++++++++++++++++++++++++++-- src/gui/src/css/dashboard.css | 25 ++++++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src/gui/src/UI/Dashboard/TabApps.js b/src/gui/src/UI/Dashboard/TabApps.js index d739182d9..f82c993d3 100644 --- a/src/gui/src/UI/Dashboard/TabApps.js +++ b/src/gui/src/UI/Dashboard/TabApps.js @@ -491,6 +491,32 @@ const TabApps = { async loadApps ($el_window) { const $container = $el_window.find('.myapps-container'); + // Subtle centered spinner, but only if the fetch is slow (>1.5s) so + // fast loads never flash it; once visible it stays for at least 1.5s + // so it doesn't blink out. Skipped on background refreshes — the + // current apps stay on screen while new data loads. + const SPINNER_DELAY = 1500; + const SPINNER_MIN_VISIBLE = 1500; + let spinnerShownAt = null; + const spinnerTimer = setTimeout(() => { + // Only cover an empty container; if tiles (or any state) are + // already on screen this is a background refresh — keep them. + if ( $container.children().length > 0 ) return; + spinnerShownAt = Date.now(); + $container.html('
'); + }, SPINNER_DELAY); + + const finish = async (render) => { + clearTimeout(spinnerTimer); + if ( spinnerShownAt !== null ) { + const remaining = SPINNER_MIN_VISIBLE - (Date.now() - spinnerShownAt); + if ( remaining > 0 ) { + await new Promise(resolve => setTimeout(resolve, remaining)); + } + } + render(); + }; + try { // Fetch both APIs in parallel const [installedRes, launchRes] = await Promise.all([ @@ -544,10 +570,12 @@ const TabApps = { } this._apps = merged; - this.renderApps($el_window); + await finish(() => this.renderApps($el_window)); } catch (e) { console.error('Failed to load installed apps:', e); - $container.html('

Failed to load apps

'); + await finish(() => { + $container.html('

Failed to load apps

'); + }); } }, diff --git a/src/gui/src/css/dashboard.css b/src/gui/src/css/dashboard.css index 26e4868b3..47ccda89a 100644 --- a/src/gui/src/css/dashboard.css +++ b/src/gui/src/css/dashboard.css @@ -747,6 +747,31 @@ input.myapps-search::-webkit-search-decoration { margin: 0; } +.myapps-loading { + flex: 1; + display: flex; + align-items: center; + justify-content: center; +} + +.myapps-spinner { + width: 28px; + height: 28px; + border: 3px solid var(--dashboard-border); + border-top-color: var(--dashboard-text-tertiary); + border-radius: 50%; + animation: myapps-spin 0.8s linear infinite, myapps-spinner-fade-in 0.3s ease-out; +} + +@keyframes myapps-spin { + to { transform: rotate(360deg); } +} + +@keyframes myapps-spinner-fade-in { + from { opacity: 0; } + to { opacity: 1; } +} + /* iOS-style pager: pages snap horizontally, dots below, hover arrows */ .myapps-pager {