mirror of
https://github.com/owntone/owntone-server.git
synced 2025-03-03 07:10:08 -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:
parent
a24cb11e17
commit
27e2274d8a
@ -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>
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}, {})
|
||||
}
|
||||
}
|
@ -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
|
||||
}, {})
|
||||
}
|
||||
}
|
@ -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
|
||||
}, {})
|
||||
}
|
||||
}
|
233
web-src/src/lib/GroupByList.js
Normal file
233
web-src/src/lib/GroupByList.js
Normal 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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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 */
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -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
|
||||
},
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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 }) => {
|
||||
|
@ -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()
|
||||
})
|
||||
},
|
||||
|
@ -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)
|
||||
})
|
||||
},
|
||||
|
||||
|
@ -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)
|
||||
})
|
||||
|
@ -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 })
|
||||
}
|
||||
},
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user