Преглед изворни кода

Add wip cache function to musicify

Toby Chui пре 3 недеља
родитељ
комит
3b1ceb9105
3 измењених фајлова са 293 додато и 14 уклоњено
  1. 36 0
      src/web/Musicify/artistsWorker.js
  2. 46 7
      src/web/Musicify/index.html
  3. 211 7
      src/web/Musicify/musicify.js

+ 36 - 0
src/web/Musicify/artistsWorker.js

@@ -0,0 +1,36 @@
+/*
+    Musicify - Artists Worker
+    Offloads listArtists fetch + JSON parsing from the main UI thread.
+*/
+
+self.onmessage = function(evt) {
+    var msg = evt && evt.data ? evt.data : {};
+    if (msg.type !== 'fetchArtists') return;
+
+    var reqId = msg.reqId;
+    var endpoint = msg.endpoint;
+
+    fetch(endpoint, {
+        method: 'POST',
+        cache: 'no-cache',
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify({})
+    }).then(function(resp) {
+        if (!resp.ok) {
+            throw new Error('HTTP ' + resp.status);
+        }
+        return resp.json();
+    }).then(function(data) {
+        self.postMessage({
+            type: 'artistsResult',
+            reqId: reqId,
+            items: Array.isArray(data) ? data : []
+        });
+    }).catch(function(err) {
+        self.postMessage({
+            type: 'artistsError',
+            reqId: reqId,
+            error: (err && err.message) ? err.message : 'Fetch failed'
+        });
+    });
+};

+ 46 - 7
src/web/Musicify/index.html

@@ -41,11 +41,19 @@
             --hover:      rgba(255,255,255,.055);
             --active-bg:  rgba(168,85,247,.13);
             --player-h:   88px;
+            --player-h-mobile: 120px;
             --sidebar-w:  240px;
             --font:       -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
         }
 
-        html, body { height: 100%; overflow: hidden; background: var(--bg); color: var(--text); font-family: var(--font); font-size: 14px; }
+        html, body { 
+            height: 100%; 
+            overflow: hidden; 
+            background: var(--bg); 
+            color: var(--text); 
+            font-family: var(--font); 
+            font-size: 14px; 
+        }
 
         /* ── App Layout ────────────────────────────────────────────────── */
         #app { display: flex; flex-direction: column; height: 100vh; overflow: hidden; }
@@ -157,7 +165,7 @@
             .sidebar.open { transform: translateX(0); }
             .sidebar-overlay { display: block; }
             .queue-panel{
-                bottom: calc(var(--player-h) - 2px) !important;
+                bottom: calc(var(--player-h-mobile)) !important;
             }
         }
 
