diff --git a/front/src/components/audio/artist/Widget.vue b/front/src/components/audio/artist/Widget.vue new file mode 100644 index 0000000000000000000000000000000000000000..b78bcbf943947652e28f0c43c386e1b19a88b4b0 --- /dev/null +++ b/front/src/components/audio/artist/Widget.vue @@ -0,0 +1,166 @@ +<template> + <div class="wrapper"> + <h3 class="ui header"> + <slot name="title"></slot> + <span class="ui tiny circular label">{{ count }}</span> + </h3> + <button v-if="controls" :disabled="!previousPage" @click="fetchData(previousPage)" :class="['ui', {disabled: !previousPage}, 'circular', 'icon', 'basic', 'button']"><i :class="['ui', 'angle left', 'icon']"></i></button> + <button v-if="controls" :disabled="!nextPage" @click="fetchData(nextPage)" :class="['ui', {disabled: !nextPage}, 'circular', 'icon', 'basic', 'button']"><i :class="['ui', 'angle right', 'icon']"></i></button> + <button v-if="controls" @click="fetchData('artists/')" :class="['ui', 'circular', 'icon', 'basic', 'button']"><i :class="['ui', 'refresh', 'icon']"></i></button> + <div class="ui hidden divider"></div> + <div class="ui three cards"> + <div v-if="isLoading" class="ui inverted active dimmer"> + <div class="ui loader"></div> + </div> + <div class="flat inline card" v-for="object in objects" :key="object.id"> + <div :class="['ui', 'image', 'with-overlay', {'default-cover': !getCover(object).original}]" v-lazy:background-image="getImageUrl(object)"> + <play-button class="play-overlay" :icon-only="true" :is-playable="object.is_playable" :button-classes="['ui', 'circular', 'large', 'orange', 'icon', 'button']" :artist="object"></play-button> + </div> + <div class="content"> + <router-link :title="object.name" :to="{name: 'library.artists.detail', params: {id: object.id}}"> + {{ object.name|truncate(30) }} + </router-link> + <div> + <i class="small sound icon"></i> + <translate translate-context="Content/Artist/Card" :translate-params="{count: object.albums.length}" :translate-n="object.albums.length" translate-plural="%{ count } albums">1 album</translate> + </div> + <tags-list label-classes="tiny" :truncate-size="20" :limit="2" :show-more="false" :tags="object.tags"></tags-list> + + <play-button + class="play-button basic icon" + :dropdown-only="true" + :is-playable="object.is_playable" + :dropdown-icon-classes="['ellipsis', 'vertical', 'large', 'grey']" + :artist="object"></play-button> + </div> + </div> + </div> + <div v-if="!isLoading && objects.length === 0">No results matching your query.</div> + </div> +</template> + +<script> +import _ from '@/lodash' +import axios from 'axios' +import PlayButton from '@/components/audio/PlayButton' +import TagsList from "@/components/tags/List" + +export default { + props: { + filters: {type: Object, required: true}, + controls: {type: Boolean, default: true}, + }, + components: { + PlayButton, + TagsList + }, + data () { + return { + objects: [], + limit: 12, + count: 0, + isLoading: false, + errors: null, + previousPage: null, + nextPage: null + } + }, + created () { + this.fetchData('artists/') + }, + methods: { + fetchData (url) { + if (!url) { + return + } + this.isLoading = true + let self = this + let params = _.clone(this.filters) + params.page_size = this.limit + params.offset = this.offset + axios.get(url, {params: params}).then((response) => { + self.previousPage = response.data.previous + self.nextPage = response.data.next + self.isLoading = false + self.objects = response.data.results + self.count = response.data.count + }, error => { + self.isLoading = false + self.errors = error.backendErrors + }) + }, + updateOffset (increment) { + if (increment) { + this.offset += this.limit + } else { + this.offset = Math.max(this.offset - this.limit, 0) + } + }, + getImageUrl (object) { + let url = '../../../assets/audio/default-cover.png' + let cover = this.getCover(object) + if (cover.original) { + url = this.$store.getters['instance/absoluteUrl'](cover.medium_square_crop) + } else { + return null + } + return url + }, + getCover (object) { + return object.albums.map((a) => { + return a.cover + }).filter((c) => { + return !!c + })[0] || {} + } + }, + watch: { + offset () { + this.fetchData() + }, + "$store.state.moderation.lastUpdate": function () { + this.fetchData('objects/') + } + } +} +</script> +<style scoped lang="scss"> +@import "../../../style/vendor/media"; + +.default-cover { + background-image: url("../../../assets/audio/default-cover.png") !important; +} + +.wrapper { + width: 100%; +} +.ui.cards { + justify-content: flex-start; +} +.play-button { + position: absolute; + right: 0; + bottom: 0; +} +.ui.three.cards .card { + width: 25em; +} +.with-overlay { + background-size: cover !important; + background-position: center !important; + height: 8em; + width: 8em; + display: flex !important; + justify-content: center !important; + align-items: center !important; +} +.flat.card .with-overlay.image { + border-radius: 50% !important; + margin: 0 auto; +} +</style> +<style> +.ui.cards .ui.button { + margin-right: 0px; +} +</style> diff --git a/front/src/components/tags/List.vue b/front/src/components/tags/List.vue index 9ffb0f1f080752787e8df137a2f6046c9cec737e..6c280822bd9bc6c5dc1859d288dd4983cbdb1600 100644 --- a/front/src/components/tags/List.vue +++ b/front/src/components/tags/List.vue @@ -1,23 +1,29 @@ <template> - <div> + <div class="tag-list"> <router-link :to="{name: 'library.tags.detail', params: {id: tag}}" - class="ui circular hashtag label" + :class="['ui', 'circular', 'hashtag', 'label', labelClasses]" v-for="tag in toDisplay" + :title="tag" :key="tag"> - #{{ tag }} + #{{ tag|truncate(truncateSize) }} </router-link> - <div role="button" @click.prevent="honorLimit = false" class="ui circular inverted teal label" v-if="toDisplay.length < tags.length"> + <div role="button" @click.prevent="honorLimit = false" class="ui circular inverted teal label" v-if="showMore && toDisplay.length < tags.length"> <translate translate-context="Content/*/Button/Label/Verb" :translate-params="{count: tags.length - toDisplay.length}" :translate-n="tags.length - toDisplay.length" translate-plural="Show %{ count } more tags">Show 1 more tag</translate> </div> </div> </template> <script> export default { - props: ['tags'], + props: { + tags: {type: Array, required: true}, + showMore: {type: Boolean, default: true}, + truncateSize: {type: Number, default: 25}, + limit: {type: Number, default: 5}, + labelClasses: {type: String, default: ''}, + }, data () { return { - limit: 5, honorLimit: true, } }, @@ -36,4 +42,7 @@ export default { padding-left: 1em !important; padding-right: 1em !important; } +.hashtag { + margin: 0.25em; +} </style> diff --git a/front/src/style/_main.scss b/front/src/style/_main.scss index a15f339566e902646e327cb1f09c906a86a80b49..90fdd8936bde3caf3b023127398099a93d46e2b3 100644 --- a/front/src/style/_main.scss +++ b/front/src/style/_main.scss @@ -342,6 +342,20 @@ td.align.right { word-wrap: break-word; } +.ui.cards > .flat.card, .flat.card { + box-shadow: none; + .content { + border: none; + } +} + +.ui.cards > .inline.card { + flex-direction: row; + .content { + padding: 0.5em 0.75em; + } +} + .ui.checkbox label { cursor: pointer; } @@ -355,6 +369,9 @@ input + .help { } +.tag-list { + margin-top: 1em; +} @import "./themes/_light.scss"; @import "./themes/_dark.scss";