Przeglądaj źródła

Add library status bar and cache-backed refresh

Toby Chui 3 tygodni temu
rodzic
commit
b2b756f38f
1 zmienionych plików z 153 dodań i 6 usunięć
  1. 153 6
      src/web/Movie/index.html

+ 153 - 6
src/web/Movie/index.html

@@ -83,6 +83,33 @@ html, body {
 /* ─── Library view ───────────────────────────────────────────────────────────── */
 #view-library { padding: 0; }
 
+/* Status bar ─────────────────────────────────────────────────────────────────── */
+#library-status-bar {
+    flex-shrink: 0;
+    display: flex;
+    align-items: center;
+    gap: 7px;
+    padding: 3px 22px;
+    font-size: 11px;
+    color: var(--text-sub);
+    border-bottom: 1px solid rgba(255,255,255,0.04);
+    min-height: 26px;
+}
+.spinner-sm {
+    width: 11px; height: 11px; flex-shrink: 0;
+    border: 2px solid rgba(255,255,255,0.15);
+    border-top-color: var(--accent);
+    border-radius: 50%;
+    animation: spin 0.8s linear infinite;
+}
+#library-refresh-btn {
+    background: none; border: none; cursor: pointer;
+    color: var(--accent); font-size: 11px; font-weight: 500;
+    padding: 0; outline: none; transition: opacity var(--transition);
+}
+#library-refresh-btn:hover   { opacity: 0.7; }
+#library-refresh-btn:disabled { opacity: 0.35; cursor: default; }
+
 #library-scroll {
     flex: 1;
     overflow-y: auto;
@@ -934,6 +961,11 @@ html, body {
 
     <!-- ═══════════════ LIBRARY VIEW ═══════════════════════════════════════ -->
     <div id="view-library" class="view active">
+        <div id="library-status-bar">
+            <div class="spinner-sm" id="library-spinner"></div>
+            <span id="library-status-text"></span>
+            <button id="library-refresh-btn" onclick="refreshLibrary()" style="display:none">↻ Refresh</button>
+        </div>
         <div id="library-scroll">
             <div id="no-content">
                 <div class="icon"><img src="img/icons/movie_white.svg" alt=""></div>
@@ -1138,6 +1170,11 @@ html, body {
 // ─── All configurable paths come from backend/common.js ──────────────────────
 // (SCRIPT_GET_LIBRARY, SCRIPT_GET_EPISODES, SCRIPT_GET_THUMBNAIL, MEDIA_API)
 
+// ─── Library cache (localStorage) ────────────────────────────────────────────
+var LIBRARY_CACHE_KEY     = 'movie_library_cache';
+var LIBRARY_CACHE_VERSION = 1;
+var libraryNeedsRedraw    = false; // set when bg-refresh finishes outside the library view
+
 // ─── App state ────────────────────────────────────────────────────────────────
 var library        = [];   // full album array from server
 var currentAlbum   = null; // album object currently shown in detail view
@@ -1601,19 +1638,124 @@ $(document).ready(function () {
     });
 });
 
