wip: Artist page written in composition API in order to improve it and resolve issue 18; style: changed tabs in Artist page

This commit is contained in:
Roberto Tonino 2020-11-14 01:13:15 +01:00
parent 2d7ec9ee6b
commit d04439857a
8 changed files with 245 additions and 212 deletions

File diff suppressed because one or more lines are too long

View File

@ -1,43 +1,36 @@
<template> <template>
<div id="artist_tab" class="relative image-header"> <div class="relative image-header">
<header <header class="flex items-center" :style="headerStyle">
class="flex items-center" <h1 class="m-0">{{ artistName }}</h1>
:style="{
'background-image':
'linear-gradient(to bottom, transparent 0%, var(--main-background) 100%), url(\'' + image + '\')'
}"
>
<h1 class="m-0">{{ title }}</h1>
<div <div
role="button"
aria-label="download"
@click.stop="addToQueue"
:data-link="link"
class="grid w-16 h-16 ml-auto rounded-full cursor-pointer bg-primary text-grayscale-870 place-items-center" class="grid w-16 h-16 ml-auto rounded-full cursor-pointer bg-primary text-grayscale-870 place-items-center"
@click.stop="sendAddToQueue(downloadLink)"
aria-label="download"
role="button"
> >
<i class="text-4xl material-icons" :title="$t('globals.download_hint')">get_app</i> <i class="text-4xl material-icons" :title="$t('globals.download_hint')">get_app</i>
</div> </div>
</header> </header>
<div class="my-4"> <ul class="my-8 section-tabs">
<button <li
v-for="(item, name) in body" v-for="(item, name) in artistReleases"
:key="name" :key="name"
class="mr-2 btn bg-background-main" class="section-tabs__tab"
:class="{ 'btn-primary': name === currentTab }"
:href="'#artist_' + name"
@click="changeTab(name)" @click="changeTab(name)"
:class="{ active: currentTab === name }"
> >
{{ $tc(`globals.listTabs.${name}`, 2) }} {{ $tc(`globals.listTabs.${name}`, 2) }}
</button> </li>
</div> </ul>
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
<th <th
v-for="data in head" v-for="data in head"
:key="data.title"
@click="data.sortKey ? sortBy(data.sortKey) : null" @click="data.sortKey ? sortBy(data.sortKey) : null"
:style="{ width: data.width ? data.width : 'auto' }" :style="{ width: data.width ? data.width : 'auto' }"
:class="{ :class="{
@ -53,22 +46,24 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="release in showTable" :key="release.id"> <tr v-for="release in showTable">
<router-link tag="td" class="flex items-center clickable" :to="{ name: 'Album', params: { id: release.id } }"> <router-link
tag="td"
class="flex items-center clickable"
:to="{ name: 'Album', params: { id: release.releaseID } }"
>
<img <img
class="rounded coverart" class="rounded coverart"
:src="release.cover_small" :src="release.releaseCover"
style="margin-right: 16px; width: 56px; height: 56px" style="margin-right: 16px; width: 56px; height: 56px"
/> />
<i v-if="release.explicit_lyrics" class="material-icons explicit-icon"> explicit </i> <i v-if="release.isReleaseExplicit" class="material-icons explicit-icon">explicit</i>
{{ release.title }} {{ release.releaseTitle }}
<i v-if="checkNewRelease(release.release_date)" class="material-icons" style="color: #ff7300"> <i v-if="checkNewRelease(release.releaseDate)" class="material-icons" style="color: #ff7300">fiber_new</i>
fiber_new
</i>
</router-link> </router-link>
<td>{{ release.release_date }}</td> <td>{{ release.releaseDate }}</td>
<td>{{ release.nb_song }}</td> <td>{{ release.releaseTracksNumber }}</td>
<td @click.stop="addToQueue" :data-link="release.link" class="clickable"> <td @click.stop="sendAddToQueue(release.releaseLink)" class="clickable">
<i class="material-icons" :title="$t('globals.download_hint')"> file_download </i> <i class="material-icons" :title="$t('globals.download_hint')"> file_download </i>
</td> </td>
</tr> </tr>
@ -78,65 +73,99 @@
</template> </template>
<script> <script>
import { isEmpty, orderBy } from 'lodash-es' import { orderBy } from 'lodash-es'
import { socket } from '@/utils/socket' import { socket } from '@/utils/socket'
import Downloads from '@/utils/downloads' import { sendAddToQueue } from '@/utils/downloads'
import EventBus from '@/utils/EventBus' import EventBus from '@/utils/EventBus'
import { formatArtistData } from '@/data/artist'
import { standardizeData } from '@/data/standardize'
import { ref, reactive, computed, onMounted, toRefs, onDeactivated } from '@vue/composition-api'
export default { export default {
data() { setup() {
return { const state = reactive({
currentTab: '', currentTab: '',
sortKey: 'release_date', sortKey: 'releaseDate',
sortOrder: 'desc', sortOrder: 'desc',
title: '', artistReleases: {},
image: '', artistID: '',
type: '', artistName: '',
link: '', artistPicture: ''
head: null, })
body: null
}
},
computed: {
showTable() {
if (this.body) {
if (this.sortKey == 'nb_song')
return orderBy(
this.body[this.currentTab],
function (o) {
return new Number(o.nb_song)
},
this.sortOrder
)
else return orderBy(this.body[this.currentTab], this.sortKey, this.sortOrder)
} else return []
}
},
mounted() {
socket.on('show_artist', this.showArtist)
EventBus.$on('artistTab:updateSelected', this.updateSelected) const currentRelease = computed(() => state.artistReleases[state.currentTab])
EventBus.$on('artistTab:changeTab', this.changeTab)
const setupData = data => {
const {
data: [{ artistID, artistName, artistPictureXL, artistReleases }]
} = standardizeData({ data: [data], hasLoaded: true }, formatArtistData)
Object.assign(state, {
artistID,
artistName,
artistPicture: artistPictureXL,
artistReleases
})
// ? Is it not granted that it's always 'all' ?
state.currentTab = Object.keys(artistReleases)[0]
}
const reset = () => {
state.currentTab = ''
state.sortKey = 'releaseDate'
state.sortOrder = 'desc'
}
onDeactivated(reset)
onMounted(() => {
socket.on('show_artist', setupData)
})
const showTable = computed(() => {
if (Object.keys(state.artistReleases).length !== 0) {
let sortKey = state.sortKey
if (sortKey == 'releaseTracksNumber') {
sortKey = o => new Number(o.releaseTracksNumber)
}
return orderBy(currentRelease.value, sortKey, state.sortOrder)
}
return []
})
return {
...toRefs(state),
downloadLink: computed(() => `https://www.deezer.com/artist/${state.artistID}`),
headerStyle: computed(() => ({
backgroundImage: `linear-gradient(to bottom, transparent 0%, var(--main-background) 100%), url(${state.artistPicture})`
})),
showTable,
sendAddToQueue,
currentRelease
}
},
data() {
const $t = this.$t.bind(this)
const $tc = this.$tc.bind(this)
return {
head: [
{ title: $tc('globals.listTabs.title', 1), sortKey: 'releaseTitle' },
{ title: $t('globals.listTabs.releaseDate'), sortKey: 'releaseDate' },
{ title: $tc('globals.listTabs.track', 2), sortKey: 'releaseTracksNumber' },
{ title: '', width: '32px' }
]
}
}, },
methods: { methods: {
reset() {
this.title = 'Loading...'
this.image = ''
this.type = ''
this.currentTab = ''
this.sortKey = 'release_date'
this.sortOrder = 'desc'
this.link = ''
this.head = []
this.body = null
},
addToQueue(e) {
e.stopPropagation()
Downloads.sendAddToQueue(e.currentTarget.dataset.link)
},
sortBy(key) { sortBy(key) {
if (key == this.sortKey) { if (key === this.sortKey) {
this.sortOrder = this.sortOrder == 'asc' ? 'desc' : 'asc' this.sortOrder = this.sortOrder === 'asc' ? 'desc' : 'asc'
} else { } else {
this.sortKey = key this.sortKey = key
this.sortOrder = 'asc' this.sortOrder = 'asc'
@ -145,9 +174,6 @@ export default {
changeTab(tab) { changeTab(tab) {
this.currentTab = tab this.currentTab = tab
}, },
updateSelected() {
// Last tab opened logic
},
checkNewRelease(date) { checkNewRelease(date) {
let g1 = new Date() let g1 = new Date()
let g2 = new Date(date) let g2 = new Date(date)
@ -155,30 +181,6 @@ export default {
g1.setHours(0, 0, 0, 0) g1.setHours(0, 0, 0, 0)
return g1.getTime() <= g2.getTime() return g1.getTime() <= g2.getTime()
},
showArtist(data) {
this.reset()
const { name, picture_xl, id, releases } = data
this.title = name
this.image = picture_xl
this.type = 'Artist'
this.link = `https://www.deezer.com/artist/${id}`
if (this.currentTab === '') this.currentTab = Object.keys(releases)[0]
this.sortKey = 'release_date'
this.sortOrder = 'desc'
this.head = [
{ title: this.$tc('globals.listTabs.title', 1), sortKey: 'title' },
{ title: this.$t('globals.listTabs.releaseDate'), sortKey: 'release_date' },
{ title: this.$tc('globals.listTabs.track', 2), sortKey: 'nb_song' },
{ title: '', width: '32px' }
]
if (isEmpty(releases)) {
this.body = null
} else {
this.body = releases
}
} }
} }
} }

View File

@ -44,7 +44,8 @@ import { sendAddToQueue } from '@/utils/downloads'
import { numberWithDots, convertDuration } from '@/utils/utils' import { numberWithDots, convertDuration } from '@/utils/utils'
import EventBus from '@/utils/EventBus' import EventBus from '@/utils/EventBus'
import { formatSearchResults, formatSingleTrack, formatAlbums, formatArtist, formatPlaylist } from '@/data/search' import { formatSingleTrack, formatAlbums, formatArtist, formatPlaylist } from '@/data/search'
import { standardizeData } from '@/data/standardize'
const resetObj = { data: [], next: 0, total: 0, hasLoaded: false } const resetObj = { data: [], next: 0, total: 0, hasLoaded: false }
@ -172,7 +173,7 @@ export default {
return this.results.allTab return this.results.allTab
} }
return formatSearchResults(this.results[this.currentTab.viewInfo], this.currentTab.formatFunc) return standardizeData(this.results[this.currentTab.viewInfo], this.currentTab.formatFunc)
}, },
changeSearchTab(tabName) { changeSearchTab(tabName) {
tabName = tabName.toLowerCase() tabName = tabName.toLowerCase()

View File

@ -31,28 +31,28 @@
<ResultsTracks <ResultsTracks
v-else-if="section === 'TRACK'" v-else-if="section === 'TRACK'"
:viewInfo="formatSearchResults(viewInfo.TRACK, formatSingleTrack)" :viewInfo="standardizeData(viewInfo.TRACK, formatSingleTrack)"
:itemsToShow="6" :itemsToShow="6"
@add-to-queue="$emit('add-to-queue', $event)" @add-to-queue="$emit('add-to-queue', $event)"
/> />
<ResultsAlbums <ResultsAlbums
v-else-if="section == 'ALBUM'" v-else-if="section == 'ALBUM'"
:viewInfo="formatSearchResults(viewInfo.ALBUM, formatAlbums)" :viewInfo="standardizeData(viewInfo.ALBUM, formatAlbums)"
:itemsToShow="6" :itemsToShow="6"
@add-to-queue="$emit('add-to-queue', $event)" @add-to-queue="$emit('add-to-queue', $event)"
/> />
<ResultsPlaylists <ResultsPlaylists
v-else-if="section == 'PLAYLIST'" v-else-if="section == 'PLAYLIST'"
:viewInfo="formatSearchResults(viewInfo.PLAYLIST, formatPlaylist)" :viewInfo="standardizeData(viewInfo.PLAYLIST, formatPlaylist)"
:itemsToShow="6" :itemsToShow="6"
@add-to-queue="$emit('add-to-queue', $event)" @add-to-queue="$emit('add-to-queue', $event)"
/> />
<ResultsArtists <ResultsArtists
v-else-if="section === 'ARTIST'" v-else-if="section === 'ARTIST'"
:viewInfo="formatSearchResults(viewInfo.ARTIST, formatArtist)" :viewInfo="standardizeData(viewInfo.ARTIST, formatArtist)"
:itemsToShow="6" :itemsToShow="6"
@add-to-queue="$emit('add-to-queue', $event)" @add-to-queue="$emit('add-to-queue', $event)"
/> />
@ -71,7 +71,8 @@ import ResultsAlbums from '@components/search/ResultsAlbums.vue'
import ResultsArtists from '@components/search/ResultsArtists.vue' import ResultsArtists from '@components/search/ResultsArtists.vue'
import ResultsPlaylists from '@components/search/ResultsPlaylists.vue' import ResultsPlaylists from '@components/search/ResultsPlaylists.vue'
import { formatSearchResults, formatSingleTrack, formatAlbums, formatArtist, formatPlaylist } from '@/data/search' import { formatSingleTrack, formatAlbums, formatArtist, formatPlaylist } from '@/data/search'
import { standardizeData } from '@/data/standardize'
export default { export default {
components: { components: {
@ -105,7 +106,7 @@ export default {
methods: { methods: {
convertDuration, convertDuration,
upperCaseFirstLowerCaseRest, upperCaseFirstLowerCaseRest,
formatSearchResults, standardizeData,
formatSingleTrack, formatSingleTrack,
formatAlbums, formatAlbums,
formatArtist, formatArtist,

35
src/data/artist.js Normal file
View File

@ -0,0 +1,35 @@
import { getPropertyWithFallback } from '@/utils/utils'
export function formatArtistData(artistData) {
return {
artistID: getPropertyWithFallback(artistData, 'id'),
artistName: getPropertyWithFallback(artistData, 'name'),
artistPictureXL: getPropertyWithFallback(artistData, 'picture_xl'),
artistReleases: formatArtistReleases(getPropertyWithFallback(artistData, 'releases'))
}
}
function formatArtistReleases(artistReleases) {
let formattedReleases = {}
for (const releaseType in artistReleases) {
if (artistReleases.hasOwnProperty(releaseType)) {
const releases = artistReleases[releaseType]
formattedReleases[releaseType] = []
for (const release of releases) {
formattedReleases[releaseType].push({
releaseID: getPropertyWithFallback(release, 'id'),
releaseCover: getPropertyWithFallback(release, 'cover_small'),
releaseTitle: getPropertyWithFallback(release, 'title'),
releaseDate: getPropertyWithFallback(release, 'release_date'),
releaseTracksNumber: getPropertyWithFallback(release, 'nb_song'),
releaseLink: getPropertyWithFallback(release, 'link'),
isReleaseExplicit: getPropertyWithFallback(release, 'explicit_lyrics')
})
}
}
}
return formattedReleases
}

View File

@ -1,7 +1,7 @@
import { getProperty } from '@/utils/utils' import { getPropertyWithFallback } from '@/utils/utils'
/** /**
* @typedef {object} ReducedSearchResult * @typedef {object} FormattedSearchResult
* @property {FormattedData} data * @property {FormattedData} data
* @property {boolean} hasLoaded * @property {boolean} hasLoaded
*/ */
@ -15,38 +15,11 @@ import { getProperty } from '@/utils/utils'
* @returns {FormattedData} formattedData * @returns {FormattedData} formattedData
*/ */
/**
* Reduces passed data to a specific format decied by the formatter passed.
*
* @param {object} rawObj
* @param {Formatter} formatFunc
* @returns {null|ReducedSearchResult}
*/
export function formatSearchResults(rawObj, formatFunc) {
if (!rawObj.hasLoaded) {
return null
} else {
const { data: rawData } = rawObj
const formattedData = []
for (const dataElement of rawData) {
let formatted = formatFunc(dataElement)
formattedData.push(formatted)
}
return {
data: formattedData,
hasLoaded: rawObj.hasLoaded
}
}
}
/** /**
* @param {FormattedData} track * @param {FormattedData} track
*/ */
export function formatSingleTrack(track) { export function formatSingleTrack(track) {
let isTrackExplicit = getProperty(track, 'explicit_lyrics', 'EXPLICIT_LYRICS') let isTrackExplicit = getPropertyWithFallback(track, 'explicit_lyrics', 'EXPLICIT_LYRICS')
if (typeof isTrackExplicit === 'string') { if (typeof isTrackExplicit === 'string') {
isTrackExplicit = isTrackExplicit !== '0' isTrackExplicit = isTrackExplicit !== '0'
@ -54,28 +27,32 @@ export function formatSingleTrack(track) {
return { return {
/* Track */ /* Track */
trackTitle: getProperty(track, 'title', 'SNG_TITLE'), trackTitle: getPropertyWithFallback(track, 'title', 'SNG_TITLE'),
trackTitleVersion: getProperty(track, 'title_version', 'VERSION'), trackTitleVersion: getPropertyWithFallback(track, 'title_version', 'VERSION'),
trackPreview: getProperty(track, 'preview'), trackPreview: getPropertyWithFallback(track, 'preview'),
trackDuration: getProperty(track, 'duration', 'DURATION'), trackDuration: getPropertyWithFallback(track, 'duration', 'DURATION'),
trackLink: getProperty(track, 'link') || `https://www.deezer.com/track/${track.SNG_ID}`, trackLink: getPropertyWithFallback(track, 'link') || `https://www.deezer.com/track/${track.SNG_ID}`,
isTrackExplicit, isTrackExplicit,
/* Artist */ /* Artist */
artistID: getProperty(track, 'artist.id', 'ART_ID'), artistID: getPropertyWithFallback(track, 'artist.id', 'ART_ID'),
artistName: getProperty(track, 'artist.name', 'ART_NAME'), artistName: getPropertyWithFallback(track, 'artist.name', 'ART_NAME'),
/* Album */ /* Album */
albumID: getProperty(track, 'album.id', 'ALB_ID'), albumID: getPropertyWithFallback(track, 'album.id', 'ALB_ID'),
albumTitle: getProperty(track, 'album.title', 'ALB_TITLE'), albumTitle: getPropertyWithFallback(track, 'album.title', 'ALB_TITLE'),
albumPicture: albumPicture:
getProperty(track, 'album.cover_small') || getPropertyWithFallback(track, 'album.cover_small') ||
`https://e-cdns-images.dzcdn.net/images/cover/${track.ALB_PICTURE}/32x32-000000-80-0-0.jpg` `https://e-cdns-images.dzcdn.net/images/cover/${track.ALB_PICTURE}/32x32-000000-80-0-0.jpg`
} }
} }
export function formatAlbums(album) { export function formatAlbums(album) {
let isAlbumExplicit = getProperty(album, 'explicit_lyrics', 'EXPLICIT_ALBUM_CONTENT.EXPLICIT_LYRICS_STATUS') let isAlbumExplicit = getPropertyWithFallback(
album,
'explicit_lyrics',
'EXPLICIT_ALBUM_CONTENT.EXPLICIT_LYRICS_STATUS'
)
if ('number' === typeof isAlbumExplicit) { if ('number' === typeof isAlbumExplicit) {
isAlbumExplicit = isAlbumExplicit === 1 isAlbumExplicit = isAlbumExplicit === 1
@ -83,49 +60,49 @@ export function formatAlbums(album) {
return { return {
/* Album */ /* Album */
albumID: getProperty(album, 'id', 'ALB_ID'), albumID: getPropertyWithFallback(album, 'id', 'ALB_ID'),
albumTitle: getProperty(album, 'title', 'ALB_TITLE'), albumTitle: getPropertyWithFallback(album, 'title', 'ALB_TITLE'),
albumCoverMedium: albumCoverMedium:
getProperty(album, 'cover_medium') || getPropertyWithFallback(album, 'cover_medium') ||
`https://e-cdns-images.dzcdn.net/images/cover/${album.ALB_PICTURE}/156x156-000000-80-0-0.jpg`, `https://e-cdns-images.dzcdn.net/images/cover/${album.ALB_PICTURE}/156x156-000000-80-0-0.jpg`,
albumLink: getProperty(album, 'link') || `https://deezer.com/album/${album.ALB_ID}`, albumLink: getPropertyWithFallback(album, 'link') || `https://deezer.com/album/${album.ALB_ID}`,
albumTracks: getProperty(album, 'nb_tracks', 'NUMBER_TRACK'), albumTracks: getPropertyWithFallback(album, 'nb_tracks', 'NUMBER_TRACK'),
isAlbumExplicit, isAlbumExplicit,
/* Artist */ /* Artist */
artistName: getProperty(album, 'artist.name', 'ART_NAME') artistName: getPropertyWithFallback(album, 'artist.name', 'ART_NAME')
} }
} }
export function formatArtist(artist) { export function formatArtist(artist) {
return { return {
/* Artist */ /* Artist */
artistID: getProperty(artist, 'id', 'ART_ID'), artistID: getPropertyWithFallback(artist, 'id', 'ART_ID'),
artistName: getProperty(artist, 'name', 'ART_NAME'), artistName: getPropertyWithFallback(artist, 'name', 'ART_NAME'),
artistPictureMedium: artistPictureMedium:
getProperty(artist, 'picture_medium') || getPropertyWithFallback(artist, 'picture_medium') ||
`https://e-cdns-images.dzcdn.net/images/artist/${artist.ART_PICTURE}/156x156-000000-80-0-0.jpg`, `https://e-cdns-images.dzcdn.net/images/artist/${artist.ART_PICTURE}/156x156-000000-80-0-0.jpg`,
artistLink: getProperty(artist, 'link') || `https://deezer.com/artist/${artist.ART_ID}`, artistLink: getPropertyWithFallback(artist, 'link') || `https://deezer.com/artist/${artist.ART_ID}`,
// TODO Fix // TODO Fix
artistAlbumsNumber: getProperty(artist, 'nb_album', 'NB_FAN') artistAlbumsNumber: getPropertyWithFallback(artist, 'nb_album', 'NB_FAN')
} }
} }
export function formatPlaylist(playlist) { export function formatPlaylist(playlist) {
return { return {
/* Playlist */ /* Playlist */
playlistID: getProperty(playlist, 'id', 'PLAYLIST_ID'), playlistID: getPropertyWithFallback(playlist, 'id', 'PLAYLIST_ID'),
playlistTitle: getProperty(playlist, 'title', 'TITLE'), playlistTitle: getPropertyWithFallback(playlist, 'title', 'TITLE'),
playlistPictureMedium: playlistPictureMedium:
getProperty(playlist, 'picture_medium') || getPropertyWithFallback(playlist, 'picture_medium') ||
`https://e-cdns-images.dzcdn.net/images/${playlist.PICTURE_TYPE}/${ `https://e-cdns-images.dzcdn.net/images/${playlist.PICTURE_TYPE}/${
playlist.PLAYLIST_PICTURE playlist.PLAYLIST_PICTURE
}/156x156-000000-80-0-0.jpg`, }/156x156-000000-80-0-0.jpg`,
playlistLink: getProperty(playlist, 'link') || `https://deezer.com/playlist/${playlist.PLAYLIST_ID}`, playlistLink: getPropertyWithFallback(playlist, 'link') || `https://deezer.com/playlist/${playlist.PLAYLIST_ID}`,
playlistTracksNumber: getProperty(playlist, 'nb_tracks', 'NB_SONG'), playlistTracksNumber: getPropertyWithFallback(playlist, 'nb_tracks', 'NB_SONG'),
/* Artist */ /* Artist */
artistName: getProperty(playlist, 'user.name') artistName: getPropertyWithFallback(playlist, 'user.name')
} }
} }

19
src/data/standardize.js Normal file
View File

@ -0,0 +1,19 @@
export function standardizeData(rawObj, formatFunc) {
if (!rawObj.hasLoaded) {
return null
} else {
const { data: rawData } = rawObj
const formattedData = []
for (const dataElement of rawData) {
let formatted = formatFunc(dataElement)
formattedData.push(formatted)
}
return {
data: formattedData,
hasLoaded: rawObj.hasLoaded
}
}
}

View File

@ -96,7 +96,7 @@ export function copyToClipboard(text) {
ghostInput.remove() ghostInput.remove()
} }
export function getProperty(obj, ...props) { export function getPropertyWithFallback(obj, ...props) {
for (const prop of props) { for (const prop of props) {
// Example: this.is.an.example // Example: this.is.an.example
let hasDotNotation = /\./.test(prop) let hasDotNotation = /\./.test(prop)