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.
This commit is contained in:
jelveh
2026-07-11 15:16:04 -07:00
parent 5160b44c4b
commit 859e88753f
2 changed files with 55 additions and 2 deletions
+30 -2
View File
@@ -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('<div class="myapps-loading"><div class="myapps-spinner"></div></div>');
}, 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('<div class="myapps-empty"><p>Failed to load apps</p></div>');
await finish(() => {
$container.html('<div class="myapps-empty"><p>Failed to load apps</p></div>');
});
}
},
+25
View File
@@ -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 {