workflow: reorganized components' folder structure

This commit is contained in:
Roberto Tonino
2020-09-26 21:10:40 +02:00
parent 064c3b29bc
commit acdd702c66
25 changed files with 145 additions and 98 deletions

View File

@@ -0,0 +1,235 @@
<template>
<div id="about_tab" class="main_tabcontent" ref="root">
<h2 class="page_heading">{{ $t('sidebar.about') }}</h2>
<ul>
<li>
{{ $t('about.updates.currentVersion') }}:
<span>{{ current || $t('about.updates.versionNotAvailable') }}</span>
</li>
<li>{{ $t('about.updates.deemixVersion') }}: {{ deemixVersion }}</li>
<li v-if="updateAvailable && latest">{{ $t('about.updates.updateAvailable', { version: latest }) }}</li>
</ul>
<ul>
<li v-html="$t('about.usesLibrary')"></li>
<li v-html="$t('about.thanks')"></li>
<li v-html="$t('about.upToDate')"></li>
</ul>
<h2>{{ $t('about.titles.usefulLinks') }}</h2>
<ul class="no-dots">
<li>
<a href="https://deemix.app" target="_blank">🌍 {{ $t('about.officialWebsite') }}</a>
</li>
<li>
<a href="https://codeberg.org/RemixDev/deemix" target="_blank">🚀 {{ $t('about.officialRepo') }}</a>
</li>
<li>
<a href="https://codeberg.org/RemixDev/deemix-webui" target="_blank">💻 {{ $t('about.officialWebuiRepo') }}</a>
</li>
<li>
<a href="https://www.reddit.com/r/deemix" target="_blank">🤖 {{ $t('about.officialSubreddit') }}</a>
</li>
<li>
<a href="https://t.me/RemixDevNews" target="_blank">📰 {{ $t('about.newsChannel') }}</a>
</li>
</ul>
<h2>
{{ $t('about.titles.bugReports') }}
<span class="subheading">
{{ $t('about.subtitles.bugReports') }}
</span>
</h2>
<ul>
<li v-html="$t('about.questions')"></li>
<li>
{{ $t('about.beforeReporting') }}
</li>
<li v-html="$t('about.beSure')"></li>
<li>
{{ $t('about.duplicateReports') }}
</li>
<li v-html="$t('about.dontOpenIssues')"></li>
</ul>
<h2>
{{ $t('about.titles.contributing') }}
<span class="subheading">
{{ $t('about.subtitles.contributing') }}
</span>
</h2>
<ul>
<li v-html="$t('about.newUI')"></li>
<li>
{{ $t('about.acceptFeatures') }}
</li>
<li v-html="$t('about.contributeWebUI')"></li>
<li>
{{ $t('about.otherLanguages') }}
</li>
<li>
{{ $t('about.understandingCode') }}
</li>
</ul>
<h2>
{{ $t('about.titles.donations') }}
<span class="subheading">
{{ $t('about.subtitles.donations') }}
</span>
</h2>
<ul>
<li v-html="$t('about.itsFree')"></li>
<li>
{{ $t('about.notObligated') }}
</li>
</ul>
<ul>
<li>
<i v-html="paypal" />
<strong>PayPal:</strong>
<a href="https://paypal.me/RemixDev" target="_blank">PayPal.me/RemixDev</a>
</li>
<li>
<i class="ethereum" v-html="ethereum" />
<strong>Ethereum:</strong> 0x1d2aa67e671485CD4062289772B662e0A6Ff976c
</li>
</ul>
<h2>{{ $t('about.titles.license') }}</h2>
<p>
<a rel="license" href="https://www.gnu.org/licenses/gpl-3.0.en.html" target="_blank">
<img
alt="GNU General Public License"
style="border-width: 0"
src="https://www.gnu.org/graphics/gplv3-127x51.png"
/>
</a>
</p>
<p v-html="$t('about.lincensedUnder')"></p>
</div>
</template>
<style lang="scss" scoped>
li,
p,
a {
letter-spacing: 0.4px;
font-size: 20px;
line-height: 1.2;
}
i {
vertical-align: middle;
}
i /deep/ svg {
fill: white;
width: 20px;
}
.ethereum /deep/ svg {
fill: var(--foreground);
}
:link {
text-decoration: none;
}
#about_tab {
margin-bottom: 40px;
}
h2 {
text-transform: capitalize;
&:not(.page_heading) {
font-size: 2rem;
border-bottom: 1px solid hsla(0, 0%, 20%, 0.25);
padding: {
top: 2rem;
bottom: 1rem;
}
}
.subheading {
display: block;
font-size: 0.5em;
margin-top: 0.5em;
font-weight: normal;
opacity: 0.8;
text-transform: none;
}
}
p {
margin: 0 !important;
}
ul {
li {
margin-bottom: 7px;
}
h2 + & {
margin-top: 1rem;
}
ul + & {
margin-top: 1.25rem;
}
&.no-dots {
list-style-type: none;
}
&:not(.no-dots) {
list-style-type: none;
li {
position: relative;
&::before {
content: '—';
position: absolute;
left: -30px;
opacity: 0.25;
}
}
}
}
</style>
<script>
import { socket } from '@/utils/socket'
import paypal from '@/assets/paypal.svg'
import ethereum from '@/assets/ethereum.svg'
import { mapGetters } from 'vuex'
export default {
data: () => ({
paypal,
ethereum,
current: null,
latest: null,
updateAvailable: false,
deemixVersion: null
}),
computed: {
...mapGetters(['getAboutInfo'])
},
methods: {
initUpdate(data) {
const { currentCommit, latestCommit, updateAvailable, deemixVersion } = data
this.current = currentCommit
this.latest = latestCommit
this.updateAvailable = updateAvailable
this.deemixVersion = deemixVersion
}
},
mounted() {
this.initUpdate(this.getAboutInfo)
}
}
</script>

View File

@@ -0,0 +1,186 @@
<template>
<div id="artist_tab" class="main_tabcontent fixed_footer image_header" ref="root">
<header
class="inline-flex"
:style="{
'background-image':
'linear-gradient(to bottom, transparent 0%, var(--main-background) 100%), url(\'' + image + '\')'
}"
>
<h1>{{ title }}</h1>
<div role="button" aria-label="download" @click.stop="addToQueue" :data-link="link" class="fab right">
<i class="material-icons" :title="$t('globals.download_hint')">get_app</i>
</div>
</header>
<div class="tab">
<button
v-for="(item, name) in body"
:key="name"
class="selective"
:class="{ active: name === currentTab }"
:href="'#artist_' + name"
@click="changeTab(name)"
>
{{ $tc(`globals.listTabs.${name}`, 2) }}
</button>
</div>
<table class="table">
<thead>
<tr>
<th
v-for="data in head"
@click="data.sortKey ? sortBy(data.sortKey) : null"
:style="{ width: data.width ? data.width : 'auto' }"
:class="{
'sort-asc': data.sortKey == sortKey && sortOrder == 'asc',
'sort-desc': data.sortKey == sortKey && sortOrder == 'desc',
sortable: data.sortKey,
clickable: data.sortKey
}"
>
<!-- Need to change this behaviour for translations -->
{{ data.title }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="release in showTable" :key="release.id">
<router-link tag="td" class="inline-flex clickable" :to="{ name: 'Album', params: { id: release.id } }">
<img
class="rounded coverart"
:src="release.cover_small"
style="margin-right: 16px; width: 56px; height: 56px"
/>
<i v-if="release.explicit_lyrics" class="material-icons explicit_icon"> explicit </i>
{{ release.title }}
<i v-if="checkNewRelease(release.release_date)" class="material-icons" style="color: #ff7300">
fiber_new
</i>
</router-link>
<td>{{ release.release_date }}</td>
<td>{{ release.nb_song }}</td>
<td @click.stop="addToQueue" :data-link="release.link" class="clickable">
<i class="material-icons" :title="$t('globals.download_hint')"> file_download </i>
</td>
</tr>
</tbody>
</table>
<footer>
<button class="back-button" @click="$router.back()">{{ $t('globals.back') }}</button>
</footer>
</div>
</template>
<script>
import { isEmpty, orderBy } from 'lodash-es'
import { socket } from '@/utils/socket'
import Downloads from '@/utils/downloads'
import EventBus from '@/utils/EventBus'
export default {
name: 'artist-tab',
data() {
return {
currentTab: '',
sortKey: 'release_date',
sortOrder: 'desc',
title: '',
image: '',
type: '',
link: '',
head: null,
body: null
}
},
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) {
if (key == this.sortKey) {
this.sortOrder = this.sortOrder == 'asc' ? 'desc' : 'asc'
} else {
this.sortKey = key
this.sortOrder = 'asc'
}
},
changeTab(tab) {
this.currentTab = tab
},
updateSelected() {
// Last tab opened logic
},
checkNewRelease(date) {
let g1 = new Date()
let g2 = new Date(date)
g2.setDate(g2.getDate() + 3)
g1.setHours(0, 0, 0, 0)
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
}
}
},
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)
EventBus.$on('artistTab:changeTab', this.changeTab)
}
}
</script>
<style>
</style>

View File

