[music UI] Implemented infinite scroll for playlist view.

Instead of loading all the tracks in the DOM (very inefficient and slow
on slow devices and/or with big playlists), we should keep a window of
100 items in the screen and roll it over the playlists as the status
change or the user scrolls.
This commit is contained in:
Fabio Manganiello 2024-01-05 02:19:38 +01:00
parent dbae2ccc40
commit afee6c5c85
Signed by untrusted user: blacklight
GPG key ID: D90FBA7F76362774
2 changed files with 84 additions and 19 deletions

View file

@ -44,7 +44,7 @@
</MusicHeader> </MusicHeader>
</div> </div>
<div class="body" ref="body"> <div class="body" ref="body" @scroll="onScroll">
<div class="no-content" v-if="!tracks?.length"> <div class="no-content" v-if="!tracks?.length">
No tracks are loaded No tracks are loaded
</div> </div>
@ -57,6 +57,7 @@
v-for="i in displayedTrackIndices" v-for="i in displayedTrackIndices"
:set="track = tracks[i]" :set="track = tracks[i]"
:key="i" :key="i"
:data-index="i"
:class="trackClass(i)" :class="trackClass(i)"
@click="onTrackClick($event, i)" @click="onTrackClick($event, i)"
@dblclick="$emit('play', {pos: i})"> @dblclick="$emit('play', {pos: i})">
@ -137,6 +138,11 @@ export default {
activeDevice: { activeDevice: {
type: String, type: String,
}, },
maxVisibleTracks: {
type: Number,
default: 100,
},
}, },
data() { data() {
@ -147,6 +153,8 @@ export default {
infoTrack: null, infoTrack: null,
sourcePos: null, sourcePos: null,
targetPos: null, targetPos: null,
centerPos: 0,
mounted: false,
} }
}, },
@ -155,20 +163,45 @@ export default {
return new Set(this.selectedTracks) return new Set(this.selectedTracks)
}, },
trackIndicesByToken() {
const indices = {}
this.tracks.forEach((track, i) => {
const token = [track?.artist, track?.album, track?.title]
.filter((field) => field?.trim()?.length)
.map((field) => field.trim().toLowerCase())
.join(' ')
if (!indices[token])
indices[token] = new Set()
indices[token].add(i)
})
return indices
},
displayedTrackIndices() { displayedTrackIndices() {
const positions = [...Array(this.tracks.length).keys()] let positions = [...Array(this.tracks.length).keys()]
if (!this.filter?.length)
return positions
const self = this if (this.filter?.length) {
const filter = (self.filter || '').toLowerCase() const filter = this.filter?.trim()?.replace(/\s+/g, ' ').toLowerCase()
const matchingPositions = new Set()
Object.entries(this.trackIndicesByToken).forEach(([key, positions]) => {
if (key.indexOf(filter) < 0)
return
return positions.filter((pos) => { matchingPositions.add(...positions)
const track = this.tracks[pos]
return (track?.artist || '').toLowerCase().indexOf(filter) >= 0
|| (track?.title || '').toLowerCase().indexOf(filter) >= 0
|| (track?.album || '').toLowerCase().indexOf(filter) >= 0
}) })
positions = [...matchingPositions]
positions.sort()
}
if (positions.length > this.maxVisibleTracks) {
const offset = Math.max(0, this.centerPos - Math.floor(this.maxVisibleTracks / 2))
positions = positions.slice(offset, offset + this.maxVisibleTracks)
}
return positions
}, },
}, },
@ -242,6 +275,17 @@ export default {
[...tracks][track].classList.add('dragover') [...tracks][track].classList.add('dragover')
}, },
onScroll() {
const offset = this.$refs.body.scrollTop
const bodyHeight = parseFloat(getComputedStyle(this.$refs.body).height)
const scrollHeight = this.$refs.body.scrollHeight
if (offset < 5)
this.centerPos = Math.max(0, parseInt(this.centerPos - (this.maxVisibleTracks / 1.5)))
else if (offset === scrollHeight - bodyHeight)
this.centerPos = Math.min(this.tracks.length - 1, parseInt(this.centerPos + (this.maxVisibleTracks / 1.5)))
},
playlistSave() { playlistSave() {
const name = prompt('Playlist name') const name = prompt('Playlist name')
if (!name?.length) if (!name?.length)
@ -249,17 +293,34 @@ export default {
this.$emit('save', name) this.$emit('save', name)
}, },
scrollToTrack(pos) {
this.centerPos = pos || this.status?.playingPos || 0
this.$nextTick(() => {
if (!this.$refs.body) {
this.$watch(() => this.$refs.body, () => {
if (!this.mounted)
this.scrollToTrack(pos)
})
return
}
[...this.$refs.body.querySelectorAll('.track')]
.filter((track) => track.classList.contains('active'))
.forEach((track) => track.scrollIntoView({block: 'center', behavior: 'smooth'}))
this.mounted = true
})
},
}, },
mounted() { mounted() {
const self = this this.scrollToTrack()
this.$watch(() => self.status?.playingPos, (pos) => { this.$watch(() => this.status, () => this.scrollToTrack())
if (pos == null) this.$watch(() => this.filter, (filter) => {
return if (!filter?.length)
this.scrollToTrack()
const trackElement = [...self.$refs.body.querySelectorAll('.track')][pos]
const offset = trackElement.offsetTop - parseFloat(getComputedStyle(self.$refs.header.$el).height)
self.$refs.body.scrollTo(0, offset)
}) })
}, },
} }

View file

@ -30,6 +30,10 @@ export default {
type: Object, type: Object,
default: () => {}, default: () => {},
}, },
pluginName: {
type: String,
},
}, },
data() { data() {