Quellcode durchsuchen

Add cache and scroll optimization to Artists playlist

Toby Chui vor 3 Wochen
Ursprung
Commit
56d74e3f0d
2 geänderte Dateien mit 120 neuen und 3 gelöschten Zeilen
  1. 45 1
      src/web/Musicify/index.html
  2. 75 2
      src/web/Musicify/musicify.js

+ 45 - 1
src/web/Musicify/index.html

@@ -988,6 +988,49 @@
                             <p>Organise your music into sub-folders under your Music directory to see artists here.</p>
                         </div>
                     </template>
+                   
+                    <div id="artist-content-body" style="height:calc(100vh - 180px);overflow-y:auto;" x-on:scroll="onArtistScroll($event)">
+                        <div :style="'height:' + artistTopSpacerHeight() + 'px'"></div>
+                        <template x-for="artist in visibleArtists()" :key="artist.path">
+                            <div>
+                                <div class="artist-row" x-on:click="selectArtist(artist, $event.currentTarget)">
+                                    <div class="artist-avatar" x-text="artist.name.charAt(0)"></div>
+                                    <div class="artist-info">
+                                        <div class="artist-name"  x-text="artist.name"></div>
+                                        <div class="artist-count" x-text="artist.songCount + ' tracks'"></div>
+                                    </div>
+                                    <button class="ctrl-btn" style="margin-right:4px;" x-on:click.stop="playList(artist.songs, 0)">
+                                        <i class="ui play icon" style="margin:0;"></i>
+                                    </button>
+                                    <i class="ui chevron right icon artist-expand"
+                                    :class="{open: selectedArtist && selectedArtist.path === artist.path}"
+                                    style="margin:0;"></i>
+                                </div>
+                                
+                                <div x-show="selectedArtist && selectedArtist.path === artist.path"
+                                    style="background:var(--bg2);border-radius:0 0 8px 8px;margin-bottom:4px;">
+                                    <template x-for="(song, idx) in artist.songs" :key="song.filepath">
+                                        <div class="song-row" :class="{active: isCurrentTrack(song)}"
+                                            style="grid-template-columns: 28px 36px 1fr 80px 28px;"
+                                            x-on:click="playSong(song, artist.songs, $event)">
+                                            <span class="song-idx"  x-text="idx + 1"></span>
+                                            <span class="song-play"><i class="ui play icon" style="margin:0;"></i></span>
+                                            <div class="song-cover-placeholder"><img :src="getCoverUrl(song)" loading="lazy" x-on:error="$event.target.src='img/placeholder.png'; $event.target.onerror=null"></div>
+                                            <div class="song-info">
+                                                <div class="song-name" x-text="song.name"></div>
+                                            </div>
+                                            <span class="song-size" x-text="song.hsize"></span>
+                                            <span class="song-menu" x-on:click.stop="promptAddToPlaylist(song, $event)">
+                                                <i class="ui ellipsis vertical icon" style="margin:0;font-size:14px;"></i>
+                                            </span>
+                                        </div>
+                                    </template>
+                                </div>
+                            </div>
+                        </template>
+                        <div :style="'height:' + artistBottomSpacerHeight() + 'px'"></div>
+                    </div>
+                    <!-- 
                     <template x-for="artist in artists" :key="artist.path">
                         <div>
                             <div class="artist-row" x-on:click="selectArtist(artist)">
@@ -1003,7 +1046,7 @@
                                    :class="{open: selectedArtist && selectedArtist.path === artist.path}"
                                    style="margin:0;"></i>
                             </div>
-                            <!-- Expanded song list -->
+                            
                             <div x-show="selectedArtist && selectedArtist.path === artist.path"
                                  style="background:var(--bg2);border-radius:0 0 8px 8px;margin-bottom:4px;">
                                 <template x-for="(song, idx) in artist.songs" :key="song.filepath">
@@ -1025,6 +1068,7 @@
                             </div>
                         </div>
                     </template>
+                    -->
                 </div>
             </div>
 

+ 75 - 2
src/web/Musicify/musicify.js

@@ -37,6 +37,10 @@ function musicifyApp() {
         _artistsWorkerReqId: 0,
         _artistsActiveReqId: 0,
         _artistsWatchdogTimer: null,
+        // Artist virtual scrolling
+        artistRowHeight: 65, // must match CSS .artist-row height
+        artistOverscan: 120, //artistRowHeight * artistOverscan = overscan px, Should be large enough for playlist expansion
+        artistScrollTop: 0,
 
         // ── Recent ──────────────────────────────────────────────────────────
         recentSongs: [],
@@ -474,11 +478,80 @@ function musicifyApp() {
         artistsUpdatedTimeText() {
             if (!this.artistsCacheUpdatedAt) return '';
             var d = new Date(this.artistsCacheUpdatedAt);
-            return 'Updated at ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
+            return 'Updated at ' + d.toLocaleTimeString([], {
+                hour: '2-digit',
+                minute: '2-digit',
+                timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
+                timeZoneName: 'short'
+            });
         },
 
         selectArtist(artist) {
-            this.selectedArtist = (this.selectedArtist && this.selectedArtist.path === artist.path) ? null : artist;
+            var isClosing = this.selectedArtist && this.selectedArtist.path === artist.path;
+            this.selectedArtist = isClosing ? null : artist;
+
+            if (isClosing) return;
+
+            var index = this.artists.findIndex(function(item) {
+                return item.path === artist.path;
+            });
+            if (index < 0) return;
+
+            // Keep the selected artist near the top of the viewport so the expanded list stays readable.
+            var targetScrollTop = Math.max(0, (index * this.artistRowHeight) - 33);
+            this.artistScrollTop = targetScrollTop;
+
+            this.$nextTick(() => {
+                var container = document.getElementById('artist-content-body');
+                if (container) {
+                    container.scrollTop = targetScrollTop;
+                }
+            });
+        },
+
+        visibleArtists() {
+            const viewportHeight = window.innerHeight;
+
+            const start =
+                Math.max(
+                    0,
+                    Math.floor(this.artistScrollTop / this.artistRowHeight)
+                    - this.artistOverscan
+                );
+
+            const count =
+                Math.ceil(viewportHeight / this.artistRowHeight)
+                + (this.artistOverscan * 2);
+
+            return this.artists.slice(start, start + count);
+        },
+
+        artistStartIndex() {
+            return Math.max(
+                0,
+                Math.floor(this.artistScrollTop / this.artistRowHeight)
+                - this.artistOverscan
+            );
+        },
+
+        artistTopSpacerHeight() {
+            return this.artistStartIndex() * this.artistRowHeight;
+        },
+
+        artistBottomSpacerHeight() {
+            const rendered =
+                this.visibleArtists().length;
+
+            return Math.max(
+                0,
+                (this.artists.length -
+                    this.artistStartIndex() -
+                    rendered) * this.artistRowHeight
+            );
+        },
+
+        onArtistScroll(e) {
+            this.artistScrollTop = e.target.scrollTop;
         },
 
         // ════════════════════════════════════════════════════════════════════