@@ -0,0 +1,199 @@
<template>
<div id="charts_tab" class="main_tabcontent" ref="root">
<h2 class="page_heading">{{ $t('charts.title') }}</h2>
<div v-if="country === ''" id="charts_selection">
<div class="release_grid charts_grid">
<template v-for="release in countries">
<div
role="button"
:aria-label="release.title"
v-if="release.title === 'Worldwide'"
class="release clickable"
@click="getTrackList"
:data-title="release.title"
:data-id="release.id"
:key="release.id"
>
<img class="rounded coverart" :src="release.picture_medium" />
</div>
</template>
<template v-for="release in countries">
<div
role="button"
:aria-label="release.title"
v-if="release.title !== 'Worldwide'"
class="release clickable"
@click="getTrackList"
:data-title="release.title"
:data-id="release.id"
:key="release.id"
>
<img class="rounded coverart" :src="release.picture_medium" />
</div>
</template>
</div>
</div>
<div v-else id="charts_table">
<button @click="onChangeCountry">{{ $t('charts.changeCountry') }}</button>
<button @click.stop="addToQueue" :data-link="'https://www.deezer.com/playlist/' + id">
{{ $t('charts.download') }}
</button>
<table class="table table--charts">
<tbody>
<tr v-for="track in chart" class="track_row">
<td class="top-tracks-position" :class="{ first: track.position === 1 }">
{{ track.position }}
</td>
<td class="table__icon table__icon--big">
<a
href="#"
@click="playPausePreview"
class="rounded"
:class="{ 'single-cover': track.preview }"
:data-preview="track.preview"
>
<i
@mouseenter="previewMouseEnter"
@mouseleave="previewMouseLeave"
v-if="track.preview"
class="material-icons preview_controls"
:title="$t('globals.play_hint')"
>
play_arrow
</i>
<img class="rounded coverart" :src="track.album.cover_small" />
</a>
</td>
<td class="table__cell--large breakline">
{{
track.title +
(track.title_version && track.title.indexOf(track.title_version) == -1 ? ' ' + track.title_version : '')
}}
</td>
<router-link
tag="td"
class="table__cell table__cell--medium table__cell--center breakline clickable"
:to="{ name: 'Artist', params: { id: track.artist.id } }"
>
{{ track.artist.name }}
</router-link>
<router-link
tag="td"
class="table__cell--medium table__cell--center breakline clickable"
:to="{ name: 'Album', params: { id: track.album.id } }"
>
{{ track.album.title }}
</router-link>
<td class="table__cell--small table__cell--center">
{{ convertDuration(track.duration) }}
</td>
<td
class="table__cell--download"
@click.stop="addToQueue"
:data-link="track.link"
role="button"
aria-label="download"
>
<i class="material-icons" :title="$t('globals.download_hint')">get_app</i>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import { socket } from '@/utils/socket'
import { sendAddToQueue } from '@/utils/downloads'
import { convertDuration } from '@/utils/utils'
import { getChartsData } from '@/data/charts'
import EventBus from '@/utils/EventBus'
export default {
data() {
return {
country: '',
id: 0,
countries: [],
chart: []
}
},
async created() {
socket.on('setChartTracks', this.setTracklist)
this.$on('hook:destroyed', () => {
socket.off('setChartTracks')
})
const chartsData = await getChartsData()
this.initCharts(chartsData)
},
methods: {
convertDuration,
playPausePreview(e) {
EventBus.$emit('trackPreview:playPausePreview', e)
},
previewMouseEnter(e) {
EventBus.$emit('trackPreview:previewMouseEnter', e)
},
previewMouseLeave(e) {
EventBus.$emit('trackPreview:previewMouseLeave', e)
},
addToQueue(e) {
e.stopPropagation()
sendAddToQueue(e.currentTarget.dataset.link)
},
getTrackList(event) {
document.getElementById('content').scrollTo(0, 0)
const {
currentTarget: {
dataset: { title }
},
currentTarget: {
dataset: { id }
}
} = event
this.country = title
localStorage.setItem('chart', this.country)
this.id = id
socket.emit('getChartTracks', this.id)
},
setTracklist(data) {
this.chart = data
},
onChangeCountry() {
this.country = ''
this.id = 0
},
initCharts(chartsData) {
this.countries = chartsData
this.country = localStorage.getItem('chart') || ''
if (!this.country) return
let i = 0
for (; i < this.countries.length; i++) {
if (this.countries[i].title == this.country) break
}
if (i !== this.countries.length) {
this.id = this.countries[i].id
socket.emit('getChartTracks', this.id)
} else {
this.country = ''
localStorage.setItem('chart', this.country)
}
}
}
}
</script>
<style>
</style>

View File

@@ -0,0 +1,39 @@
<template>
<div id="errors_tab" class="main_tabcontent">
<h1>{{ $t('errors.title', { name: title }) }}</h1>
<table class="table table--tracklist">
<tr>
<th>ID</th>
<th>{{ $tc('globals.listTabs.artist', 1) }}</th>
<th>{{ $tc('globals.listTabs.title', 1) }}</th>
<th>{{ $tc('globals.listTabs.error', 1) }}</th>
</tr>
<tr v-for="error in errors" :key="error.data.id">
<td>{{ error.data.id }}</td>
<td>{{ error.data.artist }}</td>
<td>{{ error.data.title }}</td>
<td>{{ error.errid ? $t(`errors.ids.${error.errid}`) : error.message }}</td>
</tr>
</table>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
computed: {
...mapGetters(['getErrors']),
title() {
return `${this.getErrors.artist} - ${this.getErrors.title}`
},
errors() {
return this.getErrors.errors
}
}
}
</script>
<style>
</style>

View File

@@ -0,0 +1,342 @@
<template>
<div id="favorites_tab" class="main_tabcontent">
<h2 class="page_heading">
{{ $t('favorites.title') }}
<div
@click="reloadTabs"
class="clickable reload-button reload-button--inline"
ref="reloadButton"
role="button"
aria-label="reload"
>
<i class="material-icons">sync</i>
</div>
</h2>
<div class="section-tabs">
<div
class="section-tabs__tab favorites_tablinks"
:class="{ active: activeTab === tab }"
@click="activeTab = tab"
v-for="tab in tabs"
:key="tab"
>
{{ $tc(`globals.listTabs.${tab}`, 2) }}
</div>
</div>
<div class="favorites_tabcontent" :class="{ 'favorites_tabcontent--active': activeTab === 'playlist' }">
<div v-if="playlists.length == 0">
<h1>{{ $t('favorites.noPlaylists') }}</h1>
</div>
<div class="release_grid" v-if="playlists.length > 0 || spotifyPlaylists > 0">
<router-link
tag="div"
v-for="release in playlists"
:key="release.id"
class="release clickable"
:to="{ name: 'Playlist', params: { id: release.id } }"
>
<div class="cover_container">
<img aria-hidden="true" class="rounded coverart" :src="release.picture_medium" />
<button
role="button"
aria-label="download"
@click.stop="addToQueue"
:data-link="release.link"
class="download_overlay"
tabindex="0"
>
<i class="material-icons" :title="$t('globals.download_hint')">get_app</i>
</button>
</div>
<p class="primary-text">{{ release.title }}</p>
<p class="secondary-text">
{{
`${$t('globals.by', { artist: release.creator.name })} - ${$tc(
'globals.listTabs.trackN',
release.nb_tracks
)}`
}}
</p>
</router-link>
<router-link
tag="div"
v-for="release in spotifyPlaylists"
:key="release.id"
class="release clickable"
:to="{ name: 'Spotify Playlist', params: { id: release.id } }"
>
<div class="cover_container">
<img aria-hidden="true" class="rounded coverart" :src="release.picture_medium" />
<button
role="button"
aria-label="download"
@click.stop="addToQueue"
:data-link="release.link"
class="download_overlay"
tabindex="0"
>
<i class="material-icons" :title="$t('globals.download_hint')">get_app</i>
</button>
</div>
<p class="primary-text">{{ release.title }}</p>
<p class="secondary-text">
{{
`${$t('globals.by', { artist: release.creator.name })} - ${$tc(
'globals.listTabs.trackN',
release.nb_tracks
)}`
}}
</p>
</router-link>
</div>
</div>
<div class="favorites_tabcontent" :class="{ 'favorites_tabcontent--active': activeTab === 'album' }">
<div v-if="albums.length == 0">
<h1>{{ $t('favorites.noAlbums') }}</h1>
</div>
<div class="release_grid" v-if="albums.length > 0">
<router-link
tag="div"
class="release clickable"
v-for="release in albums"
:key="release.id"
:to="{ name: 'Album', params: { id: release.id } }"
>
<div class="cover_container">
<img aria-hidden="true" class="rounded coverart" :src="release.cover_medium" />
<button
role="button"
aria-label="download"
@click.stop="addToQueue"
:data-link="release.link"
class="download_overlay"
tabindex="0"
>
<i class="material-icons" :title="$t('globals.download_hint')">get_app</i>
</button>
</div>
<p class="primary-text">{{ release.title }}</p>
<p class="secondary-text">{{ `${$t('globals.by', { artist: release.artist.name })}` }}</p>
</router-link>
</div>
</div>
<div class="favorites_tabcontent" :class="{ 'favorites_tabcontent--active': activeTab === 'artist' }">
<div v-if="artists.length == 0">
<h1>{{ $t('favorites.noArtists') }}</h1>
</div>
<div class="release_grid" v-if="artists.length > 0">
<router-link
tag="div"
class="release clickable"
v-for="release in artists"
:key="release.id"
:to="{ name: 'Artist', params: { id: release.id } }"
>
<div class="cover_container">
<img aria-hidden="true" class="circle coverart" :src="release.picture_medium" />
<button
role="button"
aria-label="download"
@click.stop="addToQueue"
:data-link="release.link"
class="download_overlay"
tabindex="0"
>
<i class="material-icons" :title="$t('globals.download_hint')">get_app</i>
</button>
</div>
<p class="primary-text">{{ release.name }}</p>
</router-link>
</div>
</div>
<div class="favorites_tabcontent" :class="{ 'favorites_tabcontent--active': activeTab === 'track' }">
<div v-if="tracks.length == 0">
<h1>{{ $t('favorites.noTracks') }}</h1>
</div>
<table v-if="tracks.length > 0" class="table">
<tr v-for="track in tracks" class="track_row">
<td class="top-tracks-position" :class="{ first: track.position === 1 }">
{{ track.position }}
</td>
<td>
<a
href="#"
class="rounded"
:class="{ 'single-cover': !!track.preview }"
@click="playPausePreview"
:data-preview="track.preview"
>
<i
@mouseenter="previewMouseEnter"
@mouseleave="previewMouseLeave"
v-if="track.preview"
class="material-icons preview_controls"
:title="$t('globals.play_hint')"
>
play_arrow
</i>
<img class="rounded coverart" :src="track.album.cover_small" />
</a>
</td>
<td class="table__cell--large breakline">
{{
track.title +
(track.title_version && track.title.indexOf(track.title_version) == -1 ? ' ' + track.title_version : '')
}}
</td>
<router-link
tag="td"
class="table__cell table__cell--medium table__cell--center breakline clickable"
:to="{ name: 'Artist', params: { id: track.artist.id } }"
>
{{ track.artist.name }}
</router-link>
<router-link
tag="td"
class="table__cell--medium table__cell--center breakline clickable"
:to="{ name: 'Album', params: { id: track.album.id } }"
>
{{ track.album.title }}
</router-link>
<td class="table__cell--small">
{{ convertDuration(track.duration) }}
</td>
<td
class="table__cell--download clickable"
@click.stop="addToQueue"
:data-link="track.link"
role="button"
aria-label="download"
>
<div class="table__cell-content table__cell-content--vertical-center">
<i class="material-icons" :title="$t('globals.download_hint')">get_app</i>
</div>
</td>
</tr>
</table>
</div>
</div>
</template>
<style lang="scss">
.favorites_tabcontent {
display: none;
&--active {
display: block;
}
}
</style>
<script>
import { socket } from '@/utils/socket'
import { sendAddToQueue } from '@/utils/downloads'
import { convertDuration } from '@/utils/utils'
import { toast } from '@/utils/toasts'
import { getFavoritesData } from '@/data/favorites'
export default {
data() {
return {
tracks: [],
albums: [],
artists: [],
playlists: [],
spotifyPlaylists: [],
activeTab: 'playlist',
tabs: ['playlist', 'album', 'artist', 'track']
}
},
async created() {
const favoritesData = await getFavoritesData()
// TODO Change with isLoggedIn vuex getter
if (Object.entries(favoritesData).length === 0) return
this.setFavorites(favoritesData)
},
mounted() {
socket.on('updated_userFavorites', this.updated_userFavorites)
socket.on('updated_userSpotifyPlaylists', this.updated_userSpotifyPlaylists)
socket.on('updated_userPlaylists', this.updated_userPlaylists)
socket.on('updated_userAlbums', this.updated_userAlbums)
socket.on('updated_userArtist', this.updated_userArtist)
socket.on('updated_userTracks', this.updated_userTracks)
this.$on('hook:destroyed', () => {
socket.off('updated_userFavorites')
socket.off('updated_userSpotifyPlaylists')
socket.off('updated_userPlaylists')
socket.off('updated_userAlbums')
socket.off('updated_userArtist')
socket.off('updated_userTracks')
})
},
methods: {
playPausePreview(e) {
EventBus.$emit('trackPreview:playPausePreview', e)
},
previewMouseEnter(e) {
EventBus.$emit('trackPreview:previewMouseEnter', e)
},
previewMouseLeave(e) {
EventBus.$emit('trackPreview:previewMouseLeave', e)
},
convertDuration,
addToQueue(e) {
sendAddToQueue(e.currentTarget.dataset.link)
},
updated_userSpotifyPlaylists(data) {
this.spotifyPlaylists = data
},
updated_userPlaylists(data) {
this.playlists = data
},
updated_userAlbums(data) {
this.albums = data
},
updated_userArtist(data) {
this.artists = data
},
updated_userTracks(data) {
this.tracks = data
},
reloadTabs() {
this.$refs.reloadButton.classList.add('spin')
socket.emit('update_userFavorites')
if (localStorage.getItem('spotifyUser')) {
socket.emit('update_userSpotifyPlaylists', localStorage.getItem('spotifyUser'))
}
},
updated_userFavorites(data) {
this.setFavorites(data)
// Removing animation class only when the animation has completed an iteration
// Prevents animation ugly stutter
this.$refs.reloadButton.addEventListener(
'animationiteration',
() => {
this.$refs.reloadButton.classList.remove('spin')
toast(this.$t('toasts.refreshFavs'), 'done', true)
},
{ once: true }
)
},
setFavorites(data) {
const { tracks, albums, artists, playlists } = data
this.tracks = tracks
this.albums = albums
this.artists = artists
this.playlists = playlists
}
}
}
</script>

