changed directory structure of js and Vue files; added plugin for translation (can try it in settings)

This commit is contained in:
Roberto Tonino
2020-07-17 00:11:28 +02:00
parent 1a300a6b1b
commit 42e44c45fe
35 changed files with 163 additions and 69 deletions

View File

@@ -1,32 +0,0 @@
<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>

View File

@@ -1,4 +0,0 @@
// https://alligator.io/vuejs/global-event-bus/
import Vue from 'vue'
export default new Vue()

View File

@@ -1,150 +0,0 @@
import Vue from 'vue'
// Object is needed for vue proxy (what does this mean?)
window.vol = {
preview_max_volume: 100
}
import App from '@/js/App.vue'
import $ from 'jquery'
import { socket } from '@/js/socket.js'
import { toast } from '@/js/toasts.js'
import { init as initTabs } from '@/js/tabs.js'
/* ===== App initialization ===== */
function startApp() {
mountApp()
initTabs()
}
function mountApp() {
// TODO Remove the App instance from the window when deemix will be a complete Vue App
window.App = new Vue({
render: h => h(App)
}).$mount('#app')
}
function initClient() {
window.clientMode = true
document.querySelector(`#open_downloads_folder`).classList.remove('hide')
}
document.addEventListener('DOMContentLoaded', startApp)
window.addEventListener('pywebviewready', initClient)
/* ===== Socketio listeners ===== */
// Debug messages for socketio
socket.on('message', function(msg) {
console.log(msg)
})
socket.on('logging_in', function() {
toast('Logging in', 'loading', false, 'login-toast')
})
socket.on('init_autologin', function() {
let arl = localStorage.getItem('arl')
let accountNum = localStorage.getItem('accountNum')
if (arl) {
arl = arl.trim()
if (accountNum != 0) {
socket.emit('login', arl, true, accountNum)
} else {
socket.emit('login', arl)
}
}
})
socket.on('logged_in', function(data) {
switch (data.status) {
case 1:
case 3:
toast('Logged in', 'done', true, 'login-toast')
if (data.arl) {
localStorage.setItem('arl', data.arl)
$('#login_input_arl').val(data.arl)
}
$('#open_login_prompt').hide()
if (data.user) {
$('#settings_username').text(data.user.name)
$('#settings_picture').attr(
'src',
`https://e-cdns-images.dzcdn.net/images/user/${data.user.picture}/125x125-000000-80-0-0.jpg`
)
// $('#logged_in_info').show()
document.getElementById('logged_in_info').classList.remove('hide')
}
document.getElementById('home_not_logged_in').classList.add('hide')
break
case 2:
toast('Already logged in', 'done', true, 'login-toast')
if (data.user) {
$('#settings_username').text(data.user.name)
$('#settings_picture').attr(
'src',
`https://e-cdns-images.dzcdn.net/images/user/${data.user.picture}/125x125-000000-80-0-0.jpg`
)
// $('#logged_in_info').show()
document.getElementById('logged_in_info').classList.remove('hide')
}
document.getElementById('home_not_logged_in').classList.add('hide')
break
case 0:
toast("Couldn't log in", 'close', true, 'login-toast')
localStorage.removeItem('arl')
$('#login_input_arl').val('')
$('#open_login_prompt').show()
document.getElementById('logged_in_info').classList.add('hide')
// $('#logged_in_info').hide()
$('#settings_username').text('Not Logged')
$('#settings_picture').attr('src', `https://e-cdns-images.dzcdn.net/images/user/125x125-000000-80-0-0.jpg`)
document.getElementById('home_not_logged_in').classList.remove('hide')
break
}
})
socket.on('logged_out', function() {
toast('Logged out', 'done', true, 'login-toast')
localStorage.removeItem('arl')
$('#login_input_arl').val('')
$('#open_login_prompt').show()
document.getElementById('logged_in_info').classList.add('hide')
$('#settings_username').text('Not Logged')
$('#settings_picture').attr('src', `https://e-cdns-images.dzcdn.net/images/user/125x125-000000-80-0-0.jpg`)
document.getElementById('home_not_logged_in').classList.remove('hide')
})
socket.on('cancellingCurrentItem', function(uuid) {
toast('Cancelling current item.', 'loading', false, 'cancelling_' + uuid)
})
socket.on('currentItemCancelled', function(uuid) {
toast('Current item cancelled.', 'done', true, 'cancelling_' + uuid)
})
socket.on('startAddingArtist', function(data) {
toast(`Adding ${data.name} albums to queue`, 'loading', false, 'artist_' + data.id)
})
socket.on('finishAddingArtist', function(data) {
toast(`Added ${data.name} albums to queue`, 'done', true, 'artist_' + data.id)
})
socket.on('startConvertingSpotifyPlaylist', function(id) {
toast('Converting spotify tracks to deezer tracks', 'loading', false, 'spotifyplaylist_' + id)
})
socket.on('finishConvertingSpotifyPlaylist', function(id) {
toast('Spotify playlist converted', 'done', true, 'spotifyplaylist_' + id)
})
socket.on('errorMessage', function(error) {
toast(error, 'error')
})
socket.on('alreadyInQueue', function(data) {
toast(`${data.title} is already in queue!`, 'playlist_add_check')
})