-// ─── Load library from backend ────────────────────────────────────────────────
+// ─── Library cache helpers ─────────────────────────────────────────────────────
+function getCachedLibrary() {
+    try {
+        var raw = localStorage.getItem(LIBRARY_CACHE_KEY);
+        if (!raw) { return null; }
+        var obj = JSON.parse(raw);
+        if (!obj || obj.version !== LIBRARY_CACHE_VERSION || !Array.isArray(obj.data)) { return null; }
+        return obj;  // { version, ts, data }
+    } catch (e) { return null; }
+}
+
+function updateLibraryCache(data) {
+    try {
+        localStorage.setItem(LIBRARY_CACHE_KEY, JSON.stringify({
+            version: LIBRARY_CACHE_VERSION,
+            ts:      Date.now(),
+            data:    data
+        }));
+    } catch (e) {} // storage full or unavailable — silently ignore
+}
+
+// spinning=true  →  show spinner, hide Refresh button
+// spinning=false →  hide spinner, show Refresh button
+function setLibraryStatus(text, spinning) {
+    $('#library-status-text').text(text);
+    $('#library-spinner').toggle(spinning);
+    $('#library-refresh-btn').toggle(!spinning);
+}
+
+function timeAgo(ts) {
+    var d = Math.floor((Date.now() - ts) / 1000);
+    if (d < 60)    { return 'just now'; }
+    if (d < 3600)  { return Math.floor(d / 60) + 'm ago'; }
+    if (d < 86400) { return Math.floor(d / 3600) + 'h ago'; }
+    return Math.floor(d / 86400) + 'd ago';
+}
+
+// Re-render the library grid, preserving an active search filter if present
+function renderCurrentLibrary() {
+    var q = $('#search-input').val().trim().toLowerCase();
+    renderLibrary(q ? library.filter(function (a) { return a.name.toLowerCase().indexOf(q) > -1; }) : library);
+}
+
+// ─── Load library (cache-first) ────────────────────────────────────────────────
 function loadLibrary() {
+    var cached = getCachedLibrary();
+
+    if (cached) {
+        // ── Have cache: show it immediately, skip the loading overlay ──────────
+        $('#loading-overlay').hide();
+        library = cached.data;
+        renderLibrary(library);
+        setLibraryStatus('Cached · ' + timeAgo(cached.ts) + ' · refreshing…', true);
+
+        // Background fetch — silently update when done
+        ao_module_agirun(SCRIPT_GET_LIBRARY, {}, function (data) {
+            if (!data || data.error) {
+                setLibraryStatus('⚠ Refresh failed · showing cached data', false);
+                return;
+            }
+            updateLibraryCache(data);
+            library = data;
+            if ($('#view-library').hasClass('active')) {
+                renderCurrentLibrary();    // user is watching — update smoothly
+                libraryNeedsRedraw = false;
+            } else {
+                libraryNeedsRedraw = true; // user navigated away — re-render on return
+            }
+            var n = library.length;
+            setLibraryStatus('✓ ' + n + ' item' + (n !== 1 ? 's' : ''), false);
+        }, function () {
+            setLibraryStatus('⚠ Refresh failed · showing cached data', false);
+        });
+
+    } else {
+        // ── No cache: full loading screen, wait for first fetch ───────────────
+        setLibraryStatus('Loading…', true);
+        ao_module_agirun(SCRIPT_GET_LIBRARY, {}, function (data) {
+            $('#loading-overlay').fadeOut(300);
+            if (!data || data.error) {
+                showToast('Failed to load library');
+                setLibraryStatus('⚠ Failed to load library', false);
+                return;
+            }
+            updateLibraryCache(data);
+            library = data;
+            renderLibrary(library);
+            var n = library.length;
+            setLibraryStatus('✓ ' + n + ' item' + (n !== 1 ? 's' : ''), false);
+        }, function () {
+            $('#loading-overlay').fadeOut(300);
+            showToast('Error loading library');
+            setLibraryStatus('⚠ Error loading library', false);
+        });
+    }
+}
+
+// ─── Manual refresh (Refresh button) ──────────────────────────────────────────
+function refreshLibrary() {
+    $('#library-refresh-btn').prop('disabled', true);
+    setLibraryStatus('Refreshing…', true);
     ao_module_agirun(SCRIPT_GET_LIBRARY, {}, function (data) {
-        $('#loading-overlay').fadeOut(300);
+        $('#library-refresh-btn').prop('disabled', false);
         if (!data || data.error) {
-            showToast('Failed to load library');
+            showToast('Failed to refresh library');
+            setLibraryStatus('⚠ Refresh failed', false);
             return;
         }
+        updateLibraryCache(data);
         library = data;
-        renderLibrary(library);
+        renderCurrentLibrary();
+        libraryNeedsRedraw = false;
+        var n = library.length;
+        setLibraryStatus('✓ ' + n + ' item' + (n !== 1 ? 's' : '') + ' · just refreshed', false);
     }, function () {
-        $('#loading-overlay').fadeOut(300);
-        showToast('Error loading library');
+        $('#library-refresh-btn').prop('disabled', false);
+        showToast('Error refreshing library');
+        setLibraryStatus('⚠ Refresh failed', false);
     });
 }
 
@@ -2003,6 +2145,11 @@ function closePlayer() {
 function showLibrary() {
     showView('library');
     currentAlbum = null;
+    // If a background refresh completed while the user was away, apply it now
+    if (libraryNeedsRedraw) {
+        libraryNeedsRedraw = false;
+        renderCurrentLibrary();
+    }
 }
 
 // ─── View switching ───────────────────────────────────────────────────────────