View File

@@ -0,0 +1,123 @@
<template>
<div id="home_tab" class="main_tabcontent" ref="root">
<h2 class="page_heading">{{ $t('globals.welcome') }}</h2>
<section class="home_section" ref="notLogged" v-if="!isLoggedIn">
<p id="home_not_logged_text">{{ $t('home.needTologin') }}</p>
<router-link tag="button" name="button" :to="{ name: 'Settings' }">
{{ $t('home.openSettings') }}
</router-link>
</section>
<section v-if="playlists.length" class="home_section">
<h3 class="section_heading">{{ $t('home.sections.popularPlaylists') }}</h3>
<div class="release_grid">
<router-link
tag="div"
v-for="release in playlists"
:key="release.id"
class="release clickable"
:to="{ name: 'Playlist', params: { id: release.id } }"
@keyup.enter.native="$router.push({ name: 'Playlist', params: { id: release.id } })"
tabindex="0"
>
<div class="cover_container">
<img aria-hidden="true" class="rounded coverart" :src="release.picture_medium" />
<button
role="button"
aria-label="download"
@click.stop="addToQueue"
:data-link="release.link"
class="download_overlay"
tabindex="0"
>
<i class="material-icons" :title="$t('globals.download_hint')">get_app</i>
</button>
</div>
<p class="primary-text">{{ release.title }}</p>
<p class="secondary-text">
{{
`${$t('globals.by', { artist: release.user.name })} - ${$tc(
'globals.listTabs.trackN',
release.nb_tracks
)}`
}}
</p>
</router-link>
</div>
</section>
<section v-if="albums.length" class="home_section">
<h3 class="section_heading">{{ $t('home.sections.popularAlbums') }}</h3>
<div class="release_grid">
<router-link
tag="div"
v-for="release in albums"
:key="release.id"
class="release clickable"
:to="{ name: 'Album', params: { id: release.id } }"
@keyup.enter.native="$router.push({ name: 'Album', params: { id: release.id } })"
:data-id="release.id"
tabindex="0"
>
<div class="cover_container">
<img aria-hidden="true" class="rounded coverart" :src="release.cover_medium" />
<button
role="button"
aria-label="download"
@click.stop="addToQueue"
:data-link="release.link"
class="download_overlay"
tabindex="0"
>
<i class="material-icons" :title="$t('globals.download_hint')">get_app</i>
</button>
</div>
<p class="primary-text">{{ release.title }}</p>
<p class="secondary-text">{{ `${$t('globals.by', { artist: release.artist.name })}` }}</p>
</router-link>
</div>
</section>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import { sendAddToQueue } from '@/utils/downloads'
import { getHomeData } from '@/data/home'
export default {
data() {
return {
playlists: [],
albums: []
}
},
async created() {
const homeData = await getHomeData()
this.initHome(homeData)
},
computed: {
...mapGetters(['isLoggedIn'])
},
methods: {
addToQueue(e) {
sendAddToQueue(e.currentTarget.dataset.link)
},
initHome(data) {
const {
playlists: { data: playlistData },
albums: { data: albumData }
} = data
this.playlists = playlistData
this.albums = albumData
}
}
}
</script>
<style>
</style>

View File

@@ -0,0 +1,222 @@
<template>
<div id="analyzer_tab" class="main_tabcontent image_header" ref="root">
<h2 class="page_heading page_heading--capitalize">{{ $t('sidebar.linkAnalyzer') }}</h2>
<div v-if="link === ''">
<p>
{{ $t('linkAnalyzer.info') }}
</p>
<p>
{{ $t('linkAnalyzer.useful') }}
</p>
</div>
<div v-else-if="link === 'error'">
<h2>{{ $t('linkAnalyzer.linkNotSupported') }}</h2>
<p>{{ $t('linkAnalyzer.linkNotSupportedYet') }}</p>
</div>
<div v-else>
<header
class="inline-flex"
:style="{
'background-image':
'linear-gradient(to bottom, transparent 0%, var(--main-background) 100%), url(\'' + image + '\')'
}"
>
<div>
<h1>{{ title }}</h1>
<h2 v-if="type === 'track'">
<i18n path="globals.by" tag="span">
<router-link
tag="span"
place="artist"
class="clickable"
:to="{ name: 'Artist', params: { id: data.artist.id } }"
>
{{ data.artist.name }}
</router-link>
</i18n>
<i18n path="globals.in" tag="span">
<router-link
tag="span"
place="album"
class="clickable"
:to="{ name: 'Album', params: { id: data.album.id } }"
>
{{ data.album.title }}
</router-link>
</i18n>
</h2>
<h2 v-else-if="type === 'album'">
<i18n path="globals.by" tag="span">
<router-link
tag="span"
place="artist"
class="clickable"
:to="{ name: 'Artist', params: { id: data.artist.id } }"
>
{{ data.artist.name }}
</router-link>
</i18n>
{{ `${$tc('globals.listTabs.trackN', data.nb_tracks)}` }}
</h2>
</div>
<div
role="button"
aria-label="download"
@contextmenu.prevent="openQualityModal"
@click.stop="addToQueue"
:data-link="link"
class="fab right"
>
<i class="material-icons" :title="$t('globals.download_hint')">get_app</i>
</div>
</header>
<table class="table">
<tr v-if="data.id">
<td>{{ $t('linkAnalyzer.table.id') }}</td>
<td>{{ data.id }}</td>
</tr>
<tr v-if="data.isrc">
<td>{{ $t('linkAnalyzer.table.isrc') }}</td>
<td>{{ data.isrc }}</td>
</tr>
<tr v-if="data.upc">
<td>{{ $t('linkAnalyzer.table.upc') }}</td>
<td>{{ data.upc }}</td>
</tr>
<tr v-if="data.duration">
<td>{{ $t('linkAnalyzer.table.duration') }}</td>
<td>{{ convertDuration(data.duration) }}</td>
</tr>
<tr v-if="data.disk_number">
<td>{{ $t('linkAnalyzer.table.diskNumber') }}</td>
<td>{{ data.disk_number }}</td>
</tr>
<tr v-if="data.track_position">
<td>{{ $t('linkAnalyzer.table.trackNumber') }}</td>
<td>{{ data.track_position }}</td>
</tr>
<tr v-if="data.release_date">
<td>{{ $t('linkAnalyzer.table.releaseDate') }}</td>
<td>{{ data.release_date }}</td>
</tr>
<tr v-if="data.bpm">
<td>{{ $t('linkAnalyzer.table.bpm') }}</td>
<td>{{ data.bpm }}</td>
</tr>
<tr v-if="data.label">
<td>{{ $t('linkAnalyzer.table.label') }}</td>
<td>{{ data.label }}</td>
</tr>
<tr v-if="data.record_type">
<td>{{ $t('linkAnalyzer.table.recordType') }}</td>
<td>{{ $tc(`globals.listTabs.${data.record_type}`, 1) }}</td>
</tr>
<tr v-if="data.genres && data.genres.data.length">
<td>{{ $t('linkAnalyzer.table.genres') }}</td>
<td>{{ data.genres.data.map(x => x.name).join('; ') }}</td>
</tr>
</table>
<div v-if="type == 'album'">
<router-link tag="button" :to="{ name: 'Album', params: { id } }">
{{ $t('linkAnalyzer.table.tracklist') }}
</router-link>
</div>
<div v-if="countries.length">
<p v-for="country in countries">{{ country[0] }} - {{ country[1] }}</p>
</div>
</div>
</div>
</template>
<script>
import { socket } from '@/utils/socket'
import { convertDuration } from '@/utils/utils'
import { COUNTRIES } from '@/utils/countries'
import EventBus from '@/utils/EventBus'
import { sendAddToQueue } from '@/utils/downloads'
export default {
data() {
return {
link: '',
title: '',
subtitle: '',
image: '',
data: {},
type: '',
id: '0',
countries: []
}
},
methods: {
convertDuration,
reset() {
this.title = 'Loading...'
this.subtitle = ''
this.image = ''
this.data = {}
this.type = ''
this.link = ''
this.countries = []
},
showTrack(data) {
this.reset()
const {
title,
title_version,
album: { cover_xl },
link,
available_countries,
id
} = data
this.title = title + (title_version && title.indexOf(title_version) == -1 ? ' ' + title_version : '')
this.image = cover_xl
this.type = 'track'
this.link = link
this.id = id
available_countries.forEach(cc => {
let temp = []
let chars = [...cc].map(c => c.charCodeAt() + 127397)
temp.push(String.fromCodePoint(...chars))
temp.push(COUNTRIES[cc])
this.countries.push(temp)
})
this.data = data
},
showAlbum(data) {
this.reset()
const { title, cover_xl, link, id } = data
this.title = title
this.image = cover_xl
this.type = 'album'
this.link = link
this.data = data
this.id = id
},
notSupported() {
this.link = 'error'
},
addToQueue(e) {
sendAddToQueue(e.currentTarget.dataset.link)
}
},
mounted() {
EventBus.$on('linkAnalyzerTab:reset', this.reset)
socket.on('analyze_track', this.showTrack)
socket.on('analyze_album', this.showAlbum)
socket.on('analyze_notSupported', this.notSupported)
}
}
</script>
<style>
</style>

