forked from platypush/platypush
[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:
parent
dbae2ccc40
commit
afee6c5c85
2 changed files with 84 additions and 19 deletions
|
@ -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)
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,10 @@ export default {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {},
|
default: () => {},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
pluginName: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
|
|
Loading…
Reference in a new issue