@@ -190,7 +198,7 @@
         /* ── Folder Grid ────────────────────────────────────────────────── */
         .folder-grid {
             display: grid;
-            grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
+            grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
             gap: 12px; margin-bottom: 20px;
         }
         .folder-card {
@@ -458,13 +466,13 @@
         }
 
         @media (max-width: 768px) {
-            .player-bar { height: 86px; padding: 8px 10px 0; flex-wrap: wrap; align-items: flex-start; gap: 0 10px; }
+            .player-bar { height: 120px; padding: 8px 10px 0; flex-wrap: wrap; align-items: flex-start; gap: 0 10px; }
             .player-track { width: auto; flex: 1; align-self: center; }
             .player-controls { display: none; }
             .player-extras { width: auto; gap: 2px; align-self: center; }
             .player-cover { width: 40px; height: 40px; }
             .player-track-name { font-size: 12px; }
-            .mobile-seek-row { display: flex !important; margin-left: 2em; margin-right: 2em; }
+            .mobile-seek-row { display: flex !important; margin-left: 2em; margin-right: 2em;;}
             /* Show minimal mobile controls */
             .mobile-player-controls {
                 display: flex !important; align-items: center; gap: 4px;
@@ -477,6 +485,15 @@
         .mobile-player-controls { display: none; }
         .mobile-seek-row { display: none; width: 100%; align-items: center; gap: 6px; padding: 2px 0 8px; }
 
+        /* Safari mobile browser (not standalone/webapp): fix bottom url bar issue */
+        @media (max-width: 768px) and (display-mode: browser) {
+            @supports (-webkit-touch-callout: none) {
+                .mobile-seek-row {
+                    padding-bottom: 2em;
+                }
+            }
+        }
+
         /* ── Loading overlay ─────────────────────────────────────────────── */
         .loading-overlay {
             display: flex; flex-direction: column; align-items: center;
@@ -940,9 +957,31 @@
 
             <!-- ── ARTISTS ───────────────────────────────────────────────── -->
             <div x-show="view === 'artists' && !loading">
-                <div class="content-header"><h2>Artists</h2></div>
+                <div class="content-header" style="display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;">
+                    <h2 style="margin:0;">Artists</h2>
+                    <div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">
+                        <span style="font-size:11px;padding:3px 8px;border-radius:999px;border:1px solid rgba(255,255,255,.16);"
+                              x-show="artistsFromCache && !artistsRefreshing">Cached</span>
+                        <span style="font-size:11px;padding:3px 8px;border-radius:999px;background:rgba(59,130,246,.2);border:1px solid rgba(59,130,246,.45);"
+                              x-show="artistsRefreshing">Refreshing</span>
+                        <span style="font-size:11px;padding:3px 8px;border-radius:999px;background:rgba(16,185,129,.15);border:1px solid rgba(16,185,129,.35);"
+                              x-show="!artistsFromCache && !artistsRefreshing && artists.length > 0">Live</span>
+                        <span style="font-size:12px;color:var(--text2);" x-text="artistsStatusText()"></span>
+                        <span style="font-size:12px;color:var(--text2);" x-show="artistsCacheUpdatedAt" x-text="artistsUpdatedTimeText()"></span>
+                        <button class="ctrl-btn" title="Refresh artists" x-on:click="_loadArtists({ forceNetwork: true })">
+                            <i class="ui sync icon" style="margin:0;"></i>
+                        </button>
+                    </div>
+                </div>
                 <div class="content-body">
-                    <template x-if="artists.length === 0">
+                    <template x-if="artists.length === 0 && artistsRefreshing">
+                        <div class="empty-state">
+                            <i class="ui sync icon"></i>
+                            <h3>Loading artists</h3>
+                            <p>Fetching your artist list in the background…</p>
+                        </div>
+                    </template>
+                    <template x-if="artists.length === 0 && !artistsRefreshing">
                         <div class="empty-state">
                             <i class="ui user icon"></i>
                             <h3>No artists found</h3>

+ 211 - 7
src/web/Musicify/musicify.js

@@ -27,6 +27,16 @@ function musicifyApp() {
         // ── Artists ─────────────────────────────────────────────────────────
         artists: [],
         selectedArtist: null,   // full artist object when expanded
+        artistsFromCache: false,
+        artistsRefreshing: false,
+        artistsCacheUpdatedAt: 0,
+        _artistsFetchInFlight: false,
+        _artistsUpdateFlash: false,
+        _artistsUpdateFlashTimer: null,
+        _artistsWorker: null,
+        _artistsWorkerReqId: 0,
+        _artistsActiveReqId: 0,
+        _artistsWatchdogTimer: null,
 
         // ── Recent ──────────────────────────────────────────────────────────
         recentSongs: [],
@@ -136,6 +146,13 @@ function musicifyApp() {
                 navigator.serviceWorker.register('sw.js').catch(function(){});
             }
 
+            window.addEventListener('beforeunload', () => {
+                if (this._artistsWorker) {
+                    this._artistsWorker.terminate();
+                    this._artistsWorker = null;
+                }
+            });
+
             // Handle #folder=<path> hash from embedded player's "Open in Musicify" button
             var _hash = window.location.hash;
             if (_hash.startsWith('#folder=')) {
@@ -170,7 +187,7 @@ function musicifyApp() {
                 if (this.folderContents.songs.length === 0 && this.folderContents.folders.length === 0) {
                     this.loadFolder(this.folderRoot);
                 }
-            } else if (v === 'artists' && this.artists.length === 0) {
+            } else if (v === 'artists') {
                 this._loadArtists();
             } else if (v === 'recent' && this.recentSongs.length === 0) {
                 this._loadRecent();
@@ -263,18 +280,201 @@ function musicifyApp() {
         // ════════════════════════════════════════════════════════════════════
         //  ARTISTS
         // ════════════════════════════════════════════════════════════════════
-        _loadArtists() {
-            this.loading = true;
-            this.loadingMsg = 'Loading artists…';
+        _loadArtists(opts) {
+            opts = opts || {};
+            var forceNetwork = !!opts.forceNetwork;
+            var cache = null;
+
+            // Artists refresh should never block the entire content panel.
+            this.loading = false;
+
+            if (!forceNetwork) {
+                cache = this._readArtistsCache();
+                if (cache && Array.isArray(cache.items)) {
+                    this.artists = cache.items;
+                    this.artistsFromCache = true;
+                    this.artistsCacheUpdatedAt = cache.updatedAt || 0;
+                }
+            }
+
+            if (this._artistsFetchInFlight) return;
+
+            this._artistsFetchInFlight = true;
+            this.artistsRefreshing = true;
+
+            var reqId = ++this._artistsWorkerReqId;
+            this._artistsActiveReqId = reqId;
+            this._startArtistsWatchdog(reqId);
+
+            // Use worker first to keep fetch + JSON parsing off the UI thread.
+            var startedInWorker = this._dispatchArtistsFetchToWorker(reqId);
+            if (!startedInWorker) {
+                // Fallback for environments where Worker is unavailable.
+                this._dispatchArtistsFetchFallback(reqId);
+            }
+        },
+
+        _dispatchArtistsFetchToWorker(reqId) {
+            if (!('Worker' in window)) return false;
             const self = this;
+
+            if (!this._artistsWorker) {
+                try {
+                    this._artistsWorker = new Worker('artistsWorker.js');
+                } catch (e) {
+                    this._artistsWorker = null;
+                    return false;
+                }
+
+                this._artistsWorker.onmessage = function(evt) {
+                    var msg = evt && evt.data ? evt.data : {};
+                    if (msg.type === 'artistsResult') {
+                        self._applyArtistsResult(msg.items, msg.reqId);
+                    } else if (msg.type === 'artistsError') {
+                        self._handleArtistsError(msg.reqId);
+                    }
+                };
+
+                this._artistsWorker.onerror = function() {
+                    self._handleArtistsError(self._artistsActiveReqId);
+                    if (self._artistsWorker) {
+                        self._artistsWorker.terminate();
+                        self._artistsWorker = null;
+                    }
+                };
+            }
+
+            try {
+                this._artistsWorker.postMessage({
+                    type: 'fetchArtists',
+                    reqId: reqId,
+                    endpoint: ao_root + 'system/ajgi/interface?script=Musicify/backend/listArtists.js'
+                });
+                return true;
+            } catch (e) {
+                return false;
+            }
+        },
+
+        _dispatchArtistsFetchFallback(reqId) {
             fetch(ao_root + 'system/ajgi/interface?script=Musicify/backend/listArtists.js', {
                 method: 'POST', cache: 'no-cache',
                 headers: { 'Content-Type': 'application/json' },
                 body: JSON.stringify({})
             }).then(r => r.json()).then(data => {
-                self.artists = data;
-                self.loading = false;
-            }).catch(() => { self.loading = false; });
+                this._applyArtistsResult(data, reqId);
+            }).catch(() => {
+                this._handleArtistsError(reqId);
+            });
+        },
+
+        _applyArtistsResult(data, reqId) {
+            if (reqId !== this._artistsActiveReqId) return;
+
+            data = Array.isArray(data) ? data : [];
+            var selectedPath = this.selectedArtist ? this.selectedArtist.path : null;
+
+            this.artists = data;
+            this.artistsFromCache = false;
+            this.artistsCacheUpdatedAt = Date.now();
+            this._writeArtistsCache(data, this.artistsCacheUpdatedAt);
+            this._flashArtistsUpdated();
+
+            if (selectedPath) {
+                var matched = null;
+                for (var i = 0; i < data.length; i++) {
+                    if (data[i].path === selectedPath) {
+                        matched = data[i];
+                        break;
+                    }
+                }
+                this.selectedArtist = matched;
+            }
+
+            this._finalizeArtistsFetch(reqId);
+        },
+
+        _handleArtistsError(reqId) {
+            if (reqId !== this._artistsActiveReqId) return;
+            this._finalizeArtistsFetch(reqId);
+        },
+
+        _startArtistsWatchdog(reqId) {
+            if (this._artistsWatchdogTimer) clearTimeout(this._artistsWatchdogTimer);
+            const self = this;
+            this._artistsWatchdogTimer = setTimeout(() => {
+                if (reqId !== self._artistsActiveReqId) return;
+                self._finalizeArtistsFetch(reqId);
+                if (self._artistsWorker) {
+                    self._artistsWorker.terminate();
+                    self._artistsWorker = null;
+                }
+            }, 25000);
+        },
+
+        _finalizeArtistsFetch(reqId) {
+            if (reqId !== this._artistsActiveReqId) return;
+            if (this._artistsWatchdogTimer) {
+                clearTimeout(this._artistsWatchdogTimer);
+                this._artistsWatchdogTimer = null;
+            }
+            this.artistsRefreshing = false;
+            this._artistsFetchInFlight = false;
+        },
+
+        _readArtistsCache() {
+            try {
+                var raw = localStorage.getItem('musicify_artists_cache');
+                if (!raw) return null;
+                var payload = JSON.parse(raw);
+                if (!payload || !Array.isArray(payload.items)) return null;
+                return {
+                    updatedAt: payload.updatedAt || 0,
+                    items: payload.items
+                };
+            } catch (e) {
+                return null;
+            }
+        },
+
+        _writeArtistsCache(items, updatedAt) {
+            try {
+                localStorage.setItem('musicify_artists_cache', JSON.stringify({
+                    updatedAt: updatedAt || Date.now(),
+                    items: Array.isArray(items) ? items : []
+                }));
+            } catch (e) {}
+        },
+
+        _flashArtistsUpdated() {
+            this._artistsUpdateFlash = true;
+            if (this._artistsUpdateFlashTimer) clearTimeout(this._artistsUpdateFlashTimer);
+            const self = this;
+            this._artistsUpdateFlashTimer = setTimeout(() => {
+                self._artistsUpdateFlash = false;
+            }, 3000);
+        },
+
+        artistsStatusText() {
+            if (this.artistsRefreshing && this.artistsFromCache) {
+                return 'Showing cached artists while refreshing in background';
+            }
+            if (this.artistsFromCache) {
+                return 'Showing cached artists';
+            }
+            if (this.artistsRefreshing) {
+                return 'Refreshing artist list';
+            }
+            if (this._artistsUpdateFlash) {
+                return 'Artist list updated';
+            }
+            return 'Live artist list';
+        },
+
+        artistsUpdatedTimeText() {
+            if (!this.artistsCacheUpdatedAt) return '';
+            var d = new Date(this.artistsCacheUpdatedAt);
+            return 'Updated at ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
         },
 
         selectArtist(artist) {
@@ -510,6 +710,10 @@ function musicifyApp() {
             this._saveRecentlyPlayed(song);
             this._setupMediaSession();
             document.title = song.name + ' – Musicify';
+            if (ao_module_virtualDesktop){
+                ao_module_setWindowTitle('Musicify - ' + song.name);
+            }
+            this.trackInfoSong = song;
         },
 
         togglePlay() {