mirror of
https://github.com/owntone/owntone-server.git
synced 2025-11-07 04:42:58 -05:00
[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:
@@ -20,6 +20,9 @@ export default {
|
||||
|
||||
computed: {
|
||||
filtered_index() {
|
||||
if (!this.index) {
|
||||
return []
|
||||
}
|
||||
const specialChars = '!"#$%&\'()*+,-./:;<=>?@[\\]^`{|}~'
|
||||
return this.index.filter((c) => !specialChars.includes(c))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
116
web-src/src/components/ListDirectories.vue
Normal file
116
web-src/src/components/ListDirectories.vue
Normal 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>
|
||||
71
web-src/src/components/ListGenres.vue
Normal file
71
web-src/src/components/ListGenres.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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'],
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user