[web] Refactor lists to improve performance

Reduces the number of Vue components that need to be created/managed.
Instead of a Vue component for each item, we now only have one Vue
component for the whole list of items.
This commit is contained in:
chme
2022-02-19 07:47:54 +01:00
parent a24cb11e17
commit 27e2274d8a
40 changed files with 979 additions and 1378 deletions

View File

@@ -20,6 +20,9 @@ export default {
computed: {
filtered_index() {
if (!this.index) {
return []
}
const specialChars = '!"#$%&\'()*+,-./:;<=>?@[\\]^`{|}~'
return this.index.filter((c) => !specialChars.includes(c))
}

View File

@@ -1,84 +1,53 @@
<template>
<div>
<div v-if="is_grouped">
<div v-for="idx in albums.indexList" :key="idx" class="mb-6">
<span
:id="'index_' + idx"
class="tag is-info is-light is-small has-text-weight-bold"
>{{ idx }}</span
>
<div
v-for="album in albums.grouped[idx]"
:key="album.id"
class="media"
:album="album"
@click="open_album(album)"
>
<div v-if="is_visible_artwork" class="media-left fd-has-action">
<p class="image is-64x64 fd-has-shadow fd-has-action">
<cover-artwork
:artwork_url="album.artwork_url"
:artist="album.artist"
:album="album.name"
:maxwidth="64"
:maxheight="64"
/>
</p>
</div>
<div class="media-content fd-has-action is-clipped">
<div style="margin-top: 0.7rem">
<h1 class="title is-6">
{{ album.name }}
</h1>
<h2 class="subtitle is-7 has-text-grey">
<b>{{ album.artist }}</b>
</h2>
<h2
v-if="album.date_released && album.media_kind === 'music'"
class="subtitle is-7 has-text-grey has-text-weight-normal"
>
{{ $filters.time(album.date_released, 'L') }}
</h2>
</div>
</div>
<div class="media-right" style="padding-top: 0.7rem">
<a @click.prevent.stop="open_dialog(album)">
<span class="icon has-text-dark"
><i class="mdi mdi-dots-vertical mdi-18px"
/></span>
</a>
</div>
<template v-for="album in albums" :key="album.itemId">
<div v-if="!album.isItem && !hide_group_title" class="mt-6 mb-5 py-2">
<span
:id="'index_' + album.groupKey"
class="tag is-info is-light is-small has-text-weight-bold"
>{{ album.groupKey }}</span
>
</div>
<div v-else-if="album.isItem" class="media" @click="open_album(album.item)">
<div v-if="is_visible_artwork" class="media-left fd-has-action">
<p class="image is-64x64 fd-has-shadow fd-has-action">
<figure>
<img
v-lazy="{
src: artwork_url_with_size(album.item.artwork_url),
lifecycle: artwork_options.lazy_lifecycle
}"
:album="album.item.name"
:artist="album.item.artist"
/>
</figure>
</p>
</div>
<div class="media-content fd-has-action is-clipped">
<div style="margin-top: 0.7rem">
<h1 class="title is-6">
{{ album.item.name }}
</h1>
<h2 class="subtitle is-7 has-text-grey">
<b>{{ album.item.artist }}</b>
</h2>
<h2
v-if="album.item.date_released && album.item.media_kind === 'music'"
class="subtitle is-7 has-text-grey has-text-weight-normal"
>
{{ $filters.time(album.item.date_released, 'L') }}
</h2>
</div>
</div>
<div class="media-right" style="padding-top: 0.7rem">
<a @click.prevent.stop="open_dialog(album.item)">
<span class="icon has-text-dark"
><i class="mdi mdi-dots-vertical mdi-18px"
/></span>
</a>
</div>
</div>
<div v-else>
<list-item-album
v-for="album in albums_list"
:key="album.id"
:album="album"
@click="open_album(album)"
>
<template v-if="is_visible_artwork" #artwork>
<p class="image is-64x64 fd-has-shadow fd-has-action">
<cover-artwork
:artwork_url="album.artwork_url"
:artist="album.artist"
:album="album.name"
:maxwidth="64"
:maxheight="64"
/>
</p>
</template>
<template #actions>
<a @click.prevent.stop="open_dialog(album)">
<span class="icon has-text-dark"
><i class="mdi mdi-dots-vertical mdi-18px"
/></span>
</a>
</template>
</list-item-album>
</div>
</template>
<teleport to="#app">
<modal-dialog-album
:show="show_details_modal"
:album="selected_album"
@@ -103,22 +72,20 @@
</p>
</template>
</modal-dialog>
</div>
</teleport>
</template>
<script>
import ListItemAlbum from '@/components/ListItemAlbum.vue'
import ModalDialogAlbum from '@/components/ModalDialogAlbum.vue'
import ModalDialog from '@/components/ModalDialog.vue'
import CoverArtwork from '@/components/CoverArtwork.vue'
import webapi from '@/webapi'
import Albums from '@/lib/Albums'
import { renderSVG } from '@/lib/SVGRenderer'
export default {
name: 'ListAlbums',
components: { ListItemAlbum, ModalDialogAlbum, ModalDialog, CoverArtwork },
components: { ModalDialogAlbum, ModalDialog },
props: ['albums', 'media_kind'],
props: ['albums', 'media_kind', 'hide_group_title'],
emits: ['play-count-changed', 'podcast-deleted'],
data() {
@@ -127,7 +94,23 @@ export default {
selected_album: {},
show_remove_podcast_modal: false,
rss_playlist_to_remove: {}
rss_playlist_to_remove: {},
artwork_options: {
width: 600,
height: 600,
font_family: 'sans-serif',
font_size: 200,
font_weight: 600,
lazy_lifecycle: {
error: (el) => {
el.src = this.dataURI(
el.attributes.album.value,
el.attributes.artist.value
)
}
}
}
}
},
@@ -141,20 +124,6 @@ export default {
media_kind_resolved: function () {
return this.media_kind ? this.media_kind : this.selected_album.media_kind
},
albums_list: function () {
if (Array.isArray(this.albums)) {
return this.albums
}
if (this.albums) {
return this.albums.sortedAndFiltered
}
return []
},
is_grouped: function () {
return this.albums instanceof Albums && this.albums.options.group
}
},
@@ -207,6 +176,43 @@ export default {
.then(() => {
this.$emit('podcast-deleted')
})
},
artwork_url_with_size: function (artwork_url) {
if (this.artwork_options.width > 0 && this.artwork_options.height > 0) {
return webapi.artwork_url_append_size_params(
artwork_url,
this.artwork_options.width,
this.artwork_options.height
)
}
return webapi.artwork_url_append_size_params(artwork_url)
},
alt_text(album, artist) {
return artist + ' - ' + album
},
caption(album, artist) {
if (album) {
return album.substring(0, 2)
}
if (artist) {
return artist.substring(0, 2)
}
return ''
},
dataURI: function (album, artist) {
const caption = this.caption(album, artist)
const alt_text = this.alt_text(album, artist)
return renderSVG(caption, alt_text, {
width: this.artwork_options.width,
height: this.artwork_options.height,
font_family: this.artwork_options.font_family,
font_size: this.artwork_options.font_size,
font_weight: this.artwork_options.font_weight
})
}
}
}

