[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

@ -1,7 +1,5 @@
# Vue 3 + Vite Migration
- Vue Dev Tools required in version 6 (currently only released as beta versions): <https://devtools.vuejs.org/guide/installation.html#beta>
- [ ] vite does not support env vars in `vite.config.js` from `.env` files
- <https://stackoverflow.com/questions/66389043/how-can-i-use-vite-env-variables-in-vite-config-js>
@ -9,7 +7,7 @@
- [ ] Documentation update
- [ ] Add linting (ESLint) ?
- [x] Add linting (ESLint)
- [x] Update dialog is missing scan options
@ -17,22 +15,22 @@
- [ ] Do not reload data, if using the index-nav
- [x] PageAlbums
- [ ] PageArtists
- [x] PageArtists
- [ ] PageGenres
- [ ] ...
- [ ] Albums page is slow to load (because of the number of vue components?)
- [ ] Evaluate virtual scroller <https://github.com/Akryum/vue-virtual-scroller/tree/next/packages/vue-virtual-scroller>
- [ ] Albums page is slow to load (because of the number of vue components - ListItem+CoverArtwork)
- [ ] Evaluate removing ListItem and CoverArtwork component
- [x] JS error on Podacst page
- Problem caused by the Slider component
- Replace with plain html
- [ ] vue-router scroll-behavior
- [x] vue-router scroll-behavior
- [x] Index list not always hidden
- [x] Check transitions
- [ ] Page display is "jumpy"
- Workaround is removing the page transition effect
- [x] Page display is "jumpy" - "fixed" by removing the page transition effect
- [x] Index navigation "scroll up/down" button does not scroll down, if index is visible
@ -54,7 +52,7 @@
- [x] vue-router does not support navigation guards in mixins: <https://github.com/vuejs/vue-router-next/issues/454>
- replace mixin with composition api? <https://next.router.vuejs.org/guide/advanced/composition-api.html#navigation-guards>
- Replace mixin with composition api? <https://next.router.vuejs.org/guide/advanced/composition-api.html#navigation-guards>
- Copied nav guards into each component
- [x] vue-router link does not support `tag` and `active-class` properties: <https://next.router.vuejs.org/guide/migration/index.html#removal-of-event-and-tag-props-in-router-link>

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 {

View File

@ -1,127 +0,0 @@
export default class Albums {
constructor(
items,
options = {
hideSingles: false,
hideSpotify: false,
sort: 'Name',
group: false
}
) {
this.items = items
this.options = options
this.grouped = {}
this.sortedAndFiltered = []
this.indexList = []
this.init()
}
init() {
this.createSortedAndFilteredList()
this.createGroupedList()
this.createIndexList()
}
getAlbumIndex(album) {
if (this.options.sort === 'Recently added') {
return album.time_added.substring(0, 4)
} else if (this.options.sort === 'Recently added (browse)') {
return this.getRecentlyAddedBrowseIndex(album.time_added)
} else if (this.options.sort === 'Recently released') {
return album.date_released ? album.date_released.substring(0, 4) : '0000'
} else if (this.options.sort === 'Release date') {
return album.date_released ? album.date_released.substring(0, 4) : '0000'
}
return album.name_sort.charAt(0).toUpperCase()
}
getRecentlyAddedBrowseIndex(recentlyAdded) {
if (!recentlyAdded) {
return '0000'
}
const diff = new Date().getTime() - new Date(recentlyAdded).getTime()
if (diff < 86400000) {
// 24h
return 'Today'
} else if (diff < 604800000) {
// 7 days
return 'Last week'
} else if (diff < 2592000000) {
// 30 days
return 'Last month'
}
return recentlyAdded.substring(0, 4)
}
isAlbumVisible(album) {
if (this.options.hideSingles && album.track_count <= 2) {
return false
}
if (this.options.hideSpotify && album.data_kind === 'spotify') {
return false
}
return true
}
createIndexList() {
this.indexList = [
...new Set(
this.sortedAndFiltered.map((album) => this.getAlbumIndex(album))
)
]
}
createSortedAndFilteredList() {
let albumsSorted = this.items
if (
this.options.hideSingles ||
this.options.hideSpotify ||
this.options.hideOther
) {
albumsSorted = albumsSorted.filter((album) => this.isAlbumVisible(album))
}
if (
this.options.sort === 'Recently added' ||
this.options.sort === 'Recently added (browse)'
) {
albumsSorted = [...albumsSorted].sort((a, b) =>
b.time_added.localeCompare(a.time_added)
)
} else if (this.options.sort === 'Recently released') {
albumsSorted = [...albumsSorted].sort((a, b) => {
if (!a.date_released) {
return 1
}
if (!b.date_released) {
return -1
}
return b.date_released.localeCompare(a.date_released)
})
} else if (this.options.sort === 'Release date') {
albumsSorted = [...albumsSorted].sort((a, b) => {
if (!a.date_released) {
return -1
}
if (!b.date_released) {
return 1
}
return a.date_released.localeCompare(b.date_released)
})
}
this.sortedAndFiltered = albumsSorted
}
createGroupedList() {
if (!this.options.group) {
this.grouped = {}
}
this.grouped = this.sortedAndFiltered.reduce((r, album) => {
const idx = this.getAlbumIndex(album)
r[idx] = [...(r[idx] || []), album]
return r
}, {})
}
}

View File

@ -1,83 +0,0 @@
export default class Artists {
constructor(
items,
options = {
hideSingles: false,
hideSpotify: false,
sort: 'Name',
group: false
}
) {
this.items = items
this.options = options
this.grouped = {}
this.sortedAndFiltered = []
this.indexList = []
this.init()
}
init() {
this.createSortedAndFilteredList()
this.createGroupedList()
this.createIndexList()
}
getArtistIndex(artist) {
if (this.options.sort === 'Name') {
return artist.name_sort.charAt(0).toUpperCase()
}
return artist.time_added.substring(0, 4)
}
isArtistVisible(artist) {
if (
this.options.hideSingles &&
artist.track_count <= artist.album_count * 2
) {
return false
}
if (this.options.hideSpotify && artist.data_kind === 'spotify') {
return false
}
return true
}
createIndexList() {
this.indexList = [
...new Set(
this.sortedAndFiltered.map((artist) => this.getArtistIndex(artist))
)
]
}
createSortedAndFilteredList() {
let artistsSorted = this.items
if (
this.options.hideSingles ||
this.options.hideSpotify ||
this.options.hideOther
) {
artistsSorted = artistsSorted.filter((artist) =>
this.isArtistVisible(artist)
)
}
if (this.options.sort === 'Recently added') {
artistsSorted = [...artistsSorted].sort((a, b) =>
b.time_added.localeCompare(a.time_added)
)
}
this.sortedAndFiltered = artistsSorted
}
createGroupedList() {
if (!this.options.group) {
this.grouped = {}
}
this.grouped = this.sortedAndFiltered.reduce((r, artist) => {
const idx = this.getArtistIndex(artist)
r[idx] = [...(r[idx] || []), artist]
return r
}, {})
}
}

View File

@ -1,85 +0,0 @@
export default class Composers {
constructor(
items,
options = {
hideSingles: false,
hideSpotify: false,
sort: 'Name',
group: false
}
) {
this.items = items
this.options = options
this.grouped = {}
this.sortedAndFiltered = []
this.indexList = []
this.init()
}
init() {
this.createSortedAndFilteredList()
this.createGroupedList()
this.createIndexList()
}
getComposerIndex(composer) {
if (this.options.sort === 'Name') {
return composer.name_sort.charAt(0).toUpperCase()
}
return composer.time_added.substring(0, 4)
}
isComposerVisible(composer) {
if (
this.options.hideSingles &&
composer.track_count <= composer.album_count * 2
) {
return false
}
if (this.options.hideSpotify && composer.data_kind === 'spotify') {
return false
}
return true
}
createIndexList() {
this.indexList = [
...new Set(
this.sortedAndFiltered.map((composer) =>
this.getComposerIndex(composer)
)
)
]
}
createSortedAndFilteredList() {
let composersSorted = this.items
if (
this.options.hideSingles ||
this.options.hideSpotify ||
this.options.hideOther
) {
composersSorted = composersSorted.filter((composer) =>
this.isComposerVisible(composer)
)
}
if (this.options.sort === 'Recently added') {
composersSorted = [...composersSorted].sort((a, b) =>
b.time_added.localeCompare(a.time_added)
)
}
this.sortedAndFiltered = composersSorted
}
createGroupedList() {
if (!this.options.group) {
this.grouped = {}
}
this.grouped = this.sortedAndFiltered.reduce((r, composer) => {
const idx = this.getComposerIndex(composer)
r[idx] = [...(r[idx] || []), composer]
return r
}, {})
}
}

View File

@ -0,0 +1,233 @@
const GROUP_KEY_NONE = 'GROUP_KEY_NONE'
export function noop() {
return {
compareFn: null,
groupKeyFn: (item) => GROUP_KEY_NONE
}
}
/*
* Keep default sorting of item list and build index and group by given field
*/
export function bySortName(field, defaultValue = '_') {
return {
// Keep the sort order of the original item list
// Assumes that the list is already ordered by name
compareFn: null,
groupKeyFn: (item) => {
const fieldValue = item[field] || defaultValue
return fieldValue.charAt(0).toUpperCase()
}
}
}
export function byName(field, defaultValue = '_') {
return {
compareFn: (a, b) => {
const fieldA = a[field] || defaultValue
const fieldB = b[field] || defaultValue
return fieldA.localeCompare(fieldB)
},
groupKeyFn: (item) => {
const fieldValue = item[field] || defaultValue
return fieldValue.charAt(0).toUpperCase()
}
}
}
export function byYear(field, { direction = 'asc', defaultValue = '0000' }) {
return {
compareFn: (a, b) => {
const fieldA = a[field] || defaultValue
const fieldB = b[field] || defaultValue
const result = fieldA.localeCompare(fieldB)
return direction === 'asc' ? result : result * -1
},
groupKeyFn: (item) => {
const fieldValue = item[field] || defaultValue
return fieldValue.substring(0, 4)
}
}
}
export function byDateSinceToday(field, defaultValue = '0000') {
return {
compareFn: (a, b) => {
const fieldA = a[field] || defaultValue
const fieldB = b[field] || defaultValue
return fieldB.localeCompare(fieldA)
},
groupKeyFn: (item) => {
const fieldValue = item[field]
if (!fieldValue) {
return defaultValue
}
const diff = new Date().getTime() - new Date(fieldValue).getTime()
if (diff < 86400000) {
// 24h
return 'Today'
} else if (diff < 604800000) {
// 7 days
return 'Last week'
} else if (diff < 2592000000) {
// 30 days
return 'Last month'
}
return fieldValue.substring(0, 4)
}
}
}
export class GroupByList {
constructor({ items = [], total = 0, offset = 0, limit = -1 } = {}) {
this.items = items
this.total = total
this.offset = offset
this.limit = limit
this.count = items.length
this.indexList = []
this.group(noop())
}
get() {
return this.itemsByGroup
}
isEmpty() {
return !this.items || this.items.length <= 0
}
group(options, filterFns = []) {
const itemsFiltered = filterFns
? this.items.filter((item) => filterFns.every((fn) => fn(item)))
: this.items
this.count = itemsFiltered.length
// Sort item list
let itemsSorted = options.compareFn
? [...itemsFiltered].sort(options.compareFn)
: itemsFiltered
// Create index list
this.indexList = [...new Set(itemsSorted.map(options.groupKeyFn))]
// Group item list
this.itemsByGroup = itemsSorted.reduce((r, item) => {
const groupKey = options.groupKeyFn(item)
r[groupKey] = [...(r[groupKey] || []), item]
return r
}, {})
}
[Symbol.iterator]() {
// Use a new index for each iterator. This makes multiple
// iterations over the iterable safe for non-trivial cases,
// such as use of break or nested looping over the same iterable.
let groupIndex = -1
let itemIndex = -1
return {
next: () => {
/*
console.log(
'[group-by-list] itemIndex=' +
itemIndex +
', groupIndex=' +
groupIndex
)
*/
if (groupIndex >= this.indexList.length) {
// We reached the end of all groups and items
//
// This should never happen, as the we already
// return "done" after we reached the last item
// of the last group
/*
console.log(
'[group-by-list] done! (groupIndex >= this.indexList.length)'
)
*/
return { done: true }
} else if (groupIndex < 0) {
// We start iterating
//
// Return the first group title as the next item
++groupIndex
itemIndex = 0
if (this.indexList[groupIndex] !== GROUP_KEY_NONE) {
// Only return the group, if it is not the "noop" default group
return {
value: {
groupKey: this.indexList[groupIndex],
itemId: this.indexList[groupIndex],
isItem: false,
item: {}
},
done: false
}
}
}
let currentGroupKey = this.indexList[groupIndex]
let currentGroupItems = this.itemsByGroup[currentGroupKey]
if (itemIndex < currentGroupItems.length) {
// We are in a group with items left
//
// Return the current item and increment the item index
const currentItem = this.itemsByGroup[currentGroupKey][itemIndex++]
return {
value: {
groupKey: currentGroupKey,
itemId: currentItem.id,
isItem: true,
item: currentItem
},
done: false
}
} else {
// We reached the end of the current groups item list
//
// Move to the next group and return the group key/title
// as the next item
++groupIndex
itemIndex = 0
if (groupIndex < this.indexList.length) {
currentGroupKey = this.indexList[groupIndex]
return {
value: {
groupKey: currentGroupKey,
itemId: currentGroupKey,
isItem: false,
item: {}
},
done: false
}
} else {
// No group left, we are done iterating
/*
console.log(
'[group-by-list] done! (groupIndex >= this.indexList.length)'
)
*/
return { done: true }
}
}
}
}
}
}

View File

@ -140,7 +140,7 @@ section.hero + section.fd-content {
/* Set minimum height to hide "option" section */
.fd-content-with-option {
min-height: calc(100vh - 3.25rem - 3.25rem - 5rem);
min-height: calc(100vh - 3.25rem - 3.25rem);
}
/* Now playing page */

View File

@ -4,7 +4,7 @@
<content-with-heading>
<template #options>
<index-button-list :index="albums_list.indexList" />
<index-button-list :index="albums.indexList" />
<div class="columns">
<div class="column">
@ -44,17 +44,20 @@
</div>
<div class="column">
<p class="heading" style="margin-bottom: 24px">Sort by</p>
<dropdown-menu v-model="sort" :options="sort_options" />
<dropdown-menu
v-model="selected_groupby_option_name"
:options="groupby_option_names"
/>
</div>
</div>
</template>
<template #heading-left>
<p class="title is-4">Albums</p>
<p class="heading">{{ albums_list.sortedAndFiltered.length }} Albums</p>
<p class="heading">{{ albums.count }} Albums</p>
</template>
<template #heading-right />
<template #content>
<list-albums :albums="albums_list" />
<list-albums :albums="albums" />
</template>
</content-with-heading>
</div>
@ -68,7 +71,7 @@ import ListAlbums from '@/components/ListAlbums.vue'
import DropdownMenu from '@/components/DropdownMenu.vue'
import webapi from '@/webapi'
import * as types from '@/store/mutation_types'
import Albums from '@/lib/Albums'
import { bySortName, byYear, GroupByList } from '@/lib/GroupByList'
const dataObject = {
load: function (to) {
@ -76,16 +79,7 @@ const dataObject = {
},
set: function (vm, response) {
vm.albums = response.data
vm.index_list = [
...new Set(
vm.albums.items
.filter(
(album) => !vm.$store.state.hide_singles || album.track_count > 2
)
.map((album) => album.name_sort.charAt(0).toUpperCase())
)
]
vm.albums_list = new GroupByList(response.data)
}
}
@ -104,8 +98,9 @@ export default {
next((vm) => dataObject.set(vm, response))
})
},
beforeRouteUpdate(to, from, next) {
if (this.albums.items.length > 0) {
if (!this.albums_list.isEmpty()) {
next()
return
}
@ -118,19 +113,53 @@ export default {
data() {
return {
albums: { items: [] },
sort_options: ['Name', 'Recently added', 'Recently released']
albums_list: new GroupByList(),
// List of group by/sort options for itemsGroupByList
groupby_options: [
{ name: 'Name', options: bySortName('name_sort') },
{
name: 'Recently added',
options: byYear('time_added', {
direction: 'desc',
defaultValue: '0000'
})
},
{
name: 'Recently released',
options: byYear('date_released', {
direction: 'desc',
defaultValue: '0000'
})
}
]
}
},
computed: {
albums_list() {
return new Albums(this.albums.items, {
hideSingles: this.hide_singles,
hideSpotify: this.hide_spotify,
sort: this.sort,
group: true
})
albums() {
const groupBy = this.groupby_options.find(
(o) => o.name === this.selected_groupby_option_name
)
this.albums_list.group(groupBy.options, [
(album) => !this.hide_singles || album.track_count <= 2,
(album) => !this.hide_spotify || album.data_kind !== 'spotify'
])
return this.albums_list
},
groupby_option_names() {
return [...this.groupby_options].map((o) => o.name)
},
selected_groupby_option_name: {
get() {
return this.$store.state.albums_sort
},
set(value) {
this.$store.commit(types.ALBUMS_SORT, value)
}
},
spotify_enabled() {
@ -153,15 +182,6 @@ export default {
set(value) {
this.$store.commit(types.HIDE_SPOTIFY, value)
}
},
sort: {
get() {
return this.$store.state.albums_sort
},
set(value) {
this.$store.commit(types.ALBUMS_SORT, value)
}
}
},

View File

@ -4,7 +4,10 @@
<div class="columns">
<div class="column">
<p class="heading" style="margin-bottom: 24px">Sort by</p>
<dropdown-menu v-model="sort" :options="sort_options" />
<dropdown-menu
v-model="selected_groupby_option_name"
:options="groupby_option_names"
/>
</div>
</div>
</template>
@ -36,7 +39,7 @@
>{{ artist.track_count }} tracks</a
>
</p>
<list-albums :albums="albums_list" />
<list-albums :albums="albums" :hide_group_title="true" />
<modal-dialog-artist
:show="show_artist_details_modal"
:artist="artist"
@ -53,7 +56,7 @@ import ModalDialogArtist from '@/components/ModalDialogArtist.vue'
import DropdownMenu from '@/components/DropdownMenu.vue'
import webapi from '@/webapi'
import * as types from '@/store/mutation_types'
import Albums from '@/lib/Albums'
import { bySortName, byYear, GroupByList } from '@/lib/GroupByList'
const dataObject = {
load: function (to) {
@ -65,7 +68,7 @@ const dataObject = {
set: function (vm, response) {
vm.artist = response[0].data
vm.albums = response[1].data
vm.albums_list = new GroupByList(response[1].data)
}
}
@ -94,22 +97,39 @@ export default {
data() {
return {
artist: {},
albums: { items: [] },
albums_list: new GroupByList(),
// List of group by/sort options for itemsGroupByList
groupby_options: [
{ name: 'Name', options: bySortName('name_sort') },
{
name: 'Release date',
options: byYear('date_released', {
direction: 'asc',
defaultValue: '0000'
})
}
],
sort_options: ['Name', 'Release date'],
show_artist_details_modal: false
}
},
computed: {
albums_list() {
return new Albums(this.albums.items, {
sort: this.sort,
group: false
})
albums() {
const groupBy = this.groupby_options.find(
(o) => o.name === this.selected_groupby_option_name
)
this.albums_list.group(groupBy.options)
return this.albums_list
},
sort: {
groupby_option_names() {
return [...this.groupby_options].map((o) => o.name)
},
selected_groupby_option_name: {
get() {
return this.$store.state.artist_albums_sort
},

View File

@ -4,7 +4,7 @@
<content-with-heading>
<template #options>
<index-button-list :index="artists_list.indexList" />
<index-button-list :index="artists.indexList" />
<div class="columns">
<div class="column">
@ -44,19 +44,20 @@
</div>
<div class="column">
<p class="heading" style="margin-bottom: 24px">Sort by</p>
<dropdown-menu v-model="sort" :options="sort_options" />
<dropdown-menu
v-model="selected_groupby_option_name"
:options="groupby_option_names"
/>
</div>
</div>
</template>
<template #heading-left>
<p class="title is-4">Artists</p>
<p class="heading">
{{ artists_list.sortedAndFiltered.length }} Artists
</p>
<p class="heading">{{ artists.count }} Artists</p>
</template>
<template #heading-right />
<template #content>
<list-artists :artists="artists_list" />
<list-artists :artists="artists" />
</template>
</content-with-heading>
</div>
@ -70,7 +71,7 @@ import ListArtists from '@/components/ListArtists.vue'
import DropdownMenu from '@/components/DropdownMenu.vue'
import webapi from '@/webapi'
import * as types from '@/store/mutation_types'
import Artists from '@/lib/Artists'
import { bySortName, byYear, GroupByList } from '@/lib/GroupByList'
const dataObject = {
load: function (to) {
@ -78,7 +79,7 @@ const dataObject = {
},
set: function (vm, response) {
vm.artists = response.data
vm.artists_list = new GroupByList(response.data)
}
}
@ -97,8 +98,9 @@ export default {
next((vm) => dataObject.set(vm, response))
})
},
beforeRouteUpdate(to, from, next) {
if (this.artists.items.length > 0) {
if (!this.artists_list.isEmpty()) {
next()
return
}
@ -111,19 +113,54 @@ export default {
data() {
return {
artists: { items: [] },
sort_options: ['Name', 'Recently added']
// Original data from API call
artists_list: new GroupByList(),
// List of group by/sort options for itemsGroupByList
groupby_options: [
{ name: 'Name', options: bySortName('name_sort') },
{
name: 'Recently added',
options: byYear('time_added', {
direction: 'desc',
defaultValue: '0000'
})
}
]
}
},
computed: {
artists_list() {
return new Artists(this.artists.items, {
hideSingles: this.hide_singles,
hideSpotify: this.hide_spotify,
sort: this.sort,
group: true
})
// Wraps GroupByList and updates it if filter or sort changes
artists() {
if (!this.artists_list) {
return []
}
const groupBy = this.groupby_options.find(
(o) => o.name === this.selected_groupby_option_name
)
this.artists_list.group(groupBy.options, [
(artist) =>
!this.hide_singles || artist.track_count <= artist.album_count * 2,
(artist) => !this.hide_spotify || artist.data_kind !== 'spotify'
])
return this.artists_list
},
// List for the drop down menu
groupby_option_names() {
return [...this.groupby_options].map((o) => o.name)
},
selected_groupby_option_name: {
get() {
return this.$store.state.artists_sort
},
set(value) {
this.$store.commit(types.ARTISTS_SORT, value)
}
},
spotify_enabled() {
@ -146,23 +183,10 @@ export default {
set(value) {
this.$store.commit(types.HIDE_SPOTIFY, value)
}
},
sort: {
get() {
return this.$store.state.artists_sort
},
set(value) {
this.$store.commit(types.ARTISTS_SORT, value)
}
}
},
methods: {
scrollToTop: function () {
window.scrollTo({ top: 0, behavior: 'smooth' })
}
}
methods: {}
}
</script>

View File

@ -4,16 +4,14 @@
<content-with-heading>
<template #options>
<index-button-list :index="albums_list.indexList" />
<index-button-list :index="albums.indexList" />
</template>
<template #heading-left>
<p class="title is-4">Audiobooks</p>
<p class="heading">
{{ albums_list.sortedAndFiltered.length }} Audiobooks
</p>
<p class="heading">{{ albums.count }} Audiobooks</p>
</template>
<template #content>
<list-albums :albums="albums_list" />
<list-albums :albums="albums" />
</template>
</content-with-heading>
</div>
@ -25,7 +23,7 @@ import IndexButtonList from '@/components/IndexButtonList.vue'
import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ListAlbums from '@/components/ListAlbums.vue'
import webapi from '@/webapi'
import Albums from '@/lib/Albums'
import { bySortName, GroupByList } from '@/lib/GroupByList'
const dataObject = {
load: function (to) {
@ -33,7 +31,8 @@ const dataObject = {
},
set: function (vm, response) {
vm.albums = response.data
vm.albums = new GroupByList(response.data)
vm.albums.group(bySortName('name_sort'))
}
}
@ -51,7 +50,12 @@ export default {
next((vm) => dataObject.set(vm, response))
})
},
beforeRouteUpdate(to, from, next) {
if (!this.albums.isEmpty()) {
next()
return
}
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
@ -61,16 +65,7 @@ export default {
data() {
return {
albums: { items: [] }
}
},
computed: {
albums_list() {
return new Albums(this.albums.items, {
sort: 'Name',
group: true
})
albums: new GroupByList()
}
},

View File

@ -25,7 +25,7 @@
<p class="heading has-text-centered-mobile">
{{ artist.album_count }} albums
</p>
<list-albums :albums="albums.items" />
<list-albums :albums="albums" />
<modal-dialog-artist
:show="show_artist_details_modal"
:artist="artist"
@ -40,6 +40,7 @@ import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ListAlbums from '@/components/ListAlbums.vue'
import ModalDialogArtist from '@/components/ModalDialogArtist.vue'
import webapi from '@/webapi'
import { GroupByList } from '../lib/GroupByList'
const dataObject = {
load: function (to) {
@ -51,7 +52,7 @@ const dataObject = {
set: function (vm, response) {
vm.artist = response[0].data
vm.albums = response[1].data
vm.albums = new GroupByList(response[1].data)
}
}
@ -64,7 +65,12 @@ export default {
next((vm) => dataObject.set(vm, response))
})
},
beforeRouteUpdate(to, from, next) {
if (!this.albums.isEmpty()) {
next()
return
}
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
@ -75,7 +81,7 @@ export default {
data() {
return {
artist: {},
albums: {},
albums: new GroupByList(),
show_artist_details_modal: false
}

View File

@ -4,17 +4,15 @@
<content-with-heading>
<template #options>
<index-button-list :index="artists_list.indexList" />
<index-button-list :index="artists.indexList" />
</template>
<template #heading-left>
<p class="title is-4">Authors</p>
<p class="heading">
{{ artists_list.sortedAndFiltered.length }} Authors
</p>
<p class="heading">{{ artists.count }} Authors</p>
</template>
<template #heading-right />
<template #content>
<list-artists :artists="artists_list" />
<list-artists :artists="artists" />
</template>
</content-with-heading>
</div>
@ -26,7 +24,7 @@ import TabsAudiobooks from '@/components/TabsAudiobooks.vue'
import IndexButtonList from '@/components/IndexButtonList.vue'
import ListArtists from '@/components/ListArtists.vue'
import webapi from '@/webapi'
import Artists from '@/lib/Artists'
import { bySortName, GroupByList } from '@/lib/GroupByList'
const dataObject = {
load: function (to) {
@ -34,7 +32,7 @@ const dataObject = {
},
set: function (vm, response) {
vm.artists = response.data
vm.artists_list = new GroupByList(response.data)
}
}
@ -52,7 +50,12 @@ export default {
next((vm) => dataObject.set(vm, response))
})
},
beforeRouteUpdate(to, from, next) {
if (!this.artists_list.isEmpty()) {
next()
return
}
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
@ -62,16 +65,17 @@ export default {
data() {
return {
artists: { items: [] }
artists_list: new GroupByList()
}
},
computed: {
artists_list() {
return new Artists(this.artists.items, {
sort: 'Name',
group: true
})
artists() {
if (!this.artists_list) {
return []
}
this.artists_list.group(bySortName('name_sort'))
return this.artists_list
}
},

View File

@ -9,7 +9,7 @@
<p class="heading">albums</p>
</template>
<template #content>
<list-albums :albums="recently_added.items" />
<list-albums :albums="recently_added" />
</template>
<template #footer>
<nav class="level">
@ -54,6 +54,7 @@ import TabsMusic from '@/components/TabsMusic.vue'
import ListAlbums from '@/components/ListAlbums.vue'
import ListTracks from '@/components/ListTracks.vue'
import webapi from '@/webapi'
import { GroupByList } from '@/lib/GroupByList'
const dataObject = {
load: function (to) {
@ -74,7 +75,7 @@ const dataObject = {
},
set: function (vm, response) {
vm.recently_added = response[0].data.albums
vm.recently_added = new GroupByList(response[0].data.albums)
vm.recently_played = response[1].data.tracks
}
}
@ -88,6 +89,7 @@ export default {
next((vm) => dataObject.set(vm, response))
})
},
beforeRouteUpdate(to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
@ -98,7 +100,7 @@ export default {
data() {
return {
recently_added: { items: [] },
recently_added: [],
recently_played: { items: [] },
show_track_details_modal: false,

View File

@ -8,7 +8,7 @@
<p class="heading">albums</p>
</template>
<template #content>
<list-albums :albums="albums_list" />
<list-albums :albums="recently_added" />
</template>
</content-with-heading>
</div>
@ -20,7 +20,7 @@ import TabsMusic from '@/components/TabsMusic.vue'
import ListAlbums from '@/components/ListAlbums.vue'
import webapi from '@/webapi'
import store from '@/store'
import Albums from '@/lib/Albums'
import { byDateSinceToday, GroupByList } from '@/lib/GroupByList'
const dataObject = {
load: function (to) {
@ -34,7 +34,13 @@ const dataObject = {
},
set: function (vm, response) {
vm.recently_added = response.data.albums
vm.recently_added = new GroupByList(response.data.albums)
vm.recently_added.group(
byDateSinceToday('time_added', {
direction: 'desc',
defaultValue: '0000'
})
)
}
}
@ -47,7 +53,12 @@ export default {
next((vm) => dataObject.set(vm, response))
})
},
beforeRouteUpdate(to, from, next) {
if (!this.recently_added.isEmpty()) {
next()
return
}
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
@ -57,18 +68,7 @@ export default {
data() {
return {
recently_added: { items: [] }
}
},
computed: {
albums_list() {
return new Albums(this.recently_added.items, {
hideSingles: false,
hideSpotify: false,
sort: 'Recently added (browse)',
group: true
})
recently_added: new GroupByList()
}
}
}

View File

@ -1,9 +1,6 @@
<template>
<div>
<content-with-heading>
<template #options>
<index-button-list :index="index_list" />
</template>
<template #heading-left>
<p class="title is-4">
{{ name }}
@ -27,28 +24,11 @@
</template>
<template #content>
<p class="heading has-text-centered-mobile">
{{ composer_albums.total }} albums |
{{ albums_list.total }} albums |
<a class="has-text-link" @click="open_tracks">tracks</a>
</p>
<list-item-albums
v-for="album in composer_albums.items"
:key="album.id"
:album="album"
@click="open_album(album)"
>
<template #actions>
<a @click="open_dialog(album)">
<span class="icon has-text-dark"
><i class="mdi mdi-dots-vertical mdi-18px"
/></span>
</a>
</template>
</list-item-albums>
<modal-dialog-album
:show="show_details_modal"
:album="selected_album"
@close="show_details_modal = false"
/>
<list-albums :albums="albums_list" :hide_group_title="true" />
<modal-dialog-composer
:show="show_composer_details_modal"
:composer="{ name: name }"
@ -61,10 +41,10 @@
<script>
import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ListItemAlbums from '@/components/ListItemAlbum.vue'
import ModalDialogAlbum from '@/components/ModalDialogAlbum.vue'
import ListAlbums from '@/components/ListAlbums.vue'
import ModalDialogComposer from '@/components/ModalDialogComposer.vue'
import webapi from '@/webapi'
import { GroupByList } from '@/lib/GroupByList'
const dataObject = {
load: function (to) {
@ -73,7 +53,7 @@ const dataObject = {
set: function (vm, response) {
vm.name = vm.$route.params.composer
vm.composer_albums = response.data.albums
vm.albums_list = new GroupByList(response.data.albums)
}
}
@ -81,8 +61,7 @@ export default {
name: 'PageComposer',
components: {
ContentWithHeading,
ListItemAlbums,
ModalDialogAlbum,
ListAlbums,
ModalDialogComposer
},
@ -102,29 +81,13 @@ export default {
data() {
return {
name: '',
composer_albums: { items: [] },
show_details_modal: false,
selected_album: {},
albums_list: new GroupByList(),
show_composer_details_modal: false
}
},
computed: {
index_list() {
return [
...new Set(
this.composer_albums.items.map((album) =>
album.name_sort.charAt(0).toUpperCase()
)
)
]
}
},
methods: {
open_tracks: function () {
this.show_details_modal = false
this.$router.push({
name: 'ComposerTracks',
params: { composer: this.name }
@ -136,15 +99,6 @@ export default {
'composer is "' + this.name + '" and media_kind is music',
true
)
},
open_album: function (album) {
this.$router.push({ path: '/music/albums/' + album.id })
},
open_dialog: function (album) {
this.selected_album = album
this.show_details_modal = true
}
}
}

View File

@ -1,9 +1,6 @@
<template>
<div>
<content-with-heading>
<template #options>
<index-button-list :index="index_list" />
</template>
<template #heading-left>
<p class="title is-4">
{{ composer }}
@ -30,25 +27,7 @@
<a class="has-text-link" @click="open_albums">albums</a> |
{{ tracks.total }} tracks
</p>
<list-item-track
v-for="(track, index) in rated_tracks"
:key="track.id"
:track="track"
@click="play_track(index)"
>
<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>
<modal-dialog-track
:show="show_details_modal"
:track="selected_track"
@close="show_details_modal = false"
/>
<list-tracks :tracks="tracks.items" :expression="play_expression" />
<modal-dialog-composer
:show="show_composer_details_modal"
:composer="{ name: composer }"
@ -61,8 +40,7 @@
<script>
import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ListItemTrack from '@/components/ListItemTrack.vue'
import ModalDialogTrack from '@/components/ModalDialogTrack.vue'
import ListTracks from '@/components/ListTracks.vue'
import ModalDialogComposer from '@/components/ModalDialogComposer.vue'
import webapi from '@/webapi'
@ -81,8 +59,7 @@ export default {
name: 'PageComposerTracks',
components: {
ContentWithHeading,
ListItemTrack,
ModalDialogTrack,
ListTracks,
ModalDialogComposer
},
@ -104,30 +81,13 @@ export default {
tracks: { items: [] },
composer: '',
min_rating: 0,
show_details_modal: false,
selected_track: {},
show_composer_details_modal: false
}
},
computed: {
index_list() {
return [
...new Set(
this.tracks.items.map((track) =>
track.title_sort.charAt(0).toUpperCase()
)
)
]
},
rated_tracks() {
return this.tracks.items.filter(
(track) => track.rating >= this.min_rating
)
play_expression() {
return 'composer is "' + this.composer + '" and media_kind is music'
}
},
@ -141,30 +101,7 @@ export default {
},
play: function () {
webapi.player_play_expression(
'composer is "' + this.composer + '" and media_kind is music',
true
)
},
play_track: function (position) {
webapi.player_play_expression(
'composer is "' + this.composer + '" and media_kind is music',
false,
position
)
},
show_rating: function (rating) {
if (rating === 0.5) {
rating = 0
}
this.min_rating = Math.ceil(rating) * 20
},
open_dialog: function (track) {
this.selected_track = track
this.show_details_modal = true
webapi.player_play_expression(this.play_expression, true)
}
}
}

View File

@ -4,16 +4,14 @@
<content-with-heading>
<template #options>
<index-button-list :index="composers_list.indexList" />
<index-button-list :index="composers.indexList" />
</template>
<template #heading-left>
<p class="title is-4">
{{ heading }}
</p>
<p class="title is-4">Composers</p>
<p class="heading">{{ composers.total }} composers</p>
</template>
<template #content>
<list-composers :composers="composers_list" />
<list-composers :composers="composers" />
</template>
</content-with-heading>
</div>
@ -25,7 +23,7 @@ import TabsMusic from '@/components/TabsMusic.vue'
import IndexButtonList from '@/components/IndexButtonList.vue'
import ListComposers from '@/components/ListComposers.vue'
import webapi from '@/webapi'
import Composers from '@/lib/Composers'
import { byName, GroupByList } from '@/lib/GroupByList'
const dataObject = {
load: function (to) {
@ -33,13 +31,8 @@ const dataObject = {
},
set: function (vm, response) {
if (response.data.composers) {
vm.composers = response.data.composers
vm.heading = vm.$route.params.genre
} else {
vm.composers = response.data
vm.heading = 'Composers'
}
vm.composers = new GroupByList(response.data)
vm.composers.group(byName('name_sort'))
}
}
@ -52,7 +45,12 @@ export default {
next((vm) => dataObject.set(vm, response))
})
},
beforeRouteUpdate(to, from, next) {
if (!this.composers.isEmpty()) {
next()
return
}
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
@ -62,46 +60,11 @@ export default {
data() {
return {
composers: { items: [] },
heading: '',
show_details_modal: false,
selected_composer: {}
composers: new GroupByList()
}
},
computed: {
index_list() {
return [
...new Set(
this.composers.items.map((composer) =>
composer.name.charAt(0).toUpperCase()
)
)
]
},
composers_list() {
return new Composers(this.composers.items, {
sort: 'Name',
group: true
})
}
},
methods: {
open_composer: function (composer) {
this.$router.push({
name: 'ComposerAlbums',
params: { composer: composer.name }
})
},
open_dialog: function (composer) {
this.selected_composer = composer
this.show_details_modal = true
}
}
methods: {}
}
</script>

View File

@ -24,94 +24,21 @@
</div>
</template>
<template #content>
<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>
<list-directories :directories="files.directories" />
<list-item-directory
v-for="directory in files.directories"
:key="directory.path"
:directory="directory"
@click="open_directory(directory)"
>
<template #actions>
<a @click="open_directory_dialog(directory)">
<span class="icon has-text-dark"
><i class="mdi mdi-dots-vertical mdi-18px"
/></span>
</a>
</template>
</list-item-directory>
<list-playlists :playlists="files.playlists.items" />
<list-item-playlist
v-for="playlist in files.playlists.items"
:key="playlist.id"
:playlist="playlist"
@click="open_playlist(playlist)"
>
<template #icon>
<span class="icon">
<i class="mdi mdi-library-music" />
</span>
</template>
<template #actions>
<a @click="open_playlist_dialog(playlist)">
<span class="icon has-text-dark"
><i class="mdi mdi-dots-vertical mdi-18px"
/></span>
</a>
</template>
</list-item-playlist>
<list-item-track
v-for="(track, index) in files.tracks.items"
:key="track.id"
:track="track"
@click="play_track(index)"
>
<template #icon>
<span class="icon">
<i class="mdi mdi-file-outline" />
</span>
</template>
<template #actions>
<a @click="open_track_dialog(track)">
<span class="icon has-text-dark"
><i class="mdi mdi-dots-vertical mdi-18px"
/></span>
</a>
</template>
</list-item-track>
<list-tracks
:tracks="files.tracks.items"
:expression="play_expression"
:show_icon="true"
/>
<modal-dialog-directory
:show="show_directory_details_modal"
:directory="selected_directory"
@close="show_directory_details_modal = false"
/>
<modal-dialog-playlist
:show="show_playlist_details_modal"
:playlist="selected_playlist"
@close="show_playlist_details_modal = false"
/>
<modal-dialog-track
:show="show_track_details_modal"
:track="selected_track"
@close="show_track_details_modal = false"
/>
</template>
</content-with-heading>
</div>
@ -119,12 +46,9 @@
<script>
import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ListItemDirectory from '@/components/ListItemDirectory.vue'
import ListItemPlaylist from '@/components/ListItemPlaylist.vue'
import ListItemTrack from '@/components/ListItemTrack.vue'
import ModalDialogDirectory from '@/components/ModalDialogDirectory.vue'
import ModalDialogPlaylist from '@/components/ModalDialogPlaylist.vue'
import ModalDialogTrack from '@/components/ModalDialogTrack.vue'
import ListDirectories from '@/components/ListDirectories.vue'
import ListPlaylists from '@/components/ListPlaylists.vue'
import ListTracks from '@/components/ListTracks.vue'
import webapi from '@/webapi'
const dataObject = {
@ -154,12 +78,9 @@ export default {
name: 'PageFiles',
components: {
ContentWithHeading,
ListItemDirectory,
ListItemPlaylist,
ListItemTrack,
ModalDialogDirectory,
ModalDialogPlaylist,
ModalDialogTrack
ListDirectories,
ListPlaylists,
ListTracks
},
beforeRouteEnter(to, from, next) {
@ -181,16 +102,7 @@ export default {
directories: [],
tracks: { items: [] },
playlists: { items: [] }
},
show_directory_details_modal: false,
selected_directory: {},
show_playlist_details_modal: false,
selected_playlist: {},
show_track_details_modal: false,
selected_track: {}
}
}
},
@ -200,72 +112,18 @@ export default {
return this.$route.query.directory
}
return '/'
},
play_expression() {
return (
'path starts with "' + this.current_directory + '" order by path asc'
)
}
},
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_directory_dialog: function (directory) {
this.selected_directory = directory
this.show_directory_details_modal = true
},
play: function () {
webapi.player_play_expression(
'path starts with "' + this.current_directory + '" order by path asc',
false
)
},
play_track: function (position) {
webapi.player_play_uri(
this.files.tracks.items.map((a) => a.uri).join(','),
false,
position
)
},
open_track_dialog: function (track) {
this.selected_track = track
this.show_track_details_modal = true
},
open_playlist: function (playlist) {
this.$router.push({ path: '/playlists/' + playlist.id + '/tracks' })
},
open_playlist_dialog: function (playlist) {
this.selected_playlist = playlist
this.show_playlist_details_modal = true
webapi.player_play_expression(this.play_expression, false)
}
}
}

View File

@ -2,7 +2,7 @@
<div>
<content-with-heading>
<template #options>
<index-button-list :index="index_list" />
<index-button-list :index="albums_list.indexList" />
</template>
<template #heading-left>
<p class="title is-4">
@ -27,10 +27,10 @@
</template>
<template #content>
<p class="heading has-text-centered-mobile">
{{ genre_albums.total }} albums |
{{ albums_list.total }} albums |
<a class="has-text-link" @click="open_tracks">tracks</a>
</p>
<list-albums :albums="genre_albums.items" />
<list-albums :albums="albums_list" />
<modal-dialog-genre
:show="show_genre_details_modal"
:genre="{ name: name }"
@ -47,6 +47,7 @@ import IndexButtonList from '@/components/IndexButtonList.vue'
import ListAlbums from '@/components/ListAlbums.vue'
import ModalDialogGenre from '@/components/ModalDialogGenre.vue'
import webapi from '@/webapi'
import { bySortName, GroupByList } from '@/lib/GroupByList'
const dataObject = {
load: function (to) {
@ -55,7 +56,8 @@ const dataObject = {
set: function (vm, response) {
vm.name = vm.$route.params.genre
vm.genre_albums = response.data.albums
vm.albums_list = new GroupByList(response.data.albums)
vm.albums_list.group(bySortName('name_sort'))
}
}
@ -74,6 +76,10 @@ export default {
})
},
beforeRouteUpdate(to, from, next) {
if (!this.albums_list.isEmpty()) {
next()
return
}
const vm = this
dataObject.load(to).then((response) => {
dataObject.set(vm, response)
@ -84,24 +90,12 @@ export default {
data() {
return {
name: '',
genre_albums: { items: [] },
albums_list: new GroupByList(),
show_genre_details_modal: false
}
},
computed: {
index_list() {
return [
...new Set(
this.genre_albums.items.map((album) =>
album.name.charAt(0).toUpperCase()
)
)
]
}
},
methods: {
open_tracks: function () {
this.show_details_modal = false
@ -113,11 +107,6 @@ export default {
'genre is "' + this.name + '" and media_kind is music',
true
)
},
open_dialog: function (album) {
this.selected_album = album
this.show_details_modal = true
}
}
}

View File

@ -4,32 +4,14 @@
<content-with-heading>
<template #options>
<index-button-list :index="index_list" />
<index-button-list :index="genres.indexList" />
</template>
<template #heading-left>
<p class="title is-4">Genres</p>
<p class="heading">{{ genres.total }} genres</p>
</template>
<template #content>
<list-item-genre
v-for="genre in genres.items"
:key="genre.name"
:genre="genre"
@click="open_genre(genre)"
>
<template #actions>
<a @click.prevent.stop="open_dialog(genre)">
<span class="icon has-text-dark"
><i class="mdi mdi-dots-vertical mdi-18px"
/></span>
</a>
</template>
</list-item-genre>
<modal-dialog-genre
:show="show_details_modal"
:genre="selected_genre"
@close="show_details_modal = false"
/>
<list-genres :genres="genres" />
</template>
</content-with-heading>
</div>
@ -39,9 +21,9 @@
import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import TabsMusic from '@/components/TabsMusic.vue'
import IndexButtonList from '@/components/IndexButtonList.vue'
import ListItemGenre from '@/components/ListItemGenre.vue'
import ModalDialogGenre from '@/components/ModalDialogGenre.vue'
import ListGenres from '@/components/ListGenres.vue'
import webapi from '@/webapi'
import { byName, GroupByList } from '@/lib/GroupByList'
const dataObject = {
load: function (to) {
@ -50,6 +32,8 @@ const dataObject = {
set: function (vm, response) {
vm.genres = response.data
vm.genres = new GroupByList(response.data)
vm.genres.group(byName('name_sort'))
}
}
@ -59,8 +43,7 @@ export default {
ContentWithHeading,
TabsMusic,
IndexButtonList,
ListItemGenre,
ModalDialogGenre
ListGenres
},
beforeRouteEnter(to, from, next) {
@ -78,33 +61,13 @@ export default {
data() {
return {
genres: { items: [] },
show_details_modal: false,
selected_genre: {}
genres: new GroupByList()
}
},
computed: {
index_list() {
return [
...new Set(
this.genres.items.map((genre) => genre.name.charAt(0).toUpperCase())
)
]
}
},
computed: {},
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
}
}
methods: {}
}
</script>

View File

@ -27,27 +27,9 @@
<p class="heading has-text-centered-mobile">
{{ album.track_count }} tracks
</p>
<list-item-track
v-for="track in tracks"
:key="track.id"
:track="track"
@click="play_track(track)"
>
<template #progress>
<progress-bar :max="track.length_ms" :value="track.seek_ms" />
</template>
<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>
<modal-dialog-track
:show="show_details_modal"
:track="selected_track"
@close="show_details_modal = false"
<list-tracks
:tracks="tracks"
:show_progress="true"
@play-count-changed="reload_tracks"
/>
<modal-dialog-album
@ -81,11 +63,9 @@
<script>
import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ListItemTrack from '@/components/ListItemTrack.vue'
import ModalDialogTrack from '@/components/ModalDialogTrack.vue'
import ListTracks from '@/components/ListTracks.vue'
import ModalDialogAlbum from '@/components/ModalDialogAlbum.vue'
import ModalDialog from '@/components/ModalDialog.vue'
import ProgressBar from '@/components/ProgressBar.vue'
import webapi from '@/webapi'
const dataObject = {
@ -106,11 +86,9 @@ export default {
name: 'PagePodcast',
components: {
ContentWithHeading,
ListItemTrack,
ModalDialogTrack,
ListTracks,
ModalDialogAlbum,
ModalDialog,
ProgressBar
ModalDialog
},
beforeRouteEnter(to, from, next) {
@ -131,9 +109,6 @@ export default {
album: {},
tracks: [],
show_details_modal: false,
selected_track: {},
show_album_details_modal: false,
show_remove_podcast_modal: false,
@ -152,15 +127,6 @@ export default {
webapi.player_play_uri(this.album.uri, false)
},
play_track: function (track) {
webapi.player_play_uri(track.uri, false)
},
open_dialog: function (track) {
this.selected_track = track
this.show_details_modal = true
},
open_remove_podcast_dialog: function () {
this.show_album_details_modal = false
webapi.library_track_playlists(this.tracks[0].id).then(({ data }) => {

View File

@ -15,27 +15,9 @@
</div>
</template>
<template #content>
<list-item-track
v-for="track in new_episodes.items"
:key="track.id"
:track="track"
@click="play_track(track)"
>
<template #progress>
<progress-bar :max="track.length_ms" :value="track.seek_ms" />
</template>
<template #actions>
<a @click="open_track_dialog(track)">
<span class="icon has-text-dark"
><i class="mdi mdi-dots-vertical mdi-18px"
/></span>
</a>
</template>
</list-item-track>
<modal-dialog-track
:show="show_track_details_modal"
:track="selected_track"
@close="show_track_details_modal = false"
<list-tracks
:tracks="new_episodes.items"
:show_progress="true"
@play-count-changed="reload_new_episodes"
/>
</template>
@ -64,7 +46,7 @@
</template>
<template #content>
<list-albums
:albums="albums.items"
:albums="albums"
@play-count-changed="reload_new_episodes()"
@podcast-deleted="reload_podcasts()"
/>
@ -80,13 +62,12 @@
<script>
import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ListItemTrack from '@/components/ListItemTrack.vue'
import ListTracks from '@/components/ListTracks.vue'
import ListAlbums from '@/components/ListAlbums.vue'
import ModalDialogTrack from '@/components/ModalDialogTrack.vue'
import ModalDialogAddRss from '@/components/ModalDialogAddRss.vue'
import ProgressBar from '@/components/ProgressBar.vue'
import * as types from '@/store/mutation_types'
import webapi from '@/webapi'
import { GroupByList } from '@/lib/GroupByList'
const dataObject = {
load: function (to) {
@ -97,7 +78,7 @@ const dataObject = {
},
set: function (vm, response) {
vm.albums = response[0].data
vm.albums = new GroupByList(response[0].data)
vm.new_episodes = response[1].data.tracks
}
}
@ -106,11 +87,9 @@ export default {
name: 'PagePodcasts',
components: {
ContentWithHeading,
ListItemTrack,
ListTracks,
ListAlbums,
ModalDialogTrack,
ModalDialogAddRss,
ProgressBar
ModalDialogAddRss
},
beforeRouteEnter(to, from, next) {
@ -118,6 +97,7 @@ export default {
next((vm) => dataObject.set(vm, response))
})
},
beforeRouteUpdate(to, from, next) {
const vm = this
dataObject.load(to).then((response) => {
@ -128,13 +108,10 @@ export default {
data() {
return {
albums: { items: [] },
albums: [],
new_episodes: { items: [] },
show_url_modal: false,
show_track_details_modal: false,
selected_track: {}
show_url_modal: false
}
},
@ -145,15 +122,6 @@ export default {
},
methods: {
play_track: function (track) {
webapi.player_play_uri(track.uri, false)
},
open_track_dialog: function (track) {
this.selected_track = track
this.show_track_details_modal = true
},
mark_all_played: function () {
this.new_episodes.items.forEach((ep) => {
webapi.library_track_update(ep.id, { play_count: 'increment' })
@ -173,7 +141,7 @@ export default {
reload_podcasts: function () {
webapi.library_albums('podcast').then(({ data }) => {
this.albums = data
this.albums = new GroupByList(data)
this.reload_new_episodes()
})
},

View File

@ -79,7 +79,7 @@
<p class="title is-4">Artists</p>
</template>
<template #content>
<list-artists :artists="artists.items" />
<list-artists :artists="artists" :hide_group_title="true" />
</template>
<template #footer>
<nav v-if="show_all_artists_button" class="level">
@ -105,7 +105,7 @@
<p class="title is-4">Albums</p>
</template>
<template #content>
<list-albums :albums="albums.items" />
<list-albums :albums="albums" :hide_group_title="true" />
</template>
<template #footer>
<nav v-if="show_all_albums_button" class="level">
@ -183,7 +183,7 @@
<p class="title is-4">Podcasts</p>
</template>
<template #content>
<list-albums :albums="podcasts.items" />
<list-albums :albums="podcasts" />
</template>
<template #footer>
<nav v-if="show_all_podcasts_button" class="level">
@ -209,7 +209,7 @@
<p class="title is-4">Audiobooks</p>
</template>
<template #content>
<list-albums :albums="audiobooks.items" />
<list-albums :albums="audiobooks" />
</template>
<template #footer>
<nav v-if="show_all_audiobooks_button" class="level">
@ -242,6 +242,7 @@ import ListComposers from '@/components/ListComposers.vue'
import ListPlaylists from '@/components/ListPlaylists.vue'
import webapi from '@/webapi'
import * as types from '@/store/mutation_types'
import { GroupByList } from '@/lib/GroupByList'
export default {
name: 'PageSearch',
@ -261,12 +262,12 @@ export default {
search_query: '',
tracks: { items: [], total: 0 },
artists: { items: [], total: 0 },
albums: { items: [], total: 0 },
artists: new GroupByList(),
albums: new GroupByList(),
composers: { items: [], total: 0 },
playlists: { items: [], total: 0 },
audiobooks: { items: [], total: 0 },
podcasts: { items: [], total: 0 }
audiobooks: new GroupByList(),
podcasts: new GroupByList()
}
},
@ -393,8 +394,8 @@ export default {
webapi.search(searchParams).then(({ data }) => {
this.tracks = data.tracks ? data.tracks : { items: [], total: 0 }
this.artists = data.artists ? data.artists : { items: [], total: 0 }
this.albums = data.albums ? data.albums : { items: [], total: 0 }
this.artists = new GroupByList(data.artists)
this.albums = new GroupByList(data.albums)
this.composers = data.composers
? data.composers
: { items: [], total: 0 }
@ -431,7 +432,7 @@ export default {
}
webapi.search(searchParams).then(({ data }) => {
this.audiobooks = data.albums ? data.albums : { items: [], total: 0 }
this.audiobooks = new GroupByList(data.albums)
})
},
@ -462,7 +463,7 @@ export default {
}
webapi.search(searchParams).then(({ data }) => {
this.podcasts = data.albums ? data.albums : { items: [], total: 0 }
this.podcasts = new GroupByList(data.albums)
})
},

View File

@ -321,7 +321,7 @@ export const router = createRouter({
if (to.meta.has_tabs) {
resolve({ el: '#top', top: 140 })
} else {
resolve({ el: '#top', top: 100 })
resolve({ el: '#top', top: 110 })
}
}, wait_ms)
})

View File

@ -90,7 +90,7 @@ export default {
if (this.$route.meta.has_tabs) {
this.$scrollTo('#top', { offset: -140 })
} else {
this.$scrollTo('#top', { offset: -100 })
this.$scrollTo('#top', { offset: -110 })
}
},