View File

@@ -0,0 +1,282 @@
<template>
<div id="search_tab" class="main_tabcontent" ref="root">
<div v-show="!showSearchTab">
<h2>{{ $t('search.startSearching') }}</h2>
<p>{{ $t('search.description') }}</p>
</div>
<div v-show="showSearchTab">
<ul class="section-tabs">
<li
class="section-tabs__tab"
v-for="tab in tabs"
:key="tab.name"
@click="currentTab = tab"
:class="{ active: currentTab.name === tab.name }"
>
{{ tab.name }}
</li>
</ul>
<keep-alive>
<component
:is="currentTab.component"
:results="results"
@add-to-queue="addToQueue"
@change-search-tab="changeSearchTab"
></component>
</keep-alive>
</div>
</div>
</template>
<script>
import BaseLoadingPlaceholder from '@components/globals/BaseLoadingPlaceholder.vue'
import ResultsAll from '@components/search/ResultsAll.vue'
import ResultsAlbums from '@components/search/ResultsAlbums.vue'
import ResultsArtists from '@components/search/ResultsArtists.vue'
import ResultsPlaylists from '@components/search/ResultsPlaylists.vue'
import ResultsTracks from '@components/search/ResultsTracks.vue'
import { socket } from '@/utils/socket'
import { sendAddToQueue } from '@/utils/downloads'
import { numberWithDots, convertDuration } from '@/utils/utils'
import EventBus from '@/utils/EventBus'
export default {
components: {
BaseLoadingPlaceholder
},
data() {
const $t = this.$t.bind(this)
const $tc = this.$tc.bind(this)
return {
currentTab: {
name: '',
component: {}
},
tabs: [
{
name: $t('globals.listTabs.all'),
searchType: 'all',
component: ResultsAll
},
{
name: $tc('globals.listTabs.track', 2),
searchType: 'track',
component: ResultsTracks
},
{
name: $tc('globals.listTabs.album', 2),
searchType: 'album',
component: ResultsAlbums
},
{
name: $tc('globals.listTabs.artist', 2),
searchType: 'artist',
component: ResultsArtists
},
{
name: $tc('globals.listTabs.playlist', 2),
searchType: 'playlist',
component: ResultsPlaylists
}
],
results: {
query: '',
allTab: {
ORDER: [],
TOP_RESULT: [],
ALBUM: {},
ARTIST: {},
TRACK: {},
PLAYLIST: {}
},
trackTab: {
data: [],
next: 0,
total: 0,
loaded: false
},
albumTab: {
data: [],
next: 0,
total: 0,
loaded: false
},
artistTab: {
data: [],
next: 0,
total: 0,
loaded: false
},
playlistTab: {
data: [],
next: 0,
total: 0,
loaded: false
}
}
}
},
computed: {
showSearchTab() {
return this.results.query !== ''
},
loadedTabs() {
const loaded = []
for (const resultKey in this.results) {
if (this.results.hasOwnProperty(resultKey)) {
const result = this.results[resultKey]
if (result.loaded) {
loaded.push(resultKey.replace(/Tab/g, ''))
}
}
}
return loaded
}
},
props: {
performScrolledSearch: {
type: Boolean,
required: false
}
},
created() {
this.currentTab = this.tabs[0]
},
mounted() {
EventBus.$on('mainSearch:checkLoadMoreContent', this.checkLoadMoreContent)
this.$root.$on('mainSearch:showNewResults', this.checkIfShowNewResults)
this.$root.$on('mainSearch:updateResults', this.checkIfUpdateResults)
socket.on('mainSearch', this.handleMainSearch)
socket.on('search', this.handleSearch)
},
methods: {
changeSearchTab(sectionName) {
sectionName = sectionName.toLowerCase()
let newTab = this.tabs.find(tab => {
return tab.searchType === sectionName
})
if (!newTab) {
console.error(`No tab ${sectionName} found`)
return
}
window.scrollTo(0, 0)
this.currentTab = newTab
},
checkIfShowNewResults(term, mainSelected) {
let needToPerformNewSearch = term !== this.results.query /* || mainSelected == 'search_tab' */
if (needToPerformNewSearch) {
this.showNewResults(term)
}
},
checkIfUpdateResults(term) {
let needToUpdateSearch = term === this.results.query && this.currentTab.searchType !== 'all'
if (needToUpdateSearch) {
let resetObj = { data: [], next: 0, total: 0, loaded: false }
this.results[this.currentTab.searchType + 'Tab'] = { ...resetObj }
this.search(this.currentTab.searchType)
}
},
showNewResults(term) {
socket.emit('mainSearch', { term })
// Showing loading placeholder
this.$root.$emit('updateSearchLoadingState', true)
this.currentTab = this.tabs[0]
},
checkLoadMoreContent(searchSelected) {
if (this.results[searchSelected.split('_')[0] + 'Tab'].data.length !== 0) return
this.search(searchSelected.split('_')[0])
},
addToQueue(e) {
sendAddToQueue(e.currentTarget.dataset.link)
},
numberWithDots,
convertDuration,
search(type) {
socket.emit('search', {
term: this.results.query,
type,
start: this.results[`${type}Tab`].next,
nb: 30
})
},
scrolledSearch() {
if (this.currentTab.searchType === 'all') return
let currentTab = `${this.currentTab.searchType}Tab`
if (this.results[currentTab].next < this.results[currentTab].total) {
this.search(this.currentTab.searchType)
}
},
handleMainSearch(result) {
// Hiding loading placeholder
this.$root.$emit('updateSearchLoadingState', false)
let resetObj = { data: [], next: 0, total: 0, loaded: false }
this.results.allTab = result
this.results.trackTab = { ...resetObj }
this.results.albumTab = { ...resetObj }
this.results.artistTab = { ...resetObj }
this.results.playlistTab = { ...resetObj }
this.results.query = result.QUERY
},
handleSearch(result) {
const { next: nextResult, total, type, data } = result
let currentTab = type + 'Tab'
let next = 0
if (nextResult) {
next = parseInt(nextResult.match(/index=(\d*)/)[1])
} else {
next = total
}
if (this.results[currentTab].total != total) {
this.results[currentTab].total = total
}
if (this.results[currentTab].next != next) {
this.results[currentTab].next = next
this.results[currentTab].data = this.results[currentTab].data.concat(data)
}
this.results[currentTab].loaded = true
},
isTabLoaded(tab) {
return this.loadedTabs.indexOf(tab.searchType) !== -1 || tab.searchType === 'all'
}
},
watch: {
performScrolledSearch(needToSearch) {
if (!needToSearch) return
this.scrolledSearch(needToSearch)
},
currentTab(newTab) {
if (this.isTabLoaded(newTab)) return
this.search(newTab.searchType)
}
}
}
</script>
<style>
</style>

View File

