changed directory structure of js and Vue files; added plugin for translation (can try it in settings)
This commit is contained in:
32
src/components/App.vue
Normal file
32
src/components/App.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<div style="height: inherit;">
|
||||
<BaseLoadingPlaceholder id="start_app_placeholder" text="Connecting to the server..." />
|
||||
|
||||
<TheSidebar />
|
||||
<TheMainContent />
|
||||
|
||||
<TheTrackPreview />
|
||||
<TheQualityModal />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TheSidebar from '@components/TheSidebar.vue'
|
||||
import TheMainContent from '@components/TheMainContent.vue'
|
||||
import TheTrackPreview from '@components/TheTrackPreview.vue'
|
||||
import TheQualityModal from '@components/TheQualityModal.vue'
|
||||
import BaseLoadingPlaceholder from '@components/BaseLoadingPlaceholder.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TheSidebar,
|
||||
TheMainContent,
|
||||
TheTrackPreview,
|
||||
TheQualityModal,
|
||||
BaseLoadingPlaceholder
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
196
src/components/ArtistTab.vue
Normal file
196
src/components/ArtistTab.vue
Normal file
@@ -0,0 +1,196 @@
|
||||
<template>
|
||||
<div id="artist_tab" class="main_tabcontent fixed_footer image_header">
|
||||
<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"
|
||||
@contextmenu.prevent="openQualityModal"
|
||||
@click.stop="addToQueue"
|
||||
:data-link="link"
|
||||
class="fab right"
|
||||
>
|
||||
<i class="material-icons">get_app</i>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="tab">
|
||||
<template v-for="(item, name, index) in body">
|
||||
<button
|
||||
:class="'selective' + (name == currentTab ? ' active' : '')"
|
||||
:href="'#artist_' + name"
|
||||
@click="changeTab(name)"
|
||||
>
|
||||
{{ name }}
|
||||
</button>
|
||||
</template>
|
||||
</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
|
||||
}"
|
||||
>
|
||||
{{ data.title }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="release in showTable">
|
||||
<td class="inline-flex clickable" @click="albumView" :data-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>
|
||||
</td>
|
||||
<td>{{ release.release_date }}</td>
|
||||
<td
|
||||
@click.stop="addToQueue"
|
||||
@contextmenu.prevent="openQualityModal"
|
||||
:data-link="release.link"
|
||||
class="clickable"
|
||||
>
|
||||
<i class="material-icons">
|
||||
file_download
|
||||
</i>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<footer>
|
||||
<button class="back-button">Back</button>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { isEmpty, orderBy } from 'lodash-es'
|
||||
import { socket } from '@/utils/socket'
|
||||
import Downloads from '@/utils/downloads'
|
||||
import { showView } from '@js/tabs'
|
||||
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: {
|
||||
albumView: showView.bind(null, 'album'),
|
||||
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)
|
||||
},
|
||||
openQualityModal(e) {
|
||||
this.$root.$emit('QualityModal:open', 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
|
||||
},
|
||||
getCurrentTab() {
|
||||
return this.currentTab
|
||||
},
|
||||
updateSelected() {
|
||||
window.currentStack.selected = this.currentTab
|
||||
},
|
||||
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) {
|
||||
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: 'Title', sortKey: 'title' },
|
||||
{ title: 'Release Date', sortKey: 'release_date' },
|
||||
{ title: '', width: '32px' }
|
||||
]
|
||||
if (isEmpty(releases)) {
|
||||
this.body = null
|
||||
} else {
|
||||
this.body = releases
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
showTable() {
|
||||
if (this.body) return orderBy(this.body[this.currentTab], this.sortKey, this.sortOrder)
|
||||
else return []
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
socket.on('show_artist', this.showArtist)
|
||||
|
||||
EventBus.$on('artistTab:reset', this.reset)
|
||||
EventBus.$on('artistTab:updateSelected', this.updateSelected)
|
||||
EventBus.$on('artistTab:changeTab', this.changeTab)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
26
src/components/BaseLoadingPlaceholder.vue
Normal file
26
src/components/BaseLoadingPlaceholder.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<template functional>
|
||||
<div :id="props.id" class="loading_placeholder">
|
||||
<span class="loading_placeholder__text">{{ props.text }}</span>
|
||||
<div class="lds-ring">
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
text: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'Loading...'
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
82
src/components/TheAboutTab.vue
Normal file
82
src/components/TheAboutTab.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<template functional>
|
||||
<div id="about_tab" class="main_tabcontent">
|
||||
<h2 class="page_heading">About</h2>
|
||||
<p>
|
||||
This app uses the <a href="https://deemix.app" target="_blank">deemix</a> library, you can use this
|
||||
library to make your own UI for deemix.</br>
|
||||
Here's the <a href="https://notabug.org/RemixDev/deemix" target="_blank">official repo</a> for the
|
||||
library.
|
||||
</p>
|
||||
<p>
|
||||
Thanks to rtonno, uhwot and lollilol for helping me with this project.<br>
|
||||
Also thanks to BasCurtiz and <a href="http://linktr.ee/scarvimane" target="_blank">scarvimane</a> for
|
||||
making the icon.
|
||||
</p>
|
||||
<p>
|
||||
Stay up to date with the updates by following the <a href="https://t.me/RemixDevNews"
|
||||
target="_blank">news channel</a> on Telegram.
|
||||
</p>
|
||||
<br />
|
||||
<h1>Bug Reports</h1>
|
||||
<p>
|
||||
If you have questions or problems with the app, search for a solution in the
|
||||
<a href="https://www.reddit.com/r/deemix" target="_blank">subreddit</a> first and then, if you don't
|
||||
find anything
|
||||
you can make a post with your issue on the subreddit.
|
||||
</p>
|
||||
<p>
|
||||
Before reporting a bug make sure you're running the latest version of the app and that the thing you
|
||||
want
|
||||
to report is actually a bug and not something that's wrong only on your end.<br />
|
||||
Make sure the bug is reproducible on another machines and also <b>DO NOT</b> report a bug if it's been
|
||||
already reported.
|
||||
</p>
|
||||
<p>
|
||||
<b>DO NOT</b> open issues for asking questions, there is a subreddit for that.
|
||||
</p>
|
||||
<br />
|
||||
<h2>Donations</h2>
|
||||
<h3>You want to contribute to this project? You can do that <b>in different ways!</b></h3>
|
||||
<p>
|
||||
If you're fluent in python you could try to make a new UI for the app using the base library, or fix
|
||||
bugs in the library with a pull request on the <a href="https://notabug.org/RemixDev/deemix"
|
||||
target="_blank">repo</a>.<br>
|
||||
I accept features as well, but no complex things, as they can be implementend directly in the app and
|
||||
not the library.</p>
|
||||
<p>
|
||||
If you're fluent in another programming language you could try to port deemix into other programming
|
||||
languages!<br>
|
||||
You need help understanding the code? Just hit RemixDev up on Telegram or Reddit.</p>
|
||||
<p>If you know JavaScript, HTML or CSS you could contribute to the <a
|
||||
href="https://notabug.org/RemixDev/deemix-webui" target="_blank">webui</a>.</p>
|
||||
<p>
|
||||
If you find some bugs you can report them in the repo, just make sure your bug isn't something that
|
||||
only
|
||||
affects you and it can be reproducible by other users as well.<br>
|
||||
Duplicate bug reports will be closed, so keep an eye out on that.</p>
|
||||
<hr>
|
||||
<h3>You want to contribute monetarily? You could make a donation!</h3>
|
||||
<p>
|
||||
If you can donate you can do that with this links.<br>
|
||||
You shoud remember that <b>this is a free project</b> and <b>you should support the artists you
|
||||
love</b>
|
||||
before supporting the developers.<br>
|
||||
Don't feel obligated to donate, I appreciate you anyway!</p>
|
||||
<p>
|
||||
<b>PayPal:</b> <a href="https://paypal.me/RemixDev" target="_blank">PayPal.me/RemixDev</a><br>
|
||||
<b>Bitcoin:</b> 1sdNymSJrMBWyHM4u2m9uco5nv6uV4Qs1<br>
|
||||
<b>Ethereum:</b> 0x1d2aa67e671485CD4062289772B662e0A6Ff976c
|
||||
</p>
|
||||
<br />
|
||||
<h2>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><br />
|
||||
This work is licensed under a <a rel="license" href="https://www.gnu.org/licenses/gpl-3.0.en.html"
|
||||
target="_blank">GNU General Public License 3.0</a>.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
202
src/components/TheChartsTab.vue
Normal file
202
src/components/TheChartsTab.vue
Normal file
@@ -0,0 +1,202 @@
|
||||
<template>
|
||||
<div id="charts_tab" class="main_tabcontent">
|
||||
<h2 class="page_heading">Charts</h2>
|
||||
<div v-if="country === ''" id="charts_selection">
|
||||
<div class="release_grid charts_grid">
|
||||
<!-- Ugly af -->
|
||||
<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="changeCountry">Change Country</button>
|
||||
<button
|
||||
@contextmenu.prevent="openQualityModal"
|
||||
@click.stop="addToQueue"
|
||||
:data-link="'https://www.deezer.com/playlist/' + id"
|
||||
>
|
||||
Download Chart
|
||||
</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"
|
||||
>
|
||||
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>
|
||||
<td
|
||||
class="table__cell--medium table__cell--center breakline clickable"
|
||||
@click="artistView"
|
||||
:data-id="track.artist.id"
|
||||
>
|
||||
{{ track.artist.name }}
|
||||
</td>
|
||||
<td
|
||||
class="table__cell--medium table__cell--center breakline clickable"
|
||||
@click="albumView"
|
||||
:data-id="track.album.id"
|
||||
>
|
||||
{{ track.album.title }}
|
||||
</td>
|
||||
<td class="table__cell--small table__cell--center">
|
||||
{{ convertDuration(track.duration) }}
|
||||
</td>
|
||||
<td
|
||||
class="table__cell--download"
|
||||
@contextmenu.prevent="openQualityModal"
|
||||
@click.stop="addToQueue"
|
||||
:data-link="track.link"
|
||||
role="button"
|
||||
aria-label="download"
|
||||
>
|
||||
<i class="material-icons">get_app</i>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { socket } from '@/utils/socket'
|
||||
import { showView } from '@js/tabs.js'
|
||||
import Downloads from '@/utils/downloads'
|
||||
import Utils from '@/utils/utils'
|
||||
|
||||
export default {
|
||||
name: 'the-charts-tab',
|
||||
data() {
|
||||
return {
|
||||
country: '',
|
||||
id: 0,
|
||||
countries: [],
|
||||
chart: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
artistView: showView.bind(null, 'artist'),
|
||||
albumView: showView.bind(null, 'album'),
|
||||
playPausePreview(e) {
|
||||
EventBus.$emit('trackPreview:playPausePreview', e)
|
||||
},
|
||||
previewMouseEnter(e) {
|
||||
EventBus.$emit('trackPreview:previewMouseEnter', e)
|
||||
},
|
||||
previewMouseLeave(e) {
|
||||
EventBus.$emit('trackPreview:previewMouseLeave', e)
|
||||
},
|
||||
convertDuration: Utils.convertDuration,
|
||||
addToQueue(e) {
|
||||
e.stopPropagation()
|
||||
Downloads.sendAddToQueue(e.currentTarget.dataset.link)
|
||||
},
|
||||
openQualityModal(e) {
|
||||
this.$root.$emit('QualityModal:open', 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
|
||||
},
|
||||
changeCountry() {
|
||||
this.country = ''
|
||||
this.id = 0
|
||||
},
|
||||
initCharts(data) {
|
||||
this.countries = data
|
||||
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)
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
socket.on('init_charts', this.initCharts)
|
||||
socket.on('setChartTracks', this.setTracklist)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
72
src/components/TheContent.vue
Normal file
72
src/components/TheContent.vue
Normal file
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<section id="content" @scroll="handleContentScroll" ref="content">
|
||||
<div id="container">
|
||||
<ArtistTab />
|
||||
<TheChartsTab />
|
||||
<TheFavoritesTab />
|
||||
<TheErrorsTab />
|
||||
<TheHomeTab />
|
||||
<TheLinkAnalyzerTab />
|
||||
<TheAboutTab />
|
||||
<TheSettingsTab />
|
||||
<TheMainSearch :scrolled-search-type="newScrolled" />
|
||||
<TracklistTab />
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ArtistTab from '@components/ArtistTab.vue'
|
||||
import TracklistTab from '@components/TracklistTab.vue'
|
||||
|
||||
import TheChartsTab from '@components/TheChartsTab.vue'
|
||||
import TheFavoritesTab from '@components/TheFavoritesTab.vue'
|
||||
import TheErrorsTab from '@components/TheErrorsTab.vue'
|
||||
import TheHomeTab from '@components/TheHomeTab.vue'
|
||||
import TheLinkAnalyzerTab from '@components/TheLinkAnalyzerTab.vue'
|
||||
import TheAboutTab from '@components/TheAboutTab.vue'
|
||||
import TheSettingsTab from '@components/TheSettingsTab.vue'
|
||||
import TheMainSearch from '@components/TheMainSearch.vue'
|
||||
|
||||
import { debounce } from '@/utils/utils'
|
||||
import EventBus from '@/utils/EventBus.js'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ArtistTab,
|
||||
TheChartsTab,
|
||||
TheFavoritesTab,
|
||||
TheErrorsTab,
|
||||
TheHomeTab,
|
||||
TheLinkAnalyzerTab,
|
||||
TheAboutTab,
|
||||
TheSettingsTab,
|
||||
TheMainSearch,
|
||||
TracklistTab
|
||||
},
|
||||
data: () => ({
|
||||
newScrolled: null
|
||||
}),
|
||||
methods: {
|
||||
handleContentScroll: debounce(async function() {
|
||||
if (this.$refs.content.scrollTop + this.$refs.content.clientHeight < this.$refs.content.scrollHeight) return
|
||||
|
||||
if (
|
||||
main_selected !== 'search_tab' ||
|
||||
['track_search', 'album_search', 'artist_search', 'playlist_search'].indexOf(window.search_selected) === -1
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
this.newScrolled = window.search_selected.split('_')[0]
|
||||
|
||||
await this.$nextTick()
|
||||
|
||||
this.newScrolled = null
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
351
src/components/TheDownloadTab.vue
Normal file
351
src/components/TheDownloadTab.vue
Normal file
@@ -0,0 +1,351 @@
|
||||
<template>
|
||||
<div
|
||||
id="download_tab_container"
|
||||
class="tab_hidden"
|
||||
@transitionend="$refs.container.style.transition = ''"
|
||||
ref="container"
|
||||
>
|
||||
<div id="download_tab_drag_handler" @mousedown.prevent="startDrag" ref="dragHandler"></div>
|
||||
<i
|
||||
id="toggle_download_tab"
|
||||
class="material-icons download_bar_icon"
|
||||
@click.prevent="toggleDownloadTab"
|
||||
ref="toggler"
|
||||
></i>
|
||||
<div id="queue_buttons">
|
||||
<i id="open_downloads_folder" class="material-icons download_bar_icon hide" @click="openDownloadsFolder">
|
||||
folder_open
|
||||
</i>
|
||||
<i id="clean_queue" class="material-icons download_bar_icon" @click="cleanQueue">clear_all</i>
|
||||
<i id="cancel_queue" class="material-icons download_bar_icon" @click="cancelQueue">delete_sweep</i>
|
||||
</div>
|
||||
<div id="download_list" @click="handleListClick" ref="list"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import $ from 'jquery'
|
||||
import { socket } from '@/utils/socket'
|
||||
import { toast } from '@/utils/toasts'
|
||||
|
||||
const tabMinWidth = 250
|
||||
const tabMaxWidth = 500
|
||||
|
||||
export default {
|
||||
data: () => ({
|
||||
cachedTabWidth: parseInt(localStorage.getItem('downloadTabWidth')) || 300,
|
||||
queue: [],
|
||||
queueList: {},
|
||||
queueComplete: []
|
||||
}),
|
||||
mounted() {
|
||||
socket.on('startDownload', this.startDownload)
|
||||
socket.on('init_downloadQueue', this.initQueue)
|
||||
socket.on('addedToQueue', this.addToQueue)
|
||||
socket.on('updateQueue', this.updateQueue)
|
||||
socket.on('removedFromQueue', this.removeFromQueue)
|
||||
socket.on('finishDownload', this.finishDownload)
|
||||
socket.on('removedAllDownloads', this.removeAllDownloads)
|
||||
socket.on('removedFinishedDownloads', this.removedFinishedDownloads)
|
||||
|
||||
// Check if download tab has slim entries
|
||||
if ('true' === localStorage.getItem('slimDownloads')) {
|
||||
this.$refs.list.classList.add('slim')
|
||||
}
|
||||
|
||||
if ('true' === localStorage.getItem('downloadTabOpen')) {
|
||||
this.$refs.container.classList.remove('tab_hidden')
|
||||
|
||||
this.setTabWidth(this.cachedTabWidth)
|
||||
}
|
||||
|
||||
document.addEventListener('mouseup', () => {
|
||||
document.removeEventListener('mousemove', this.handleDrag)
|
||||
})
|
||||
|
||||
window.addEventListener('beforeunload', () => {
|
||||
localStorage.setItem('downloadTabWidth', this.cachedTabWidth)
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
setTabWidth(newWidth) {
|
||||
if (undefined === newWidth) {
|
||||
this.$refs.container.style.width = ''
|
||||
this.$refs.list.style.width = ''
|
||||
} else {
|
||||
this.$refs.container.style.width = newWidth + 'px'
|
||||
this.$refs.list.style.width = newWidth + 'px'
|
||||
}
|
||||
},
|
||||
handleListClick(event) {
|
||||
const { target } = event
|
||||
|
||||
if (!target.matches('.queue_icon[data-uuid]')) {
|
||||
return
|
||||
}
|
||||
|
||||
let icon = target.innerText
|
||||
let uuid = $(target).data('uuid')
|
||||
|
||||
switch (icon) {
|
||||
case 'remove':
|
||||
socket.emit('removeFromQueue', uuid)
|
||||
break
|
||||
default:
|
||||
}
|
||||
},
|
||||
initQueue(data) {
|
||||
const { queue: initQueue, queueComplete: initQueueComplete, currentItem, queueList: initQueueList } = data
|
||||
|
||||
if (initQueueComplete.length) {
|
||||
initQueueComplete.forEach(item => {
|
||||
initQueueList[item].init = true
|
||||
this.addToQueue(initQueueList[item])
|
||||
})
|
||||
}
|
||||
|
||||
if (currentItem) {
|
||||
initQueueList[currentItem].init = true
|
||||
this.addToQueue(initQueueList[currentItem], true)
|
||||
}
|
||||
|
||||
initQueue.forEach(item => {
|
||||
initQueueList[item].init = true
|
||||
this.addToQueue(initQueueList[item])
|
||||
})
|
||||
},
|
||||
addToQueue(queueItem, current = false) {
|
||||
this.queueList[queueItem.uuid] = queueItem
|
||||
|
||||
if (queueItem.downloaded + queueItem.failed == queueItem.size) {
|
||||
if (this.queueComplete.indexOf(queueItem.uuid) == -1) {
|
||||
this.queueComplete.push(queueItem.uuid)
|
||||
}
|
||||
} else {
|
||||
if (this.queue.indexOf(queueItem.uuid) == -1) {
|
||||
this.queue.push(queueItem.uuid)
|
||||
}
|
||||
}
|
||||
|
||||
let queueDOM = document.getElementById('download_' + queueItem.uuid)
|
||||
|
||||
if (typeof queueDOM == 'undefined' || queueDOM == null) {
|
||||
$(this.$refs.list).append(
|
||||
`<div class="download_object" id="download_${queueItem.uuid}" data-deezerid="${queueItem.id}">
|
||||
<div class="download_info">
|
||||
<img width="75px" class="rounded coverart" src="${queueItem.cover}" alt="Cover ${queueItem.title}"/>
|
||||
<div class="download_info_data">
|
||||
<span class="download_line">${queueItem.title}</span> <span class="download_slim_separator"> - </span>
|
||||
<span class="secondary-text">${queueItem.artist}</span>
|
||||
</div>
|
||||
<div class="download_info_status">
|
||||
<span class="download_line"><span class="queue_downloaded">${queueItem.downloaded + queueItem.failed}</span>/${
|
||||
queueItem.size
|
||||
}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="download_bar">
|
||||
<div class="progress"><div id="bar_${queueItem.uuid}" class="indeterminate"></div></div>
|
||||
<i class="material-icons queue_icon" data-uuid="${queueItem.uuid}">remove</i>
|
||||
</div>
|
||||
</div>`
|
||||
)
|
||||
}
|
||||
|
||||
if (queueItem.progress > 0 || current) {
|
||||
this.startDownload(queueItem.uuid)
|
||||
}
|
||||
|
||||
$('#bar_' + queueItem.uuid).css('width', queueItem.progress + '%')
|
||||
|
||||
if (queueItem.failed >= 1 && $('#download_' + queueItem.uuid + ' .queue_failed').length == 0) {
|
||||
$('#download_' + queueItem.uuid + ' .download_info_status').append(
|
||||
`<span class="secondary-text inline-flex"><span class="download_slim_separator">(</span><span class="queue_failed_button inline-flex"><span class="queue_failed">${queueItem.failed}</span><i class="material-icons">error_outline</i></span><span class="download_slim_separator">)</span></span>`
|
||||
)
|
||||
}
|
||||
|
||||
if (queueItem.downloaded + queueItem.failed == queueItem.size) {
|
||||
let resultIcon = $('#download_' + queueItem.uuid).find('.queue_icon')
|
||||
|
||||
if (queueItem.failed == 0) {
|
||||
resultIcon.text('done')
|
||||
} else {
|
||||
let failedButton = $('#download_' + queueItem.uuid).find('.queue_failed_button')
|
||||
|
||||
resultIcon.addClass('clickable')
|
||||
failedButton.addClass('clickable')
|
||||
|
||||
resultIcon.bind('click', { item: queueItem }, this.showErrorsTab)
|
||||
failedButton.bind('click', { item: queueItem }, this.showErrorsTab)
|
||||
|
||||
if (queueItem.failed >= queueItem.size) {
|
||||
resultIcon.text('error')
|
||||
} else {
|
||||
resultIcon.text('warning')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!queueItem.init) {
|
||||
toast(`${queueItem.title} added to queue`, 'playlist_add_check')
|
||||
}
|
||||
},
|
||||
updateQueue(update) {
|
||||
// downloaded and failed default to false?
|
||||
const { uuid, downloaded, failed, progress, error, data } = update
|
||||
|
||||
if (uuid && this.queue.indexOf(uuid) > -1) {
|
||||
if (downloaded) {
|
||||
this.queueList[uuid].downloaded++
|
||||
$('#download_' + uuid + ' .queue_downloaded').text(
|
||||
this.queueList[uuid].downloaded + this.queueList[uuid].failed
|
||||
)
|
||||
}
|
||||
|
||||
if (failed) {
|
||||
this.queueList[uuid].failed++
|
||||
$('#download_' + uuid + ' .queue_downloaded').text(
|
||||
this.queueList[uuid].downloaded + this.queueList[uuid].failed
|
||||
)
|
||||
if (this.queueList[uuid].failed == 1 && $('#download_' + uuid + ' .queue_failed').length == 0) {
|
||||
$('#download_' + uuid + ' .download_info_status').append(
|
||||
`<span class="secondary-text inline-flex"><span class="download_slim_separator">(</span><span class="queue_failed_button inline-flex"><span class="queue_failed">1</span> <i class="material-icons">error_outline</i></span><span class="download_slim_separator">)</span></span>`
|
||||
)
|
||||
} else {
|
||||
$('#download_' + uuid + ' .queue_failed').text(this.queueList[uuid].failed)
|
||||
}
|
||||
|
||||
this.queueList[uuid].errors.push({ message: error, data: data })
|
||||
}
|
||||
|
||||
if (progress) {
|
||||
this.queueList[uuid].progress = progress
|
||||
$('#bar_' + uuid).css('width', progress + '%')
|
||||
}
|
||||
}
|
||||
},
|
||||
removeFromQueue(uuid) {
|
||||
let index = this.queue.indexOf(uuid)
|
||||
|
||||
if (index > -1) {
|
||||
this.queue.splice(index, 1)
|
||||
$(`#download_${this.queueList[uuid].uuid}`).remove()
|
||||
delete this.queueList[uuid]
|
||||
}
|
||||
},
|
||||
removeAllDownloads(currentItem) {
|
||||
this.queueComplete = []
|
||||
|
||||
if (currentItem == '') {
|
||||
this.queue = []
|
||||
this.queueList = {}
|
||||
$(listEl).html('')
|
||||
} else {
|
||||
this.queue = [currentItem]
|
||||
let tempQueueItem = this.queueList[currentItem]
|
||||
this.queueList = {}
|
||||
this.queueList[currentItem] = tempQueueItem
|
||||
|
||||
$('.download_object').each(function(index) {
|
||||
if ($(this).attr('id') != 'download_' + currentItem) $(this).remove()
|
||||
})
|
||||
}
|
||||
},
|
||||
removedFinishedDownloads() {
|
||||
this.queueComplete.forEach(item => {
|
||||
$('#download_' + item).remove()
|
||||
})
|
||||
|
||||
this.queueComplete = []
|
||||
},
|
||||
toggleDownloadTab(clickEvent) {
|
||||
this.setTabWidth()
|
||||
|
||||
this.$refs.container.style.transition = 'all 250ms ease-in-out'
|
||||
|
||||
// Toggle returns a Boolean based on the action it performed
|
||||
let isHidden = this.$refs.container.classList.toggle('tab_hidden')
|
||||
|
||||
if (!isHidden) {
|
||||
this.setTabWidth(this.cachedTabWidth)
|
||||
}
|
||||
|
||||
localStorage.setItem('downloadTabOpen', !isHidden)
|
||||
},
|
||||
cleanQueue() {
|
||||
socket.emit('removeFinishedDownloads')
|
||||
},
|
||||
cancelQueue() {
|
||||
socket.emit('cancelAllDownloads')
|
||||
},
|
||||
finishDownload(uuid) {
|
||||
if (this.queue.indexOf(uuid) > -1) {
|
||||
toast(`${this.queueList[uuid].title} finished downloading.`, 'done')
|
||||
|
||||
$('#bar_' + uuid).css('width', '100%')
|
||||
|
||||
let resultIcon = $('#download_' + uuid).find('.queue_icon')
|
||||
|
||||
if (this.queueList[uuid].failed == 0) {
|
||||
resultIcon.text('done')
|
||||
} else {
|
||||
let failedButton = $('#download_' + uuid).find('.queue_failed_button')
|
||||
|
||||
resultIcon.addClass('clickable')
|
||||
failedButton.addClass('clickable')
|
||||
|
||||
resultIcon.bind('click', { item: this.queueList[uuid] }, this.showErrorsTab)
|
||||
failedButton.bind('click', { item: this.queueList[uuid] }, this.showErrorsTab)
|
||||
|
||||
if (this.queueList[uuid].failed >= this.queueList[uuid].size) {
|
||||
resultIcon.text('error')
|
||||
} else {
|
||||
resultIcon.text('warning')
|
||||
}
|
||||
}
|
||||
|
||||
let index = this.queue.indexOf(uuid)
|
||||
if (index > -1) {
|
||||
this.queue.splice(index, 1)
|
||||
this.queueComplete.push(uuid)
|
||||
}
|
||||
|
||||
if (this.queue.length <= 0) {
|
||||
toast('All downloads completed!', 'done_all')
|
||||
}
|
||||
}
|
||||
},
|
||||
openDownloadsFolder() {
|
||||
if (window.clientMode) {
|
||||
socket.emit('openDownloadsFolder')
|
||||
}
|
||||
},
|
||||
handleDrag(event) {
|
||||
let newWidth = window.innerWidth - event.pageX + 2
|
||||
|
||||
if (newWidth < tabMinWidth) {
|
||||
newWidth = tabMinWidth
|
||||
} else if (newWidth > tabMaxWidth) {
|
||||
newWidth = tabMaxWidth
|
||||
}
|
||||
|
||||
this.cachedTabWidth = newWidth
|
||||
this.setTabWidth(newWidth)
|
||||
},
|
||||
startDrag() {
|
||||
document.addEventListener('mousemove', this.handleDrag)
|
||||
},
|
||||
startDownload(uuid) {
|
||||
$('#bar_' + uuid)
|
||||
.removeClass('indeterminate')
|
||||
.addClass('determinate')
|
||||
},
|
||||
showErrorsTab(clickEvent) {
|
||||
this.$root.$emit('showTabErrors', clickEvent.data.item, clickEvent.target)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
52
src/components/TheErrorsTab.vue
Normal file
52
src/components/TheErrorsTab.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div id="errors_tab" class="main_tabcontent">
|
||||
<h1>Errors for {{ title }}</h1>
|
||||
<table>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Artist</th>
|
||||
<th>Title</th>
|
||||
<th>Error</th>
|
||||
</tr>
|
||||
<tr v-for="error in errors">
|
||||
<td>{{ error.data.id }}</td>
|
||||
<td>{{ error.data.artist }}</td>
|
||||
<td>{{ error.data.title }}</td>
|
||||
<td>{{ error.message }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { changeTab } from '@js/tabs.js'
|
||||
|
||||
import EventBus from '@/utils/EventBus'
|
||||
|
||||
export default {
|
||||
name: 'the-errors-tab',
|
||||
data: () => ({
|
||||
title: '',
|
||||
errors: []
|
||||
}),
|
||||
methods: {
|
||||
reset() {
|
||||
this.title = ''
|
||||
this.errors = []
|
||||
},
|
||||
showErrors(data, eventTarget) {
|
||||
this.title = data.artist + ' - ' + data.title
|
||||
this.errors = data.errors
|
||||
|
||||
changeTab(eventTarget, 'main', 'errors_tab')
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
EventBus.$on('showTabErrors', this.showErrors)
|
||||
this.$root.$on('showTabErrors', this.showErrors)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
310
src/components/TheFavoritesTab.vue
Normal file
310
src/components/TheFavoritesTab.vue
Normal file
@@ -0,0 +1,310 @@
|
||||
<template>
|
||||
<div id="favorites_tab" class="main_tabcontent" @click="handleFavoritesTabClick">
|
||||
<h2 class="page_heading">
|
||||
Favorites
|
||||
<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" id="favorites_playlist_tab">Playlists</div>
|
||||
<div class="section-tabs__tab favorites_tablinks" id="favorites_album_tab">Albums</div>
|
||||
<div class="section-tabs__tab favorites_tablinks" id="favorites_artist_tab">Artists</div>
|
||||
<div class="section-tabs__tab favorites_tablinks" id="favorites_track_tab">Tracks</div>
|
||||
</div>
|
||||
|
||||
<div id="playlist_favorites" class="favorites_tabcontent">
|
||||
<div v-if="playlists.length == 0">
|
||||
<h1>No Playlists found</h1>
|
||||
</div>
|
||||
<div class="release_grid" v-if="playlists.length > 0 || spotifyPlaylists > 0">
|
||||
<div v-for="release in playlists" class="release clickable" @click="playlistView" :data-id="release.id">
|
||||
<div class="cover_container">
|
||||
<img aria-hidden="true" class="rounded coverart" :src="release.picture_medium" />
|
||||
<div
|
||||
role="button"
|
||||
aria-label="download"
|
||||
@contextmenu.prevent="openQualityModal"
|
||||
@click.stop="addToQueue"
|
||||
:data-link="release.link"
|
||||
class="download_overlay"
|
||||
>
|
||||
<i class="material-icons">get_app</i>
|
||||
</div>
|
||||
</div>
|
||||
<p class="primary-text">{{ release.title }}</p>
|
||||
<p class="secondary-text">{{ 'by ' + release.creator.name + ' - ' + release.nb_tracks + ' tracks' }}</p>
|
||||
</div>
|
||||
<div
|
||||
v-for="release in spotifyPlaylists"
|
||||
class="release clickable"
|
||||
@click="spotifyPlaylistView"
|
||||
:data-id="release.id"
|
||||
>
|
||||
<div class="cover_container">
|
||||
<img aria-hidden="true" class="rounded coverart" :src="release.picture_medium" />
|
||||
<div
|
||||
role="button"
|
||||
aria-label="download"
|
||||
@contextmenu.prevent="openQualityModal"
|
||||
@click.stop="addToQueue"
|
||||
:data-link="release.link"
|
||||
class="download_overlay"
|
||||
>
|
||||
<i class="material-icons">get_app</i>
|
||||
</div>
|
||||
</div>
|
||||
<p class="primary-text">{{ release.title }}</p>
|
||||
<p class="secondary-text">{{ 'by ' + release.creator.name + ' - ' + release.nb_tracks + ' tracks' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="album_favorites" class="favorites_tabcontent">
|
||||
<div v-if="albums.length == 0">
|
||||
<h1>No Favorite Albums found</h1>
|
||||
</div>
|
||||
<div class="release_grid" v-if="albums.length > 0">
|
||||
<div v-for="release in albums" class="release clickable" @click="albumView" :data-id="release.id">
|
||||
<div class="cover_container">
|
||||
<img aria-hidden="true" class="rounded coverart" :src="release.cover_medium" />
|
||||
<div
|
||||
role="button"
|
||||
aria-label="download"
|
||||
@contextmenu.prevent="openQualityModal"
|
||||
@click.stop="addToQueue"
|
||||
:data-link="release.link"
|
||||
class="download_overlay"
|
||||
>
|
||||
<i class="material-icons">get_app</i>
|
||||
</div>
|
||||
</div>
|
||||
<p class="primary-text">{{ release.title }}</p>
|
||||
<p class="secondary-text">{{ 'by ' + release.artist.name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="artist_favorites" class="favorites_tabcontent">
|
||||
<div v-if="artists.length == 0">
|
||||
<h1>No Favorite Artist found</h1>
|
||||
</div>
|
||||
<div class="release_grid" v-if="artists.length > 0">
|
||||
<div v-for="release in artists" class="release clickable" @click="artistView" :data-id="release.id">
|
||||
<div class="cover_container">
|
||||
<img aria-hidden="true" class="circle coverart" :src="release.picture_medium" />
|
||||
<div
|
||||
role="button"
|
||||
aria-label="download"
|
||||
@contextmenu.prevent="openQualityModal"
|
||||
@click.stop="addToQueue"
|
||||
:data-link="release.link"
|
||||
class="download_overlay"
|
||||
>
|
||||
<i class="material-icons">get_app</i>
|
||||
</div>
|
||||
</div>
|
||||
<p class="primary-text">{{ release.name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="track_favorites" class="favorites_tabcontent">
|
||||
<div v-if="tracks.length == 0">
|
||||
<h1>No Favorite Tracks found</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"
|
||||
>
|
||||
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>
|
||||
<td
|
||||
class="table__cell--medium table__cell--center breakline clickable"
|
||||
@click="artistView"
|
||||
:data-id="track.artist.id"
|
||||
>
|
||||
{{ track.artist.name }}
|
||||
</td>
|
||||
<td
|
||||
class="table__cell--medium table__cell--center breakline clickable"
|
||||
@click="albumView"
|
||||
:data-id="track.album.id"
|
||||
>
|
||||
{{ track.album.title }}
|
||||
</td>
|
||||
<td class="table__cell--small">
|
||||
{{ convertDuration(track.duration) }}
|
||||
</td>
|
||||
<td
|
||||
class="table__cell--download clickable"
|
||||
@contextmenu.prevent="openQualityModal"
|
||||
@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">get_app</i>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { socket } from '@/utils/socket'
|
||||
import { showView, changeTab } from '@js/tabs.js'
|
||||
import Downloads from '@/utils/downloads'
|
||||
import Utils from '@/utils/utils'
|
||||
import { toast } from '@/utils/toasts'
|
||||
|
||||
export default {
|
||||
name: 'the-favorites-tab',
|
||||
data() {
|
||||
return {
|
||||
tracks: [],
|
||||
albums: [],
|
||||
artists: [],
|
||||
playlists: [],
|
||||
spotifyPlaylists: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
artistView: showView.bind(null, 'artist'),
|
||||
albumView: showView.bind(null, 'album'),
|
||||
playlistView: showView.bind(null, 'playlist'),
|
||||
spotifyPlaylistView: showView.bind(null, 'spotifyplaylist'),
|
||||
playPausePreview(e) {
|
||||
EventBus.$emit('trackPreview:playPausePreview', e)
|
||||
},
|
||||
previewMouseEnter(e) {
|
||||
EventBus.$emit('trackPreview:previewMouseEnter', e)
|
||||
},
|
||||
previewMouseLeave(e) {
|
||||
EventBus.$emit('trackPreview:previewMouseLeave', e)
|
||||
},
|
||||
convertDuration: Utils.convertDuration,
|
||||
handleFavoritesTabClick(event) {
|
||||
const {
|
||||
target,
|
||||
target: { id }
|
||||
} = event
|
||||
let selectedTab = null
|
||||
|
||||
switch (id) {
|
||||
case 'favorites_playlist_tab':
|
||||
selectedTab = 'playlist_favorites'
|
||||
break
|
||||
case 'favorites_album_tab':
|
||||
selectedTab = 'album_favorites'
|
||||
break
|
||||
case 'favorites_artist_tab':
|
||||
selectedTab = 'artist_favorites'
|
||||
break
|
||||
case 'favorites_track_tab':
|
||||
selectedTab = 'track_favorites'
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if (!selectedTab) return
|
||||
|
||||
changeTab(target, 'favorites', selectedTab)
|
||||
},
|
||||
addToQueue(e) {
|
||||
e.stopPropagation()
|
||||
Downloads.sendAddToQueue(e.currentTarget.dataset.link)
|
||||
},
|
||||
openQualityModal(e) {
|
||||
this.$root.$emit('QualityModal:open', 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) {
|
||||
const { tracks, albums, artists, playlists } = data
|
||||
this.tracks = tracks
|
||||
this.albums = albums
|
||||
this.artists = artists
|
||||
this.playlists = playlists
|
||||
|
||||
// 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('Refresh completed!', 'done', true)
|
||||
},
|
||||
{ once: true }
|
||||
)
|
||||
},
|
||||
initFavorites(data) {
|
||||
this.updated_userFavorites(data)
|
||||
document.getElementById('favorites_playlist_tab').click()
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
socket.on('init_favorites', this.initFavorites)
|
||||
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)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
102
src/components/TheHomeTab.vue
Normal file
102
src/components/TheHomeTab.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<div id="home_tab" class="main_tabcontent">
|
||||
<h2 class="page_heading">{{ $t('welcome') }}</h2>
|
||||
<section id="home_not_logged_in" class="home_section" ref="notLogged">
|
||||
<p id="home_not_logged_text">You need to log into your deezer account before you can start downloading.</p>
|
||||
<button type="button" name="button" @click="openSettings">Open Settings</button>
|
||||
</section>
|
||||
<section v-if="playlists.length" class="home_section">
|
||||
<h3 class="section_heading">Popular playlists</h3>
|
||||
<div class="release_grid">
|
||||
<div v-for="release in playlists" class="release clickable" @click="playlistView" :data-id="release.id">
|
||||
<div class="cover_container">
|
||||
<img aria-hidden="true" class="rounded coverart" :src="release.picture_medium" />
|
||||
<div
|
||||
role="button"
|
||||
aria-label="download"
|
||||
@contextmenu.prevent="openQualityModal"
|
||||
@click.stop="addToQueue"
|
||||
:data-link="release.link"
|
||||
class="download_overlay"
|
||||
>
|
||||
<i class="material-icons">get_app</i>
|
||||
</div>
|
||||
</div>
|
||||
<p class="primary-text">{{ release.title }}</p>
|
||||
<p class="secondary-text">{{ 'by ' + release.user.name + ' - ' + release.nb_tracks + ' tracks' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section v-if="albums.length" class="home_section">
|
||||
<h3 class="section_heading">Most streamed albums</h3>
|
||||
<div class="release_grid">
|
||||
<div v-for="release in albums" class="release clickable" @click="albumView" :data-id="release.id">
|
||||
<div class="cover_container">
|
||||
<img aria-hidden="true" class="rounded coverart" :src="release.cover_medium" />
|
||||
<div
|
||||
role="button"
|
||||
aria-label="download"
|
||||
@contextmenu.prevent="openQualityModal"
|
||||
@click.stop="addToQueue"
|
||||
:data-link="release.link"
|
||||
class="download_overlay"
|
||||
>
|
||||
<i class="material-icons">get_app</i>
|
||||
</div>
|
||||
</div>
|
||||
<p class="primary-text">{{ release.title }}</p>
|
||||
<p class="secondary-text">{{ 'by ' + release.artist.name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { socket } from '@/utils/socket'
|
||||
import { showView } from '@js/tabs.js'
|
||||
import Downloads from '@/utils/downloads'
|
||||
|
||||
export default {
|
||||
name: 'the-home-tab',
|
||||
data() {
|
||||
return {
|
||||
playlists: [],
|
||||
albums: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
artistView: showView.bind(null, 'artist'),
|
||||
albumView: showView.bind(null, 'album'),
|
||||
playlistView: showView.bind(null, 'playlist'),
|
||||
openSettings() {
|
||||
document.getElementById('main_settings_tablink').click()
|
||||
},
|
||||
addToQueue(e) {
|
||||
Downloads.sendAddToQueue(e.currentTarget.dataset.link)
|
||||
},
|
||||
openQualityModal(e) {
|
||||
this.$root.$emit('QualityModal:open', e.currentTarget.dataset.link)
|
||||
},
|
||||
initHome(data) {
|
||||
const {
|
||||
playlists: { data: playlistData },
|
||||
albums: { data: albumData }
|
||||
} = data
|
||||
|
||||
this.playlists = playlistData
|
||||
this.albums = albumData
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (localStorage.getItem('arl')) {
|
||||
this.$refs.notLogged.classList.add('hide')
|
||||
}
|
||||
|
||||
socket.on('init_home', this.initHome)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
186
src/components/TheLinkAnalyzerTab.vue
Normal file
186
src/components/TheLinkAnalyzerTab.vue
Normal file
@@ -0,0 +1,186 @@
|
||||
<template>
|
||||
<div id="analyzer_tab" class="main_tabcontent image_header">
|
||||
<h2 class="page_heading">Link Analyzer</h2>
|
||||
<div v-if="link == ''">
|
||||
<p>
|
||||
You can use this section to find out more information about the link you are trying to download<br />This is
|
||||
usefull if you're trying to download some tracks that are not available in your country and want to know where
|
||||
they are available
|
||||
</p>
|
||||
</div>
|
||||
<div v-else-if="link == 'error'">
|
||||
<h2>This link is not supported</h2>
|
||||
<p>Seems like this link is not yet supported, try analyzing another one.</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'">
|
||||
by <span class="clickable" @click="artistView" :data-id="data.artist.id">{{ data.artist.name }}</span> • in
|
||||
<span class="clickable" @click="albumView" :data-id="data.album.id">{{ data.album.title }}</span>
|
||||
</h2>
|
||||
<h2 v-else-if="type == 'album'">
|
||||
by <span class="clickable" @click="artistView" :data-id="data.artist.id">{{ data.artist.name }}</span> •
|
||||
{{ data.nb_tracks }} 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">get_app</i>
|
||||
</div>
|
||||
</header>
|
||||
<table class="table">
|
||||
<tr v-if="data.id">
|
||||
<td>ID</td>
|
||||
<td>{{ data.id }}</td>
|
||||
</tr>
|
||||
<tr v-if="data.isrc">
|
||||
<td>ISRC</td>
|
||||
<td>{{ data.isrc }}</td>
|
||||
</tr>
|
||||
<tr v-if="data.upc">
|
||||
<td>UPC</td>
|
||||
<td>{{ data.upc }}</td>
|
||||
</tr>
|
||||
<tr v-if="data.duration">
|
||||
<td>Duration</td>
|
||||
<td>{{ convertDuration(data.duration) }}</td>
|
||||
</tr>
|
||||
<tr v-if="data.disk_number">
|
||||
<td>Disk Number</td>
|
||||
<td>{{ data.disk_number }}</td>
|
||||
</tr>
|
||||
<tr v-if="data.track_position">
|
||||
<td>Track Number</td>
|
||||
<td>{{ data.track_position }}</td>
|
||||
</tr>
|
||||
<tr v-if="data.release_date">
|
||||
<td>Release Date</td>
|
||||
<td>{{ data.release_date }}</td>
|
||||
</tr>
|
||||
<tr v-if="data.bpm">
|
||||
<td>BPM</td>
|
||||
<td>{{ data.bpm }}</td>
|
||||
</tr>
|
||||
<tr v-if="data.label">
|
||||
<td>Label</td>
|
||||
<td>{{ data.label }}</td>
|
||||
</tr>
|
||||
<tr v-if="data.record_type">
|
||||
<td>Record Type</td>
|
||||
<td>{{ data.record_type }}</td>
|
||||
</tr>
|
||||
<tr v-if="data.genres && data.genres.data.length">
|
||||
<td>Genres</td>
|
||||
<td>{{ data.genres.data.map(x => x.name).join('; ') }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div v-if="type == 'album'">
|
||||
<button @click="albumView" :data-id="id">Tracklist</button>
|
||||
</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 { showView } from '@js/tabs.js'
|
||||
import Utils from '@/utils/utils'
|
||||
import EventBus from '@/utils/EventBus'
|
||||
|
||||
export default {
|
||||
name: 'the-link-analyzer-tab',
|
||||
data() {
|
||||
return {
|
||||
title: '',
|
||||
subtitle: '',
|
||||
image: '',
|
||||
data: {},
|
||||
type: '',
|
||||
link: '',
|
||||
id: '0',
|
||||
countries: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
artistView: showView.bind(null, 'artist'),
|
||||
albumView: showView.bind(null, 'album'),
|
||||
convertDuration: Utils.convertDuration,
|
||||
reset() {
|
||||
this.title = 'Loading...'
|
||||
this.subtitle = ''
|
||||
this.image = ''
|
||||
this.data = {}
|
||||
this.type = ''
|
||||
this.link = ''
|
||||
this.countries = []
|
||||
},
|
||||
showTrack(data) {
|
||||
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(Utils.COUNTRIES[cc])
|
||||
this.countries.push(temp)
|
||||
})
|
||||
|
||||
this.data = data
|
||||
},
|
||||
showAlbum(data) {
|
||||
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'
|
||||
}
|
||||
},
|
||||
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>
|
||||
21
src/components/TheMainContent.vue
Normal file
21
src/components/TheMainContent.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<main id="main_content">
|
||||
<TheMiddleSection />
|
||||
<TheDownloadTab />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TheMiddleSection from '@components/TheMiddleSection.vue'
|
||||
import TheDownloadTab from '@components/TheDownloadTab.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TheMiddleSection,
|
||||
TheDownloadTab
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
702
src/components/TheMainSearch.vue
Normal file
702
src/components/TheMainSearch.vue
Normal file
@@ -0,0 +1,702 @@
|
||||
<template>
|
||||
<div id="search_tab" class="main_tabcontent" @click="handleSearchTabClick">
|
||||
<div :class="{ hide: results.query != '' }">
|
||||
<h2>Start searching!</h2>
|
||||
<p>
|
||||
You can search a track, a whole album, an artist, a playlist.... everything! You can also paste a Deezer link
|
||||
</p>
|
||||
</div>
|
||||
<div v-show="results.query !== ''">
|
||||
<ul class="section-tabs">
|
||||
<li class="section-tabs__tab search_tablinks" id="search_all_tab">All</li>
|
||||
<li class="section-tabs__tab search_tablinks" id="search_track_tab">Tracks</li>
|
||||
<li class="section-tabs__tab search_tablinks" id="search_album_tab">Album</li>
|
||||
<li class="section-tabs__tab search_tablinks" id="search_artist_tab">Artist</li>
|
||||
<li class="section-tabs__tab search_tablinks" id="search_playlist_tab">Playlist</li>
|
||||
</ul>
|
||||
<div id="search_tab_content">
|
||||
<!-- ### Main Search Tab ### -->
|
||||
<div id="main_search" class="search_tabcontent">
|
||||
<template v-for="section in results.allTab.ORDER">
|
||||
<section
|
||||
v-if="
|
||||
(section != 'TOP_RESULT' && results.allTab[section].data.length > 0) ||
|
||||
results.allTab[section].length > 0
|
||||
"
|
||||
class="search_section"
|
||||
>
|
||||
<h2
|
||||
@click="changeSearchTab(section)"
|
||||
class="search_header"
|
||||
:class="{ top_result_header: section === 'TOP_RESULT' }"
|
||||
>
|
||||
{{ names[section] }}
|
||||
</h2>
|
||||
<!-- Top result -->
|
||||
<div
|
||||
v-if="section == 'TOP_RESULT'"
|
||||
class="top_result clickable"
|
||||
@click="handleClickTopResult"
|
||||
:data-id="results.allTab.TOP_RESULT[0].id"
|
||||
>
|
||||
<div class="cover_container">
|
||||
<img
|
||||
aria-hidden="true"
|
||||
:src="results.allTab.TOP_RESULT[0].picture"
|
||||
:class="(results.allTab.TOP_RESULT[0].type == 'artist' ? 'circle' : 'rounded') + ' coverart'"
|
||||
/>
|
||||
<div
|
||||
role="button"
|
||||
aria-label="download"
|
||||
@contextmenu.prevent="openQualityModal"
|
||||
@click.stop="addToQueue"
|
||||
:data-link="results.allTab.TOP_RESULT[0].link"
|
||||
class="download_overlay"
|
||||
>
|
||||
<i class="material-icons">get_app</i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info_box">
|
||||
<p class="primary-text">{{ results.allTab.TOP_RESULT[0].title }}</p>
|
||||
<p class="secondary-text">
|
||||
{{
|
||||
results.allTab.TOP_RESULT[0].type == 'artist'
|
||||
? numberWithDots(results.allTab.TOP_RESULT[0].nb_fan) + ' fans'
|
||||
: 'by ' +
|
||||
results.allTab.TOP_RESULT[0].artist +
|
||||
' - ' +
|
||||
results.allTab.TOP_RESULT[0].nb_song +
|
||||
' tracks'
|
||||
}}
|
||||
</p>
|
||||
<span class="tag">{{
|
||||
results.allTab.TOP_RESULT[0].type.charAt(0).toUpperCase() +
|
||||
results.allTab.TOP_RESULT[0].type.substring(1)
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="section == 'TRACK'">
|
||||
<table class="table table--tracks">
|
||||
<tbody>
|
||||
<tr v-for="track in results.allTab.TRACK.data.slice(0, 6)">
|
||||
<td class="table__icon" aria-hidden="true">
|
||||
<img
|
||||
class="rounded coverart"
|
||||
:src="
|
||||
'https://e-cdns-images.dzcdn.net/images/cover/' +
|
||||
track.ALB_PICTURE +
|
||||
'/32x32-000000-80-0-0.jpg'
|
||||
"
|
||||
/>
|
||||
</td>
|
||||
<td class="table__cell table__cell--large breakline">
|
||||
<div class="table__cell-content table__cell-content--vertical-center">
|
||||
<i v-if="track.EXPLICIT_LYRICS == 1" class="material-icons explicit_icon">
|
||||
explicit
|
||||
</i>
|
||||
{{ track.SNG_TITLE + (track.VERSION ? ' ' + track.VERSION : '') }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="table__cell table__cell--medium table__cell--center breakline">
|
||||
<span
|
||||
class="clickable"
|
||||
@click="artistView"
|
||||
:data-id="artist.ART_ID"
|
||||
v-for="artist in track.ARTISTS"
|
||||
>{{ artist.ART_NAME }}
|
||||
</span>
|
||||
</td>
|
||||
<td
|
||||
class="table__cell--medium table__cell--center breakline clickable"
|
||||
@click="albumView"
|
||||
:data-id="track.ALB_ID"
|
||||
>
|
||||
{{ track.ALB_TITLE }}
|
||||
</td>
|
||||
<td class="table__cell table__cell--center">
|
||||
{{ convertDuration(track.DURATION) }}
|
||||
</td>
|
||||
<td
|
||||
class="table__cell--download table__cell--center clickable"
|
||||
@contextmenu.prevent="openQualityModal"
|
||||
@click.stop="addToQueue"
|
||||
:data-link="'https://www.deezer.com/track/' + track.SNG_ID"
|
||||
role="button"
|
||||
aria-label="download"
|
||||
>
|
||||
<i class="material-icons">
|
||||
get_app
|
||||
</i>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div v-else-if="section == 'ARTIST'" class="release_grid firstrow_only">
|
||||
<div
|
||||
v-for="release in results.allTab.ARTIST.data.slice(0, 10)"
|
||||
class="release clickable"
|
||||
@click="artistView"
|
||||
:data-id="release.ART_ID"
|
||||
>
|
||||
<div class="cover_container">
|
||||
<img
|
||||
aria-hidden="true"
|
||||
class="circle coverart"
|
||||
:src="
|
||||
'https://e-cdns-images.dzcdn.net/images/artist/' +
|
||||
release.ART_PICTURE +
|
||||
'/156x156-000000-80-0-0.jpg'
|
||||
"
|
||||
/>
|
||||
<div
|
||||
role="button"
|
||||
aria-label="download"
|
||||
@contextmenu.prevent="openQualityModal"
|
||||
@click.stop="addToQueue"
|
||||
:data-link="'https://deezer.com/artist/' + release.ART_ID"
|
||||
class="download_overlay"
|
||||
>
|
||||
<i class="material-icons">get_app</i>
|
||||
</div>
|
||||
</div>
|
||||
<p class="primary-text">{{ release.ART_NAME }}</p>
|
||||
<p class="secondary-text">{{ numberWithDots(release.NB_FAN) + ' fans' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="section == 'ALBUM'" class="release_grid firstrow_only">
|
||||
<div
|
||||
v-for="release in results.allTab.ALBUM.data.slice(0, 10)"
|
||||
class="release clickable"
|
||||
@click="albumView"
|
||||
:data-id="release.ALB_ID"
|
||||
>
|
||||
<div class="cover_container">
|
||||
<img
|
||||
aria-hidden="true"
|
||||
class="rounded coverart"
|
||||
:src="
|
||||
'https://e-cdns-images.dzcdn.net/images/cover/' +
|
||||
release.ALB_PICTURE +
|
||||
'/156x156-000000-80-0-0.jpg'
|
||||
"
|
||||
/>
|
||||
<div
|
||||
role="button"
|
||||
aria-label="download"
|
||||
@contextmenu.prevent="openQualityModal"
|
||||
@click.stop="addToQueue"
|
||||
:data-link="'https://deezer.com/album/' + release.ALB_ID"
|
||||
class="download_overlay"
|
||||
>
|
||||
<i class="material-icons">get_app</i>
|
||||
</div>
|
||||
</div>
|
||||
<p class="primary-text inline-flex">
|
||||
<i
|
||||
v-if="[1, 4].indexOf(release.EXPLICIT_ALBUM_CONTENT.EXPLICIT_LYRICS_STATUS) != -1"
|
||||
class="material-icons explicit_icon"
|
||||
>explicit</i
|
||||
>
|
||||
{{ release.ALB_TITLE }}
|
||||
</p>
|
||||
<p class="secondary-text">{{ release.ART_NAME + ' - ' + release.NUMBER_TRACK + ' tracks' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="section == 'PLAYLIST'" class="release_grid firstrow_only">
|
||||
<div
|
||||
v-for="release in results.allTab.PLAYLIST.data.slice(0, 10)"
|
||||
class="release clickable"
|
||||
@click="playlistView"
|
||||
:data-id="release.PLAYLIST_ID"
|
||||
>
|
||||
<div class="cover_container">
|
||||
<img
|
||||
aria-hidden="true"
|
||||
class="rounded coverart"
|
||||
:src="
|
||||
'https://e-cdns-images.dzcdn.net/images/' +
|
||||
release.PICTURE_TYPE +
|
||||
'/' +
|
||||
release.PLAYLIST_PICTURE +
|
||||
'/156x156-000000-80-0-0.jpg'
|
||||
"
|
||||
/>
|
||||
<div
|
||||
role="button"
|
||||
aria-label="download"
|
||||
@contextmenu.prevent="openQualityModal"
|
||||
@click.stop="addToQueue"
|
||||
:data-link="'https://deezer.com/playlist/' + release.PLAYLIST_ID"
|
||||
class="download_overlay"
|
||||
>
|
||||
<i class="material-icons">get_app</i>
|
||||
</div>
|
||||
</div>
|
||||
<p class="primary-text">{{ release.TITLE }}</p>
|
||||
<p class="secondary-text">{{ release.NB_SONG + ' tracks' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
<div
|
||||
v-if="
|
||||
results.allTab.ORDER.every(section =>
|
||||
section == 'TOP_RESULT' ? results.allTab[section].length == 0 : results.allTab[section].data.length == 0
|
||||
)
|
||||
"
|
||||
>
|
||||
<h1>No results</h1>
|
||||
</div>
|
||||
</div>
|
||||
<!-- ### Track Search Tab ### -->
|
||||
<div id="track_search" class="search_tabcontent">
|
||||
<base-loading-placeholder v-if="!results.trackTab.loaded"></base-loading-placeholder>
|
||||
<div v-else-if="results.trackTab.data.length == 0">
|
||||
<h1>No Tracks found</h1>
|
||||
</div>
|
||||
<table class="table table--tracks" v-if="results.trackTab.data.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">Title</th>
|
||||
<th>Artists</th>
|
||||
<th>Album</th>
|
||||
<th>
|
||||
<i class="material-icons">
|
||||
timer
|
||||
</i>
|
||||
</th>
|
||||
<th style="width: 56px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="track in results.trackTab.data">
|
||||
<td class="table__icon table__icon--big">
|
||||
<a
|
||||
href="#"
|
||||
@click="playPausePreview"
|
||||
:class="'rounded' + (track.preview ? ' single-cover' : '')"
|
||||
:data-preview="track.preview"
|
||||
>
|
||||
<i
|
||||
@mouseenter="previewMouseEnter"
|
||||
@mouseleave="previewMouseLeave"
|
||||
v-if="track.preview"
|
||||
class="material-icons preview_controls"
|
||||
>
|
||||
play_arrow
|
||||
</i>
|
||||
<img class="rounded coverart" :src="track.album.cover_small" />
|
||||
</a>
|
||||
</td>
|
||||
<td class="table__cell table__cell--large breakline">
|
||||
<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>
|
||||
<td
|
||||
class="table__cell table__cell--medium table__cell--center breakline clickable"
|
||||
@click="artistView"
|
||||
:data-id="track.artist.id"
|
||||
>
|
||||
{{ track.artist.name }}
|
||||
</td>
|
||||
<td
|
||||
class="table__cell table__cell--medium table__cell--center breakline clickable"
|
||||
@click="albumView"
|
||||
:data-id="track.album.id"
|
||||
>
|
||||
{{ track.album.title }}
|
||||
</td>
|
||||
<td class="table__cell table__cell--small table__cell--center">
|
||||
{{ convertDuration(track.duration) }}
|
||||
</td>
|
||||
<td
|
||||
class="table__cell--download table__cell--center clickable"
|
||||
@contextmenu.prevent="openQualityModal"
|
||||
@click.stop="addToQueue"
|
||||
:data-link="track.link"
|
||||
role="button"
|
||||
aria-label="download"
|
||||
>
|
||||
<i class="material-icons">
|
||||
get_app
|
||||
</i>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- ### Album Search Tab ### -->
|
||||
<div id="album_search" class="search_tabcontent">
|
||||
<base-loading-placeholder v-if="!results.albumTab.loaded"></base-loading-placeholder>
|
||||
<div v-else-if="results.albumTab.data.length == 0">
|
||||
<h1>No Albums found</h1>
|
||||
</div>
|
||||
<div class="release_grid" v-if="results.albumTab.data.length > 0">
|
||||
<div
|
||||
v-for="release in results.albumTab.data"
|
||||
class="release clickable"
|
||||
@click="albumView"
|
||||
:data-id="release.id"
|
||||
>
|
||||
<div class="cover_container">
|
||||
<img aria-hidden="true" class="rounded coverart" :src="release.cover_medium" />
|
||||
<div
|
||||
role="button"
|
||||
aria-label="download"
|
||||
@contextmenu.prevent="openQualityModal"
|
||||
@click.stop="addToQueue"
|
||||
:data-link="release.link"
|
||||
class="download_overlay"
|
||||
>
|
||||
<i class="material-icons">get_app</i>
|
||||
</div>
|
||||
</div>
|
||||
<p class="primary-text inline-flex">
|
||||
<i v-if="release.explicit_lyrics" class="material-icons explicit_icon">explicit</i>
|
||||
{{ release.title }}
|
||||
</p>
|
||||
<p class="secondary-text">{{ 'by ' + release.artist.name + ' - ' + release.nb_tracks + ' tracks' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- ### Artist Search Tab ### -->
|
||||
<div id="artist_search" class="search_tabcontent">
|
||||
<base-loading-placeholder v-if="!results.artistTab.loaded"></base-loading-placeholder>
|
||||
<div v-else-if="results.artistTab.data.length == 0">
|
||||
<h1>No Artists found</h1>
|
||||
</div>
|
||||
<div class="release_grid" v-if="results.artistTab.data.length > 0">
|
||||
<div
|
||||
v-for="release in results.artistTab.data"
|
||||
class="release clickable"
|
||||
@click="artistView"
|
||||
:data-id="release.id"
|
||||
>
|
||||
<div class="cover_container">
|
||||
<img aria-hidden="true" class="circle coverart" :src="release.picture_medium" />
|
||||
<div
|
||||
role="button"
|
||||
aria-label="download"
|
||||
@contextmenu.prevent="openQualityModal"
|
||||
@click.stop="addToQueue"
|
||||
:data-link="release.link"
|
||||
class="download_overlay"
|
||||
>
|
||||
<i class="material-icons">get_app</i>
|
||||
</div>
|
||||
</div>
|
||||
<p class="primary-text">{{ release.name }}</p>
|
||||
<p class="secondary-text">{{ release.nb_album + ' releases' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- ### Playlist Search Tab ### -->
|
||||
<div id="playlist_search" class="search_tabcontent">
|
||||
<base-loading-placeholder v-if="!results.playlistTab.loaded"></base-loading-placeholder>
|
||||
<div v-else-if="results.playlistTab.data.length == 0">
|
||||
<h1>No Playlists found</h1>
|
||||
</div>
|
||||
<div class="release_grid" v-if="results.playlistTab.data.length > 0">
|
||||
<div
|
||||
v-for="release in results.playlistTab.data"
|
||||
class="release clickable"
|
||||
@click="playlistView"
|
||||
:data-id="release.id"
|
||||
>
|
||||
<div class="cover_container">
|
||||
<img aria-hidden="true" class="rounded coverart" :src="release.picture_medium" />
|
||||
<div
|
||||
role="button"
|
||||
aria-label="download"
|
||||
@contextmenu.prevent="openQualityModal"
|
||||
@click.stop="addToQueue"
|
||||
:data-link="release.link"
|
||||
class="download_overlay"
|
||||
>
|
||||
<i class="material-icons">get_app</i>
|
||||
</div>
|
||||
</div>
|
||||
<p class="primary-text">{{ release.title }}</p>
|
||||
<p class="secondary-text">{{ 'by ' + release.user.name + ' - ' + release.nb_tracks + ' tracks' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { socket } from '@/utils/socket'
|
||||
import { showView } from '@js/tabs.js'
|
||||
import Downloads from '@/utils/downloads'
|
||||
import Utils from '@/utils/utils'
|
||||
import BaseLoadingPlaceholder from '@components/BaseLoadingPlaceholder.vue'
|
||||
|
||||
import { changeTab } from '@js/tabs.js'
|
||||
import EventBus from '@/utils/EventBus.js'
|
||||
|
||||
export default {
|
||||
name: 'the-main-search-tab',
|
||||
components: {
|
||||
BaseLoadingPlaceholder
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
names: {
|
||||
TOP_RESULT: 'Top Result',
|
||||
TRACK: 'Tracks',
|
||||
ARTIST: 'Artists',
|
||||
ALBUM: 'Albums',
|
||||
PLAYLIST: 'Playlists'
|
||||
},
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
props: {
|
||||
scrolledSearchType: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
EventBus.$on('mainSearch:checkLoadMoreContent', this.checkLoadMoreContent)
|
||||
|
||||
this.$root.$on('mainSearch:showNewResults', this.showNewResults)
|
||||
socket.on('mainSearch', this.handleMainSearch)
|
||||
socket.on('search', this.handleSearch)
|
||||
},
|
||||
methods: {
|
||||
artistView: showView.bind(null, 'artist'),
|
||||
albumView: showView.bind(null, 'album'),
|
||||
playlistView: showView.bind(null, 'playlist'),
|
||||
playPausePreview(e) {
|
||||
EventBus.$emit('trackPreview:playPausePreview', e)
|
||||
},
|
||||
previewMouseEnter(e) {
|
||||
EventBus.$emit('trackPreview:previewMouseEnter', e)
|
||||
},
|
||||
previewMouseLeave(e) {
|
||||
EventBus.$emit('trackPreview:previewMouseLeave', e)
|
||||
},
|
||||
handleSearchTabClick(event) {
|
||||
const {
|
||||
target,
|
||||
target: { id }
|
||||
} = event
|
||||
let selectedTab = null
|
||||
|
||||
switch (id) {
|
||||
case 'search_all_tab':
|
||||
selectedTab = 'main_search'
|
||||
break
|
||||
case 'search_track_tab':
|
||||
selectedTab = 'track_search'
|
||||
break
|
||||
case 'search_album_tab':
|
||||
selectedTab = 'album_search'
|
||||
break
|
||||
case 'search_artist_tab':
|
||||
selectedTab = 'artist_search'
|
||||
break
|
||||
case 'search_playlist_tab':
|
||||
selectedTab = 'playlist_search'
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if (!selectedTab) return
|
||||
|
||||
changeTab(target, 'search', selectedTab)
|
||||
},
|
||||
handleClickTopResult(event) {
|
||||
let topResultType = this.results.allTab.TOP_RESULT[0].type
|
||||
|
||||
switch (topResultType) {
|
||||
case 'artist':
|
||||
this.artistView(event)
|
||||
break
|
||||
case 'album':
|
||||
this.albumView(event)
|
||||
break
|
||||
case 'playlist':
|
||||
this.playlistView(event)
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
},
|
||||
showNewResults(term, mainSelected) {
|
||||
if (term !== this.results.query || mainSelected == 'search_tab') {
|
||||
document.getElementById('search_tab_content').style.display = 'none'
|
||||
socket.emit('mainSearch', { term })
|
||||
|
||||
// Showing loading placeholder
|
||||
document.getElementById('content').style.display = 'none'
|
||||
document.getElementById('search_placeholder').classList.toggle('loading_placeholder--hidden')
|
||||
} else {
|
||||
document.getElementById('search_tab_content').style.display = 'block'
|
||||
document.getElementById('main_search_tablink').click()
|
||||
}
|
||||
},
|
||||
checkLoadMoreContent(searchSelected) {
|
||||
if (this.results[searchSelected.split('_')[0] + 'Tab'].data.length !== 0) return
|
||||
|
||||
this.search(searchSelected.split('_')[0])
|
||||
},
|
||||
changeSearchTab(section) {
|
||||
if (section === 'TOP_RESULT') return
|
||||
|
||||
let tabID
|
||||
|
||||
// Using the switch beacuse it's tricky to find refernces of the belo IDs
|
||||
switch (section) {
|
||||
case 'TRACK':
|
||||
tabID = 'search_track_tab'
|
||||
break
|
||||
case 'ALBUM':
|
||||
tabID = 'search_album_tab'
|
||||
break
|
||||
case 'ARTIST':
|
||||
tabID = 'search_artist_tab'
|
||||
break
|
||||
case 'PLAYLIST':
|
||||
tabID = 'search_playlist_tab'
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
document.getElementById(tabID).click()
|
||||
},
|
||||
addToQueue(e) {
|
||||
Downloads.sendAddToQueue(e.currentTarget.dataset.link)
|
||||
},
|
||||
openQualityModal(e) {
|
||||
this.$root.$emit('QualityModal:open', e.currentTarget.dataset.link)
|
||||
},
|
||||
numberWithDots: Utils.numberWithDots,
|
||||
convertDuration: Utils.convertDuration,
|
||||
search(type) {
|
||||
socket.emit('search', {
|
||||
term: this.results.query,
|
||||
type: type,
|
||||
start: this.results[type + 'Tab'].next,
|
||||
nb: 30
|
||||
})
|
||||
},
|
||||
scrolledSearch(type) {
|
||||
let currentTab = type + 'Tab'
|
||||
|
||||
if (this.results[currentTab].next < this.results[currentTab].total) {
|
||||
socket.emit('search', {
|
||||
term: this.results.query,
|
||||
type: type,
|
||||
start: this.results[currentTab].next,
|
||||
nb: 30
|
||||
})
|
||||
}
|
||||
},
|
||||
handleMainSearch(result) {
|
||||
// Hiding loading placeholder
|
||||
document.getElementById('content').style.display = ''
|
||||
document.getElementById('search_placeholder').classList.toggle('loading_placeholder--hidden')
|
||||
|
||||
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 }
|
||||
|
||||
if (this.results.query == '') document.getElementById('search_all_tab').click()
|
||||
|
||||
this.results.query = result.QUERY
|
||||
document.getElementById('search_tab_content').style.display = 'block'
|
||||
document.getElementById('main_search_tablink').click()
|
||||
},
|
||||
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
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
scrolledSearchType(newType) {
|
||||
if (!newType) return
|
||||
|
||||
this.scrolledSearch(newType)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
31
src/components/TheMiddleSection.vue
Normal file
31
src/components/TheMiddleSection.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div id="middle_section">
|
||||
<TheSearchBar />
|
||||
<TheContent />
|
||||
|
||||
<div id="search_placeholder" class="loading_placeholder loading_placeholder--hidden">
|
||||
<span class="loading_placeholder__text">Searching...</span>
|
||||
<div class="lds-ring">
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TheContent from '@components/TheContent.vue'
|
||||
import TheSearchBar from '@components/TheSearchBar.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TheContent,
|
||||
TheSearchBar
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
80
src/components/TheQualityModal.vue
Normal file
80
src/components/TheQualityModal.vue
Normal file
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<div id="modal_quality" class="smallmodal" v-show="open" @click="tryToDownloadTrack($event)" ref="modal">
|
||||
<div class="smallmodal-content">
|
||||
<button class="quality-button" data-quality-value="9">Download FLAC</button>
|
||||
<button class="quality-button" data-quality-value="3">Download MP3 320kbps</button>
|
||||
<button class="quality-button" data-quality-value="1">Download MP3 128kbps</button>
|
||||
<button class="quality-button" data-quality-value="15">Download 360 Reality Audio [HQ]</button>
|
||||
<button class="quality-button" data-quality-value="14">Download 360 Reality Audio [MQ]</button>
|
||||
<button class="quality-button" data-quality-value="13">Download 360 Reality Audio [LQ]</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style>
|
||||
.smallmodal {
|
||||
position: fixed;
|
||||
z-index: 1250;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
background-color: hsla(0, 0%, 0%, 0.4);
|
||||
animation-duration: 0.3s;
|
||||
}
|
||||
|
||||
.smallmodal-content {
|
||||
background-color: transparent;
|
||||
margin: auto;
|
||||
width: var(--modal-content-width);
|
||||
position: relative;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.smallmodal-content button {
|
||||
width: 100%;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
import Downloads from '@/utils/downloads'
|
||||
|
||||
export default {
|
||||
data: () => ({
|
||||
open: false,
|
||||
url: ''
|
||||
}),
|
||||
mounted() {
|
||||
this.$root.$on('QualityModal:open', this.openModal)
|
||||
|
||||
this.$refs.modal.addEventListener('webkitAnimationEnd', this.handleAnimationEnd)
|
||||
},
|
||||
methods: {
|
||||
tryToDownloadTrack(event) {
|
||||
const { target } = event
|
||||
|
||||
this.$refs.modal.classList.add('animated', 'fadeOut')
|
||||
|
||||
// If true, the click did not happen on a button but outside
|
||||
if (!target.matches('.quality-button')) return
|
||||
|
||||
Downloads.sendAddToQueue(this.url, target.dataset.qualityValue)
|
||||
},
|
||||
openModal(link) {
|
||||
this.url = link
|
||||
this.open = true
|
||||
this.$refs.modal.classList.add('animated', 'fadeIn')
|
||||
},
|
||||
handleAnimationEnd(event) {
|
||||
const { animationName } = event
|
||||
|
||||
this.$refs.modal.classList.remove('animated', animationName)
|
||||
|
||||
if (animationName === 'fadeIn') return
|
||||
|
||||
this.open = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
57
src/components/TheSearchBar.vue
Normal file
57
src/components/TheSearchBar.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<header id="search">
|
||||
<div class="search__icon">
|
||||
<i class="material-icons">search</i>
|
||||
</div>
|
||||
<input
|
||||
id="searchbar"
|
||||
autocomplete="off"
|
||||
type="search"
|
||||
name="searchbar"
|
||||
value=""
|
||||
placeholder="Search what you want (or just paste a link)"
|
||||
autofocus
|
||||
ref="searchbar"
|
||||
@keyup="handleSearchBarKeyup($event)"
|
||||
/>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { isValidURL } from '@/utils/utils'
|
||||
import Downloads from '@/utils/downloads'
|
||||
|
||||
import EventBus from '@/utils/EventBus.js'
|
||||
import { socket } from '@/utils/socket'
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
handleSearchBarKeyup(keyEvent) {
|
||||
// Enter key
|
||||
if (keyEvent.keyCode !== 13) return
|
||||
|
||||
let term = this.$refs.searchbar.value
|
||||
|
||||
if (isValidURL(term)) {
|
||||
if (keyEvent.ctrlKey) {
|
||||
this.$root.$emit('QualityModal:open', term)
|
||||
} else {
|
||||
if (main_selected === 'analyzer_tab') {
|
||||
EventBus.$emit('linkAnalyzerTab:reset')
|
||||
socket.emit('analyzeLink', term)
|
||||
} else {
|
||||
Downloads.sendAddToQueue(term)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (term === '') return
|
||||
|
||||
this.$root.$emit('mainSearch:showNewResults', term, main_selected)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
685
src/components/TheSettingsTab.vue
Normal file
685
src/components/TheSettingsTab.vue
Normal file
@@ -0,0 +1,685 @@
|
||||
<template>
|
||||
<div id="settings_tab" class="main_tabcontent fixed_footer">
|
||||
<h2 class="page_heading">{{ $t('settings.title') }}</h2>
|
||||
|
||||
<div id="logged_in_info" ref="loggedInInfo">
|
||||
<img id="settings_picture" src="" alt="Profile Picture" ref="userpicture" class="circle" />
|
||||
<p>You are logged in as <strong id="settings_username" ref="username"></strong></p>
|
||||
<button id="settings_btn_logout" @click="logout">Logout</button>
|
||||
<select v-if="accounts.length" id="family_account" v-model="accountNum" @change="changeAccount">
|
||||
<option v-for="(account, i) in accounts" :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>Login
|
||||
</h3>
|
||||
<div class="inline-flex">
|
||||
<input autocomplete="off" type="password" id="login_input_arl" ref="loginInput" placeholder="ARL" />
|
||||
<button id="settings_btn_copyArl" @click="copyARLtoClipboard">
|
||||
<i class="material-icons">assignment</i>
|
||||
</button>
|
||||
</div>
|
||||
<a href="https://notabug.org/RemixDevs/DeezloaderRemix/wiki/Login+via+userToken" target="_blank">
|
||||
{{ $t('settings.login.arl.question') }}
|
||||
</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>
|
||||
<span
|
||||
v-for="locale in locales"
|
||||
:key="locale"
|
||||
style="width: 50px; height: 50px; cursor:pointer; border: 1px solid var(--foreground); flex: 1 50px; display: flex; justify-content: center; align-items: center;"
|
||||
:data-locale="locale"
|
||||
@click="$i18n.locale = locale"
|
||||
>
|
||||
{{ locale.toUpperCase() }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="settings-group">
|
||||
<h3 class="settings-group__header settings-group__header--with-icon">
|
||||
<i class="material-icons">web</i>Appearance
|
||||
</h3>
|
||||
<label class="with_checkbox">
|
||||
<input type="checkbox" v-model="changeSlimDownloads" />
|
||||
<span class="checkbox_text">Slim download tab</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="settings-group">
|
||||
<h3 class="settings-group__header settings-group__header--with-icon">
|
||||
<i class="material-icons">folder</i>Download Path
|
||||
</h3>
|
||||
<input type="text" v-model="settings.downloadLocation" />
|
||||
</div>
|
||||
|
||||
<div class="settings-group">
|
||||
<h3 class="settings-group__header settings-group__header--with-icon">
|
||||
<i class="material-icons">font_download</i>Templates
|
||||
</h3>
|
||||
|
||||
<p>Trackname template</p>
|
||||
<input type="text" v-model="settings.tracknameTemplate" />
|
||||
|
||||
<p>Album track template</p>
|
||||
<input type="text" v-model="settings.albumTracknameTemplate" />
|
||||
|
||||
<p>Playlist track template</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>Folders
|
||||
</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">Create folder for playlist</span>
|
||||
</label>
|
||||
<div class="input_group" v-if="settings.createPlaylistFolder">
|
||||
<p class="input_group_text">Playlist folder template</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">Create folder for artist</span>
|
||||
</label>
|
||||
|
||||
<div class="input_group" v-if="settings.createArtistFolder">
|
||||
<p class="input_group_text">Artist folder template</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">Create folder for album</span>
|
||||
</label>
|
||||
|
||||
<div class="input_group" v-if="settings.createAlbumFolder">
|
||||
<p class="input_group_text">Album folder template</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">Create folder for CDs</span>
|
||||
</label>
|
||||
|
||||
<label class="with_checkbox">
|
||||
<input type="checkbox" v-model="settings.createStructurePlaylist" />
|
||||
<span class="checkbox_text">Create folder structure for playlists</span>
|
||||
</label>
|
||||
|
||||
<label class="with_checkbox">
|
||||
<input type="checkbox" v-model="settings.createSingleFolder" />
|
||||
<span class="checkbox_text">Create folder structure for singles</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="settings-group">
|
||||
<h3 class="settings-group__header settings-group__header--with-icon">
|
||||
<i class="material-icons">title</i>Track titles
|
||||
</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">Pad tracks</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="settings-container__third">
|
||||
<div class="input_group">
|
||||
<p class="input_group_text">Overwrite padding size</p>
|
||||
<input type="number" v-model="settings.paddingSize" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-container__third">
|
||||
<div class="input_group">
|
||||
<p class="input_group_text">Illegal Character replacer</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>Downloads
|
||||
</h3>
|
||||
|
||||
<div class="input_group">
|
||||
<p class="input_group_text">Concurrent Downloads</p>
|
||||
<input type="number" v-model.number="settings.queueConcurrency" />
|
||||
</div>
|
||||
|
||||
<div class="input_group">
|
||||
<p class="input_group_text">Preferred Bitrate</p>
|
||||
<select v-model="settings.maxBitrate">
|
||||
<option value="9">FLAC 1411kbps</option>
|
||||
<option value="3">MP3 320kbps</option>
|
||||
<option value="1">MP3 128kbps</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="input_group">
|
||||
<p class="input_group_text">Should I overwrite the files?</p>
|
||||
<select v-model="settings.overwriteFile">
|
||||
<option value="y">Yes, overwrite the file</option>
|
||||
<option value="n">No, don't overwrite the file</option>
|
||||
<option value="t">Overwrite only the tags</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">Bitrate fallback</span>
|
||||
</label>
|
||||
|
||||
<label class="with_checkbox">
|
||||
<input type="checkbox" v-model="settings.fallbackSearch" />
|
||||
<span class="checkbox_text">Search fallback</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">Create log file for errors</span>
|
||||
</label>
|
||||
|
||||
<label class="with_checkbox">
|
||||
<input type="checkbox" v-model="settings.logSearched" />
|
||||
<span class="checkbox_text">Create log file for searched tracks</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">Create .lyr files (Sync Lyrics)</span>
|
||||
</label>
|
||||
|
||||
<label class="with_checkbox">
|
||||
<input type="checkbox" v-model="settings.createM3U8File" />
|
||||
<span class="checkbox_text">Create playlist file</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input_group" v-if="settings.createM3U8File">
|
||||
<p class="input_group_text">Playlist filename template</p>
|
||||
<input type="text" v-model="settings.playlistFilenameTemplate" />
|
||||
</div>
|
||||
|
||||
<label class="with_checkbox">
|
||||
<input type="checkbox" v-model="settings.saveDownloadQueue" />
|
||||
<span class="checkbox_text">Save download queue when closing the app</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="settings-group">
|
||||
<h3 class="settings-group__header settings-group__header--with-icon">
|
||||
<i class="material-icons">album</i>Album covers
|
||||
</h3>
|
||||
|
||||
<label class="with_checkbox">
|
||||
<input type="checkbox" v-model="settings.saveArtwork" />
|
||||
<span class="checkbox_text">Save covers</span>
|
||||
</label>
|
||||
|
||||
<div class="input_group" v-if="settings.saveArtwork">
|
||||
<p class="input_group_text">Cover name template</p>
|
||||
<input type="text" v-model="settings.coverImageTemplate" />
|
||||
</div>
|
||||
|
||||
<label class="with_checkbox">
|
||||
<input type="checkbox" v-model="settings.saveArtworkArtist" />
|
||||
<span class="checkbox_text">Save artist image</span>
|
||||
</label>
|
||||
|
||||
<div class="input_group" v-if="settings.saveArtworkArtist">
|
||||
<p class="input_group_text">Artist image name template</p>
|
||||
<input type="text" v-model="settings.artistImageTemplate" />
|
||||
</div>
|
||||
|
||||
<div class="input_group">
|
||||
<p class="input_group_text">Local artwork size</p>
|
||||
<input type="number" min="100" max="1800" step="100" v-model.number="settings.localArtworkSize" />
|
||||
</div>
|
||||
|
||||
<div class="input_group">
|
||||
<p class="input_group_text">Embedded artwork size</p>
|
||||
<input type="number" min="100" max="1800" step="100" v-model.number="settings.embeddedArtworkSize" />
|
||||
</div>
|
||||
|
||||
<label class="with_checkbox">
|
||||
<input type="checkbox" v-model="settings.PNGcovers" />
|
||||
<span class="checkbox_text">Save images as png</span>
|
||||
</label>
|
||||
|
||||
<div class="input_group">
|
||||
<p class="input_group_text">JPEG image quality</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>Which tags to save
|
||||
</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">Title</span>
|
||||
</label>
|
||||
<label class="with_checkbox">
|
||||
<input type="checkbox" v-model="settings.tags.artist" />
|
||||
<span class="checkbox_text">Artists</span>
|
||||
</label>
|
||||
<label class="with_checkbox">
|
||||
<input type="checkbox" v-model="settings.tags.album" />
|
||||
<span class="checkbox_text">Album</span>
|
||||
</label>
|
||||
<label class="with_checkbox">
|
||||
<input type="checkbox" v-model="settings.tags.cover" />
|
||||
<span class="checkbox_text">Cover</span>
|
||||
</label>
|
||||
<label class="with_checkbox">
|
||||
<input type="checkbox" v-model="settings.tags.trackNumber" />
|
||||
<span class="checkbox_text">Track Number</span>
|
||||
</label>
|
||||
<label class="with_checkbox">
|
||||
<input type="checkbox" v-model="settings.tags.trackTotal" />
|
||||
<span class="checkbox_text">Track Total</span>
|
||||
</label>
|
||||
<label class="with_checkbox">
|
||||
<input type="checkbox" v-model="settings.tags.discNumber" />
|
||||
<span class="checkbox_text">Disc Number</span>
|
||||
</label>
|
||||
<label class="with_checkbox">
|
||||
<input type="checkbox" v-model="settings.tags.discTotal" />
|
||||
<span class="checkbox_text">Disc Total</span>
|
||||
</label>
|
||||
<label class="with_checkbox">
|
||||
<input type="checkbox" v-model="settings.tags.albumArtist" />
|
||||
<span class="checkbox_text">Album Artist</span>
|
||||
</label>
|
||||
<label class="with_checkbox">
|
||||
<input type="checkbox" v-model="settings.tags.genre" />
|
||||
<span class="checkbox_text">Genre</span>
|
||||
</label>
|
||||
<label class="with_checkbox">
|
||||
<input type="checkbox" v-model="settings.tags.year" />
|
||||
<span class="checkbox_text">Year</span>
|
||||
</label>
|
||||
<label class="with_checkbox">
|
||||
<input type="checkbox" v-model="settings.tags.date" />
|
||||
<span class="checkbox_text">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">Explicit Lyrics</span>
|
||||
</label>
|
||||
<label class="with_checkbox">
|
||||
<input type="checkbox" v-model="settings.tags.isrc" />
|
||||
<span class="checkbox_text">ISRC</span>
|
||||
</label>
|
||||
<label class="with_checkbox">
|
||||
<input type="checkbox" v-model="settings.tags.length" />
|
||||
<span class="checkbox_text">Track Length</span>
|
||||
</label>
|
||||
<label class="with_checkbox">
|
||||
<input type="checkbox" v-model="settings.tags.barcode" />
|
||||
<span class="checkbox_text">Album Barcode (UPC)</span>
|
||||
</label>
|
||||
<label class="with_checkbox">
|
||||
<input type="checkbox" v-model="settings.tags.bpm" />
|
||||
<span class="checkbox_text">BPM</span>
|
||||
</label>
|
||||
<label class="with_checkbox">
|
||||
<input type="checkbox" v-model="settings.tags.replayGain" />
|
||||
<span class="checkbox_text">Replay Gain</span>
|
||||
</label>
|
||||
<label class="with_checkbox">
|
||||
<input type="checkbox" v-model="settings.tags.label" />
|
||||
<span class="checkbox_text">Album Label</span>
|
||||
</label>
|
||||
<label class="with_checkbox">
|
||||
<input type="checkbox" v-model="settings.tags.lyrics" />
|
||||
<span class="checkbox_text">Unsynchronized Lyrics</span>
|
||||
</label>
|
||||
<label class="with_checkbox">
|
||||
<input type="checkbox" v-model="settings.tags.copyright" />
|
||||
<span class="checkbox_text">Copyright</span>
|
||||
</label>
|
||||
<label class="with_checkbox">
|
||||
<input type="checkbox" v-model="settings.tags.composer" />
|
||||
<span class="checkbox_text">Composer</span>
|
||||
</label>
|
||||
<label class="with_checkbox">
|
||||
<input type="checkbox" v-model="settings.tags.involvedPeople" />
|
||||
<span class="checkbox_text">Involved People</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>Other</h3>
|
||||
|
||||
<label class="with_checkbox">
|
||||
<input type="checkbox" v-model="settings.tags.savePlaylistAsCompilation" />
|
||||
<span class="checkbox_text">Save playlists as compilation</span>
|
||||
</label>
|
||||
|
||||
<label class="with_checkbox">
|
||||
<input type="checkbox" v-model="settings.tags.useNullSeparator" />
|
||||
<span class="checkbox_text">Use null separator</span>
|
||||
</label>
|
||||
|
||||
<label class="with_checkbox">
|
||||
<input type="checkbox" v-model="settings.tags.saveID3v1" />
|
||||
<span class="checkbox_text">Save ID3v1 as well</span>
|
||||
</label>
|
||||
|
||||
<div class="input_group">
|
||||
<p class="input_group_text">How would you like to separate your artists?</p>
|
||||
<select v-model="settings.tags.multitagSeparator">
|
||||
<option value="default">Using standard specification</option>
|
||||
<option value="andFeat">Using & and feat.</option>
|
||||
<option value=" & ">Using " & "</option>
|
||||
<option value=",">Using ","</option>
|
||||
<option value=", ">Using ", "</option>
|
||||
<option value="/">Using "/"</option>
|
||||
<option value=" / ">Using "/ "</option>
|
||||
<option value=";">Using ";"</option>
|
||||
<option value="; ">Using "; "</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<label class="with_checkbox">
|
||||
<input type="checkbox" v-model="settings.albumVariousArtists" />
|
||||
<span class="checkbox_text">Keep "Various Artists" in the Album Artists</span>
|
||||
</label>
|
||||
|
||||
<label class="with_checkbox">
|
||||
<input type="checkbox" v-model="settings.removeAlbumVersion" />
|
||||
<span class="checkbox_text">Remove "album version" from track title</span>
|
||||
</label>
|
||||
|
||||
<label class="with_checkbox">
|
||||
<input type="checkbox" v-model="settings.removeDuplicateArtists" />
|
||||
<span class="checkbox_text">Remove combinations of artists</span>
|
||||
</label>
|
||||
|
||||
<div class="input_group">
|
||||
<p class="input_group_text">Date format for FLAC files</p>
|
||||
<select v-model="settings.dateFormat">
|
||||
<option value="Y-M-D">YYYY-MM-DD</option>
|
||||
<option value="Y-D-M">YYYY-DD-MM</option>
|
||||
<option value="D-M-Y">DD-MM-YYYY</option>
|
||||
<option value="M-D-Y">MM-DD-YYYY</option>
|
||||
<option value="Y">YYYY</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="input_group">
|
||||
<p class="input_group_text">What should I do with featured artists?</p>
|
||||
<select v-model="settings.featuredToTitle">
|
||||
<option value="0">Nothing</option>
|
||||
<option value="1">Remove it from the title</option>
|
||||
<option value="3">Remove it from the title and the album title</option>
|
||||
<option value="2">Move it to the title</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="input_group">
|
||||
<p class="input_group_text">Title casing</p>
|
||||
<select v-model="settings.titleCasing">
|
||||
<option value="nothing">Keep unchanged</option>
|
||||
<option value="lower">lowercase</option>
|
||||
<option value="upper">UPPERCASE</option>
|
||||
<option value="start">Start Of Each Word</option>
|
||||
<option value="sentence">Like a sentence</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="input_group">
|
||||
<p class="input_group_text">Artist casing</p>
|
||||
<select v-model="settings.artistCasing">
|
||||
<option value="nothing">Keep unchanged</option>
|
||||
<option value="lower">lowercase</option>
|
||||
<option value="upper">UPPERCASE</option>
|
||||
<option value="start">Start Of Each Word</option>
|
||||
<option value="sentence">Like a sentence</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="input_group">
|
||||
<p class="input_group_text">Preview Volume</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">Command to execute after download</p>
|
||||
<p class="secondary-text">Leave blank for no action</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>
|
||||
Spotify Features
|
||||
</h3>
|
||||
|
||||
<div class="input_group">
|
||||
<p class="input_group_text">Spotify clientID</p>
|
||||
<input type="text" v-model="spotifyFeatures.clientId" />
|
||||
</div>
|
||||
|
||||
<div class="input_group">
|
||||
<p class="input_group_text">Spotify Client Secret</p>
|
||||
<input type="password" v-model="spotifyFeatures.clientSecret" />
|
||||
</div>
|
||||
|
||||
<div class="input_group">
|
||||
<p class="input_group_text">Spotify username</p>
|
||||
<input type="text" v-model="spotifyUser" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<button @click="resetSettings">Reset to Default</button>
|
||||
<button @click="saveSettings">Save</button>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { toast } from '@/utils/toasts'
|
||||
import { socket } from '@/utils/socket'
|
||||
import EventBus from '@/utils/EventBus'
|
||||
|
||||
export default {
|
||||
name: 'the-settings-tab',
|
||||
data: () => ({
|
||||
locales: [],
|
||||
settings: { tags: {} },
|
||||
lastSettings: {},
|
||||
spotifyFeatures: {},
|
||||
lastCredentials: {},
|
||||
defaultSettings: {},
|
||||
lastUser: '',
|
||||
spotifyUser: '',
|
||||
slimDownloads: false,
|
||||
previewVolume: window.vol,
|
||||
accountNum: 0,
|
||||
accounts: []
|
||||
}),
|
||||
computed: {
|
||||
changeSlimDownloads: {
|
||||
get() {
|
||||
return this.slimDownloads
|
||||
},
|
||||
set(wantSlimDownloads) {
|
||||
this.slimDownloads = wantSlimDownloads
|
||||
document.getElementById('download_list').classList.toggle('slim', wantSlimDownloads)
|
||||
localStorage.setItem('slimDownloads', wantSlimDownloads)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
revertSettings() {
|
||||
this.settings = { ...this.lastSettings }
|
||||
},
|
||||
revertCredentials() {
|
||||
this.spotifyCredentials = { ...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('ARL copied to clipboard', 'assignment')
|
||||
},
|
||||
updateMaxVolume() {
|
||||
localStorage.setItem('previewVolume', this.previewVolume.preview_max_volume)
|
||||
},
|
||||
saveSettings() {
|
||||
this.lastSettings = { ...this.settings }
|
||||
this.lastCredentials = { ...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)
|
||||
},
|
||||
loadSettings(settings, spotifyCredentials, defaults = null) {
|
||||
if (defaults) {
|
||||
this.defaultSettings = { ...defaults }
|
||||
}
|
||||
|
||||
this.lastSettings = { ...settings }
|
||||
this.lastCredentials = { ...spotifyCredentials }
|
||||
this.settings = settings
|
||||
this.spotifyFeatures = spotifyCredentials
|
||||
},
|
||||
login() {
|
||||
let arl = this.$refs.loginInput.value.trim()
|
||||
if (arl != '' && arl != localStorage.getItem('arl')) {
|
||||
socket.emit('login', arl, true, this.accountNum)
|
||||
}
|
||||
},
|
||||
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, defaults) {
|
||||
this.loadSettings(settings, credentials, defaults)
|
||||
toast('Settings loaded!', 'settings')
|
||||
},
|
||||
updateSettings(settings, credentials) {
|
||||
this.loadSettings(settings, credentials)
|
||||
toast('Settings updated!', 'settings')
|
||||
},
|
||||
resetSettings() {
|
||||
this.settings = { ...this.defaultSettings }
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.locales = this.$i18n.availableLocales
|
||||
|
||||
EventBus.$on('settingsTab:revertSettings', this.revertSettings)
|
||||
EventBus.$on('settingsTab:revertCredentials', this.revertCredentials)
|
||||
|
||||
this.$refs.loggedInInfo.classList.add('hide')
|
||||
|
||||
if (localStorage.getItem('arl')) {
|
||||
this.$refs.loginInput.value = localStorage.getItem('arl').trim()
|
||||
}
|
||||
if (localStorage.getItem('accountNum')) {
|
||||
this.accountNum = localStorage.getItem('accountNum')
|
||||
}
|
||||
|
||||
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('init_settings', this.initSettings)
|
||||
socket.on('updateSettings', this.updateSettings)
|
||||
socket.on('accountChanged', this.accountChanged)
|
||||
socket.on('familyAccounts', this.initAccounts)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
185
src/components/TheSidebar.vue
Normal file
185
src/components/TheSidebar.vue
Normal file
@@ -0,0 +1,185 @@
|
||||
<template>
|
||||
<aside id="sidebar" role="navigation" @click="handleSidebarClick">
|
||||
<span id="main_home_tablink" class="main_tablinks" role="link" aria-label="home">
|
||||
<i class="material-icons side_icon">home</i>
|
||||
<span class="main_tablinks_text">Home</span>
|
||||
</span>
|
||||
<span id="main_search_tablink" class="main_tablinks" role="link" aria-label="search">
|
||||
<i class="material-icons side_icon">search</i>
|
||||
<span class="main_tablinks_text">Search</span>
|
||||
</span>
|
||||
<span id="main_charts_tablink" class="main_tablinks" role="link" aria-label="charts">
|
||||
<i class="material-icons side_icon">bubble_chart</i>
|
||||
<span class="main_tablinks_text">Charts</span>
|
||||
</span>
|
||||
<span id="main_favorites_tablink" class="main_tablinks" role="link" aria-label="favorites">
|
||||
<i class="material-icons side_icon">album</i>
|
||||
<span class="main_tablinks_text">Favorites</span>
|
||||
</span>
|
||||
<span id="main_analyzer_tablink" class="main_tablinks" role="link" aria-label="link analyzer">
|
||||
<i class="material-icons side_icon">link</i>
|
||||
<span class="main_tablinks_text">Link Analyzer</span>
|
||||
</span>
|
||||
<span id="main_settings_tablink" class="main_tablinks" role="link" aria-label="settings">
|
||||
<i class="material-icons side_icon">settings</i>
|
||||
<span class="main_tablinks_text">Settings</span>
|
||||
</span>
|
||||
<span id="main_about_tablink" class="main_tablinks" role="link" aria-label="info">
|
||||
<i class="material-icons side_icon">info</i>
|
||||
<span class="main_tablinks_text">About</span>
|
||||
</span>
|
||||
<span id="theme_selector" class="main_tablinks" role="link" aria-label="theme selector">
|
||||
<i class="material-icons side_icon side_icon--theme">palette</i>
|
||||
<div id="theme_togglers">
|
||||
<div
|
||||
class="theme_toggler theme_toggler--purple"
|
||||
:class="{ 'theme_toggler--active': activeTheme === 'purple' }"
|
||||
@click="changeTheme('purple')"
|
||||
/>
|
||||
<div
|
||||
class="theme_toggler theme_toggler--dark"
|
||||
:class="{ 'theme_toggler--active': activeTheme === 'dark' }"
|
||||
@click="changeTheme('dark')"
|
||||
/>
|
||||
<div
|
||||
class="theme_toggler theme_toggler--light"
|
||||
:class="{ 'theme_toggler--active': activeTheme === 'light' }"
|
||||
@click="changeTheme('light')"
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
<div id="network-status" :class="{ online: appOnline, offline: !appOnline }">
|
||||
<i v-if="appOnline" class="material-icons">wifi</i>
|
||||
<i v-else class="material-icons">
|
||||
<!-- wifi_off icon not working, maybe need to include it? -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path d="M24 .01c0-.01 0-.01 0 0L0 0v24h24V.01zM0 0h24v24H0V0zm0 0h24v24H0V0z" fill="none" />
|
||||
<path
|
||||
d="M22.99 9C19.15 5.16 13.8 3.76 8.84 4.78l2.52 2.52c3.47-.17 6.99 1.05 9.63 3.7l2-2zm-4 4c-1.29-1.29-2.84-2.13-4.49-2.56l3.53 3.53.96-.97zM2 3.05L5.07 6.1C3.6 6.82 2.22 7.78 1 9l1.99 2c1.24-1.24 2.67-2.16 4.2-2.77l2.24 2.24C7.81 10.89 6.27 11.73 5 13v.01L6.99 15c1.36-1.36 3.14-2.04 4.92-2.06L18.98 20l1.27-1.26L3.29 1.79 2 3.05zM9 17l3 3 3-3c-1.65-1.66-4.34-1.66-6 0z"
|
||||
/>
|
||||
</svg>
|
||||
</i>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
#network-status {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
margin-top: auto;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
#network-status.online i.material-icons {
|
||||
color: hsl(151, 100%, 31%);
|
||||
}
|
||||
|
||||
#network-status.offline i.material-icons svg {
|
||||
fill: red;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { changeTab } from '@js/tabs.js'
|
||||
|
||||
export default {
|
||||
name: 'the-sidebar',
|
||||
data() {
|
||||
return {
|
||||
appOnline: null,
|
||||
activeTheme: 'light'
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
/* === Online status handling === */
|
||||
this.appOnline = navigator.onLine
|
||||
|
||||
window.addEventListener('online', () => {
|
||||
this.appOnline = true
|
||||
})
|
||||
|
||||
window.addEventListener('offline', () => {
|
||||
this.appOnline = false
|
||||
})
|
||||
|
||||
/* === Current theme handling === */
|
||||
this.activeTheme = localStorage.getItem('selectedTheme') || 'light'
|
||||
},
|
||||
methods: {
|
||||
changeTheme(newTheme) {
|
||||
if (newTheme === this.activeTheme) return
|
||||
|
||||
this.activeTheme = newTheme
|
||||
document.documentElement.setAttribute('data-theme', newTheme)
|
||||
localStorage.setItem('selectedTheme', newTheme)
|
||||
|
||||
// Animating everything to have a smoother theme switch
|
||||
document.querySelectorAll('*').forEach(el => {
|
||||
el.style.transition = 'all 200ms ease-in-out'
|
||||
})
|
||||
|
||||
document.documentElement.addEventListener('transitionend', function transitionHandler() {
|
||||
document.querySelectorAll('*').forEach(el => {
|
||||
el.style.transition = ''
|
||||
})
|
||||
|
||||
document.documentElement.removeEventListener('transitionend', transitionHandler)
|
||||
})
|
||||
},
|
||||
/**
|
||||
* Handles click Event on the sidebar and changes tab
|
||||
* according to clicked icon.
|
||||
* Uses event delegation
|
||||
* @param {Event} event
|
||||
*/
|
||||
handleSidebarClick(event) {
|
||||
const { target } = event
|
||||
|
||||
const wantToChangeTab = target.matches('.main_tablinks') || target.parentElement.matches('.main_tablinks')
|
||||
|
||||
if (!wantToChangeTab) return
|
||||
|
||||
let sidebarEl = target.matches('.main_tablinks') ? target : target.parentElement
|
||||
let targetID = sidebarEl.id
|
||||
let selectedTab = null
|
||||
|
||||
switch (targetID) {
|
||||
case 'main_search_tablink':
|
||||
selectedTab = 'search_tab'
|
||||
break
|
||||
case 'main_home_tablink':
|
||||
selectedTab = 'home_tab'
|
||||
break
|
||||
case 'main_charts_tablink':
|
||||
selectedTab = 'charts_tab'
|
||||
break
|
||||
case 'main_favorites_tablink':
|
||||
selectedTab = 'favorites_tab'
|
||||
break
|
||||
case 'main_analyzer_tablink':
|
||||
selectedTab = 'analyzer_tab'
|
||||
break
|
||||
case 'main_settings_tablink':
|
||||
selectedTab = 'settings_tab'
|
||||
break
|
||||
case 'main_about_tablink':
|
||||
selectedTab = 'about_tab'
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if (!selectedTab) return
|
||||
|
||||
changeTab(sidebarEl, 'main', selectedTab)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
123
src/components/TheTrackPreview.vue
Normal file
123
src/components/TheTrackPreview.vue
Normal file
@@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<audio id="preview-track" @canplay="onCanPlay" @timeupdate="onTimeUpdate" ref="preview">
|
||||
<source id="preview-track_source" src="" type="audio/mpeg" />
|
||||
</audio>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import $ from 'jquery'
|
||||
import EventBus from '@/utils/EventBus'
|
||||
|
||||
export default {
|
||||
data: () => ({
|
||||
previewStopped: false
|
||||
}),
|
||||
mounted() {
|
||||
this.$refs.preview.volume = 1
|
||||
|
||||
EventBus.$on('trackPreview:playPausePreview', this.playPausePreview)
|
||||
EventBus.$on('trackPreview:stopStackedTabsPreview', this.stopStackedTabsPreview)
|
||||
EventBus.$on('trackPreview:previewMouseEnter', this.previewMouseEnter)
|
||||
EventBus.$on('trackPreview:previewMouseLeave', this.previewMouseLeave)
|
||||
},
|
||||
methods: {
|
||||
async onCanPlay() {
|
||||
await this.$refs.preview.play()
|
||||
|
||||
this.previewStopped = false
|
||||
$(this.$refs.preview).animate({ volume: vol.preview_max_volume / 100 }, 500)
|
||||
},
|
||||
onTimeUpdate() {
|
||||
// Prevents first time entering in this function
|
||||
if (isNaN(this.$refs.preview.duration)) return
|
||||
if (this.$refs.preview.currentTime <= this.$refs.preview.duration - 1) return
|
||||
|
||||
$(this.$refs.preview).animate({ volume: 0 }, 800)
|
||||
|
||||
this.previewStopped = true
|
||||
|
||||
$('a[playing] > .preview_controls').css({ opacity: 0 })
|
||||
$('*').removeAttr('playing')
|
||||
$('.preview_controls').text('play_arrow')
|
||||
$('.preview_playlist_controls').text('play_arrow')
|
||||
},
|
||||
playPausePreview(e) {
|
||||
e.preventDefault()
|
||||
|
||||
const { currentTarget: obj } = event
|
||||
|
||||
var $icon = obj.tagName == 'I' ? $(obj) : $(obj).children('i')
|
||||
|
||||
if ($(obj).attr('playing')) {
|
||||
if (this.$refs.preview.paused) {
|
||||
this.$refs.preview.play()
|
||||
this.previewStopped = false
|
||||
|
||||
$icon.text('pause')
|
||||
|
||||
$(this.$refs.preview).animate({ volume: vol.preview_max_volume / 100 }, 500)
|
||||
} else {
|
||||
this.previewStopped = true
|
||||
|
||||
$icon.text('play_arrow')
|
||||
|
||||
$(this.$refs.preview).animate({ volume: 0 }, 250, 'swing', () => {
|
||||
this.$refs.preview.pause()
|
||||
})
|
||||
}
|
||||
} else {
|
||||
$('*').removeAttr('playing')
|
||||
$(obj).attr('playing', true)
|
||||
|
||||
$('.preview_controls').text('play_arrow')
|
||||
$('.preview_playlist_controls').text('play_arrow')
|
||||
$('.preview_controls').css({ opacity: 0 })
|
||||
|
||||
$icon.text('pause')
|
||||
$icon.css({ opacity: 1 })
|
||||
|
||||
this.previewStopped = false
|
||||
|
||||
$(this.$refs.preview).animate({ volume: 0 }, 250, 'swing', () => {
|
||||
this.$refs.preview.pause()
|
||||
$('#preview-track_source').prop('src', $(obj).data('preview'))
|
||||
this.$refs.preview.load()
|
||||
})
|
||||
}
|
||||
},
|
||||
stopStackedTabsPreview() {
|
||||
if (
|
||||
$('.preview_playlist_controls').filter(function() {
|
||||
return $(this).attr('playing')
|
||||
}).length > 0
|
||||
) {
|
||||
$(this.$refs.preview).animate({ volume: 0 }, 800)
|
||||
this.previewStopped = true
|
||||
$('.preview_playlist_controls').removeAttr('playing')
|
||||
$('.preview_playlist_controls').text('play_arrow')
|
||||
}
|
||||
},
|
||||
previewMouseEnter(e) {
|
||||
$(e.currentTarget).css({ opacity: 1 })
|
||||
},
|
||||
previewMouseLeave(event) {
|
||||
const { currentTarget: obj } = event
|
||||
|
||||
if (
|
||||
($(obj)
|
||||
.parent()
|
||||
.attr('playing') &&
|
||||
this.previewStopped) ||
|
||||
!$(obj)
|
||||
.parent()
|
||||
.attr('playing')
|
||||
) {
|
||||
$(obj).css({ opacity: 0 }, 200)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
300
src/components/TracklistTab.vue
Normal file
300
src/components/TracklistTab.vue
Normal file
@@ -0,0 +1,300 @@
|
||||
<template>
|
||||
<div id="tracklist_tab" class="main_tabcontent fixed_footer image_header">
|
||||
<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>Song</th>
|
||||
<th>Artist</th>
|
||||
<th v-if="type == 'Playlist'">Album</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 !== 'Spotify Playlist'">
|
||||
<template v-for="track in body">
|
||||
<tr v-if="track.type == '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"
|
||||
>
|
||||
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>
|
||||
<td
|
||||
class="table__cell--medium table__cell--center clickable"
|
||||
@click="artistView"
|
||||
:data-id="track.artist.id"
|
||||
>
|
||||
{{ track.artist.name }}
|
||||
</td>
|
||||
<td
|
||||
v-if="type == 'Playlist'"
|
||||
class="table__cell--medium table__cell--center clickable"
|
||||
@click="albumView"
|
||||
:data-id="track.album.id"
|
||||
>
|
||||
{{ track.album.title }}
|
||||
</td>
|
||||
<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"
|
||||
>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.40;margin-top: 8px;display: inline-block;font-size: 13px;">{{ label }}</span>
|
||||
<footer>
|
||||
<button @contextmenu.prevent="openQualityModal" @click.stop="addToQueue" :data-link="link">
|
||||
Download {{ type }}
|
||||
</button>
|
||||
<button
|
||||
class="with_icon"
|
||||
@contextmenu.prevent="openQualityModal"
|
||||
@click.stop="addToQueue"
|
||||
:data-link="selectedLinks()"
|
||||
>
|
||||
Download selection<i class="material-icons">file_download</i>
|
||||
</button>
|
||||
<button class="back-button">Back</button>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { isEmpty } from 'lodash-es'
|
||||
import { socket } from '@/utils/socket'
|
||||
import { showView } from '@js/tabs.js'
|
||||
import Downloads from '@/utils/downloads'
|
||||
import Utils from '@/utils/utils'
|
||||
import EventBus from '@/utils/EventBus'
|
||||
|
||||
export default {
|
||||
name: 'tracklist-tab',
|
||||
data: () => ({
|
||||
title: '',
|
||||
metadata: '',
|
||||
release_date: '',
|
||||
label: '',
|
||||
explicit: false,
|
||||
image: '',
|
||||
type: '',
|
||||
link: '',
|
||||
body: []
|
||||
}),
|
||||
methods: {
|
||||
artistView: showView.bind(null, 'artist'),
|
||||
albumView: showView.bind(null, 'album'),
|
||||
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 = ''
|
||||
this.body = []
|
||||
},
|
||||
addToQueue(e) {
|
||||
Downloads.sendAddToQueue(e.currentTarget.dataset.link)
|
||||
},
|
||||
openQualityModal(e) {
|
||||
this.$root.$emit('QualityModal:open', 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 == 'Spotify Playlist' ? item.uri : item.link)
|
||||
})
|
||||
}
|
||||
return selected.join(';')
|
||||
},
|
||||
convertDuration: Utils.convertDuration,
|
||||
showAlbum(data) {
|
||||
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} • ${numberOfTracks} songs`
|
||||
this.release_date = release_date.substring(0, 10)
|
||||
this.image = cover_xl
|
||||
|
||||
if (isEmpty(albumTracks)) {
|
||||
this.body = null
|
||||
} else {
|
||||
this.body = albumTracks
|
||||
}
|
||||
},
|
||||
showPlaylist(data) {
|
||||
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 = `by ${creatorName} • ${numberOfTracks} songs`
|
||||
|
||||
if (isEmpty(playlistTracks)) {
|
||||
this.body = null
|
||||
} else {
|
||||
this.body = playlistTracks
|
||||
}
|
||||
},
|
||||
showSpotifyPlaylist(data) {
|
||||
const {
|
||||
uri: playlistURI,
|
||||
name: playlistName,
|
||||
images,
|
||||
images: { length: numberOfImages },
|
||||
owner: { display_name: ownerName },
|
||||
tracks: playlistTracks,
|
||||
tracks: { length: numberOfTracks }
|
||||
} = data
|
||||
|
||||
this.type = 'Spotify Playlist'
|
||||
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 = `by ${ownerName} • ${numberOfTracks} songs`
|
||||
|
||||
if (isEmpty(playlistTracks)) {
|
||||
this.body = null
|
||||
} else {
|
||||
this.body = playlistTracks
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
EventBus.$on('tracklistTab:reset', this.reset)
|
||||
|
||||
socket.on('show_album', this.showAlbum)
|
||||
socket.on('show_playlist', this.showPlaylist)
|
||||
socket.on('show_spotifyplaylist', this.showSpotifyPlaylist)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
Reference in New Issue
Block a user