View File

@@ -1,63 +1,51 @@
<template>
<div>
<div v-if="is_grouped">
<div v-for="idx in artists.indexList" :key="idx" class="mb-6">
<template v-for="artist in artists" :key="artist.itemId">
<div v-if="!artist.isItem && !hide_group_title" class="mt-6 mb-5 py-2">
<div class="media-content is-clipped">
<span
:id="'index_' + idx"
:id="'index_' + artist.groupKey"
class="tag is-info is-light is-small has-text-weight-bold"
>{{ idx }}</span
>{{ artist.groupKey }}</span
>
<list-item-artist
v-for="artist in artists.grouped[idx]"
:key="artist.id"
:artist="artist"
@click="open_artist(artist)"
>
<template #actions>
<a @click.prevent.stop="open_dialog(artist)">
<span class="icon has-text-dark"
><i class="mdi mdi-dots-vertical mdi-18px"
/></span>
</a>
</template>
</list-item-artist>
</div>
</div>
<div v-else>
<list-item-artist
v-for="artist in artists_list"
:key="artist.id"
:artist="artist"
@click="open_artist(artist)"
>
<template #actions>
<a @click.prevent.stop="open_dialog(artist)">
<span class="icon has-text-dark"
><i class="mdi mdi-dots-vertical mdi-18px"
/></span>
</a>
</template>
</list-item-artist>
<div
v-else-if="artist.isItem"
class="media"
@click="open_artist(artist.item)"
>
<div class="media-content fd-has-action is-clipped">
<h1 class="title is-6">
{{ artist.item.name }}
</h1>
</div>
<div class="media-right">
<a @click.prevent.stop="open_dialog(artist.item)">
<span class="icon has-text-dark"
><i class="mdi mdi-dots-vertical mdi-18px"
/></span>
</a>
</div>
</div>
</template>
<teleport to="#app">
<modal-dialog-artist
:show="show_details_modal"
:artist="selected_artist"
:media_kind="media_kind"
@close="show_details_modal = false"
/>
</div>
</teleport>
</template>
<script>
import ListItemArtist from '@/components/ListItemArtist.vue'
import ModalDialogArtist from '@/components/ModalDialogArtist.vue'
import Artists from '@/lib/Artists'
export default {
name: 'ListArtists',
components: { ListItemArtist, ModalDialogArtist },
components: { ModalDialogArtist },
props: ['artists', 'media_kind'],
props: ['artists', 'media_kind', 'hide_group_title'],
data() {
return {
@@ -69,17 +57,6 @@ export default {
computed: {
media_kind_resolved: function () {
return this.media_kind ? this.media_kind : this.selected_artist.media_kind
},
artists_list: function () {
if (Array.isArray(this.artists)) {
return this.artists
}
return this.artists.sortedAndFiltered
},
is_grouped: function () {
return this.artists instanceof Artists && this.artists.options.group
}
},

View File

@@ -1,63 +1,51 @@
<template>
<div>
<div v-if="is_grouped">
<div v-for="idx in composers.indexList" :key="idx" class="mb-6">
<template v-for="composer in composers" :key="composer.itemId">
<div v-if="!composer.isItem && !hide_group_title" class="mt-6 mb-5 py-2">
<div class="media-content is-clipped">
<span
:id="'index_' + idx"
:id="'index_' + composer.groupKey"
class="tag is-info is-light is-small has-text-weight-bold"
>{{ idx }}</span
>{{ composer.groupKey }}</span
>
<list-item-composer
v-for="composer in composers.grouped[idx]"
:key="composer.id"
:composer="composer"
@click="open_composer(composer)"
>
<template #actions>
<a @click.prevent.stop="open_dialog(composer)">
<span class="icon has-text-dark"
><i class="mdi mdi-dots-vertical mdi-18px"
/></span>
</a>
</template>
</list-item-composer>
</div>
</div>
<div v-else>
<list-item-composer
v-for="composer in composers_list"
:key="composer.id"
:composer="composer"
@click="open_composer(composer)"
>
<template #actions>
<a @click.prevent.stop="open_dialog(composer)">
<span class="icon has-text-dark"
><i class="mdi mdi-dots-vertical mdi-18px"
/></span>
</a>
</template>
</list-item-composer>
<div
v-else-if="composer.isItem"
class="media"
@click="open_composer(composer.item)"
>
<div class="media-content fd-has-action is-clipped">
<h1 class="title is-6">
{{ composer.item.name }}
</h1>
</div>
<div class="media-right">
<a @click.prevent.stop="open_dialog(composer.item)">
<span class="icon has-text-dark"
><i class="mdi mdi-dots-vertical mdi-18px"
/></span>
</a>
</div>
</div>
</template>
<teleport to="#app">
<modal-dialog-composer
:show="show_details_modal"
:composer="selected_composer"
:media_kind="media_kind"
@close="show_details_modal = false"
/>
</div>
</teleport>
</template>
<script>
import ListItemComposer from '@/components/ListItemComposer.vue'
import ModalDialogComposer from '@/components/ModalDialogComposer.vue'
import Composers from '@/lib/Composers'
export default {
name: 'ListComposers',
components: { ListItemComposer, ModalDialogComposer },
components: { ModalDialogComposer },
props: ['composers', 'media_kind'],
props: ['composers', 'media_kind', 'hide_group_title'],
data() {
return {
@@ -71,17 +59,6 @@ export default {
return this.media_kind
? this.media_kind
: this.selected_composer.media_kind
},
composers_list: function () {
if (Array.isArray(this.composers)) {
return this.composers
}
return this.composers.sortedAndFiltered
},
is_grouped: function () {
return this.composers instanceof Composers && this.composers.options.group
}
},

View File

@@ -0,0 +1,116 @@
<template>
<div
v-if="$route.query.directory"
class="media"
@click="open_parent_directory()"
>
<figure class="media-left fd-has-action">
<span class="icon">
<i class="mdi mdi-subdirectory-arrow-left" />
</span>
</figure>
<div class="media-content fd-has-action is-clipped">
<h1 class="title is-6">..</h1>
</div>
<div class="media-right">
<slot name="actions" />
</div>
</div>
<template v-for="directory in directories" :key="directory.path">
<div class="media" @click="open_directory(directory)">
<figure class="media-left fd-has-action">
<span class="icon">
<i class="mdi mdi-folder" />
</span>
</figure>
<div class="media-content fd-has-action is-clipped">
<h1 class="title is-6">
{{ directory.path.substring(directory.path.lastIndexOf('/') + 1) }}
</h1>
<h2 class="subtitle is-7 has-text-grey-light">
{{ directory.path }}
</h2>
</div>
<div class="media-right">
<a @click.prevent.stop="open_dialog(directory)">
<span class="icon has-text-dark"
><i class="mdi mdi-dots-vertical mdi-18px"
/></span>
</a>
</div>
</div>
</template>
<teleport to="#app">
<modal-dialog-directory
:show="show_details_modal"
:directory="selected_directory"
@close="show_details_modal = false"
/>
</teleport>
</template>
<script>
import ModalDialogDirectory from '@/components/ModalDialogDirectory.vue'
export default {
name: 'ListDirectories',
components: { ModalDialogDirectory },
props: ['directories'],
data() {
return {
show_details_modal: false,
selected_directory: {}
}
},
computed: {
current_directory() {
if (this.$route.query && this.$route.query.directory) {
return this.$route.query.directory
}
return '/'
}
},
methods: {
open_parent_directory: function () {
const parent = this.current_directory.slice(
0,
this.current_directory.lastIndexOf('/')
)
if (
parent === '' ||
this.$store.state.config.directories.includes(this.current_directory)
) {
this.$router.push({ path: '/files' })
} else {
this.$router.push({
path: '/files',
query: {
directory: this.current_directory.slice(
0,
this.current_directory.lastIndexOf('/')
)
}
})
}
},
open_directory: function (directory) {
this.$router.push({
path: '/files',
query: { directory: directory.path }
})
},
open_dialog: function (directory) {
this.selected_directory = directory
this.show_details_modal = true
}
}
}
</script>
<style></style>

View File

@@ -0,0 +1,71 @@
<template>
<template v-for="genre in genres" :key="genre.itemId">
<div v-if="!genre.isItem && !hide_group_title" class="mt-6 mb-5 py-2">
<div class="media-content is-clipped">
<span
:id="'index_' + genre.groupKey"
class="tag is-info is-light is-small has-text-weight-bold"
>{{ genre.groupKey }}</span
>
</div>
</div>
<div v-else-if="genre.isItem" class="media" @click="open_genre(genre.item)">
<div class="media-content fd-has-action is-clipped">
<h1 class="title is-6">
{{ genre.item.name }}
</h1>
</div>
<div class="media-right">
<a @click.prevent.stop="open_dialog(genre.item)">
<span class="icon has-text-dark"
><i class="mdi mdi-dots-vertical mdi-18px"
/></span>
</a>
</div>
</div>
</template>
<teleport to="#app">
<modal-dialog-genre
:show="show_details_modal"
:genre="selected_genre"
@close="show_details_modal = false"
/>
</teleport>
</template>
<script>
import ModalDialogGenre from '@/components/ModalDialogGenre.vue'
export default {
name: 'ListGenres',
components: { ModalDialogGenre },
props: ['genres', 'media_kind', 'hide_group_title'],
data() {
return {
show_details_modal: false,
selected_genre: {}
}
},
computed: {
media_kind_resolved: function () {
return this.media_kind ? this.media_kind : this.selected_genre.media_kind
}
},
methods: {
open_genre: function (genre) {
this.$router.push({ name: 'Genre', params: { genre: genre.name } })
},
open_dialog: function (genre) {
this.selected_genre = genre
this.show_details_modal = true
}
}
}
</script>
<style></style>

View File

@@ -1,35 +0,0 @@
<template>
<div :id="'index_' + album.name_sort.charAt(0).toUpperCase()" class="media">
<div v-if="$slots['artwork']" class="media-left fd-has-action">
<slot name="artwork" />
</div>
<div class="media-content fd-has-action is-clipped">
<div style="margin-top: 0.7rem">
<h1 class="title is-6">
{{ album.name }}
</h1>
<h2 class="subtitle is-7 has-text-grey">
<b>{{ album.artist }}</b>
</h2>
<h2
v-if="album.date_released && album.media_kind === 'music'"
class="subtitle is-7 has-text-grey has-text-weight-normal"
>
{{ $filters.time(album.date_released, 'L') }}
</h2>
</div>
</div>
<div class="media-right" style="padding-top: 0.7rem">
<slot name="actions" />
</div>
</div>
</template>
<script>
export default {
name: 'ListItemAlbum',
props: ['album', 'media_kind']
}
</script>
<style></style>

View File

@@ -1,21 +0,0 @@
<template>
<div class="media">
<div class="media-content fd-has-action is-clipped">
<h1 class="title is-6">
{{ artist.name }}
</h1>
</div>
<div class="media-right">
<slot name="actions" />
</div>
</div>
</template>
<script>
export default {
name: 'ListItemArtist',
props: ['artist']
}
</script>
<style></style>

View File

@@ -1,21 +0,0 @@
<template>
<div :id="'index_' + composer.name.charAt(0).toUpperCase()" class="media">
<div class="media-content fd-has-action is-clipped">
<h1 class="title is-6">
{{ composer.name }}
</h1>
</div>
<div class="media-right">
<slot name="actions" />
</div>
</div>
</template>
<script>
export default {
name: 'ListItemComposer',
props: ['composer']
}
</script>
<style></style>

View File

@@ -1,29 +0,0 @@
<template>
<div class="media">
<figure class="media-left fd-has-action">
<span class="icon">
<i class="mdi mdi-folder" />
</span>
</figure>
<div class="media-content fd-has-action is-clipped">
<h1 class="title is-6">
{{ directory.path.substring(directory.path.lastIndexOf('/') + 1) }}
</h1>
<h2 class="subtitle is-7 has-text-grey-light">
{{ directory.path }}
</h2>
</div>
<div class="media-right">
<slot name="actions" />
</div>
</div>
</template>
<script>
export default {
name: 'ListItemDirectory',
props: ['directory']
}
</script>
<style></style>

View File

@@ -1,21 +0,0 @@
<template>
<div :id="'index_' + genre.name.charAt(0).toUpperCase()" class="media">
<div class="media-content fd-has-action is-clipped">
<h1 class="title is-6">
{{ genre.name }}
</h1>
</div>
<div class="media-right">
<slot name="actions" />
</div>
</div>
</template>
<script>
export default {
name: 'ListItemGenre',
props: ['genre']
}
</script>
<style></style>

View File

@@ -1,24 +0,0 @@
<template>
<div class="media">
<figure v-if="$slots.icon" class="media-left fd-has-action">
<slot name="icon" />
</figure>
<div class="media-content fd-has-action is-clipped">
<h1 class="title is-6">
{{ playlist.name }}
</h1>
</div>
<div class="media-right">
<slot name="actions" />
</div>
</div>
</template>
<script>
export default {
name: 'ListItemPlaylist',
props: ['playlist']
}
</script>
<style></style>

View File

@@ -1,41 +0,0 @@
<template>
<div
:id="'index_' + track.title_sort.charAt(0).toUpperCase()"
class="media"
:class="{ 'with-progress': $slots.progress }"
>
<figure v-if="$slots.icon" class="media-left fd-has-action">
<slot name="icon" />
</figure>
<div class="media-content fd-has-action is-clipped">
<h1
class="title is-6"
:class="{
'has-text-grey':
track.media_kind === 'podcast' && track.play_count > 0
}"
>
{{ track.title }}
</h1>
<h2 class="subtitle is-7 has-text-grey">
<b>{{ track.artist }}</b>
</h2>
<h2 class="subtitle is-7 has-text-grey">
{{ track.album }}
</h2>
<slot name="progress" />
</div>
<div class="media-right">
<slot name="actions" />
</div>
</div>
</template>
<script>
export default {
name: 'ListItemTrack',
props: ['track']
}
</script>
<style></style>

View File

@@ -1,46 +1,51 @@
<template>
<div>
<list-item-playlist
v-for="playlist in playlists"
:key="playlist.id"
:playlist="playlist"
@click="open_playlist(playlist)"
>
<template #icon>
<span class="icon">
<i
class="mdi"
:class="{
'mdi-library-music': playlist.type !== 'folder',
'mdi-rss': playlist.type === 'rss',
'mdi-folder': playlist.type === 'folder'
}"
/>
</span>
</template>
<template #actions>
<a @click.prevent.stop="open_dialog(playlist)">
<span class="icon has-text-dark"
><i class="mdi mdi-dots-vertical mdi-18px"
/></span>
</a>
</template>
</list-item-playlist>
<div
v-for="playlist in playlists"
:key="playlist.id"
class="media"
:playlist="playlist"
@click="open_playlist(playlist)"
>
<figure class="media-left fd-has-action">
<span class="icon">
<i
class="mdi"
:class="{
'mdi-library-music': playlist.type !== 'folder',
'mdi-rss': playlist.type === 'rss',
'mdi-folder': playlist.type === 'folder'
}"
/>
</span>
</figure>
<div class="media-content fd-has-action is-clipped">
<h1 class="title is-6">
{{ playlist.name }}
</h1>
</div>
<div class="media-right">
<a @click.prevent.stop="open_dialog(playlist)">
<span class="icon has-text-dark"
><i class="mdi mdi-dots-vertical mdi-18px"
/></span>
</a>
</div>
</div>
<teleport to="#app">
<modal-dialog-playlist
:show="show_details_modal"
:playlist="selected_playlist"
@close="show_details_modal = false"
/>
</div>
</teleport>
</template>
<script>
import ListItemPlaylist from '@/components/ListItemPlaylist.vue'
import ModalDialogPlaylist from '@/components/ModalDialogPlaylist.vue'
export default {
name: 'ListPlaylists',
components: { ListItemPlaylist, ModalDialogPlaylist },
components: { ModalDialogPlaylist },
props: ['playlists'],

View File

@@ -1,37 +1,69 @@
<template>
<div>
<list-item-track
v-for="(track, index) in tracks"
:key="track.id"
:track="track"
@click="play_track(index, track)"
>
<template #actions>
<a @click.prevent.stop="open_dialog(track)">
<span class="icon has-text-dark"
><i class="mdi mdi-dots-vertical mdi-18px"
/></span>
</a>
</template>
</list-item-track>
<div
v-for="(track, index) in tracks"
:id="'index_' + track.title_sort.charAt(0).toUpperCase()"
:key="track.id"
class="media"
:class="{ 'with-progress': show_progress }"
@click="play_track(index, track)"
>
<figure v-if="show_icon" class="media-left fd-has-action">
<span class="icon">
<i class="mdi mdi-file-outline" />
</span>
</figure>
<div class="media-content fd-has-action is-clipped">
<h1
class="title is-6"
:class="{
'has-text-grey':
track.media_kind === 'podcast' && track.play_count > 0
}"
>
{{ track.title }}
</h1>
<h2 class="subtitle is-7 has-text-grey">
<b>{{ track.artist }}</b>
</h2>
<h2 class="subtitle is-7 has-text-grey">
{{ track.album }}
</h2>
<progress-bar
v-if="show_progress"
:max="track.length_ms"
:value="track.seek_ms"
/>
</div>
<div class="media-right">
<a @click.prevent.stop="open_dialog(track)">
<span class="icon has-text-dark"
><i class="mdi mdi-dots-vertical mdi-18px"
/></span>
</a>
</div>
</div>
<teleport to="#app">
<modal-dialog-track
:show="show_details_modal"
:track="selected_track"
@close="show_details_modal = false"
@play-count-changed="$emit('play-count-changed')"
/>
</div>
</teleport>
</template>
<script>
import ListItemTrack from '@/components/ListItemTrack.vue'
import ModalDialogTrack from '@/components/ModalDialogTrack.vue'
import ProgressBar from '@/components/ProgressBar.vue'
import webapi from '@/webapi'
export default {
name: 'ListTracks',
components: { ListItemTrack, ModalDialogTrack },
components: { ModalDialogTrack, ProgressBar },
props: ['tracks', 'uris', 'expression'],
props: ['tracks', 'uris', 'expression', 'show_progress', 'show_icon'],
emits: ['play-count-changed'],
data() {
return {