mirror of
https://github.com/owntone/owntone-server.git
synced 2025-11-11 14:30:20 -05:00
[web-src] add composer support
This commit is contained in:
101
web-src/src/pages/PageComposer.vue
Normal file
101
web-src/src/pages/PageComposer.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<div>
|
||||
<tabs-music></tabs-music>
|
||||
|
||||
<content-with-heading>
|
||||
<template slot="heading-left">
|
||||
<p class="title is-4">{{ name }}</p>
|
||||
</template>
|
||||
<template slot="heading-right">
|
||||
<div class="buttons is-centered">
|
||||
<a class="button is-small is-light is-rounded" @click="show_composer_details_modal = true">
|
||||
<span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span>
|
||||
</a>
|
||||
<a class="button is-small is-dark is-rounded" @click="play">
|
||||
<span class="icon"><i class="mdi mdi-shuffle"></i></span> <span>Shuffle</span>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
<template slot="content">
|
||||
<p class="heading has-text-centered-mobile">{{ composer_albums.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 slot="actions">
|
||||
<a @click="open_dialog(album)">
|
||||
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
|
||||
</a>
|
||||
</template>
|
||||
</list-item-albums>
|
||||
<modal-dialog-album :show="show_details_modal" :album="selected_album" @close="show_details_modal = false" />
|
||||
<modal-dialog-composer :show="show_composer_details_modal" :composer="{ 'name': name }" @close="show_composer_details_modal = false" />
|
||||
</template>
|
||||
</content-with-heading>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { LoadDataBeforeEnterMixin } from './mixin'
|
||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||
import TabsMusic from '@/components/TabsMusic'
|
||||
import ListItemAlbums from '@/components/ListItemAlbum'
|
||||
import ModalDialogAlbum from '@/components/ModalDialogAlbum'
|
||||
import ModalDialogComposer from '@/components/ModalDialogComposer'
|
||||
import webapi from '@/webapi'
|
||||
|
||||
const composerData = {
|
||||
load: function (to) {
|
||||
return webapi.library_composer(to.params.composer, 'albums')
|
||||
},
|
||||
|
||||
set: function (vm, response) {
|
||||
vm.name = vm.$route.params.composer
|
||||
vm.composer_albums = response.data.albums
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'PageComposer',
|
||||
mixins: [LoadDataBeforeEnterMixin(composerData)],
|
||||
components: { ContentWithHeading, TabsMusic, ListItemAlbums, ModalDialogAlbum, ModalDialogComposer },
|
||||
|
||||
data () {
|
||||
return {
|
||||
name: '',
|
||||
composer_albums: { items: [] },
|
||||
show_details_modal: false,
|
||||
selected_album: {},
|
||||
|
||||
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 } })
|
||||
},
|
||||
|
||||
play: function () {
|
||||
webapi.player_play_expression('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
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
116
web-src/src/pages/PageComposerTracks.vue
Normal file
116
web-src/src/pages/PageComposerTracks.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<div>
|
||||
<tabs-music></tabs-music>
|
||||
|
||||
<content-with-heading>
|
||||
<template slot="heading-left">
|
||||
<p class="title is-4">{{ composer }}</p>
|
||||
<p class="heading">{{ tracks.total }} tracks</p>
|
||||
</template>
|
||||
<template slot="heading-right">
|
||||
<div class="buttons is-centered">
|
||||
<a class="button is-small is-light is-rounded" @click="show_composer_details_modal = true">
|
||||
<span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span>
|
||||
</a>
|
||||
<a class="button is-small is-dark is-rounded" @click="play">
|
||||
<span class="icon"><i class="mdi mdi-shuffle"></i></span> <span>Shuffle</span>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
<template slot="content">
|
||||
<p class="heading has-text-centered-mobile"><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 slot="actions">
|
||||
<a @click="open_dialog(track)">
|
||||
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
|
||||
</a>
|
||||
</template>
|
||||
</list-item-track>
|
||||
<modal-dialog-track :show="show_details_modal" :track="selected_track" @close="show_details_modal = false" />
|
||||
<modal-dialog-composer :show="show_composer_details_modal" :composer="{ 'name': composer }" @close="show_composer_details_modal = false" />
|
||||
</template>
|
||||
</content-with-heading>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { LoadDataBeforeEnterMixin } from './mixin'
|
||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||
import TabsMusic from '@/components/TabsMusic'
|
||||
import ListItemTrack from '@/components/ListItemTrack'
|
||||
import ModalDialogTrack from '@/components/ModalDialogTrack'
|
||||
import ModalDialogComposer from '@/components/ModalDialogComposer'
|
||||
import webapi from '@/webapi'
|
||||
|
||||
const tracksData = {
|
||||
load: function (to) {
|
||||
return webapi.library_composer(to.params.composer, 'tracks')
|
||||
},
|
||||
|
||||
set: function (vm, response) {
|
||||
vm.composer = vm.$route.params.composer
|
||||
vm.tracks = response.data.tracks
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'PageComposerTracks',
|
||||
mixins: [LoadDataBeforeEnterMixin(tracksData)],
|
||||
components: { ContentWithHeading, TabsMusic, ListItemTrack, ModalDialogTrack, ModalDialogComposer },
|
||||
|
||||
data () {
|
||||
return {
|
||||
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)
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
open_albums: function () {
|
||||
this.show_details_modal = false
|
||||
this.$router.push({ name: 'ComposerAlbums', params: { composer: this.composer } })
|
||||
},
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
88
web-src/src/pages/PageComposers.vue
Normal file
88
web-src/src/pages/PageComposers.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<div>
|
||||
<tabs-music></tabs-music>
|
||||
|
||||
<content-with-heading>
|
||||
<template slot="heading-left">
|
||||
<p class="title is-4">{{ heading }}</p>
|
||||
<p class="heading">{{ composers.total }} composers</p>
|
||||
</template>
|
||||
<template slot="content">
|
||||
<list-item-composer v-for="composer in composers.items" :key="composer.name" :composer="composer" @click="open_composer(composer)">
|
||||
<template slot="actions">
|
||||
<a @click="open_dialog(composer)">
|
||||
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
|
||||
</a>
|
||||
</template>
|
||||
</list-item-composer>
|
||||
<modal-dialog-composer :show="show_details_modal" :composer="selected_composer" @close="show_details_modal = false" />
|
||||
</template>
|
||||
</content-with-heading>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { LoadDataBeforeEnterMixin } from './mixin'
|
||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||
import TabsMusic from '@/components/TabsMusic'
|
||||
import ListItemComposer from '@/components/ListItemComposer'
|
||||
import ModalDialogComposer from '@/components/ModalDialogComposer'
|
||||
import webapi from '@/webapi'
|
||||
|
||||
const composersData = {
|
||||
load: function (to) {
|
||||
if (to.params.genre) {
|
||||
return webapi.library_genre_composers(to.params.genre)
|
||||
} else {
|
||||
return webapi.library_composers()
|
||||
}
|
||||
},
|
||||
|
||||
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'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'PageComposers',
|
||||
mixins: [LoadDataBeforeEnterMixin(composersData)],
|
||||
components: { ContentWithHeading, TabsMusic, ListItemComposer, ModalDialogComposer },
|
||||
|
||||
data () {
|
||||
return {
|
||||
composers: { items: [] },
|
||||
heading: '',
|
||||
|
||||
show_details_modal: false,
|
||||
selected_composer: {}
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
index_list () {
|
||||
return [...new Set(this.composers.items
|
||||
.map(composer => composer.name.charAt(0).toUpperCase()))]
|
||||
}
|
||||
},
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -94,6 +94,28 @@
|
||||
</template>
|
||||
</content-text>
|
||||
|
||||
<!-- Composers -->
|
||||
<content-with-heading v-if="show_composers && composers.total">
|
||||
<template slot="heading-left">
|
||||
<p class="title is-4">Composers</p>
|
||||
</template>
|
||||
<template slot="content">
|
||||
<list-composers :composers="composers.items"></list-composers>
|
||||
</template>
|
||||
<template slot="footer">
|
||||
<nav v-if="show_all_composers_button" class="level">
|
||||
<p class="level-item">
|
||||
<a class="button is-light is-small is-rounded" v-on:click="open_search_composers">Show all {{ composers.total }} composers</a>
|
||||
</p>
|
||||
</nav>
|
||||
</template>
|
||||
</content-with-heading>
|
||||
<content-text v-if="show_composers && !composers.total">
|
||||
<template slot="content">
|
||||
<p><i>No composers found</i></p>
|
||||
</template>
|
||||
</content-text>
|
||||
|
||||
<!-- Playlists -->
|
||||
<content-with-heading v-if="show_playlists && playlists.total">
|
||||
<template slot="heading-left">
|
||||
@@ -169,13 +191,14 @@ import TabsSearch from '@/components/TabsSearch'
|
||||
import ListTracks from '@/components/ListTracks'
|
||||
import ListArtists from '@/components/ListArtists'
|
||||
import ListAlbums from '@/components/ListAlbums'
|
||||
import ListComposers from '@/components/ListComposers'
|
||||
import ListPlaylists from '@/components/ListPlaylists'
|
||||
import webapi from '@/webapi'
|
||||
import * as types from '@/store/mutation_types'
|
||||
|
||||
export default {
|
||||
name: 'PageSearch',
|
||||
components: { ContentWithHeading, ContentText, TabsSearch, ListTracks, ListArtists, ListAlbums, ListPlaylists },
|
||||
components: { ContentWithHeading, ContentText, TabsSearch, ListTracks, ListArtists, ListAlbums, ListPlaylists, ListComposers },
|
||||
|
||||
data () {
|
||||
return {
|
||||
@@ -184,6 +207,7 @@ export default {
|
||||
tracks: { items: [], total: 0 },
|
||||
artists: { items: [], total: 0 },
|
||||
albums: { items: [], total: 0 },
|
||||
composers: { items: [], total: 0 },
|
||||
playlists: { items: [], total: 0 },
|
||||
audiobooks: { items: [], total: 0 },
|
||||
podcasts: { items: [], total: 0 }
|
||||
@@ -216,6 +240,13 @@ export default {
|
||||
return this.albums.total > this.albums.items.length
|
||||
},
|
||||
|
||||
show_composers () {
|
||||
return this.$route.query.type && this.$route.query.type.includes('composer')
|
||||
},
|
||||
show_all_composers_button () {
|
||||
return this.composers.total > this.composers.items.length
|
||||
},
|
||||
|
||||
show_playlists () {
|
||||
return this.$route.query.type && this.$route.query.type.includes('playlist')
|
||||
},
|
||||
@@ -282,6 +313,7 @@ export default {
|
||||
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.composers = data.composers ? data.composers : { items: [], total: 0 }
|
||||
this.playlists = data.playlists ? data.playlists : { items: [], total: 0 }
|
||||
})
|
||||
},
|
||||
@@ -346,7 +378,7 @@ export default {
|
||||
this.$router.push({
|
||||
path: '/search/library',
|
||||
query: {
|
||||
type: 'track,artist,album,playlist,audiobook,podcast',
|
||||
type: 'track,artist,album,playlist,audiobook,podcast,composer',
|
||||
query: this.search_query,
|
||||
limit: 3,
|
||||
offset: 0
|
||||
@@ -385,6 +417,16 @@ export default {
|
||||
})
|
||||
},
|
||||
|
||||
open_search_composers: function () {
|
||||
this.$router.push({
|
||||
path: '/search/library',
|
||||
query: {
|
||||
type: 'tracks',
|
||||
query: this.$route.query.query
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
open_search_playlists: function () {
|
||||
this.$router.push({
|
||||
path: '/search/library',
|
||||
@@ -415,9 +457,42 @@ export default {
|
||||
})
|
||||
},
|
||||
|
||||
open_composer: function (composer) {
|
||||
this.$router.push({ name: 'ComposerAlbums', params: { composer: composer.name } })
|
||||
},
|
||||
|
||||
open_playlist: function (playlist) {
|
||||
this.$router.push({ path: '/playlists/' + playlist.id + '/tracks' })
|
||||
},
|
||||
|
||||
open_recent_search: function (query) {
|
||||
this.search_query = query
|
||||
this.new_search()
|
||||
},
|
||||
|
||||
open_track_dialog: function (track) {
|
||||
this.selected_track = track
|
||||
this.show_track_details_modal = true
|
||||
},
|
||||
|
||||
open_album_dialog: function (album) {
|
||||
this.selected_album = album
|
||||
this.show_album_details_modal = true
|
||||
},
|
||||
|
||||
open_artist_dialog: function (artist) {
|
||||
this.selected_artist = artist
|
||||
this.show_artist_details_modal = true
|
||||
},
|
||||
|
||||
open_composer_dialog: function (composer) {
|
||||
this.selected_composer = composer
|
||||
this.show_composer_details_modal = true
|
||||
},
|
||||
|
||||
open_playlist_dialog: function (playlist) {
|
||||
this.selected_playlist = playlist
|
||||
this.show_playlist_details_modal = true
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user