View File

@@ -1,196 +0,0 @@
<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 '@/js/socket.js'
import Downloads from '@/js/downloads.js'
import { showView } from '@/js/tabs.js'
import EventBus from '@/js/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>

View File

@@ -1,26 +0,0 @@
<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>

View File

@@ -1,82 +0,0 @@
<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>

View File

@@ -1,202 +0,0 @@
<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 '@/js/socket.js'
import { showView } from '@/js/tabs.js'
import Downloads from '@/js/downloads.js'
import Utils from '@/js/utils.js'
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>

View File

@@ -1,72 +0,0 @@
<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 '@/js/utils.js'
import EventBus from '@/js/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>

View File

@@ -1,351 +0,0 @@
<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 '@/js/socket.js'
import { toast } from '@/js/toasts.js'
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>

View File

@@ -1,52 +0,0 @@
<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 '@/js/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>

View File

@@ -1,310 +0,0 @@
<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 '@/js/socket.js'
import { showView, changeTab } from '@/js/tabs.js'
import Downloads from '@/js/downloads.js'
import Utils from '@/js/utils.js'
import { toast } from '@/js/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>

View File

@@ -1,102 +0,0 @@
<template>
<div id="home_tab" class="main_tabcontent">
<h2 class="page_heading">Welcome to deemix</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 '@/js/socket.js'
import { showView } from '@/js/tabs.js'
import Downloads from '@/js/downloads.js'
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>

View File

@@ -1,186 +0,0 @@
<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 '@/js/socket.js'
import { showView } from '@/js/tabs.js'
import Utils from '@/js/utils.js'
import EventBus from '@/js/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>

View File

@@ -1,21 +0,0 @@
<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>

View File

@@ -1,702 +0,0 @@
<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 '@/js/socket.js'
import { showView } from '@/js/tabs.js'
import Downloads from '@/js/downloads.js'
import Utils from '@/js/utils.js'
import BaseLoadingPlaceholder from '@components/BaseLoadingPlaceholder.vue'
import { changeTab } from '@/js/tabs.js'
import EventBus from '@/js/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>

View File

@@ -1,31 +0,0 @@
<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>

View File

@@ -1,80 +0,0 @@
<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 '@/js/downloads.js'
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>

View File

@@ -1,57 +0,0 @@
<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 '@/js/utils.js'
import Downloads from '@/js/downloads.js'
import EventBus from '@/js/EventBus.js'
import { socket } from '@/js/socket.js'
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>

View File