@@ -0,0 +1,862 @@
<template>
<div id="settings_tab" class="main_tabcontent fixed_footer" ref="root">
<h2 class="page_heading">{{ $t('settings.title') }}</h2>
<div id="logged_in_info" v-if="isLoggedIn" ref="loggedInInfo">
<img id="settings_picture" :src="pictureHref" alt="Profile Picture" ref="userpicture" class="circle" />
<i18n path="settings.login.loggedIn" tag="p">
<strong place="username" id="settings_username" ref="username">{{ user.name || 'not logged' }}</strong>
</i18n>
<button id="settings_btn_logout" @click="logout">{{ $t('settings.login.logout') }}</button>
<select v-if="accounts.length" id="family_account" v-model="accountNum" @change="changeAccount">
<option v-for="(account, i) in accounts" :key="account" :value="i.toString()">{{ account.BLOG_NAME }}</option>
</select>
</div>
<div class="settings-group">
<h3 class="settings-group__header settings-group__header--with-icon">
<i class="material-icons">person</i>{{ $t('settings.login.title') }}
</h3>
<div class="inline-flex">
<input
autocomplete="off"
type="password"
:value="arl"
id="login_input_arl"
ref="loginInput"
placeholder="ARL"
/>
<button id="settings_btn_copyArl" class="only_icon" @click="copyARLtoClipboard">
<i class="material-icons">assignment</i>
</button>
</div>
<a href="https://codeberg.org/RemixDev/deemix/wiki/Getting-your-own-ARL" target="_blank">
{{ $t('settings.login.arl.question') }}
</a>
<a id="settings_btn_applogin" v-if="clientMode" href="#" @click="appLogin">
{{ $t('settings.login.login') }}
</a>
<button id="settings_btn_updateArl" @click="login" style="width: 100%">
{{ $t('settings.login.arl.update') }}
</button>
</div>
<div class="settings-group">
<h3 class="settings-group__header settings-group__header--with-icon">
<i class="material-icons">language</i>{{ $t('settings.languages') }}
</h3>
<div>
<span
v-for="locale in locales"
:key="locale"
class="locale-flag"
:class="{ 'locale-flag--current': currentLocale === locale }"
@click="changeLocale(locale)"
v-html="flags[locale]"
:title="locale"
>
</span>
</div>
</div>
<div class="settings-group">
<h3 class="settings-group__header settings-group__header--with-icon">
<i class="material-icons">web</i>{{ $t('settings.appearance.title') }}
</h3>
<label class="with_checkbox">
<input type="checkbox" v-model="changeSlimDownloads" />
<span class="checkbox_text">{{ $t('settings.appearance.slimDownloadTab') }}</span>
</label>
</div>
<div class="settings-group">
<h3 class="settings-group__header settings-group__header--with-icon">
<i class="material-icons">folder</i>{{ $t('settings.downloadPath.title') }}
</h3>
<div class="inline-flex">
<input autocomplete="off" type="text" v-model="settings.downloadLocation" />
<button id="select_downloads_folder" v-if="clientMode" class="only_icon" @click="selectDownloadFolder">
<i class="material-icons">folder</i>
</button>
</div>
</div>
<div class="settings-group">
<h3 class="settings-group__header settings-group__header--with-icon">
<i class="material-icons">font_download</i>{{ $t('settings.templates.title') }}
</h3>
<p>{{ $t('settings.templates.tracknameTemplate') }}</p>
<input type="text" v-model="settings.tracknameTemplate" />
<p>{{ $t('settings.templates.albumTracknameTemplate') }}</p>
<input type="text" v-model="settings.albumTracknameTemplate" />
<p>{{ $t('settings.templates.playlistTracknameTemplate') }}</p>
<input type="text" v-model="settings.playlistTracknameTemplate" />
</div>
<div class="settings-group">
<h3 class="settings-group__header settings-group__header--with-icon">
<i class="material-icons">create_new_folder</i>{{ $t('settings.folders.title') }}
</h3>
<div class="settings-container">
<div class="settings-container__third">
<label class="with_checkbox">
<input type="checkbox" v-model="settings.createPlaylistFolder" />
<span class="checkbox_text">{{ $t('settings.folders.createPlaylistFolder') }}</span>
</label>
<div class="input_group" v-if="settings.createPlaylistFolder">
<p class="input_group_text">{{ $t('settings.folders.playlistNameTemplate') }}</p>
<input type="text" v-model="settings.playlistNameTemplate" />
</div>
</div>
<div class="settings-container__third">
<label class="with_checkbox">
<input type="checkbox" v-model="settings.createArtistFolder" />
<span class="checkbox_text">{{ $t('settings.folders.createArtistFolder') }}</span>
</label>
<div class="input_group" v-if="settings.createArtistFolder">
<p class="input_group_text">{{ $t('settings.folders.artistNameTemplate') }}</p>
<input type="text" v-model="settings.artistNameTemplate" />
</div>
</div>
<div class="settings-container__third">
<label class="with_checkbox">
<input type="checkbox" v-model="settings.createAlbumFolder" />
<span class="checkbox_text">{{ $t('settings.folders.createAlbumFolder') }}</span>
</label>
<div class="input_group" v-if="settings.createAlbumFolder">
<p class="input_group_text">{{ $t('settings.folders.albumNameTemplate') }}</p>
<input type="text" v-model="settings.albumNameTemplate" />
</div>
</div>
</div>
<label class="with_checkbox">
<input type="checkbox" v-model="settings.createCDFolder" />
<span class="checkbox_text">{{ $t('settings.folders.createCDFolder') }}</span>
</label>
<label class="with_checkbox">
<input type="checkbox" v-model="settings.createStructurePlaylist" />
<span class="checkbox_text">{{ $t('settings.folders.createStructurePlaylist') }}</span>
</label>
<label class="with_checkbox">
<input type="checkbox" v-model="settings.createSingleFolder" />
<span class="checkbox_text">{{ $t('settings.folders.createSingleFolder') }}</span>
</label>
</div>
<div class="settings-group">
<h3 class="settings-group__header settings-group__header--with-icon">
<i class="material-icons">title</i>{{ $t('settings.trackTitles.title') }}
</h3>
<div class="settings-container">
<div class="settings-container__third settings-container__third--only-checkbox">
<label class="with_checkbox">
<input type="checkbox" v-model="settings.padTracks" />
<span class="checkbox_text">{{ $t('settings.trackTitles.padTracks') }}</span>
</label>
</div>
<div class="settings-container__third">
<div class="input_group">
<p class="input_group_text">{{ $t('settings.trackTitles.paddingSize') }}</p>
<input max="10" type="number" v-model="settings.paddingSize" />
</div>
</div>
<div class="settings-container__third">
<div class="input_group">
<p class="input_group_text">{{ $t('settings.trackTitles.illegalCharacterReplacer') }}</p>
<input type="text" v-model="settings.illegalCharacterReplacer" />
</div>
</div>
</div>
</div>
<div class="settings-group">
<h3 class="settings-group__header settings-group__header--with-icon">
<i class="material-icons">get_app</i>{{ $t('settings.downloads.title') }}
</h3>
<div class="input_group">
<p class="input_group_text">{{ $t('settings.downloads.queueConcurrency') }}</p>
<input type="number" min="1" v-model.number="settings.queueConcurrency" />
</div>
<div class="input_group">
<p class="input_group_text">{{ $t('settings.downloads.maxBitrate.title') }}</p>
<select v-model="settings.maxBitrate">
<option value="9">{{ $t('settings.downloads.maxBitrate.9') }}</option>
<option value="3">{{ $t('settings.downloads.maxBitrate.3') }}</option>
<option value="1">{{ $t('settings.downloads.maxBitrate.1') }}</option>
</select>
</div>
<div class="input_group">
<p class="input_group_text">{{ $t('settings.downloads.overwriteFile.title') }}</p>
<select v-model="settings.overwriteFile">
<option value="y">{{ $t('settings.downloads.overwriteFile.y') }}</option>
<option value="n">{{ $t('settings.downloads.overwriteFile.n') }}</option>
<option value="e">{{ $t('settings.downloads.overwriteFile.e') }}</option>
<option value="b">{{ $t('settings.downloads.overwriteFile.b') }}</option>
<option value="t">{{ $t('settings.downloads.overwriteFile.t') }}</option>
</select>
</div>
<div class="settings-container">
<div class="settings-container__third settings-container__third--only-checkbox">
<label class="with_checkbox">
<input type="checkbox" v-model="settings.fallbackBitrate" />
<span class="checkbox_text">{{ $t('settings.downloads.fallbackBitrate') }}</span>
</label>
<label class="with_checkbox">
<input type="checkbox" v-model="settings.fallbackSearch" />
<span class="checkbox_text">{{ $t('settings.downloads.fallbackSearch') }}</span>
</label>
</div>
<div class="settings-container__third settings-container__third--only-checkbox">
<label class="with_checkbox">
<input type="checkbox" v-model="settings.logErrors" />
<span class="checkbox_text">{{ $t('settings.downloads.logErrors') }}</span>
</label>
<label class="with_checkbox">
<input type="checkbox" v-model="settings.logSearched" />
<span class="checkbox_text">{{ $t('settings.downloads.logSearched') }}</span>
</label>
</div>
<div class="settings-container__third settings-container__third--only-checkbox">
<label class="with_checkbox">
<input type="checkbox" v-model="settings.syncedLyrics" />
<span class="checkbox_text">{{ $t('settings.downloads.syncedLyrics') }}</span>
</label>
<label class="with_checkbox">
<input type="checkbox" v-model="settings.createM3U8File" />
<span class="checkbox_text">{{ $t('settings.downloads.createM3U8File') }}</span>
</label>
</div>
</div>
<div class="input_group" v-if="settings.createM3U8File">
<p class="input_group_text">{{ $t('settings.downloads.playlistFilenameTemplate') }}</p>
<input type="text" v-model="settings.playlistFilenameTemplate" />
</div>
<label class="with_checkbox">
<input type="checkbox" v-model="settings.saveDownloadQueue" />
<span class="checkbox_text">{{ $t('settings.downloads.saveDownloadQueue') }}</span>
</label>
</div>
<div class="settings-group">
<h3 class="settings-group__header settings-group__header--with-icon">
<i class="material-icons">album</i>{{ $t('settings.covers.title') }}
</h3>
<label class="with_checkbox">
<input type="checkbox" v-model="settings.saveArtwork" />
<span class="checkbox_text">{{ $t('settings.covers.saveArtwork') }}</span>
</label>
<div class="input_group" v-if="settings.saveArtwork">
<p class="input_group_text">{{ $t('settings.covers.coverImageTemplate') }}</p>
<input type="text" v-model="settings.coverImageTemplate" />
</div>
<label class="with_checkbox">
<input type="checkbox" v-model="settings.saveArtworkArtist" />
<span class="checkbox_text">{{ $t('settings.covers.saveArtworkArtist') }}</span>
</label>
<div class="input_group" v-if="settings.saveArtworkArtist">
<p class="input_group_text">{{ $t('settings.covers.artistImageTemplate') }}</p>
<input type="text" v-model="settings.artistImageTemplate" />
</div>
<div class="input_group">
<p class="input_group_text">{{ $t('settings.covers.localArtworkSize') }}</p>
<input type="number" min="100" max="10000" step="100" v-model.number="settings.localArtworkSize" />
<p v-if="settings.localArtworkSize > 1200" class="input_group_text" style="opacity: 0.75; color: #ffcc22">
{{ $t('settings.covers.imageSizeWarning') }}
</p>
</div>
<div class="input_group">
<p class="input_group_text">{{ $t('settings.covers.embeddedArtworkSize') }}</p>
<input type="number" min="100" max="10000" step="100" v-model.number="settings.embeddedArtworkSize" />
<p v-if="settings.embeddedArtworkSize > 1200" class="input_group_text" style="opacity: 0.75; color: #ffcc22">
{{ $t('settings.covers.imageSizeWarning') }}
</p>
</div>
<div class="input_group">
<p class="input_group_text">{{ $t('settings.covers.localArtworkFormat.title') }}</p>
<select v-model="settings.localArtworkFormat">
<option value="jpg">{{ $t('settings.covers.localArtworkFormat.jpg') }}</option>
<option value="png">{{ $t('settings.covers.localArtworkFormat.png') }}</option>
<option value="jpg,png">{{ $t('settings.covers.localArtworkFormat.both') }}</option>
</select>
</div>
<label class="with_checkbox">
<input type="checkbox" v-model="settings.embeddedArtworkPNG" />
<span class="checkbox_text">{{ $t('settings.covers.embeddedArtworkPNG') }}</span>
</label>
<p v-if="settings.embeddedArtworkPNG" style="opacity: 0.75; color: #ffcc22">
{{ $t('settings.covers.embeddedPNGWarning') }}
</p>
<label class="with_checkbox">
<input type="checkbox" v-model="settings.tags.coverDescriptionUTF8" />
<span class="checkbox_text">{{ $t('settings.covers.coverDescriptionUTF8') }}</span>
</label>
<div class="input_group">
<p class="input_group_text">{{ $t('settings.covers.jpegImageQuality') }}</p>
<input type="number" min="1" max="100" v-model.number="settings.jpegImageQuality" />
</div>
</div>
<div class="settings-group">
<h3 class="settings-group__header settings-group__header--with-icon">
<i class="material-icons" style="width: 1em; height: 1em">bookmarks</i>{{ $t('settings.tags.head') }}
</h3>
<div class="settings-container">
<div class="settings-container__half">
<label class="with_checkbox">
<input type="checkbox" v-model="settings.tags.title" />
<span class="checkbox_text">{{ $t('settings.tags.title') }}</span>
</label>
<label class="with_checkbox">
<input type="checkbox" v-model="settings.tags.artist" />
<span class="checkbox_text">{{ $t('settings.tags.artist') }}</span>
</label>
<label class="with_checkbox">
<input type="checkbox" v-model="settings.tags.album" />
<span class="checkbox_text">{{ $t('settings.tags.album') }}</span>
</label>
<label class="with_checkbox">
<input type="checkbox" v-model="settings.tags.cover" />
<span class="checkbox_text">{{ $t('settings.tags.cover') }}</span>
</label>
<label class="with_checkbox">
<input type="checkbox" v-model="settings.tags.trackNumber" />
<span class="checkbox_text">{{ $t('settings.tags.trackNumber') }}</span>
</label>
<label class="with_checkbox">
<input type="checkbox" v-model="settings.tags.trackTotal" />
<span class="checkbox_text">{{ $t('settings.tags.trackTotal') }}</span>
</label>
<label class="with_checkbox">
<input type="checkbox" v-model="settings.tags.discNumber" />
<span class="checkbox_text">{{ $t('settings.tags.discNumber') }}</span>
</label>
<label class="with_checkbox">
<input type="checkbox" v-model="settings.tags.discTotal" />
<span class="checkbox_text">{{ $t('settings.tags.discTotal') }}</span>
</label>
<label class="with_checkbox">
<input type="checkbox" v-model="settings.tags.albumArtist" />
<span class="checkbox_text">{{ $t('settings.tags.albumArtist') }}</span>
</label>
<label class="with_checkbox">
<input type="checkbox" v-model="settings.tags.genre" />
<span class="checkbox_text">{{ $t('settings.tags.genre') }}</span>
</label>
<label class="with_checkbox">
<input type="checkbox" v-model="settings.tags.year" />
<span class="checkbox_text">{{ $t('settings.tags.year') }}</span>
</label>
<label class="with_checkbox">
<input type="checkbox" v-model="settings.tags.date" />
<span class="checkbox_text">{{ $t('settings.tags.date') }}</span>
</label>
</div>
<div class="settings-container__half">
<label class="with_checkbox">
<input type="checkbox" v-model="settings.tags.explicit" />
<span class="checkbox_text">{{ $t('settings.tags.explicit') }}</span>
</label>
<label class="with_checkbox">
<input type="checkbox" v-model="settings.tags.isrc" />
<span class="checkbox_text">{{ $t('settings.tags.isrc') }}</span>
</label>
<label class="with_checkbox">
<input type="checkbox" v-model="settings.tags.length" />
<span class="checkbox_text">{{ $t('settings.tags.length') }}</span>
</label>
<label class="with_checkbox">
<input type="checkbox" v-model="settings.tags.barcode" />
<span class="checkbox_text">{{ $t('settings.tags.barcode') }}</span>
</label>
<label class="with_checkbox">
<input type="checkbox" v-model="settings.tags.bpm" />
<span class="checkbox_text">{{ $t('settings.tags.bpm') }}</span>
</label>
<label class="with_checkbox">
<input type="checkbox" v-model="settings.tags.replayGain" />
<span class="checkbox_text">{{ $t('settings.tags.replayGain') }}</span>
</label>
<label class="with_checkbox">
<input type="checkbox" v-model="settings.tags.label" />
<span class="checkbox_text">{{ $t('settings.tags.label') }}</span>
</label>
<label class="with_checkbox">
<input type="checkbox" v-model="settings.tags.lyrics" />
<span class="checkbox_text">{{ $t('settings.tags.lyrics') }}</span>
</label>
<label class="with_checkbox">
<input type="checkbox" v-model="settings.tags.syncedLyrics" />
<span class="checkbox_text">{{ $t('settings.tags.syncedLyrics') }}</span>
</label>
<label class="with_checkbox">
<input type="checkbox" v-model="settings.tags.copyright" />
<span class="checkbox_text">{{ $t('settings.tags.copyright') }}</span>
</label>
<label class="with_checkbox">
<input type="checkbox" v-model="settings.tags.composer" />
<span class="checkbox_text">{{ $t('settings.tags.composer') }}</span>
</label>
<label class="with_checkbox">
<input type="checkbox" v-model="settings.tags.involvedPeople" />
<span class="checkbox_text">{{ $t('settings.tags.involvedPeople') }}</span>
</label>
</div>
</div>
</div>
<div class="settings-group">
<h3 class="settings-group__header settings-group__header--with-icon">
<i class="material-icons">list</i>{{ $t('settings.other.title') }}
</h3>
<label class="with_checkbox">
<input type="checkbox" v-model="settings.tags.savePlaylistAsCompilation" />
<span class="checkbox_text">{{ $t('settings.other.savePlaylistAsCompilation') }}</span>
</label>
<label class="with_checkbox">
<input type="checkbox" v-model="settings.tags.useNullSeparator" />
<span class="checkbox_text">{{ $t('settings.other.useNullSeparator') }}</span>
</label>
<label class="with_checkbox">
<input type="checkbox" v-model="settings.tags.saveID3v1" />
<span class="checkbox_text">{{ $t('settings.other.saveID3v1') }}</span>
</label>
<div class="input_group">
<p class="input_group_text">{{ $t('settings.other.multiArtistSeparator.title') }}</p>
<select v-model="settings.tags.multiArtistSeparator">
<option value="nothing">{{ $t('settings.other.multiArtistSeparator.nothing') }}</option>
<option value="default">{{ $t('settings.other.multiArtistSeparator.default') }}</option>
<option value="andFeat">{{ $t('settings.other.multiArtistSeparator.andFeat') }}</option>
<option value=" & ">{{ $t('settings.other.multiArtistSeparator.using', { separator: ' & ' }) }}</option>
<option value=",">{{ $t('settings.other.multiArtistSeparator.using', { separator: ',' }) }}</option>
<option value=", ">{{ $t('settings.other.multiArtistSeparator.using', { separator: ', ' }) }}</option>
<option value="/">{{ $t('settings.other.multiArtistSeparator.using', { separator: '/' }) }}</option>
<option value=" / ">{{ $t('settings.other.multiArtistSeparator.using', { separator: ' / ' }) }}</option>
<option value=";">{{ $t('settings.other.multiArtistSeparator.using', { separator: ';' }) }}</option>
<option value="; ">{{ $t('settings.other.multiArtistSeparator.using', { separator: '; ' }) }}</option>
</select>
</div>
<label class="with_checkbox">
<input type="checkbox" v-model="settings.tags.singleAlbumArtist" />
<span class="checkbox_text">{{ $t('settings.other.singleAlbumArtist') }}</span>
</label>
<label class="with_checkbox">
<input type="checkbox" v-model="settings.albumVariousArtists" />
<span class="checkbox_text">{{ $t('settings.other.albumVariousArtists') }}</span>
</label>
<label class="with_checkbox">
<input type="checkbox" v-model="settings.removeAlbumVersion" />
<span class="checkbox_text">{{ $t('settings.other.removeAlbumVersion') }}</span>
</label>
<label class="with_checkbox">
<input type="checkbox" v-model="settings.removeDuplicateArtists" />
<span class="checkbox_text">{{ $t('settings.other.removeDuplicateArtists') }}</span>
</label>
<div class="input_group">
<p class="input_group_text">{{ $t('settings.other.dateFormat.title') }}</p>
<select v-model="settings.dateFormat">
<option value="Y-M-D">
{{
`${$t('settings.other.dateFormat.year')}-${$t('settings.other.dateFormat.month')}-${$t(
'settings.other.dateFormat.day'
)}`
}}
</option>
<option value="Y-D-M">
{{
`${$t('settings.other.dateFormat.year')}-${$t('settings.other.dateFormat.day')}-${$t(
'settings.other.dateFormat.month'
)}`
}}
</option>
<option value="D-M-Y">
{{
`${$t('settings.other.dateFormat.day')}-${$t('settings.other.dateFormat.month')}-${$t(
'settings.other.dateFormat.year'
)}`
}}
</option>
<option value="M-D-Y">
{{
`${$t('settings.other.dateFormat.month')}-${$t('settings.other.dateFormat.day')}-${$t(
'settings.other.dateFormat.year'
)}`
}}
</option>
<option value="Y">{{ $t('settings.other.dateFormat.year') }}</option>
</select>
</div>
<div class="input_group">
<p class="input_group_text">{{ $t('settings.other.featuredToTitle.title') }}</p>
<select v-model="settings.featuredToTitle">
<option value="0">{{ $t('settings.other.featuredToTitle.0') }}</option>
<option value="1">{{ $t('settings.other.featuredToTitle.1') }}</option>
<option value="3">{{ $t('settings.other.featuredToTitle.3') }}</option>
<option value="2">{{ $t('settings.other.featuredToTitle.2') }}</option>
</select>
</div>
<div class="input_group">
<p class="input_group_text">{{ $t('settings.other.titleCasing') }}</p>
<select v-model="settings.titleCasing">
<option value="nothing">{{ $t('settings.other.casing.nothing') }}</option>
<option value="lower">{{ $t('settings.other.casing.lower') }}</option>
<option value="upper">{{ $t('settings.other.casing.upper') }}</option>
<option value="start">{{ $t('settings.other.casing.start') }}</option>
<option value="sentence">{{ $t('settings.other.casing.sentence') }}</option>
</select>
</div>
<div class="input_group">
<p class="input_group_text">{{ $t('settings.other.artistCasing') }}</p>
<select v-model="settings.artistCasing">
<option value="nothing">{{ $t('settings.other.casing.nothing') }}</option>
<option value="lower">{{ $t('settings.other.casing.lower') }}</option>
<option value="upper">{{ $t('settings.other.casing.upper') }}</option>
<option value="start">{{ $t('settings.other.casing.start') }}</option>
<option value="sentence">{{ $t('settings.other.casing.sentence') }}</option>
</select>
</div>
<div class="input_group">
<p class="input_group_text">{{ $t('settings.other.previewVolume') }}</p>
<input
type="range"
@change="updateMaxVolume"
min="0"
max="100"
step="1"
class="slider"
v-model.number="previewVolume.preview_max_volume"
/>
<span>{{ previewVolume.preview_max_volume }}%</span>
</div>
<div class="input_group">
<p class="input_group_text">{{ $t('settings.other.executeCommand.title') }}</p>
<p class="secondary-text">{{ $t('settings.other.executeCommand.description') }}</p>
<input type="text" v-model="settings.executeCommand" />
</div>
</div>
<div class="settings-group">
<h3 class="settings-group__header settings-group__header--with-icon">
<svg id="spotify_icon" enable-background="new 0 0 24 24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path
d="m12 24c6.624 0 12-5.376 12-12s-5.376-12-12-12-12 5.376-12 12 5.376 12 12 12zm4.872-6.344v.001c-.807 0-3.356-2.828-10.52-1.36-.189.049-.436.126-.576.126-.915 0-1.09-1.369-.106-1.578 3.963-.875 8.013-.798 11.467 1.268.824.526.474 1.543-.265 1.543zm1.303-3.173c-.113-.03-.08.069-.597-.203-3.025-1.79-7.533-2.512-11.545-1.423-.232.063-.358.126-.576.126-1.071 0-1.355-1.611-.188-1.94 4.716-1.325 9.775-.552 13.297 1.543.392.232.547.533.547.953-.005.522-.411.944-.938.944zm-13.627-7.485c4.523-1.324 11.368-.906 15.624 1.578 1.091.629.662 2.22-.498 2.22l-.001-.001c-.252 0-.407-.063-.625-.189-3.443-2.056-9.604-2.549-13.59-1.436-.175.048-.393.125-.625.125-.639 0-1.127-.499-1.127-1.142 0-.657.407-1.029.842-1.155z"
/>
</svg>
{{ $t('settings.spotify.title') }}
</h3>
<a href="https://codeberg.org/RemixDev/deemix/wiki/Enabling-Spotify-Features" target="_blank">
{{ $t('settings.spotify.question') }}
</a>
<div class="input_group">
<p class="input_group_text">{{ $t('settings.spotify.clientID') }}</p>
<input type="text" v-model="spotifyFeatures.clientId" />
</div>
<div class="input_group">
<p class="input_group_text">{{ $t('settings.spotify.clientSecret') }}</p>
<input type="password" v-model="spotifyFeatures.clientSecret" />
</div>
<div class="input_group">
<p class="input_group_text">{{ $t('settings.spotify.username') }}</p>
<input type="text" v-model="spotifyUser" />
</div>
</div>
<footer>
<button @click="resetSettings">{{ $t('settings.reset') }}</button>
<button @click="saveSettings">{{ $t('settings.save') }}</button>
</footer>
</div>
</template>
<style lang="scss">
#logged_in_info {
height: 250px;
display: flex;
flex-direction: column;
justify-content: space-evenly;
align-items: center;
}
.locale-flag {
width: 60px;
display: inline-flex;
justify-content: center;
align-items: center;
cursor: pointer;
&:not(:last-child) {
margin-right: 10px;
}
&.locale-flag--current {
svg {
filter: brightness(1);
}
}
svg {
width: 40px !important;
height: 40px !important;
filter: brightness(0.5);
}
}
</style>
<script>
import { mapActions, mapGetters } from 'vuex'
import { getSettingsData } from '@/data/settings'
import { toast } from '@/utils/toasts'
import { socket } from '@/utils/socket'
import { flags } from '@/utils/flags'
export default {
data() {
return {
flags,
currentLocale: this.$i18n.locale,
locales: this.$i18n.availableLocales,
settings: {
tags: {}
},
lastSettings: {},
spotifyFeatures: {},
lastCredentials: {},
defaultSettings: {},
lastUser: '',
spotifyUser: '',
slimDownloads: false,
previewVolume: window.vol,
accountNum: 0,
accounts: []
}
},
computed: {
...mapGetters({
arl: 'getARL',
user: 'getUser',
isLoggedIn: 'isLoggedIn',
clientMode: 'getClientMode'
}),
needToWait() {
return Object.keys(this.getSettings).length === 0
},
changeSlimDownloads: {
get() {
return this.slimDownloads
},
set(wantSlimDownloads) {
this.slimDownloads = wantSlimDownloads
document.getElementById('download_list').classList.toggle('slim', wantSlimDownloads)
localStorage.setItem('slimDownloads', wantSlimDownloads)
}
},
pictureHref() {
// Default image: https://e-cdns-images.dzcdn.net/images/user/125x125-000000-80-0-0.jpg
return `https://e-cdns-images.dzcdn.net/images/user/${this.user.picture}/125x125-000000-80-0-0.jpg`
}
},
async mounted() {
const { settingsData, defaultSettingsData, spotifyCredentials } = await getSettingsData()
this.defaultSettings = defaultSettingsData
this.initSettings(settingsData, spotifyCredentials)
let storedAccountNum = localStorage.getItem('accountNum')
if (storedAccountNum) {
this.accountNum = storedAccountNum
}
let spotifyUser = localStorage.getItem('spotifyUser')
if (spotifyUser) {
this.lastUser = spotifyUser
this.spotifyUser = spotifyUser
socket.emit('update_userSpotifyPlaylists', spotifyUser)
}
this.changeSlimDownloads = 'true' === localStorage.getItem('slimDownloads')
let volume = parseInt(localStorage.getItem('previewVolume'))
if (isNaN(volume)) {
volume = 80
localStorage.setItem('previewVolume', volume)
}
window.vol.preview_max_volume = volume
socket.on('updateSettings', this.updateSettings)
socket.on('accountChanged', this.accountChanged)
socket.on('familyAccounts', this.initAccounts)
socket.on('downloadFolderSelected', this.downloadFolderSelected)
socket.on('applogin_arl', this.loggedInViaDeezer)
this.$on('hook:destroyed', () => {
socket.off('updateSettings')
socket.off('accountChanged')
socket.off('familyAccounts')
socket.off('downloadFolderSelected')
socket.off('applogin_arl')
})
},
methods: {
...mapActions({
dispatchARL: 'setARL'
}),
revertSettings() {
this.settings = JSON.parse(JSON.stringify(this.lastSettings))
},
revertCredentials() {
this.spotifyCredentials = JSON.parse(JSON.stringify(this.lastCredentials))
this.spotifyUser = (' ' + this.lastUser).slice(1)
},
copyARLtoClipboard() {
let copyText = this.$refs.loginInput
copyText.setAttribute('type', 'text')
copyText.select()
copyText.setSelectionRange(0, 99999)
document.execCommand('copy')
copyText.setAttribute('type', 'password')
toast(this.$t('settings.toasts.ARLcopied'), 'assignment')
},
changeLocale(newLocale) {
this.$i18n.locale = newLocale
this.currentLocale = newLocale
localStorage.setItem('locale', newLocale)
},
updateMaxVolume() {
localStorage.setItem('previewVolume', this.previewVolume.preview_max_volume)
},
saveSettings() {
this.lastSettings = JSON.parse(JSON.stringify(this.settings))
this.lastCredentials = JSON.parse(JSON.stringify(this.spotifyFeatures))
let changed = false
if (this.lastUser != this.spotifyUser) {
// force cloning without linking
this.lastUser = (' ' + this.spotifyUser).slice(1)
localStorage.setItem('spotifyUser', this.lastUser)
changed = true
}
socket.emit('saveSettings', this.lastSettings, this.lastCredentials, changed ? this.lastUser : false)
},
selectDownloadFolder() {
socket.emit('selectDownloadFolder')
},
downloadFolderSelected(folder) {
this.$set(this.settings, 'downloadLocation', folder)
},
loadSettings(data) {
this.lastSettings = JSON.parse(JSON.stringify(data))
this.settings = JSON.parse(JSON.stringify(data))
},
loadCredentials(credentials) {
this.lastCredentials = JSON.parse(JSON.stringify(credentials))
this.spotifyFeatures = JSON.parse(JSON.stringify(credentials))
},
loggedInViaDeezer(arl) {
this.dispatchARL({ arl })
socket.emit('login', arl, true, this.accountNum)
// this.login()
},
login() {
let newArl = this.$refs.loginInput.value.trim()
if (newArl && newArl !== this.arl) {
socket.emit('login', newArl, true, this.accountNum)
}
},
appLogin(e) {
socket.emit('applogin')
},
changeAccount() {
socket.emit('changeAccount', this.accountNum)
},
accountChanged(user, accountNum) {
this.$refs.username.innerText = user.name
this.$refs.userpicture.src = `https://e-cdns-images.dzcdn.net/images/user/${user.picture}/125x125-000000-80-0-0.jpg`
this.accountNum = accountNum
localStorage.setItem('accountNum', this.accountNum)
},
initAccounts(accounts) {
this.accounts = accounts
},
logout() {
socket.emit('logout')
},
initSettings(settings, credentials) {
// this.loadDefaultSettings()
this.loadSettings(settings)
this.loadCredentials(credentials)
toast(this.$t('settings.toasts.init'), 'settings')
},
updateSettings(newSettings, newCredentials) {
this.loadSettings(newSettings)
this.loadCredentials(newCredentials)
toast(this.$t('settings.toasts.update'), 'settings')
},
resetSettings() {
this.settings = JSON.parse(JSON.stringify(this.defaultSettings))
}
}
}
</script>

