[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>
</div>
<div class="body" ref="body">
<div class="body" ref="body" @scroll="onScroll">
<div class="no-content" v-if="!tracks?.length">
No tracks are loaded
</div>
@ -57,6 +57,7 @@
v-for="i in displayedTrackIndices"
:set="track = tracks[i]"
:key="i"
:data-index="i"
:class="trackClass(i)"
@click="onTrackClick($event, i)"
@dblclick="$emit('play', {pos: i})">
@ -137,6 +138,11 @@ export default {
activeDevice: {
type: String,
},
maxVisibleTracks: {
type: Number,
default: 100,
},
},
data() {
@ -147,6 +153,8 @@ export default {
infoTrack: null,
sourcePos: null,
targetPos: null,
centerPos: 0,
mounted: false,
}
},
@ -155,20 +163,45 @@ export default {
return new Set(this.selectedTracks)
},
displayedTrackIndices() {
const positions = [...Array(this.tracks.length).keys()]
if (!this.filter?.length)
return positions
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(' ')
const self = this
const filter = (self.filter || '').toLowerCase()
return positions.filter((pos) => {
const track = this.tracks[pos]
return (track?.artist || '').toLowerCase().indexOf(filter) >= 0
|| (track?.title || '').toLowerCase().indexOf(filter) >= 0
|| (track?.album || '').toLowerCase().indexOf(filter) >= 0
if (!indices[token])
indices[token] = new Set()
indices[token].add(i)
})
return indices
},
displayedTrackIndices() {
let positions = [...Array(this.tracks.length).keys()]
if (this.filter?.length) {
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
matchingPositions.add(...positions)
})
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')
},
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() {
const name = prompt('Playlist name')
if (!name?.length)
@ -249,17 +293,34 @@ export default {
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() {
const self = this
this.$watch(() => self.status?.playingPos, (pos) => {
if (pos == null)
return
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)
this.scrollToTrack()
this.$watch(() => this.status, () => this.scrollToTrack())
this.$watch(() => this.filter, (filter) => {
if (!filter?.length)
this.scrollToTrack()
})
},
}

View file

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