@@ -1,665 +0,0 @@
<template>
<div id="settings_tab" class="main_tabcontent fixed_footer">
<h2 class="page_heading">Settings</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">
How do I get my own ARL?
</a>
<button id="settings_btn_updateArl" @click="login" style="width:100%;">Update ARL</button>
</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 '@/js/toasts.js'
import { socket } from '@/js/socket.js'
import EventBus from '@/js/EventBus'
export default {
name: 'the-settings-tab',
data: () => ({
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() {
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>

View File

@@ -1,185 +0,0 @@
<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>

View File

@@ -1,123 +0,0 @@
<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 '@/js/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>

View File

@@ -1,300 +0,0 @@
<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 '@/js/socket.js'
import { showView } from '@/js/tabs.js'
import Downloads from '@/js/downloads.js'
import Utils from '@/js/utils.js'
import EventBus from '@/js/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>

View File

@@ -1,11 +0,0 @@
import { socket } from '@/js/socket.js'
function sendAddToQueue(url, bitrate = null) {
if (url != '') {
socket.emit('addToQueue', { url: url, bitrate: bitrate }, () => {})
}
}
export default {
sendAddToQueue
}

View File

@@ -1,5 +0,0 @@
export const socket = io.connect(window.location.href)
socket.on('connect', () => {
document.getElementById('start_app_placeholder').classList.add('loading_placeholder--hidden')
})

View File

@@ -1,5 +1,5 @@
import { socket } from '@/js/socket.js'
import EventBus from '@/js/EventBus'
import { socket } from '@/utils/socket'
import EventBus from '@/utils/EventBus'
/* ===== Globals ====== */
window.search_selected = ''

View File

@@ -1,47 +0,0 @@
import Toastify from 'toastify-js'
import $ from 'jquery'
import { socket } from './socket.js'
let toastsWithId = {}
export const toast = function (msg, icon = null, dismiss = true, id = null) {
if (toastsWithId[id]) {
let toastObj = toastsWithId[id]
let toastDOM = $(`div.toastify[toast_id=${id}]`)
if (msg) {
toastDOM.find('.toast-message').html(msg)
}
if (icon) {
if (icon == 'loading') icon = `<div class="circle-loader"></div>`
else icon = `<i class="material-icons">${icon}</i>`
toastDOM.find('.toast-icon').html(icon)
}
if (dismiss !== null && dismiss) {
setTimeout(function () {
toastObj.hideToast()
delete toastsWithId[id]
}, 3000)
}
} else {
if (icon == null) icon = ''
else if (icon == 'loading') icon = `<div class="circle-loader"></div>`
else icon = `<i class="material-icons">${icon}</i>`
let toastObj = Toastify({
text: `<span class="toast-icon">${icon}</span><span class="toast-message">${msg}</toast>`,
duration: dismiss ? 3000 : 0,
gravity: 'bottom',
position: 'left'
}).showToast()
if (id) {
toastsWithId[id] = toastObj
$(toastObj.toastElement).attr('toast_id', id)
}
}
}
socket.on('toast', data => {
const { msg, icon, dismiss, id } = data
toast(msg, icon || null, dismiss !== undefined ? dismiss : true, id || null)
})

View File

@@ -1,316 +0,0 @@
export function isValidURL(text) {
let lowerCaseText = text.toLowerCase()
if (lowerCaseText.startsWith('http')) {
if (lowerCaseText.indexOf('deezer.com') >= 0 || lowerCaseText.indexOf('open.spotify.com') >= 0) {
return true
}
} else if (lowerCaseText.startsWith('spotify:')) {
return true
}
return false
}
export function convertDuration(duration) {
// Convert from seconds only to mm:ss format
let mm, ss
mm = Math.floor(duration / 60)
ss = duration - mm * 60
// Add leading zero if ss < 0
if (ss < 10) {
ss = '0' + ss
}
return mm + ':' + ss
}
export function convertDurationSeparated(duration) {
let hh, mm, ss
mm = Math.floor(duration / 60)
hh = Math.floor(mm / 60)
ss = duration - mm * 60
mm -= hh * 60
return [hh, mm, ss]
}
export function numberWithDots(x) {
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, '.')
}
// On scroll event, returns currentTarget = null
// Probably on other events too
export function debounce(func, wait, immediate) {
var timeout
return function() {
var context = this
var args = arguments
var later = function() {
timeout = null
if (!immediate) func.apply(context, args)
}
var callNow = immediate && !timeout
clearTimeout(timeout)
timeout = setTimeout(later, wait)
if (callNow) func.apply(context, args)
}
}
export const COUNTRIES = {
AF: 'Afghanistan',
AX: '\u00c5land Islands',
AL: 'Albania',
DZ: 'Algeria',
AS: 'American Samoa',
AD: 'Andorra',
AO: 'Angola',
AI: 'Anguilla',
AQ: 'Antarctica',
AG: 'Antigua and Barbuda',
AR: 'Argentina',
AM: 'Armenia',
AW: 'Aruba',
AU: 'Australia',
AT: 'Austria',
AZ: 'Azerbaijan',
BS: 'Bahamas',
BH: 'Bahrain',
BD: 'Bangladesh',
BB: 'Barbados',
BY: 'Belarus',
BE: 'Belgium',
BZ: 'Belize',
BJ: 'Benin',
BM: 'Bermuda',
BT: 'Bhutan',
BO: 'Bolivia, Plurinational State of',
BQ: 'Bonaire, Sint Eustatius and Saba',
BA: 'Bosnia and Herzegovina',
BW: 'Botswana',
BV: 'Bouvet Island',
BR: 'Brazil',
IO: 'British Indian Ocean Territory',
BN: 'Brunei Darussalam',
BG: 'Bulgaria',
BF: 'Burkina Faso',
BI: 'Burundi',
KH: 'Cambodia',
CM: 'Cameroon',
CA: 'Canada',
CV: 'Cape Verde',
KY: 'Cayman Islands',
CF: 'Central African Republic',
TD: 'Chad',
CL: 'Chile',
CN: 'China',
CX: 'Christmas Island',
CC: 'Cocos (Keeling) Islands',
CO: 'Colombia',
KM: 'Comoros',
CG: 'Congo',
CD: 'Congo, the Democratic Republic of the',
CK: 'Cook Islands',
CR: 'Costa Rica',
CI: "C\u00f4te d'Ivoire",
HR: 'Croatia',
CU: 'Cuba',
CW: 'Cura\u00e7ao',
CY: 'Cyprus',
CZ: 'Czech Republic',
DK: 'Denmark',
DJ: 'Djibouti',
DM: 'Dominica',
DO: 'Dominican Republic',
EC: 'Ecuador',
EG: 'Egypt',
SV: 'El Salvador',
GQ: 'Equatorial Guinea',
ER: 'Eritrea',
EE: 'Estonia',
ET: 'Ethiopia',
FK: 'Falkland Islands (Malvinas)',
FO: 'Faroe Islands',
FJ: 'Fiji',
FI: 'Finland',
FR: 'France',
GF: 'French Guiana',
PF: 'French Polynesia',
TF: 'French Southern Territories',
GA: 'Gabon',
GM: 'Gambia',
GE: 'Georgia',
DE: 'Germany',
GH: 'Ghana',
GI: 'Gibraltar',
GR: 'Greece',
GL: 'Greenland',
GD: 'Grenada',
GP: 'Guadeloupe',
GU: 'Guam',
GT: 'Guatemala',
GG: 'Guernsey',
GN: 'Guinea',
GW: 'Guinea-Bissau',
GY: 'Guyana',
HT: 'Haiti',
HM: 'Heard Island and McDonald Islands',
VA: 'Holy See (Vatican City State)',
HN: 'Honduras',
HK: 'Hong Kong',
HU: 'Hungary',
IS: 'Iceland',
IN: 'India',
ID: 'Indonesia',
IR: 'Iran, Islamic Republic of',
IQ: 'Iraq',
IE: 'Ireland',
IM: 'Isle of Man',
IL: 'Israel',
IT: 'Italy',
JM: 'Jamaica',
JP: 'Japan',
JE: 'Jersey',
JO: 'Jordan',
KZ: 'Kazakhstan',
KE: 'Kenya',
KI: 'Kiribati',
KP: "Korea, Democratic People's Republic of",
KR: 'Korea, Republic of',
KW: 'Kuwait',
KG: 'Kyrgyzstan',
LA: "Lao People's Democratic Republic",
LV: 'Latvia',
LB: 'Lebanon',
LS: 'Lesotho',
LR: 'Liberia',
LY: 'Libya',
LI: 'Liechtenstein',
LT: 'Lithuania',
LU: 'Luxembourg',
MO: 'Macao',
MK: 'Macedonia, the Former Yugoslav Republic of',
MG: 'Madagascar',
MW: 'Malawi',
MY: 'Malaysia',
MV: 'Maldives',
ML: 'Mali',
MT: 'Malta',
MH: 'Marshall Islands',
MQ: 'Martinique',
MR: 'Mauritania',
MU: 'Mauritius',
YT: 'Mayotte',
MX: 'Mexico',
FM: 'Micronesia, Federated States of',
MD: 'Moldova, Republic of',
MC: 'Monaco',
MN: 'Mongolia',
ME: 'Montenegro',
MS: 'Montserrat',
MA: 'Morocco',
MZ: 'Mozambique',
MM: 'Myanmar',
NA: 'Namibia',
NR: 'Nauru',
NP: 'Nepal',
NL: 'Netherlands',
NC: 'New Caledonia',
NZ: 'New Zealand',
NI: 'Nicaragua',
NE: 'Niger',
NG: 'Nigeria',
NU: 'Niue',
NF: 'Norfolk Island',
MP: 'Northern Mariana Islands',
NO: 'Norway',
OM: 'Oman',
PK: 'Pakistan',
PW: 'Palau',
PS: 'Palestine, State of',
PA: 'Panama',
PG: 'Papua New Guinea',
PY: 'Paraguay',
PE: 'Peru',
PH: 'Philippines',
PN: 'Pitcairn',
PL: 'Poland',
PT: 'Portugal',
PR: 'Puerto Rico',
QA: 'Qatar',
RE: 'R\u00e9union',
RO: 'Romania',
RU: 'Russian Federation',
RW: 'Rwanda',
BL: 'Saint Barth\u00e9lemy',
SH: 'Saint Helena, Ascension and Tristan da Cunha',
KN: 'Saint Kitts and Nevis',
LC: 'Saint Lucia',
MF: 'Saint Martin (French part)',
PM: 'Saint Pierre and Miquelon',
VC: 'Saint Vincent and the Grenadines',
WS: 'Samoa',
SM: 'San Marino',
ST: 'Sao Tome and Principe',
SA: 'Saudi Arabia',
SN: 'Senegal',
RS: 'Serbia',
SC: 'Seychelles',
SL: 'Sierra Leone',
SG: 'Singapore',
SX: 'Sint Maarten (Dutch part)',
SK: 'Slovakia',
SI: 'Slovenia',
SB: 'Solomon Islands',
SO: 'Somalia',
ZA: 'South Africa',
GS: 'South Georgia and the South Sandwich Islands',
SS: 'South Sudan',
ES: 'Spain',
LK: 'Sri Lanka',
SD: 'Sudan',
SR: 'Suriname',
SJ: 'Svalbard and Jan Mayen',
SZ: 'Swaziland',
SE: 'Sweden',
CH: 'Switzerland',
SY: 'Syrian Arab Republic',
TW: 'Taiwan, Province of China',
TJ: 'Tajikistan',
TZ: 'Tanzania, United Republic of',
TH: 'Thailand',
TL: 'Timor-Leste',
TG: 'Togo',
TK: 'Tokelau',
TO: 'Tonga',
TT: 'Trinidad and Tobago',
TN: 'Tunisia',
TR: 'Turkey',
TM: 'Turkmenistan',
TC: 'Turks and Caicos Islands',
TV: 'Tuvalu',
UG: 'Uganda',
UA: 'Ukraine',
AE: 'United Arab Emirates',
GB: 'United Kingdom',
US: 'United States',
UM: 'United States Minor Outlying Islands',
UY: 'Uruguay',
UZ: 'Uzbekistan',
VU: 'Vanuatu',
VE: 'Venezuela, Bolivarian Republic of',
VN: 'Viet Nam',
VG: 'Virgin Islands, British',
VI: 'Virgin Islands, U.S.',
WF: 'Wallis and Futuna',
EH: 'Western Sahara',
YE: 'Yemen',
ZM: 'Zambia',
ZW: 'Zimbabwe'
}
export default {
isValidURL,
convertDuration,
convertDurationSeparated,
numberWithDots,
debounce,
COUNTRIES
}