View File

@@ -0,0 +1,306 @@
<template>
<div id="tracklist_tab" class="main_tabcontent fixed_footer image_header" ref="root">
<header
:style="{
'background-image':
'linear-gradient(to bottom, transparent 0%, var(--main-background) 100%), url(\'' + image + '\')'
}"
>
<h1 class="inline-flex">
{{ title }} <i v-if="explicit" class="material-icons explicit_icon explicit_icon--right">explicit</i>
</h1>
<h2 class="inline-flex">
<span v-if="metadata">{{ metadata }}</span>
<span class="right" v-if="release_date">{{ release_date }}</span>
</h2>
</header>
<table class="table table--tracklist">
<thead>
<tr>
<th>
<i class="material-icons">music_note</i>
</th>
<th>#</th>
<th>{{ $tc('globals.listTabs.title', 1) }}</th>
<th>{{ $tc('globals.listTabs.artist', 1) }}</th>
<th v-if="type === 'playlist'">{{ $tc('globals.listTabs.album', 1) }}</th>
<th>
<i class="material-icons">timer</i>
</th>
<th class="table__icon table__cell--center clickable">
<input @click="toggleAll" class="selectAll" type="checkbox" />
</th>
</tr>
</thead>
<tbody>
<template v-if="type !== 'spotifyPlaylist'">
<template v-for="(track, index) in body">
<tr v-if="track.type == 'track'" @click="selectRow(index, track)">
<td class="table__cell--x-small table__cell--center">
<div class="table__cell-content table__cell-content--vertical-center">
<i
class="material-icons"
:class="{ preview_playlist_controls: track.preview, disabled: !track.preview }"
v-on="{ click: track.preview ? playPausePreview : false }"
:data-preview="track.preview"
:title="$t('globals.play_hint')"
>
play_arrow
</i>
</div>
</td>
<td class="table__cell--small table__cell--center track_position">
{{ type === 'album' ? track.track_position : body.indexOf(track) + 1 }}
</td>
<td class="table__cell--large table__cell--with-icon">
<div class="table__cell-content table__cell-content--vertical-center">
<i v-if="track.explicit_lyrics" class="material-icons explicit_icon"> explicit </i>
{{
track.title +
(track.title_version && track.title.indexOf(track.title_version) == -1
? ' ' + track.title_version
: '')
}}
</div>
</td>
<router-link
tag="td"
class="table__cell--medium table__cell--center clickable"
:to="{ name: 'Artist', params: { id: track.artist.id } }"
>
{{ track.artist.name }}
</router-link>
<router-link
tag="td"
v-if="type === 'playlist'"
class="table__cell--medium table__cell--center clickable"
:to="{ name: 'Album', params: { id: track.album.id } }"
>
{{ track.album.title }}
</router-link>
<td
class="table__cell--center"
:class="{ 'table__cell--small': type === 'album', 'table__cell--x-small': type === 'playlist' }"
>
{{ convertDuration(track.duration) }}
</td>
<td class="table__icon table__cell--center">
<input class="clickable" type="checkbox" v-model="track.selected" />
</td>
</tr>
<tr v-else-if="track.type == 'disc_separator'" class="table__row-no-highlight" style="opacity: 0.54">
<td>
<div class="table__cell-content table__cell-content--vertical-center" style="opacity: 0.54">
<i class="material-icons">album</i>
</div>
</td>
<td class="table__cell--center">
{{ track.number }}
</td>
<td colspan="4"></td>
</tr>
</template>
</template>
<template v-else>
<tr v-for="(track, i) in body">
<td>
<i
v-if="track.preview_url"
@click="playPausePreview"
:class="'material-icons' + (track.preview_url ? ' preview_playlist_controls' : '')"
:data-preview="track.preview_url"
:title="$t('globals.play_hint')"
>
play_arrow
</i>
<i v-else class="material-icons disabled">play_arrow</i>
</td>
<td>{{ i + 1 }}</td>
<td class="inline-flex">
<i v-if="track.explicit" class="material-icons explicit_icon">explicit</i>
{{ track.name }}
</td>
<td>{{ track.artists[0].name }}</td>
<td>{{ track.album.name }}</td>
<td>{{ convertDuration(Math.floor(track.duration_ms / 1000)) }}</td>
<td><input class="clickable" type="checkbox" v-model="track.selected" /></td>
</tr>
</template>
</tbody>
</table>
<span v-if="label" style="opacity: 0.4; margin-top: 8px; display: inline-block; font-size: 13px">{{ label }}</span>
<footer>
<button @click.stop="addToQueue" :data-link="link">
{{ `${$t('globals.download', { thing: $tc(`globals.listTabs.${type}`, 1) })}` }}
</button>
<button class="with_icon" @click.stop="addToQueue" :data-link="selectedLinks()">
{{ $t('tracklist.downloadSelection') }}<i class="material-icons">file_download</i>
</button>
<button class="back-button" @click="$router.back()">{{ $t('globals.back') }}</button>
</footer>
</div>
</template>
<script>
import { isEmpty } from 'lodash-es'
import { socket } from '@/utils/socket'
import Downloads from '@/utils/downloads'
import Utils from '@/utils/utils'
import EventBus from '@/utils/EventBus'
export default {
data() {
return {
title: '',
metadata: '',
release_date: '',
label: '',
explicit: false,
image: '',
type: 'empty',
link: '',
body: []
}
},
methods: {
playPausePreview(e) {
EventBus.$emit('trackPreview:playPausePreview', e)
},
reset() {
this.title = 'Loading...'
this.image = ''
this.metadata = ''
this.label = ''
this.release_date = ''
this.explicit = false
this.type = 'empty'
this.body = []
},
addToQueue(e) {
Downloads.sendAddToQueue(e.currentTarget.dataset.link)
},
toggleAll(e) {
this.body.forEach(item => {
if (item.type == 'track') {
item.selected = e.currentTarget.checked
}
})
},
selectedLinks() {
var selected = []
if (this.body) {
this.body.forEach(item => {
if (item.type == 'track' && item.selected)
selected.push(this.type == 'spotifyPlaylist' ? item.uri : item.link)
})
}
return selected.join(';')
},
convertDuration: Utils.convertDuration,
showAlbum(data) {
this.reset()
const {
id: albumID,
title: albumTitle,
explicit_lyrics,
label: albumLabel,
artist: { name: artistName },
tracks: albumTracks,
tracks: { length: numberOfTracks },
release_date,
cover_xl
} = data
this.type = 'album'
this.link = `https://www.deezer.com/album/${albumID}`
this.title = albumTitle
this.explicit = explicit_lyrics
this.label = albumLabel
this.metadata = `${artistName}${this.$tc('globals.listTabs.trackN', numberOfTracks)}`
this.release_date = release_date.substring(0, 10)
this.image = cover_xl
if (isEmpty(albumTracks)) {
this.body = null
} else {
this.body = albumTracks
}
},
showPlaylist(data) {
this.reset()
const {
id: playlistID,
title: playlistTitle,
picture_xl: playlistCover,
creation_date,
creator: { name: creatorName },
tracks: playlistTracks,
tracks: { length: numberOfTracks }
} = data
this.type = 'playlist'
this.link = `https://www.deezer.com/playlist/${playlistID}`
this.title = playlistTitle
this.image = playlistCover
this.release_date = creation_date.substring(0, 10)
this.metadata = `${this.$t('globals.by', { artist: creatorName })} • ${this.$tc(
'globals.listTabs.trackN',
numberOfTracks
)}`
if (isEmpty(playlistTracks)) {
this.body = null
} else {
this.body = playlistTracks
}
},
showSpotifyPlaylist(data) {
this.reset()
const {
uri: playlistURI,
name: playlistName,
images,
images: { length: numberOfImages },
owner: { display_name: ownerName },
tracks: playlistTracks,
tracks: { length: numberOfTracks }
} = data
this.type = 'spotifyPlaylist'
this.link = playlistURI
this.title = playlistName
this.image = numberOfImages
? images[0].url
: 'https://e-cdns-images.dzcdn.net/images/cover/d41d8cd98f00b204e9800998ecf8427e/1000x1000-000000-80-0-0.jpg'
this.release_date = ''
this.metadata = `${this.$t('globals.by', { artist: ownerName })} • ${this.$tc(
'globals.listTabs.trackN',
numberOfTracks
)}`
if (isEmpty(playlistTracks)) {
this.body = null
} else {
this.body = playlistTracks
}
},
selectRow(index, track) {
track.selected = !track.selected
}
},
mounted() {
EventBus.$on('tracklistTab:selectRow', this.selectRow)
socket.on('show_album', this.showAlbum)
socket.on('show_playlist', this.showPlaylist)
socket.on('show_spotifyplaylist', this.showSpotifyPlaylist)
}
}
</script>
<style>
</style>