workflow: reset feature/search-page-improvement branch

This commit is contained in:
Roberto Tonino 2020-11-02 12:25:08 +01:00
parent 66b1ebe244
commit 7f0d621f62
44 changed files with 3265 additions and 1592 deletions

2
.gitignore vendored
View File

@ -20,8 +20,8 @@ yarn-debug.log*
yarn-error.log* yarn-error.log*
# Editor directories and files # Editor directories and files
# .vscode
.idea .idea
.vscode
*.suo *.suo
*.ntvs* *.ntvs*
*.njsproj *.njsproj

28
.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,28 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "build",
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": [],
"label": "npm: build",
"detail": "npm-run-all --sequential clean build:js"
},
{
// Workaround for dev script
"type": "npm",
"script": "dev:gui",
"problemMatcher": [],
"label": "npm: dev:gui",
"detail": "npm-run-all --parallel serve:gui watch:js",
"group": {
"kind": "test",
"isDefault": true
}
}
]
}

2991
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,12 +4,12 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>deemix</title> <title>deemix</title>
<link rel="stylesheet" type="text/css" href="/css/vendor/material-icons.css">
<link rel="stylesheet" type="text/css" href="/css/vendor/OpenSans.css">
<link rel="shortcut icon" href="/favicon.ico"> <link rel="shortcut icon" href="/favicon.ico">
<meta name="viewport" <meta name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=0"> content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=0">
<script> <script>
if (localStorage.getItem('selectedTheme')) { if (localStorage.getItem('selectedTheme')) {
document.documentElement.setAttribute('data-theme', localStorage.getItem('selectedTheme')) document.documentElement.setAttribute('data-theme', localStorage.getItem('selectedTheme'))

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
<template> <template>
<div> <div id="app">
<div class="app-container"> <div class="app-container">
<TheSidebar /> <TheSidebar />
@ -12,8 +12,8 @@
</div> </div>
<BaseLoadingPlaceholder <BaseLoadingPlaceholder
:hidden="isSocketConnected"
text="Connecting to the server..." text="Connecting to the server..."
:hidden="isSocketConnected"
additionalClasses="absolute top-0 left-0 w-screen h-screen bg-black bg-opacity-50 z-50" additionalClasses="absolute top-0 left-0 w-screen h-screen bg-black bg-opacity-50 z-50"
/> />
@ -44,7 +44,7 @@ import BaseLoadingPlaceholder from '@components/globals/BaseLoadingPlaceholder.v
import TheContextMenu from '@components/globals/TheContextMenu.vue' import TheContextMenu from '@components/globals/TheContextMenu.vue'
import TheTrackPreview from '@components/globals/TheTrackPreview.vue' import TheTrackPreview from '@components/globals/TheTrackPreview.vue'
import TheQualityModal from '@components/globals/TheQualityModal.vue' import TheQualityModal from '@components/globals/TheQualityModal.vue'
import ConfirmModal from '@components/globals/ConfirmModal.vue' // import ConfirmModal from '@components/globals/ConfirmModal.vue'
import TheSidebar from '@components/TheSidebar.vue' import TheSidebar from '@components/TheSidebar.vue'
import TheSearchBar from '@components/TheSearchBar.vue' import TheSearchBar from '@components/TheSearchBar.vue'
@ -65,8 +65,8 @@ export default {
TheQualityModal, TheQualityModal,
BaseLoadingPlaceholder, BaseLoadingPlaceholder,
TheContextMenu, TheContextMenu,
TheContent, TheContent
ConfirmModal // ConfirmModal
}, },
mounted() { mounted() {
socket.on('connect', () => { socket.on('connect', () => {

View File

@ -5,8 +5,12 @@ window.vol = {
preview_max_volume: 100 preview_max_volume: 100
} }
import '@/styles/css/material-icons.css'
import '@/styles/css/OpenSans.css'
import '@/styles/scss/style.scss' import '@/styles/scss/style.scss'
import '@/styles/css/components.css' import '@/styles/css/components.css'
import '@/styles/css/helpers.css'
import App from '@/App.vue' import App from '@/App.vue'
import i18n from '@/plugins/i18n' import i18n from '@/plugins/i18n'
@ -174,9 +178,9 @@ socket.on('errorMessage', function(error) {
socket.on('queueError', function(queueItem) { socket.on('queueError', function(queueItem) {
if (queueItem.errid) { if (queueItem.errid) {
toast(queueItem.link+ " - " +i18n.t(`errors.ids.${queueItem.errid}`), 'error') toast(queueItem.link + ' - ' + i18n.t(`errors.ids.${queueItem.errid}`), 'error')
} else { } else {
toast(queueItem.link+ " - " +queueItem.error, 'error') toast(queueItem.link + ' - ' + queueItem.error, 'error')
} }
}) })

View File

@ -31,8 +31,6 @@
</template> </template>
<style lang="scss"> <style lang="scss">
@import '../styles/scss/base/_variables.scss';
// src/components/TheContent.vue
#container { #container {
--container-width: 95%; --container-width: 95%;
@ -41,11 +39,11 @@
width: var(--container-width); width: var(--container-width);
transform: scale(1); transform: scale(1);
@media only screen and (min-width: $small) { @media only screen and (min-width: 601px) {
--container-width: 85%; --container-width: 85%;
} }
@media only screen and (min-width: $medium) { @media only screen and (min-width: 993px) {
--container-width: 70%; --container-width: 70%;
} }
} }

View File

@ -168,7 +168,8 @@ export default {
} }
} else { } else {
if (isShowingSearch && sameAsLastSearch) { if (isShowingSearch && sameAsLastSearch) {
this.$root.$emit('mainSearch:updateResults', term) // ? Has this any sense since we're not performing any call?
// this.$root.$emit('mainSearch:updateResults', term)
return return
} }

View File

@ -13,7 +13,7 @@
<span <span
v-if="hasFails" v-if="hasFails"
class="inline-flex" class="flex items-center"
:class="{ clickable: finishedWithFails }" :class="{ clickable: finishedWithFails }"
style="justify-content: center" style="justify-content: center"
@click="finishedWithFails ? $emit('show-errors', queueItem) : null" @click="finishedWithFails ? $emit('show-errors', queueItem) : null"
@ -181,6 +181,36 @@
} }
} }
} }
@keyframes indeterminate {
0% {
left: -35%;
right: 100%;
}
60% {
left: 100%;
right: -90%;
}
100% {
left: 100%;
right: -90%;
}
}
@keyframes indeterminate-short {
0% {
left: -200%;
right: 100%;
}
60% {
left: 107%;
right: -8%;
}
100% {
left: 107%;
right: -8%;
}
}
</style> </style>
<script> <script>

View File

@ -1,11 +1,10 @@
<template functional> <template functional>
<div <div
:id="props.id"
class="flex justify-center items-center flex-col flex-1 h-full" class="flex justify-center items-center flex-col flex-1 h-full"
:class="props.additionalClasses" :class="props.additionalClasses"
v-show="!props.hidden" v-show="!props.hidden"
> >
<span class="mb-5">{{ props.text }}</span> <span class="mb-5">{{ props.text || 'Loading...' }}</span>
<div class="lds-ring"> <div class="lds-ring">
<div></div> <div></div>
@ -60,27 +59,3 @@
} }
</style> </style>
<script>
export default {
props: {
text: {
type: String,
required: false,
default: 'Loading...'
},
id: {
type: String,
required: false
},
hidden: {
type: Boolean,
required: false,
default: false
},
additionalClasses: {
type: String,
required: false
}
}
}
</script>

View File

@ -23,8 +23,6 @@
</div> </div>
</template> </template>
<style lang="scss"> <style lang="scss">
@import '../../styles/scss/base/_variables.scss';
.smallmodal { .smallmodal {
position: fixed; position: fixed;
z-index: 1250; z-index: 1250;
@ -47,11 +45,11 @@
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
@media only screen and (min-width: $small) { @media only screen and (min-width: 601px) {
--modal-content-width: 85%; --modal-content-width: 85%;
} }
@media only screen and (min-width: $medium) { @media only screen and (min-width: 993px) {
--modal-content-width: 70%; --modal-content-width: 70%;
} }
} }

View File

@ -23,10 +23,10 @@
<a href="https://deemix.app" target="_blank">🌍 {{ $t('about.officialWebsite') }}</a> <a href="https://deemix.app" target="_blank">🌍 {{ $t('about.officialWebsite') }}</a>
</li> --> </li> -->
<li> <li>
<a href="https://git.fuwafuwa.moe/RemixDev/deemix" target="_blank">🚀 {{ $t('about.officialRepo') }}</a> <a href="https://codeberg.org/RemixDev/deemix" target="_blank">🚀 {{ $t('about.officialRepo') }}</a>
</li> </li>
<li> <li>
<a href="https://git.fuwafuwa.moe/RemixDev/deemix-webui" target="_blank">💻 {{ $t('about.officialWebuiRepo') }}</a> <a href="https://codeberg.org/RemixDev/deemix-webui" target="_blank">💻 {{ $t('about.officialWebuiRepo') }}</a>
</li> </li>
<li> <li>
<a href="https://www.reddit.com/r/deemix" target="_blank">🤖 {{ $t('about.officialSubreddit') }}</a> <a href="https://www.reddit.com/r/deemix" target="_blank">🤖 {{ $t('about.officialSubreddit') }}</a>

View File

@ -1,7 +1,7 @@
<template> <template>
<div id="artist_tab" class="relative image-header" ref="root"> <div id="artist_tab" class="relative image-header" ref="root">
<header <header
class="inline-flex" class="flex items-center"
:style="{ :style="{
'background-image': 'background-image':
'linear-gradient(to bottom, transparent 0%, var(--main-background) 100%), url(\'' + image + '\')' 'linear-gradient(to bottom, transparent 0%, var(--main-background) 100%), url(\'' + image + '\')'
@ -14,7 +14,7 @@
aria-label="download" aria-label="download"
@click.stop="addToQueue" @click.stop="addToQueue"
:data-link="link" :data-link="link"
class="rounded-full bg-primary text-grayscale-870 cursor-pointer w-16 h-16 grid place-items-center right" class="rounded-full bg-primary text-grayscale-870 cursor-pointer w-16 h-16 grid place-items-center ml-auto"
> >
<i class="material-icons text-4xl" :title="$t('globals.download_hint')">get_app</i> <i class="material-icons text-4xl" :title="$t('globals.download_hint')">get_app</i>
</div> </div>
@ -54,13 +54,13 @@
</thead> </thead>
<tbody> <tbody>
<tr v-for="release in showTable" :key="release.id"> <tr v-for="release in showTable" :key="release.id">
<router-link tag="td" class="inline-flex clickable" :to="{ name: 'Album', params: { id: release.id } }"> <router-link tag="td" class="flex items-center clickable" :to="{ name: 'Album', params: { id: release.id } }">
<img <img
class="rounded coverart" class="rounded coverart"
:src="release.cover_small" :src="release.cover_small"
style="margin-right: 16px; width: 56px; height: 56px" style="margin-right: 16px; width: 56px; height: 56px"
/> />
<i v-if="release.explicit_lyrics" class="material-icons explicit_icon"> explicit </i> <i v-if="release.explicit_lyrics" class="material-icons explicit-icon"> explicit </i>
{{ release.title }} {{ release.title }}
<i v-if="checkNewRelease(release.release_date)" class="material-icons" style="color: #ff7300"> <i v-if="checkNewRelease(release.release_date)" class="material-icons" style="color: #ff7300">
fiber_new fiber_new

View File

@ -27,7 +27,7 @@
<table class="table table--charts"> <table class="table table--charts">
<tbody> <tbody>
<tr v-for="track in chart" class="track_row"> <tr v-for="track in chart" class="track_row">
<td class="top-tracks-position" :class="{ first: track.position === 1 }"> <td class="p-3 text-center cursor-default" :class="{ first: track.position === 1 }">
{{ track.position }} {{ track.position }}
</td> </td>
<td class="table__icon table__icon--big"> <td class="table__icon table__icon--big">

View File

@ -26,9 +26,7 @@
</ul> </ul>
<button class="btn btn-primary" v-if="!activeTabEmpty" style="margin-bottom: 2rem" @click="downloadAllOfType"> <button class="btn btn-primary" v-if="!activeTabEmpty" style="margin-bottom: 2rem" @click="downloadAllOfType">
{{ {{ $t('globals.downloadAll', { thing: $tc(`globals.listTabs.${activeTab}`, 2) }) }}
$t('globals.download', { thing: $tc(`globals.listTabs.${activeTab}N`, getTabLenght() )})
}}
</button> </button>
<div class="favorites_tabcontent" :class="{ 'favorites_tabcontent--active': activeTab === 'playlist' }"> <div class="favorites_tabcontent" :class="{ 'favorites_tabcontent--active': activeTab === 'playlist' }">
@ -166,7 +164,7 @@
</div> </div>
<table v-if="tracks.length > 0" class="table"> <table v-if="tracks.length > 0" class="table">
<tr v-for="track in tracks" class="track_row"> <tr v-for="track in tracks" class="track_row">
<td class="top-tracks-position" :class="{ first: track.position === 1 }"> <td class="p-3 text-center cursor-default" :class="{ first: track.position === 1 }">
{{ track.position }} {{ track.position }}
</td> </td>
<td> <td>
@ -389,12 +387,6 @@ export default {
return toDownload return toDownload
}, },
getTabLenght(tab = this.activeTab) {
let total = this[`${tab}s`].length
// TODO: Add Spotify playlists to downlaod queue as well
//if (tab === "playlist") total += this.spotifyPlaylists.length
return total
},
getLovedTracksPlaylist() { getLovedTracksPlaylist() {
let lovedTracks = this.playlists.filter(playlist => { let lovedTracks = this.playlists.filter(playlist => {
return playlist.is_loved_track return playlist.is_loved_track

View File

@ -4,7 +4,7 @@
<section class="py-6 border-0 border-t border-solid border-grayscale-500" ref="notLogged" v-if="!isLoggedIn"> <section class="py-6 border-0 border-t border-solid border-grayscale-500" ref="notLogged" v-if="!isLoggedIn">
<p id="home_not_logged_text" class="mb-4">{{ $t('home.needTologin') }}</p> <p id="home_not_logged_text" class="mb-4">{{ $t('home.needTologin') }}</p>
<router-link tag="button" name="button" :to="{ name: 'Settings' }" class="btn btn-primary"> <router-link tag="button" class="btn btn-primary" name="button" :to="{ name: 'Settings' }">
{{ $t('home.openSettings') }} {{ $t('home.openSettings') }}
</router-link> </router-link>
</section> </section>

View File

@ -17,7 +17,7 @@
<div v-else> <div v-else>
<header <header
class="inline-flex" class="flex items-center"
:style="{ :style="{
'background-image': 'background-image':
'linear-gradient(to bottom, transparent 0%, var(--main-background) 100%), url(\'' + image + '\')' 'linear-gradient(to bottom, transparent 0%, var(--main-background) 100%), url(\'' + image + '\')'
@ -68,7 +68,7 @@
@contextmenu.prevent="openQualityModal" @contextmenu.prevent="openQualityModal"
@click.stop="addToQueue" @click.stop="addToQueue"
:data-link="link" :data-link="link"
class="rounded-full bg-primary text-grayscale-870 cursor-pointer w-16 h-16 grid place-items-center right" class="rounded-full bg-primary text-grayscale-870 cursor-pointer w-16 h-16 grid place-items-center ml-auto"
> >
<i class="material-icons text-4xl" :title="$t('globals.download_hint')">get_app</i> <i class="material-icons text-4xl" :title="$t('globals.download_hint')">get_app</i>
</div> </div>
@ -121,10 +121,11 @@
</table> </table>
<div v-if="type == 'album'"> <div v-if="type == 'album'">
<router-link tag="button" :to="{ name: 'Album', params: { id } }"> <router-link tag="button" class="btn btn-primary" name="button" :to="{ name: 'Album', params: { id } }">
{{ $t('linkAnalyzer.table.tracklist') }} {{ $t('linkAnalyzer.table.tracklist') }}
</router-link> </router-link>
</div> </div>
<div v-if="countries.length"> <div v-if="countries.length">
<p v-for="country in countries">{{ country[0] }} - {{ country[1] }}</p> <p v-for="country in countries">{{ country[0] }} - {{ country[1] }}</p>
</div> </div>

View File

@ -21,7 +21,8 @@
<keep-alive> <keep-alive>
<component <component
:is="currentTab.component" :is="currentTab.component"
:results="results" :viewInfo="getViewInfo()"
want-headers
@add-to-queue="addToQueue" @add-to-queue="addToQueue"
@change-search-tab="changeSearchTab" @change-search-tab="changeSearchTab"
></component> ></component>
@ -43,10 +44,20 @@ import { sendAddToQueue } from '@/utils/downloads'
import { numberWithDots, convertDuration } from '@/utils/utils' import { numberWithDots, convertDuration } from '@/utils/utils'
import EventBus from '@/utils/EventBus' import EventBus from '@/utils/EventBus'
import { reduceSearchResults, formatSingleTrack, formatAlbums, formatArtist, formatPlaylist } from '@/data/search'
const resetObj = { data: [], next: 0, total: 0, hasLoaded: false }
export default { export default {
components: { components: {
BaseLoadingPlaceholder BaseLoadingPlaceholder
}, },
props: {
performScrolledSearch: {
type: Boolean,
required: false
}
},
data() { data() {
const $t = this.$t.bind(this) const $t = this.$t.bind(this)
const $tc = this.$tc.bind(this) const $tc = this.$tc.bind(this)
@ -54,33 +65,45 @@ export default {
return { return {
currentTab: { currentTab: {
name: '', name: '',
component: {} searchType: '',
component: {},
viewInfo: '',
formatFunc: () => {}
}, },
tabs: [ tabs: [
{ {
name: $t('globals.listTabs.all'), name: $t('globals.listTabs.all'),
searchType: 'all', searchType: 'all',
component: ResultsAll component: ResultsAll,
viewInfo: 'allTab'
}, },
{ {
name: $tc('globals.listTabs.track', 2), name: $tc('globals.listTabs.track', 2),
searchType: 'track', searchType: 'track',
component: ResultsTracks component: ResultsTracks,
viewInfo: 'trackTab',
formatFunc: formatSingleTrack
}, },
{ {
name: $tc('globals.listTabs.album', 2), name: $tc('globals.listTabs.album', 2),
searchType: 'album', searchType: 'album',
component: ResultsAlbums component: ResultsAlbums,
viewInfo: 'albumTab',
formatFunc: formatAlbums
}, },
{ {
name: $tc('globals.listTabs.artist', 2), name: $tc('globals.listTabs.artist', 2),
searchType: 'artist', searchType: 'artist',
component: ResultsArtists component: ResultsArtists,
viewInfo: 'artistTab',
formatFunc: formatArtist
}, },
{ {
name: $tc('globals.listTabs.playlist', 2), name: $tc('globals.listTabs.playlist', 2),
searchType: 'playlist', searchType: 'playlist',
component: ResultsPlaylists component: ResultsPlaylists,
viewInfo: 'playlistTab',
formatFunc: formatPlaylist
} }
], ],
results: { results: {
@ -88,35 +111,23 @@ export default {
allTab: { allTab: {
ORDER: [], ORDER: [],
TOP_RESULT: [], TOP_RESULT: [],
ALBUM: {}, ALBUM: {
ARTIST: {}, hasLoaded: false
TRACK: {},
PLAYLIST: {}
}, },
trackTab: { ARTIST: {
data: [], hasLoaded: false
next: 0,
total: 0,
loaded: false
}, },
albumTab: { TRACK: {
data: [], hasLoaded: false
next: 0,
total: 0,
loaded: false
}, },
artistTab: { PLAYLIST: {
data: [], hasLoaded: false
next: 0,
total: 0,
loaded: false
},
playlistTab: {
data: [],
next: 0,
total: 0,
loaded: false
} }
},
trackTab: { ...resetObj },
albumTab: { ...resetObj },
artistTab: { ...resetObj },
playlistTab: { ...resetObj }
} }
} }
}, },
@ -125,87 +136,82 @@ export default {
return this.results.query !== '' return this.results.query !== ''
}, },
loadedTabs() { loadedTabs() {
const loaded = [] const tabsLoaded = []
for (const resultKey in this.results) { for (const resultKey in this.results) {
if (this.results.hasOwnProperty(resultKey)) { if (this.results.hasOwnProperty(resultKey)) {
const result = this.results[resultKey] const currentResult = this.results[resultKey]
if (result.loaded) { if (currentResult.hasLoaded) {
loaded.push(resultKey.replace(/Tab/g, '')) tabsLoaded.push(resultKey.replace(/Tab/g, ''))
} }
} }
} }
return loaded return tabsLoaded
}
},
props: {
performScrolledSearch: {
type: Boolean,
required: false
} }
}, },
created() { created() {
this.currentTab = this.tabs[0] this.currentTab = this.tabs[0]
}, },
mounted() { mounted() {
EventBus.$on('mainSearch:checkLoadMoreContent', this.checkLoadMoreContent) this.$root.$on('mainSearch:showNewResults', this.checkIfPerformNewMainSearch)
this.$root.$on('mainSearch:showNewResults', this.checkIfShowNewResults)
this.$root.$on('mainSearch:updateResults', this.checkIfUpdateResults) this.$root.$on('mainSearch:updateResults', this.checkIfUpdateResults)
socket.on('mainSearch', this.handleMainSearch) socket.on('mainSearch', this.saveMainSearchResult)
socket.on('search', this.handleSearch) socket.on('search', this.handleSearch)
}, },
methods: { methods: {
changeSearchTab(sectionName) { numberWithDots,
sectionName = sectionName.toLowerCase() convertDuration,
addToQueue(e) {
sendAddToQueue(e.currentTarget.dataset.link)
},
getViewInfo() {
if (this.currentTab.searchType === 'all') {
return this.results.allTab
}
let newTab = this.tabs.find(tab => { return reduceSearchResults(this.results[this.currentTab.viewInfo], this.currentTab.formatFunc)
return tab.searchType === sectionName },
changeSearchTab(tabName) {
tabName = tabName.toLowerCase()
const newTab = this.tabs.find(tab => {
return tab.searchType === tabName
}) })
if (!newTab) { if (!newTab) {
console.error(`No tab ${sectionName} found`) console.error(`No tab ${tabName} found`)
return return
} }
window.scrollTo(0, 0) window.scrollTo(0, 0)
this.currentTab = newTab this.currentTab = newTab
}, },
checkIfShowNewResults(term, mainSelected) { checkIfPerformNewMainSearch(searchTerm) {
let needToPerformNewSearch = term !== this.results.query /* || mainSelected == 'search_tab' */ let needToPerformNewMainSearch = searchTerm !== this.results.query
if (needToPerformNewSearch) { if (needToPerformNewMainSearch) {
this.showNewResults(term) this.performNewMainSearch(searchTerm)
} }
}, },
checkIfUpdateResults(term) { performNewMainSearch(term) {
let needToUpdateSearch = term === this.results.query && this.currentTab.searchType !== 'all'
if (needToUpdateSearch) {
let resetObj = { data: [], next: 0, total: 0, loaded: false }
this.results[this.currentTab.searchType + 'Tab'] = { ...resetObj }
this.search(this.currentTab.searchType)
}
},
showNewResults(term) {
socket.emit('mainSearch', { term }) socket.emit('mainSearch', { term })
// Showing loading placeholder // Showing loading placeholder
this.$root.$emit('updateSearchLoadingState', true) this.$root.$emit('updateSearchLoadingState', true)
this.currentTab = this.tabs[0] this.currentTab = this.tabs[0]
}, },
checkLoadMoreContent(searchSelected) { // ! Updates search only if the search term is the same as before AND we're not in the ALL tab. Wtf
if (this.results[searchSelected.split('_')[0] + 'Tab'].data.length !== 0) return checkIfUpdateResults(term) {
let needToUpdateSearch = term === this.results.query && this.currentTab.searchType !== 'all'
this.search(searchSelected.split('_')[0]) if (needToUpdateSearch) {
this.results[this.currentTab.searchType + 'Tab'] = { ...resetObj }
this.search(this.currentTab.searchType)
}
}, },
addToQueue(e) {
sendAddToQueue(e.currentTarget.dataset.link)
},
numberWithDots,
convertDuration,
search(type) { search(type) {
socket.emit('search', { socket.emit('search', {
term: this.results.query, term: this.results.query,
@ -217,29 +223,34 @@ export default {
scrolledSearch() { scrolledSearch() {
if (this.currentTab.searchType === 'all') return if (this.currentTab.searchType === 'all') return
let currentTab = `${this.currentTab.searchType}Tab` const currentTabKey = `${this.currentTab.searchType}Tab`
const needToPerformScrolledSearch = this.results[currentTabKey].next < this.results[currentTabKey].total
if (this.results[currentTab].next < this.results[currentTab].total) { if (needToPerformScrolledSearch) {
this.search(this.currentTab.searchType) this.search(this.currentTab.searchType)
} }
}, },
handleMainSearch(result) { saveMainSearchResult(searchResult) {
// Hiding loading placeholder // Hide loading placeholder
this.$root.$emit('updateSearchLoadingState', false) this.$root.$emit('updateSearchLoadingState', false)
let resetObj = { data: [], next: 0, total: 0, loaded: false } this.results.query = searchResult.QUERY
this.results.allTab = searchResult
this.results.allTab.TRACK.hasLoaded = true
this.results.allTab.ALBUM.hasLoaded = true
this.results.allTab.ARTIST.hasLoaded = true
this.results.allTab.PLAYLIST.hasLoaded = true
this.results.allTab = result
this.results.trackTab = { ...resetObj } this.results.trackTab = { ...resetObj }
this.results.albumTab = { ...resetObj } this.results.albumTab = { ...resetObj }
this.results.artistTab = { ...resetObj } this.results.artistTab = { ...resetObj }
this.results.playlistTab = { ...resetObj } this.results.playlistTab = { ...resetObj }
this.results.query = result.QUERY
}, },
handleSearch(result) { handleSearch(result) {
const { next: nextResult, total, type, data } = result const { next: nextResult, total, type, data: newData } = result
let currentTab = type + 'Tab' const currentTabKey = type + 'Tab'
let next = 0 let next = 0
if (nextResult) { if (nextResult) {
@ -248,16 +259,16 @@ export default {
next = total next = total
} }
if (this.results[currentTab].total != total) { if (this.results[currentTabKey].total !== total) {
this.results[currentTab].total = total this.results[currentTabKey].total = total
} }
if (this.results[currentTab].next != next) { if (this.results[currentTabKey].next !== next) {
this.results[currentTab].next = next this.results[currentTabKey].next = next
this.results[currentTab].data = this.results[currentTab].data.concat(data) this.results[currentTabKey].data = this.results[currentTabKey].data.concat(newData)
} }
this.results[currentTab].loaded = true this.results[currentTabKey].hasLoaded = true
}, },
isTabLoaded(tab) { isTabLoaded(tab) {
return this.loadedTabs.indexOf(tab.searchType) !== -1 || tab.searchType === 'all' return this.loadedTabs.indexOf(tab.searchType) !== -1 || tab.searchType === 'all'

View File

@ -22,7 +22,7 @@
<h3 class="settings-group__header settings-group__header--with-icon"> <h3 class="settings-group__header settings-group__header--with-icon">
<i class="material-icons">person</i>{{ $t('settings.login.title') }} <i class="material-icons">person</i>{{ $t('settings.login.title') }}
</h3> </h3>
<div class="inline-flex"> <div class="flex items-center">
<input <input
autocomplete="off" autocomplete="off"
type="password" type="password"
@ -68,11 +68,11 @@
<h3 class="settings-group__header settings-group__header--with-icon"> <h3 class="settings-group__header settings-group__header--with-icon">
<i class="material-icons">web</i>{{ $t('settings.appearance.title') }} <i class="material-icons">web</i>{{ $t('settings.appearance.title') }}
</h3> </h3>
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="changeSlimDownloads" /> <input type="checkbox" v-model="changeSlimDownloads" />
<span class="checkbox_text">{{ $t('settings.appearance.slimDownloadTab') }}</span> <span class="checkbox_text">{{ $t('settings.appearance.slimDownloadTab') }}</span>
</label> </label>
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="changeSlimSidebar" /> <input type="checkbox" v-model="changeSlimSidebar" />
<span class="checkbox_text">{{ $t('settings.appearance.slimSidebar') }}</span> <span class="checkbox_text">{{ $t('settings.appearance.slimSidebar') }}</span>
</label> </label>
@ -82,7 +82,7 @@
<h3 class="settings-group__header settings-group__header--with-icon"> <h3 class="settings-group__header settings-group__header--with-icon">
<i class="material-icons">folder</i>{{ $t('settings.downloadPath.title') }} <i class="material-icons">folder</i>{{ $t('settings.downloadPath.title') }}
</h3> </h3>
<div class="inline-flex"> <div class="flex items-center">
<input autocomplete="off" type="text" v-model="settings.downloadLocation" /> <input autocomplete="off" type="text" v-model="settings.downloadLocation" />
<button <button
id="select_downloads_folder" id="select_downloads_folder"
@ -116,7 +116,7 @@
</h3> </h3>
<div class="settings-container"> <div class="settings-container">
<div class="settings-container__third"> <div class="settings-container__third">
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.createPlaylistFolder" /> <input type="checkbox" v-model="settings.createPlaylistFolder" />
<span class="checkbox_text">{{ $t('settings.folders.createPlaylistFolder') }}</span> <span class="checkbox_text">{{ $t('settings.folders.createPlaylistFolder') }}</span>
</label> </label>
@ -126,7 +126,7 @@
</div> </div>
</div> </div>
<div class="settings-container__third"> <div class="settings-container__third">
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.createArtistFolder" /> <input type="checkbox" v-model="settings.createArtistFolder" />
<span class="checkbox_text">{{ $t('settings.folders.createArtistFolder') }}</span> <span class="checkbox_text">{{ $t('settings.folders.createArtistFolder') }}</span>
</label> </label>
@ -137,7 +137,7 @@
</div> </div>
</div> </div>
<div class="settings-container__third"> <div class="settings-container__third">
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.createAlbumFolder" /> <input type="checkbox" v-model="settings.createAlbumFolder" />
<span class="checkbox_text">{{ $t('settings.folders.createAlbumFolder') }}</span> <span class="checkbox_text">{{ $t('settings.folders.createAlbumFolder') }}</span>
</label> </label>
@ -149,17 +149,17 @@
</div> </div>
</div> </div>
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.createCDFolder" /> <input type="checkbox" v-model="settings.createCDFolder" />
<span class="checkbox_text">{{ $t('settings.folders.createCDFolder') }}</span> <span class="checkbox_text">{{ $t('settings.folders.createCDFolder') }}</span>
</label> </label>
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.createStructurePlaylist" /> <input type="checkbox" v-model="settings.createStructurePlaylist" />
<span class="checkbox_text">{{ $t('settings.folders.createStructurePlaylist') }}</span> <span class="checkbox_text">{{ $t('settings.folders.createStructurePlaylist') }}</span>
</label> </label>
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.createSingleFolder" /> <input type="checkbox" v-model="settings.createSingleFolder" />
<span class="checkbox_text">{{ $t('settings.folders.createSingleFolder') }}</span> <span class="checkbox_text">{{ $t('settings.folders.createSingleFolder') }}</span>
</label> </label>
@ -172,7 +172,7 @@
<div class="settings-container"> <div class="settings-container">
<div class="settings-container__third settings-container__third--only-checkbox"> <div class="settings-container__third settings-container__third--only-checkbox">
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.padTracks" /> <input type="checkbox" v-model="settings.padTracks" />
<span class="checkbox_text">{{ $t('settings.trackTitles.padTracks') }}</span> <span class="checkbox_text">{{ $t('settings.trackTitles.padTracks') }}</span>
</label> </label>
@ -224,34 +224,34 @@
<div class="settings-container"> <div class="settings-container">
<div class="settings-container__third settings-container__third--only-checkbox"> <div class="settings-container__third settings-container__third--only-checkbox">
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.fallbackBitrate" /> <input type="checkbox" v-model="settings.fallbackBitrate" />
<span class="checkbox_text">{{ $t('settings.downloads.fallbackBitrate') }}</span> <span class="checkbox_text">{{ $t('settings.downloads.fallbackBitrate') }}</span>
</label> </label>
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.fallbackSearch" /> <input type="checkbox" v-model="settings.fallbackSearch" />
<span class="checkbox_text">{{ $t('settings.downloads.fallbackSearch') }}</span> <span class="checkbox_text">{{ $t('settings.downloads.fallbackSearch') }}</span>
</label> </label>
</div> </div>
<div class="settings-container__third settings-container__third--only-checkbox"> <div class="settings-container__third settings-container__third--only-checkbox">
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.logErrors" /> <input type="checkbox" v-model="settings.logErrors" />
<span class="checkbox_text">{{ $t('settings.downloads.logErrors') }}</span> <span class="checkbox_text">{{ $t('settings.downloads.logErrors') }}</span>
</label> </label>
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.logSearched" /> <input type="checkbox" v-model="settings.logSearched" />
<span class="checkbox_text">{{ $t('settings.downloads.logSearched') }}</span> <span class="checkbox_text">{{ $t('settings.downloads.logSearched') }}</span>
</label> </label>
</div> </div>
<div class="settings-container__third settings-container__third--only-checkbox"> <div class="settings-container__third settings-container__third--only-checkbox">
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.syncedLyrics" /> <input type="checkbox" v-model="settings.syncedLyrics" />
<span class="checkbox_text">{{ $t('settings.downloads.syncedLyrics') }}</span> <span class="checkbox_text">{{ $t('settings.downloads.syncedLyrics') }}</span>
</label> </label>
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.createM3U8File" /> <input type="checkbox" v-model="settings.createM3U8File" />
<span class="checkbox_text">{{ $t('settings.downloads.createM3U8File') }}</span> <span class="checkbox_text">{{ $t('settings.downloads.createM3U8File') }}</span>
</label> </label>
@ -263,7 +263,7 @@
<input type="text" v-model="settings.playlistFilenameTemplate" /> <input type="text" v-model="settings.playlistFilenameTemplate" />
</div> </div>
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.saveDownloadQueue" /> <input type="checkbox" v-model="settings.saveDownloadQueue" />
<span class="checkbox_text">{{ $t('settings.downloads.saveDownloadQueue') }}</span> <span class="checkbox_text">{{ $t('settings.downloads.saveDownloadQueue') }}</span>
</label> </label>
@ -274,7 +274,7 @@
<i class="material-icons">album</i>{{ $t('settings.covers.title') }} <i class="material-icons">album</i>{{ $t('settings.covers.title') }}
</h3> </h3>
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.saveArtwork" /> <input type="checkbox" v-model="settings.saveArtwork" />
<span class="checkbox_text">{{ $t('settings.covers.saveArtwork') }}</span> <span class="checkbox_text">{{ $t('settings.covers.saveArtwork') }}</span>
</label> </label>
@ -284,7 +284,7 @@
<input type="text" v-model="settings.coverImageTemplate" /> <input type="text" v-model="settings.coverImageTemplate" />
</div> </div>
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.saveArtworkArtist" /> <input type="checkbox" v-model="settings.saveArtworkArtist" />
<span class="checkbox_text">{{ $t('settings.covers.saveArtworkArtist') }}</span> <span class="checkbox_text">{{ $t('settings.covers.saveArtworkArtist') }}</span>
</label> </label>
@ -319,7 +319,7 @@
</select> </select>
</div> </div>
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.embeddedArtworkPNG" /> <input type="checkbox" v-model="settings.embeddedArtworkPNG" />
<span class="checkbox_text">{{ $t('settings.covers.embeddedArtworkPNG') }}</span> <span class="checkbox_text">{{ $t('settings.covers.embeddedArtworkPNG') }}</span>
</label> </label>
@ -327,7 +327,7 @@
{{ $t('settings.covers.embeddedPNGWarning') }} {{ $t('settings.covers.embeddedPNGWarning') }}
</p> </p>
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.coverDescriptionUTF8" /> <input type="checkbox" v-model="settings.tags.coverDescriptionUTF8" />
<span class="checkbox_text">{{ $t('settings.covers.coverDescriptionUTF8') }}</span> <span class="checkbox_text">{{ $t('settings.covers.coverDescriptionUTF8') }}</span>
</label> </label>
@ -345,106 +345,106 @@
<div class="settings-container"> <div class="settings-container">
<div class="settings-container__half"> <div class="settings-container__half">
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.title" /> <input type="checkbox" v-model="settings.tags.title" />
<span class="checkbox_text">{{ $t('settings.tags.title') }}</span> <span class="checkbox_text">{{ $t('settings.tags.title') }}</span>
</label> </label>
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.artist" /> <input type="checkbox" v-model="settings.tags.artist" />
<span class="checkbox_text">{{ $t('settings.tags.artist') }}</span> <span class="checkbox_text">{{ $t('settings.tags.artist') }}</span>
</label> </label>
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.album" /> <input type="checkbox" v-model="settings.tags.album" />
<span class="checkbox_text">{{ $t('settings.tags.album') }}</span> <span class="checkbox_text">{{ $t('settings.tags.album') }}</span>
</label> </label>
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.cover" /> <input type="checkbox" v-model="settings.tags.cover" />
<span class="checkbox_text">{{ $t('settings.tags.cover') }}</span> <span class="checkbox_text">{{ $t('settings.tags.cover') }}</span>
</label> </label>
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.trackNumber" /> <input type="checkbox" v-model="settings.tags.trackNumber" />
<span class="checkbox_text">{{ $t('settings.tags.trackNumber') }}</span> <span class="checkbox_text">{{ $t('settings.tags.trackNumber') }}</span>
</label> </label>
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.trackTotal" /> <input type="checkbox" v-model="settings.tags.trackTotal" />
<span class="checkbox_text">{{ $t('settings.tags.trackTotal') }}</span> <span class="checkbox_text">{{ $t('settings.tags.trackTotal') }}</span>
</label> </label>
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.discNumber" /> <input type="checkbox" v-model="settings.tags.discNumber" />
<span class="checkbox_text">{{ $t('settings.tags.discNumber') }}</span> <span class="checkbox_text">{{ $t('settings.tags.discNumber') }}</span>
</label> </label>
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.discTotal" /> <input type="checkbox" v-model="settings.tags.discTotal" />
<span class="checkbox_text">{{ $t('settings.tags.discTotal') }}</span> <span class="checkbox_text">{{ $t('settings.tags.discTotal') }}</span>
</label> </label>
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.albumArtist" /> <input type="checkbox" v-model="settings.tags.albumArtist" />
<span class="checkbox_text">{{ $t('settings.tags.albumArtist') }}</span> <span class="checkbox_text">{{ $t('settings.tags.albumArtist') }}</span>
</label> </label>
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.genre" /> <input type="checkbox" v-model="settings.tags.genre" />
<span class="checkbox_text">{{ $t('settings.tags.genre') }}</span> <span class="checkbox_text">{{ $t('settings.tags.genre') }}</span>
</label> </label>
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.year" /> <input type="checkbox" v-model="settings.tags.year" />
<span class="checkbox_text">{{ $t('settings.tags.year') }}</span> <span class="checkbox_text">{{ $t('settings.tags.year') }}</span>
</label> </label>
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.date" /> <input type="checkbox" v-model="settings.tags.date" />
<span class="checkbox_text">{{ $t('settings.tags.date') }}</span> <span class="checkbox_text">{{ $t('settings.tags.date') }}</span>
</label> </label>
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.explicit" /> <input type="checkbox" v-model="settings.tags.explicit" />
<span class="checkbox_text">{{ $t('settings.tags.explicit') }}</span> <span class="checkbox_text">{{ $t('settings.tags.explicit') }}</span>
</label> </label>
</div> </div>
<div class="settings-container__half"> <div class="settings-container__half">
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.isrc" /> <input type="checkbox" v-model="settings.tags.isrc" />
<span class="checkbox_text">{{ $t('settings.tags.isrc') }}</span> <span class="checkbox_text">{{ $t('settings.tags.isrc') }}</span>
</label> </label>
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.length" /> <input type="checkbox" v-model="settings.tags.length" />
<span class="checkbox_text">{{ $t('settings.tags.length') }}</span> <span class="checkbox_text">{{ $t('settings.tags.length') }}</span>
</label> </label>
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.barcode" /> <input type="checkbox" v-model="settings.tags.barcode" />
<span class="checkbox_text">{{ $t('settings.tags.barcode') }}</span> <span class="checkbox_text">{{ $t('settings.tags.barcode') }}</span>
</label> </label>
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.bpm" /> <input type="checkbox" v-model="settings.tags.bpm" />
<span class="checkbox_text">{{ $t('settings.tags.bpm') }}</span> <span class="checkbox_text">{{ $t('settings.tags.bpm') }}</span>
</label> </label>
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.replayGain" /> <input type="checkbox" v-model="settings.tags.replayGain" />
<span class="checkbox_text">{{ $t('settings.tags.replayGain') }}</span> <span class="checkbox_text">{{ $t('settings.tags.replayGain') }}</span>
</label> </label>
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.label" /> <input type="checkbox" v-model="settings.tags.label" />
<span class="checkbox_text">{{ $t('settings.tags.label') }}</span> <span class="checkbox_text">{{ $t('settings.tags.label') }}</span>
</label> </label>
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.lyrics" /> <input type="checkbox" v-model="settings.tags.lyrics" />
<span class="checkbox_text">{{ $t('settings.tags.lyrics') }}</span> <span class="checkbox_text">{{ $t('settings.tags.lyrics') }}</span>
</label> </label>
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.syncedLyrics" /> <input type="checkbox" v-model="settings.tags.syncedLyrics" />
<span class="checkbox_text">{{ $t('settings.tags.syncedLyrics') }}</span> <span class="checkbox_text">{{ $t('settings.tags.syncedLyrics') }}</span>
</label> </label>
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.copyright" /> <input type="checkbox" v-model="settings.tags.copyright" />
<span class="checkbox_text">{{ $t('settings.tags.copyright') }}</span> <span class="checkbox_text">{{ $t('settings.tags.copyright') }}</span>
</label> </label>
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.composer" /> <input type="checkbox" v-model="settings.tags.composer" />
<span class="checkbox_text">{{ $t('settings.tags.composer') }}</span> <span class="checkbox_text">{{ $t('settings.tags.composer') }}</span>
</label> </label>
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.involvedPeople" /> <input type="checkbox" v-model="settings.tags.involvedPeople" />
<span class="checkbox_text">{{ $t('settings.tags.involvedPeople') }}</span> <span class="checkbox_text">{{ $t('settings.tags.involvedPeople') }}</span>
</label> </label>
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.source" /> <input type="checkbox" v-model="settings.tags.source" />
<span class="checkbox_text">{{ $t('settings.tags.source') }}</span> <span class="checkbox_text">{{ $t('settings.tags.source') }}</span>
</label> </label>
@ -457,17 +457,17 @@
<i class="material-icons">list</i>{{ $t('settings.other.title') }} <i class="material-icons">list</i>{{ $t('settings.other.title') }}
</h3> </h3>
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.savePlaylistAsCompilation" /> <input type="checkbox" v-model="settings.tags.savePlaylistAsCompilation" />
<span class="checkbox_text">{{ $t('settings.other.savePlaylistAsCompilation') }}</span> <span class="checkbox_text">{{ $t('settings.other.savePlaylistAsCompilation') }}</span>
</label> </label>
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.useNullSeparator" /> <input type="checkbox" v-model="settings.tags.useNullSeparator" />
<span class="checkbox_text">{{ $t('settings.other.useNullSeparator') }}</span> <span class="checkbox_text">{{ $t('settings.other.useNullSeparator') }}</span>
</label> </label>
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.saveID3v1" /> <input type="checkbox" v-model="settings.tags.saveID3v1" />
<span class="checkbox_text">{{ $t('settings.other.saveID3v1') }}</span> <span class="checkbox_text">{{ $t('settings.other.saveID3v1') }}</span>
</label> </label>
@ -488,22 +488,22 @@
</select> </select>
</div> </div>
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.singleAlbumArtist" /> <input type="checkbox" v-model="settings.tags.singleAlbumArtist" />
<span class="checkbox_text">{{ $t('settings.other.singleAlbumArtist') }}</span> <span class="checkbox_text">{{ $t('settings.other.singleAlbumArtist') }}</span>
</label> </label>
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.albumVariousArtists" /> <input type="checkbox" v-model="settings.albumVariousArtists" />
<span class="checkbox_text">{{ $t('settings.other.albumVariousArtists') }}</span> <span class="checkbox_text">{{ $t('settings.other.albumVariousArtists') }}</span>
</label> </label>
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.removeAlbumVersion" /> <input type="checkbox" v-model="settings.removeAlbumVersion" />
<span class="checkbox_text">{{ $t('settings.other.removeAlbumVersion') }}</span> <span class="checkbox_text">{{ $t('settings.other.removeAlbumVersion') }}</span>
</label> </label>
<label class="with_checkbox"> <label class="with-checkbox">
<input type="checkbox" v-model="settings.removeDuplicateArtists" /> <input type="checkbox" v-model="settings.removeDuplicateArtists" />
<span class="checkbox_text">{{ $t('settings.other.removeDuplicateArtists') }}</span> <span class="checkbox_text">{{ $t('settings.other.removeDuplicateArtists') }}</span>
</label> </label>
@ -643,7 +643,7 @@
.locale-flag { .locale-flag {
width: 60px; width: 60px;
display: inline-flex; display: flex items-center;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
@ -724,10 +724,6 @@ export default {
set(wantSlimSidebar) { set(wantSlimSidebar) {
this.slimSidebar = wantSlimSidebar this.slimSidebar = wantSlimSidebar
document.getElementById('sidebar').classList.toggle('slim', wantSlimSidebar) document.getElementById('sidebar').classList.toggle('slim', wantSlimSidebar)
// Moves all toast messages when the option changes
Array.from(document.getElementsByClassName('toastify')).forEach((toast)=>{
toast.style.transform = `translate(${wantSlimSidebar ? '3rem' : '14rem'}, 0)`;
})
localStorage.setItem('slimSidebar', wantSlimSidebar) localStorage.setItem('slimSidebar', wantSlimSidebar)
} }
}, },

View File

@ -6,8 +6,8 @@
'linear-gradient(to bottom, transparent 0%, var(--main-background) 100%), url(\'' + image + '\')' 'linear-gradient(to bottom, transparent 0%, var(--main-background) 100%), url(\'' + image + '\')'
}" }"
> >
<h1 class="inline-flex m-0 text-5xl"> <h1 class="flex items-center m-0 text-5xl">
{{ title }} <i v-if="explicit" class="material-icons explicit_icon explicit_icon--right">explicit</i> {{ title }} <i v-if="explicit" class="material-icons explicit-icon explicit-icon--right">explicit</i>
</h1> </h1>
<h2 class="m-0 mb-3 text-lg"> <h2 class="m-0 mb-3 text-lg">
@ -60,7 +60,7 @@
</td> </td>
<td class="table__cell--large table__cell--with-icon"> <td class="table__cell--large table__cell--with-icon">
<div class="table__cell-content table__cell-content--vertical-center"> <div class="table__cell-content table__cell-content--vertical-center">
<i v-if="track.explicit_lyrics" class="material-icons explicit_icon"> explicit </i> <i v-if="track.explicit_lyrics" class="material-icons explicit-icon"> explicit </i>
{{ {{
track.title + track.title +
(track.title_version && track.title.indexOf(track.title_version) == -1 (track.title_version && track.title.indexOf(track.title_version) == -1
@ -126,8 +126,8 @@
<i v-else class="material-icons disabled">play_arrow</i> <i v-else class="material-icons disabled">play_arrow</i>
</td> </td>
<td>{{ i + 1 }}</td> <td>{{ i + 1 }}</td>
<td class="inline-flex"> <td class="flex items-center">
<i v-if="track.explicit" class="material-icons explicit_icon">explicit</i> <i v-if="track.explicit" class="material-icons explicit-icon">explicit</i>
{{ track.name }} {{ track.name }}
</td> </td>
<td>{{ track.artists[0].name }}</td> <td>{{ track.artists[0].name }}</td>

View File

@ -1,51 +1,79 @@
<template> <template>
<div id="album_search" class="search_tabcontent"> <section>
<BaseLoadingPlaceholder v-if="!results.albumTab.loaded" /> <BaseLoadingPlaceholder v-if="isLoading" />
<div v-else-if="results.albumTab.data.length == 0">
<template v-else>
<div v-if="viewInfo.data.length === 0">
<h1>{{ $t('search.noResultsAlbum') }}</h1> <h1>{{ $t('search.noResultsAlbum') }}</h1>
</div> </div>
<div class="release_grid" v-if="results.albumTab.data.length > 0">
<div class="release_grid" v-else>
<router-link <router-link
tag="div" tag="div"
v-for="release in results.albumTab.data" v-for="release in viewInfo.data.slice(0, itemsToShow)"
:key="release.id" :key="release.albumID"
class="release clickable" class="release clickable"
:to="{ name: 'Album', params: { id: release.id } }" :to="{ name: 'Album', params: { id: release.albumID } }"
> >
<div class="cover_container"> <div class="cover_container">
<img aria-hidden="true" class="rounded coverart" :src="release.cover_medium" /> <img aria-hidden="true" class="rounded coverart" :src="release.albumCoverMedium" />
<button <button
role="button" role="button"
aria-label="download" aria-label="download"
@click.stop="$emit('add-to-queue', $event)" @click.stop="$emit('add-to-queue', $event)"
:data-link="release.link" :data-link="release.albumLink"
class="download_overlay" class="download_overlay"
tabindex="0" tabindex="0"
> >
<i class="material-icons" :title="$t('globals.download_hint')">get_app</i> <i class="material-icons" :title="$t('globals.download_hint')">get_app</i>
</button> </button>
</div> </div>
<p class="primary-text inline-flex"> <p class="primary-text flex items-center">
<i v-if="release.explicit_lyrics" class="material-icons explicit_icon">explicit</i> <i v-if="release.isAlbumExplicit" class="material-icons explicit-icon">explicit</i>
{{ release.title }} {{ release.albumTitle }}
</p> </p>
<p class="secondary-text"> <p class="secondary-text">
{{ {{
$t('globals.by', { artist: release.artist.name }) + $t('globals.by', { artist: release.artistName }) +
' - ' + ' - ' +
$tc('globals.listTabs.trackN', release.nb_tracks) $tc('globals.listTabs.trackN', release.albumTracks)
}} }}
</p> </p>
</router-link> </router-link>
</div> </div>
</div> </template>
</section>
</template> </template>
<script> <script>
import BaseLoadingPlaceholder from '@components/globals/BaseLoadingPlaceholder.vue' import BaseLoadingPlaceholder from '@components/globals/BaseLoadingPlaceholder.vue'
export default { export default {
props: ['results'], props: {
viewInfo: {
validator: function (value) {
let isNull = Object.is(value, null)
let isObject = Object.prototype.toString.call(value) === '[object Object]'
return isNull || isObject
},
required: true
},
itemsToShow: {
type: Number,
required: false
},
wantHeaders: {
type: Boolean,
required: false,
default: false
}
},
computed: {
isLoading() {
return !this.viewInfo || !this.viewInfo.hasLoaded
}
},
components: { components: {
BaseLoadingPlaceholder BaseLoadingPlaceholder
} }

View File

@ -1,221 +1,66 @@
<template> <template>
<div id="main_search" class="search_tabcontent"> <section>
<template v-for="section in results.allTab.ORDER"> <div v-if="!thereAreResults">
<h1>{{ $t('search.noResults') }}</h1>
</div>
<template v-else>
<section <section
v-if=" v-for="section in viewInfo.ORDER"
(section != 'TOP_RESULT' && results.allTab[section].data.length > 0) || results.allTab[section].length > 0 :key="section"
" class="float-none py-5 border-grayscale-500 border-t first:border-t-0"
class="search_section"
> >
<h2 <h2
@click="$emit('change-search-tab', section)" @click="$emit('change-search-tab', section)"
class="search_header" class="mb-6 capitalize"
:class="{ top_result_header: section === 'TOP_RESULT' }" :class="{
'text-4xl text-center': section === 'TOP_RESULT',
'inline-block cursor-pointer text-3xl hover:text-primary transition-colors duration-200 ease-in-out':
section !== 'TOP_RESULT'
}"
> >
{{ $tc(`globals.listTabs.${section.toLowerCase()}`, 2) }} {{ $tc(`globals.listTabs.${section.toLowerCase()}`, 2) }}
</h2> </h2>
<!-- Top result -->
<router-link <TopResult
tag="div" v-if="section === 'TOP_RESULT'"
v-if="section == 'TOP_RESULT'" :info="viewInfo.TOP_RESULT[0]"
class="top_result clickable" @add-to-queue="$emit('add-to-queue', $event)"
:to="{ name: upperCaseFirstLowerCaseRest(topResultType), params: { 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'"
/> />
<button
role="button" <ResultsTracks
aria-label="download" v-else-if="section === 'TRACK'"
@click.stop="$emit('add-to-queue', $event)" :viewInfo="reduceSearchResults(viewInfo.TRACK, formatSingleTrack)"
:data-link="results.allTab.TOP_RESULT[0].link" :itemsToShow="6"
class="download_overlay" @add-to-queue="$emit('add-to-queue', $event)"
tabindex="0"
>
<i class="material-icons" :title="$t('globals.download_hint')">get_app</i>
</button>
</div>
<div class="info_box">
<p class="primary-text">{{ results.allTab.TOP_RESULT[0].title }}</p>
<p class="secondary-text">
{{ fansNumber }}
</p>
<span class="tag">{{ $tc(`globals.listTabs.${results.allTab.TOP_RESULT[0].type}`, 1) }}</span>
</div>
</router-link>
<div v-else-if="section == 'TRACK'">
<table class="table table--tracks">
<tbody>
<tr v-for="track in results.allTab.TRACK.data.slice(0, 6)" :key="track.SNG_ID">
<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"> <ResultsAlbums
<div class="table__cell-content table__cell-content--vertical-center"> v-else-if="section == 'ALBUM'"
<i v-if="track.EXPLICIT_LYRICS == 1" class="material-icons explicit_icon"> explicit </i> :viewInfo="reduceSearchResults(viewInfo.ALBUM, formatAlbums)"
{{ track.SNG_TITLE + (track.VERSION ? ' ' + track.VERSION : '') }} :itemsToShow="6"
</div> @add-to-queue="$emit('add-to-queue', $event)"
</td>
<td class="table__cell table__cell--medium table__cell--center breakline">
<router-link
tag="span"
v-for="artist in track.ARTISTS"
:key="artist.ART_ID"
class="clickable"
:to="{
name: 'Artist',
params: { id: artist.ART_ID }
}"
>
{{ artist.ART_NAME }}
</router-link>
</td>
<router-link
tag="td"
class="table__cell--medium table__cell--center breakline clickable"
:to="{ name: 'Album', params: { id: track.ALB_ID } }"
>
{{ track.ALB_TITLE }}
</router-link>
<td class="table__cell table__cell--center">
{{ convertDuration(track.DURATION) }}
</td>
<td
class="table__cell--download table__cell--center clickable"
@click.stop="$emit('add-to-queue', $event)"
:data-link="'https://www.deezer.com/track/' + track.SNG_ID"
role="button"
aria-label="download"
>
<i class="material-icons" :title="$t('globals.download_hint')"> get_app </i>
</td>
</tr>
</tbody>
</table>
</div>
<div v-else-if="section == 'ARTIST'" class="release_grid firstrow_only">
<router-link
tag="div"
v-for="release in results.allTab.ARTIST.data.slice(0, 10)"
class="release clickable"
:key="release.ART_ID"
:to="{ name: 'Artist', params: { 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'
"
/> />
<button
role="button" <ResultsPlaylists
aria-label="download" v-else-if="section == 'PLAYLIST'"
@click.stop="$emit('add-to-queue', $event)" :viewInfo="reduceSearchResults(viewInfo.PLAYLIST, formatPlaylist)"
:data-link="'https://deezer.com/artist/' + release.ART_ID" :itemsToShow="6"
class="download_overlay" @add-to-queue="$emit('add-to-queue', $event)"
tabindex="0"
>
<i class="material-icons" :title="$t('globals.download_hint')">get_app</i>
</button>
</div>
<p class="primary-text">{{ release.ART_NAME }}</p>
<p class="secondary-text">{{ $t('search.fans', { n: $n(release.NB_FAN) }) }}</p>
</router-link>
</div>
<div v-else-if="section == 'ALBUM'" class="release_grid firstrow_only">
<router-link
tag="div"
v-for="release in results.allTab.ALBUM.data.slice(0, 10)"
:key="release.ALB_ID"
class="release clickable"
:to="{ name: 'Album', params: { 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'
"
/> />
<button
role="button" <ResultsArtists
aria-label="download" v-else-if="section === 'ARTIST'"
@click.stop="$emit('add-to-queue', $event)" :viewInfo="reduceSearchResults(viewInfo.ARTIST, formatArtist)"
:data-link="'https://deezer.com/album/' + release.ALB_ID" :itemsToShow="6"
class="download_overlay" @add-to-queue="$emit('add-to-queue', $event)"
tabindex="0"
>
<i class="material-icons" :title="$t('globals.download_hint')">get_app</i>
</button>
</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 + ' - ' + $tc('globals.listTabs.trackN', release.NUMBER_TRACK) }}
</p>
</router-link>
</div>
<div v-else-if="section == 'PLAYLIST'" class="release_grid firstrow_only">
<router-link
tag="div"
v-for="release in results.allTab.PLAYLIST.data.slice(0, 10)"
class="release clickable"
:key="release.PLAYLIST_ID"
:to="{ name: 'Playlist', params: { 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'
"
/> />
<button
role="button"
aria-label="download"
@click.stop="$emit('add-to-queue', $event)"
:data-link="'https://deezer.com/playlist/' + release.PLAYLIST_ID"
class="download_overlay"
tabindex="0"
>
<i class="material-icons" :title="$t('globals.download_hint')">get_app</i>
</button>
</div>
<p class="primary-text">{{ release.TITLE }}</p>
<p class="secondary-text">{{ $tc('globals.listTabs.trackN', release.NB_SONG) }}</p>
</router-link>
</div>
</section> </section>
</template> </template>
<div v-if="noResults"> </section>
<h1>{{ $t('search.noResults') }}</h1>
</div>
</div>
</template> </template>
<style lang="scss" scoped> <style scoped>
.tag { .tag {
background-color: var(--tag-background); background-color: var(--tag-background);
border-radius: 2px; border-radius: 2px;
@ -230,39 +75,66 @@
<script> <script>
import { convertDuration } from '@/utils/utils' import { convertDuration } from '@/utils/utils'
import { upperCaseFirstLowerCaseRest } from '@/utils/texts' import { upperCaseFirstLowerCaseRest } from '@/utils/texts'
import TopResult from '@/components/search/TopResult.vue'
import ResultsTracks from '@components/search/ResultsTracks.vue'
import ResultsAlbums from '@components/search/ResultsAlbums.vue'
import ResultsArtists from '@components/search/ResultsArtists.vue'
import ResultsPlaylists from '@components/search/ResultsPlaylists.vue'
import { reduceSearchResults, formatSingleTrack, formatAlbums, formatArtist, formatPlaylist } from '@/data/search'
export default { export default {
props: ['results'], components: {
computed: { TopResult,
topResultType() { ResultsTracks,
return this.results.allTab.TOP_RESULT[0].type ResultsAlbums,
ResultsArtists,
ResultsPlaylists
}, },
noResults() { props: {
return this.results.allTab.ORDER.every(section => viewInfo: {
section == 'TOP_RESULT' type: Object,
? this.results.allTab[section].length == 0 required: false
: this.results.allTab[section].data.length == 0 }
},
computed: {
thereAreResults() {
let areInfosLoaded = !!this.viewInfo
if (!areInfosLoaded) {
return false
}
let noResultsPresent = this.viewInfo.ORDER.every(section =>
section === 'TOP_RESULT' ? this.viewInfo[section].length === 0 : this.viewInfo[section].data.length === 0
) )
return !noResultsPresent
}, },
fansNumber() { fansNumber() {
let number let number
try { try {
number = this.$n(this.results.allTab.TOP_RESULT[0].nb_fan) number = this.$n(this.viewInfo.TOP_RESULT[0].nb_fan)
} catch (error) { } catch (error) {
number = this.$n(this.results.allTab.TOP_RESULT[0].nb_fan, { locale: 'en' }) number = this.$n(this.viewInfo.TOP_RESULT[0].nb_fan, { locale: 'en' })
} }
return this.results.allTab.TOP_RESULT[0].type == 'artist' return this.viewInfo.TOP_RESULT[0].type == 'artist'
? this.$t('search.fans', { n: number }) ? this.$t('search.fans', { n: number })
: this.$t('globals.by', { artist: this.results.allTab.TOP_RESULT[0].artist }) + : this.$t('globals.by', { artist: this.viewInfo.TOP_RESULT[0].artist }) +
' - ' + ' - ' +
this.$tc('globals.listTabs.trackN', this.results.allTab.TOP_RESULT[0].nb_song) this.$tc('globals.listTabs.trackN', this.viewInfo.TOP_RESULT[0].nb_song)
} }
}, },
methods: { methods: {
convertDuration, convertDuration,
upperCaseFirstLowerCaseRest upperCaseFirstLowerCaseRest,
reduceSearchResults,
formatSingleTrack,
formatAlbums,
formatArtist,
formatPlaylist
} }
} }
</script> </script>

View File

@ -1,44 +1,72 @@
<template> <template>
<div id="artist_search" class="search_tabcontent"> <section>
<BaseLoadingPlaceholder v-if="!results.artistTab.loaded"></BaseLoadingPlaceholder> <BaseLoadingPlaceholder v-if="isLoading" />
<div v-else-if="results.artistTab.data.length == 0">
<template v-else>
<div v-if="viewInfo.data.length === 0">
<h1>{{ $t('search.noResultsArtist') }}</h1> <h1>{{ $t('search.noResultsArtist') }}</h1>
</div> </div>
<div class="release_grid" v-if="results.artistTab.data.length > 0">
<div v-else class="release_grid">
<router-link <router-link
tag="div" tag="div"
v-for="release in results.artistTab.data" v-for="release in viewInfo.data.slice(0, itemsToShow)"
class="release clickable" class="release clickable"
:key="release.id" :key="release.artistID"
:to="{ name: 'Artist', params: { id: release.id } }" :to="{ name: 'Artist', params: { id: release.artistID } }"
> >
<div class="cover_container"> <div class="cover_container">
<img aria-hidden="true" class="circle coverart" :src="release.picture_medium" /> <img aria-hidden="true" class="circle coverart" :src="release.artistPictureMedium" />
<button <button
role="button" role="button"
aria-label="download" aria-label="download"
@click.stop="$emit('add-to-queue', $event)" @click.stop="$emit('add-to-queue', $event)"
:data-link="release.link" :data-link="release.artistLink"
class="download_overlay" class="download_overlay"
tabindex="0" tabindex="0"
> >
<i class="material-icons" :title="$t('globals.download_hint')">get_app</i> <i class="material-icons" :title="$t('globals.download_hint')">get_app</i>
</button> </button>
</div> </div>
<p class="primary-text">{{ release.name }}</p> <p class="primary-text">{{ release.artistName }}</p>
<p class="secondary-text">{{ $tc('globals.listTabs.releaseN', release.nb_album) }}</p> <p class="secondary-text">{{ $tc('globals.listTabs.releaseN', release.artistAlbumsNumber) }}</p>
</router-link> </router-link>
</div> </div>
</div> </template>
</section>
</template> </template>
<script> <script>
import BaseLoadingPlaceholder from '@components/globals/BaseLoadingPlaceholder.vue' import BaseLoadingPlaceholder from '@components/globals/BaseLoadingPlaceholder.vue'
export default { export default {
props: ['results'],
components: { components: {
BaseLoadingPlaceholder BaseLoadingPlaceholder
},
props: {
viewInfo: {
validator: function (value) {
let isNull = Object.is(value, null)
let isObject = Object.prototype.toString.call(value) === '[object Object]'
return isNull || isObject
},
required: true
},
itemsToShow: {
type: Number,
required: false
},
wantHeaders: {
type: Boolean,
required: false,
default: false
}
},
computed: {
isLoading() {
return !this.viewInfo || !this.viewInfo.hasLoaded
}
} }
} }
</script> </script>

View File

@ -1,48 +1,78 @@
<template> <template>
<div id="playlist_search" class="search_tabcontent"> <section>
<BaseLoadingPlaceholder v-if="!results.playlistTab.loaded" /> <BaseLoadingPlaceholder v-if="isLoading" />
<div v-else-if="results.playlistTab.data.length == 0">
<template v-else>
<div v-if="viewInfo.data.length === 0">
<h1>{{ $t('search.noResultsPlaylist') }}</h1> <h1>{{ $t('search.noResultsPlaylist') }}</h1>
</div> </div>
<div class="release_grid" v-if="results.playlistTab.data.length > 0"> <div class="release_grid" v-else>
<router-link <router-link
tag="div" tag="div"
v-for="release in results.playlistTab.data" v-for="playlist in viewInfo.data.slice(0, itemsToShow)"
class="release clickable" class="release clickable"
:key="release.id" :key="playlist.playlistID"
:to="{ name: 'Playlist', params: { id: release.id } }" :to="{ name: 'Playlist', params: { id: playlist.playlistID } }"
> >
<div class="cover_container"> <div class="cover_container">
<img aria-hidden="true" class="rounded coverart" :src="release.picture_medium" /> <img aria-hidden="true" class="rounded coverart" :src="playlist.playlistPictureMedium" />
<button <button
role="button" role="button"
aria-label="download" aria-label="download"
@click.stop="$emit('add-to-queue', $event)" @click.stop="$emit('add-to-queue', $event)"
:data-link="release.link" :data-link="playlist.playlistLink"
class="download_overlay" class="download_overlay"
tabindex="0" tabindex="0"
> >
<i class="material-icons" :title="$t('globals.download_hint')">get_app</i> <i class="material-icons" :title="$t('globals.download_hint')">get_app</i>
</button> </button>
</div> </div>
<p class="primary-text">{{ release.title }}</p> <p class="primary-text">{{ playlist.playlistTitle }}</p>
<p class="secondary-text"> <p class="secondary-text">
{{ {{
`${$t('globals.by', { artist: release.user.name })} - ${$tc('globals.listTabs.trackN', release.nb_tracks)}` `${$t('globals.by', { artist: playlist.artistName })} - ${$tc(
'globals.listTabs.trackN',
playlist.playlistTracksNumber
)}`
}} }}
</p> </p>
</router-link> </router-link>
</div> </div>
</div> </template>
</section>
</template> </template>
<script> <script>
import BaseLoadingPlaceholder from '@components/globals/BaseLoadingPlaceholder.vue' import BaseLoadingPlaceholder from '@components/globals/BaseLoadingPlaceholder.vue'
export default { export default {
props: ['results'],
components: { components: {
BaseLoadingPlaceholder BaseLoadingPlaceholder
},
props: {
viewInfo: {
validator: function (value) {
let isNull = Object.is(value, null)
let isObject = Object.prototype.toString.call(value) === '[object Object]'
return isNull || isObject
},
required: true
},
itemsToShow: {
type: Number,
required: false
},
wantHeaders: {
type: Boolean,
required: false,
default: false
}
},
computed: {
isLoading() {
return !this.viewInfo || !this.viewInfo.hasLoaded
}
} }
} }
</script> </script>

View File

@ -1,66 +1,67 @@
<template> <template>
<div id="track_search" class="search_tabcontent"> <section>
<BaseLoadingPlaceholder v-if="!results.trackTab.loaded" /> <BaseLoadingPlaceholder v-if="isLoading" />
<div v-else-if="results.trackTab.data.length == 0">
<template v-else>
<div v-if="viewInfo.data.length === 0">
<h1>{{ $t('search.noResultsTrack') }}</h1> <h1>{{ $t('search.noResultsTrack') }}</h1>
</div> </div>
<table class="table table--tracks" v-if="results.trackTab.data.length > 0">
<thead> <table v-else class="table table--tracks">
<tr> <thead v-if="wantHeaders">
<tr class="capitalize">
<th colspan="2">{{ $tc('globals.listTabs.title', 1) }}</th> <th colspan="2">{{ $tc('globals.listTabs.title', 1) }}</th>
<th>{{ $tc('globals.listTabs.artist', 1) }}</th> <th>{{ $tc('globals.listTabs.artist', 1) }}</th>
<th>{{ $tc('globals.listTabs.album', 1) }}</th> <th>{{ $tc('globals.listTabs.album', 1) }}</th>
<th> <th>
<i class="material-icons">timer</i> <i class="material-icons">timer</i>
</th> </th>
<th style="width: 56px"></th> <th style="width: 3.5rem"></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="track in results.trackTab.data"> <tr v-for="track in viewInfo.data.slice(0, itemsToShow)" :key="track.trackLink">
<td class="table__icon table__icon--big"> <td class="table__icon table__icon--big">
<a <a
href="#" href="#"
@click="playPausePreview" @click="playPausePreview"
class="rounded" class="rounded"
:class="{ 'single-cover': !!track.preview }" :class="{ 'single-cover': !!track.trackPreview }"
:data-preview="track.preview" :data-preview="track.trackPreview"
> >
<PreviewControls v-if="track.preview" /> <PreviewControls v-if="track.trackPreview" />
<img class="rounded coverart" :src="track.album.cover_small" /> <img class="rounded coverart" :src="track.albumPicture" />
</a> </a>
</td> </td>
<td class="table__cell table__cell--large breakline"> <td class="table__cell table__cell--large breakline">
<div class="table__cell-content table__cell-content--vertical-center"> <div class="table__cell-content table__cell-content--vertical-center">
<i v-if="track.explicit_lyrics" class="material-icons explicit_icon"> explicit </i> <i v-if="track.isTrackExplicit" class="material-icons explicit-icon">explicit</i>
{{ {{ getTitle(track) }}
track.title +
(track.title_version && track.title.indexOf(track.title_version) == -1 ? ' ' + track.title_version : '')
}}
</div> </div>
</td> </td>
<router-link <router-link
tag="td" tag="td"
class="table__cell table__cell--medium table__cell--center breakline clickable" class="table__cell table__cell--medium table__cell--center breakline clickable"
:to="{ name: 'Artist', params: { id: track.artist.id } }" :to="{ name: 'Artist', params: { id: track.artistID } }"
> >
{{ track.artist.name }} {{ track.artistName }}
</router-link> </router-link>
<router-link <router-link
tag="td" tag="td"
class="table__cell table__cell--medium table__cell--center breakline clickable" class="table__cell table__cell--medium table__cell--center breakline clickable"
:to="{ name: 'Album', params: { id: track.album.id } }" :to="{ name: 'Album', params: { id: track.albumID } }"
> >
{{ track.album.title }} {{ track.albumTitle }}
</router-link> </router-link>
<td class="table__cell table__cell--small table__cell--center"> <td class="table__cell table__cell--small table__cell--center">
{{ convertDuration(track.duration) }} {{ convertDuration(track.trackDuration) }}
</td> </td>
<td <td
class="table__cell--download table__cell--center clickable" class="table__cell--download table__cell--center clickable"
@click.stop="$emit('add-to-queue', $event)" @click.stop="$emit('add-to-queue', $event)"
:data-link="track.link" :data-link="track.trackLink"
role="button" role="button"
aria-label="download" aria-label="download"
> >
@ -69,7 +70,8 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </template>
</section>
</template> </template>
<script> <script>
@ -80,15 +82,44 @@ import EventBus from '@/utils/EventBus'
import { convertDuration } from '@/utils/utils' import { convertDuration } from '@/utils/utils'
export default { export default {
props: ['results'],
components: { components: {
BaseLoadingPlaceholder, BaseLoadingPlaceholder,
PreviewControls PreviewControls
}, },
props: {
viewInfo: {
validator: function (value) {
let isNull = Object.is(value, null)
let isObject = Object.prototype.toString.call(value) === '[object Object]'
return isNull || isObject
},
required: true
},
itemsToShow: {
type: Number,
required: false
},
wantHeaders: {
type: Boolean,
required: false,
default: false
}
},
computed: {
isLoading() {
return !this.viewInfo || !this.viewInfo.hasLoaded
}
},
methods: { methods: {
convertDuration, convertDuration,
playPausePreview(e) { playPausePreview(e) {
EventBus.$emit('trackPreview:playPausePreview', e) EventBus.$emit('trackPreview:playPausePreview', e)
},
getTitle(track) {
const hasTitleVersion = track.trackTitleVersion && track.trackTitle.indexOf(track.trackTitleVersion) === -1
return `${track.trackTitle}${hasTitleVersion ? ` ${track.trackTitleVersion}` : ''}`
} }
} }
} }

View File

@ -0,0 +1,73 @@
<template>
<router-link
tag="div"
class="top_result cursor-pointer flex items-center flex-col"
:to="{ name: upperCaseFirstLowerCaseRest($attrs.info.type), params: { id: $attrs.info.id } }"
>
<div class="cover_container">
<img
aria-hidden="true"
class="coverart"
:src="$attrs.info.picture"
:class="$attrs.info.type == 'artist' ? 'circle' : 'rounded'"
/>
<button
role="button"
aria-label="download"
@click.stop="$emit('add-to-queue', $event)"
:data-link="$attrs.info.link"
class="download_overlay"
tabindex="0"
>
<i class="material-icons" :title="$t('globals.download_hint')">get_app</i>
</button>
</div>
<div class="info_box">
<p class="primary-text">{{ $attrs.info.title }}</p>
<p class="secondary-text">
{{ fansNumber }}
</p>
<span class="tag">{{ $tc(`globals.listTabs.${$attrs.info.type}`, 1) }}</span>
</div>
</router-link>
</template>
<style scoped>
.tag {
background-color: var(--tag-background);
border-radius: 2px;
color: var(--tag-text);
display: inline-block;
font-size: 10px;
padding: 3px 6px;
text-transform: capitalize;
}
</style>
<script>
import { upperCaseFirstLowerCaseRest } from '@/utils/texts'
export default {
methods: {
upperCaseFirstLowerCaseRest
},
computed: {
fansNumber() {
let number
try {
number = this.$n(this.$attrs.info.nb_fan)
} catch (error) {
number = this.$n(this.$attrs.info.nb_fan, { locale: 'en' })
}
return this.$attrs.info.type == 'artist'
? this.$t('search.fans', { n: number })
: this.$t('globals.by', { artist: this.$attrs.info.artist }) +
' - ' +
this.$tc('globals.listTabs.trackN', this.$attrs.info.nb_song)
}
}
}
</script>

117
src/data/search.js Normal file
View File

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

View File

@ -4,6 +4,7 @@ const en = {
back: 'back', back: 'back',
loading: 'loading', loading: 'loading',
download: 'Download {thing}', download: 'Download {thing}',
downloadAll: 'Download all {thing}',
by: 'by {artist}', by: 'by {artist}',
in: 'in {album}', in: 'in {album}',
download_hint: 'Download', download_hint: 'Download',
@ -27,6 +28,7 @@ const en = {
single: 'single | singles', single: 'single | singles',
title: 'title | titles', title: 'title | titles',
track: 'track | tracks', track: 'track | tracks',
trackN: '0 tracks | {n} track | {n} tracks',
releaseN: '0 releases | {n} release | {n} releases', releaseN: '0 releases | {n} release | {n} releases',
playlist: 'playlist | playlists', playlist: 'playlist | playlists',
compile: 'compilation | compilations', compile: 'compilation | compilations',
@ -36,11 +38,7 @@ const en = {
featured: 'Featured in', featured: 'Featured in',
spotifyPlaylist: 'spotify playlist | spotify playlists', spotifyPlaylist: 'spotify playlist | spotify playlists',
releaseDate: 'release date', releaseDate: 'release date',
error: 'error', error: 'error'
trackN: '0 tracks | {n} track | {n} tracks',
albumN: '0 albums | {n} album | {n} albums',
artistN: '0 artists | {n} artist | {n} artists',
playlistN: '0 playlists | {n} playlist | {n} playlists',
} }
}, },
about: { about: {

View File

@ -4,6 +4,7 @@ const fr = {
back: 'retour', back: 'retour',
loading: 'chargement en cours', loading: 'chargement en cours',
download: 'Télécharger {thing}', download: 'Télécharger {thing}',
downloadAll: "Télécharger l'intégralité des {thing}",
by: 'par {artist}', by: 'par {artist}',
in: 'dans {album}', in: 'dans {album}',
download_hint: 'Télécharger', download_hint: 'Télécharger',
@ -27,6 +28,7 @@ const fr = {
single: 'single | singles', single: 'single | singles',
title: 'titre | titres', title: 'titre | titres',
track: 'piste | pistes', track: 'piste | pistes',
trackN: '0 piste | {n} piste | {n} pistes',
releaseN: '0 sortie | {n} sortie | {n} sorties', releaseN: '0 sortie | {n} sortie | {n} sorties',
playlist: 'playlist | playlists', playlist: 'playlist | playlists',
compile: 'compilation | compilations', compile: 'compilation | compilations',
@ -36,11 +38,7 @@ const fr = {
featured: 'Apparaît dans', featured: 'Apparaît dans',
spotifyPlaylist: 'playlist spotify | playlists spotify', spotifyPlaylist: 'playlist spotify | playlists spotify',
releaseDate: 'date de sortie', releaseDate: 'date de sortie',
error: 'erreur', error: 'erreur'
trackN: '0 piste | {n} piste | {n} pistes',
albumN: '0 album | {n} album | {n} albums',
artistN: '0 artiste | {n} artiste | {n} artistes',
playlistN: '0 playlist | {n} playlist | {n} playlists'
} }
}, },
about: { about: {
@ -201,8 +199,7 @@ const fr = {
}, },
appearance: { appearance: {
title: 'Apparence', title: 'Apparence',
slimDownloadTab: 'Onglet de téléchargement compact', slimDownloadTab: 'Onglet de téléchargement compact'
slimSidebar: 'Barre latérale compacte'
}, },
downloadPath: { downloadPath: {
title: 'Emplacement De Téléchargement' title: 'Emplacement De Téléchargement'
@ -302,8 +299,7 @@ const fr = {
syncedLyrics: 'Paroles Synchronisées', syncedLyrics: 'Paroles Synchronisées',
copyright: "Droits d'Auteur (Copyright)", copyright: "Droits d'Auteur (Copyright)",
composer: 'Compositeur', composer: 'Compositeur',
involvedPeople: 'Personnes Impliquées', involvedPeople: 'Personnes Impliquées'
source: 'ID de la source et de la piste'
}, },
other: { other: {
title: 'Autre', title: 'Autre',

View File

@ -4,6 +4,7 @@ const it = {
back: 'indietro', back: 'indietro',
loading: 'caricamento', loading: 'caricamento',
download: 'Scarica {thing}', download: 'Scarica {thing}',
downloadAll: 'Scarica ogni {thing}',
by: 'di {artist}', by: 'di {artist}',
in: 'in {album}', in: 'in {album}',
download_hint: 'Scarica', download_hint: 'Scarica',
@ -26,6 +27,7 @@ const it = {
single: 'singolo | singoli', single: 'singolo | singoli',
title: 'titolo | titoli', title: 'titolo | titoli',
track: 'brano | brani', track: 'brano | brani',
trackN: '0 brani | {n} brano | {n} brani',
releaseN: '0 dischi | {n} disco | {n} dischi', releaseN: '0 dischi | {n} disco | {n} dischi',
playlist: 'playlist', playlist: 'playlist',
compile: 'compilation', compile: 'compilation',
@ -36,11 +38,7 @@ const it = {
spotifyPlaylist: 'playlist spotify', spotifyPlaylist: 'playlist spotify',
releaseDate: 'data di uscita', releaseDate: 'data di uscita',
error: 'errore', error: 'errore',
empty: '', empty: ''
trackN: '0 brani | {n} brano | {n} brani',
albumN: '{n} album',
artistN: '0 artisti | {n} artista | {n} artisti',
playlistN: '{n} playlist',
} }
}, },
about: { about: {

View File

@ -0,0 +1,7 @@
.changing-theme {
transition: all 200ms ease-in-out;
}
[v-cloak] {
display: none;
}

View File

@ -25,3 +25,22 @@
font-feature-settings: 'liga'; font-feature-settings: 'liga';
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
} }
.material-icons.explicit-icon {
margin-right: 0.3125em;
margin-left: -3px;
color: hsl(240, 5%, 59%);
}
.material-icons.explicit-icon.explicit-icon--right {
margin-right: 0px;
margin-left: 0.3125em;
}
.material-icons.disabled {
@apply opacity-50 cursor-default;
}
.material-icons.mirrored {
transform: scaleX(-1);
}

View File

@ -6,33 +6,3 @@
transform: rotate(360deg); transform: rotate(360deg);
} }
} }
@keyframes indeterminate {
0% {
left: -35%;
right: 100%;
}
60% {
left: 100%;
right: -90%;
}
100% {
left: 100%;
right: -90%;
}
}
@keyframes indeterminate-short {
0% {
left: -200%;
right: 100%;
}
60% {
left: 107%;
right: -8%;
}
100% {
left: 107%;
right: -8%;
}
}

View File

@ -1,7 +0,0 @@
// Breakpoints
// TODO Change them in more proper values
$small: 601px;
$medium: 993px;
// Static variables (not an oxymoron)
$explicit-separator: 0.3125em;

View File

@ -2,55 +2,55 @@ input[type='text'],
input[type='password'], input[type='password'],
input[type='number'] { input[type='number'] {
appearance: none; appearance: none;
width: calc(100% - 16px); margin-bottom: 8px;
border: 0px solid black; border: 0px solid black;
line-height: 36px;
padding: 0px 8px;
border-radius: 4px; border-radius: 4px;
background-color: var(--secondary-background); background-color: var(--secondary-background);
padding: 0px 8px;
width: calc(100% - 16px);
line-height: 36px;
color: var(--foreground); color: var(--foreground);
margin-bottom: 8px;
} }
input[type='checkbox'] { input[type='checkbox'] {
appearance: none; appearance: none;
background-color: none;
border: 2px solid gray;
opacity: 0.5;
border-radius: 2px;
padding: 7px;
margin: 3px;
display: inline-block; display: inline-block;
position: relative; position: relative;
opacity: 0.5;
margin: 3px;
border: 2px solid gray;
border-radius: 2px;
background-color: none;
padding: 7px;
&:checked { &:checked {
opacity: 1; opacity: 1;
margin: 3px;
border: 0px solid var(--primary-color);
border-radius: 2px;
background-color: var(--primary-color); background-color: var(--primary-color);
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' height='18' viewBox='3 3 18 18' width='18'%3E%3Cpath fill='%23ffffff' d='M 10,17 5,12 6.41,10.59 10,14.17 17.59,6.58 19,8 Z'/%3E%3Cpath d='M0 0h24v24H0z' fill='none'/%3E%3C/svg%3E"); background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' height='18' viewBox='3 3 18 18' width='18'%3E%3Cpath fill='%23ffffff' d='M 10,17 5,12 6.41,10.59 10,14.17 17.59,6.58 19,8 Z'/%3E%3Cpath d='M0 0h24v24H0z' fill='none'/%3E%3C/svg%3E");
background-position: center center; background-position: center center;
border: 0px solid var(--primary-color);
border-radius: 2px;
padding: 9px; padding: 9px;
margin: 3px;
color: var(--primary-text); color: var(--primary-text);
} }
} }
select { select {
appearance: none; appearance: none;
width: 100%; margin-bottom: 8px;
border: 0px solid black; border: 0px solid black;
line-height: 36px;
padding: 0px 40px 0px 8px;
border-radius: 4px; border-radius: 4px;
background-clip: border-box;
background-color: var(--secondary-background); background-color: var(--secondary-background);
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' height='24' viewBox='0 0 24 24' width='24'%3E%3Cpath style='fill%3A%23000000%3Bfill-opacity%3A0.25' d='M7 10l5 5 5-5z'/%3E%3Cpath d='M0 0h24v24H0z' fill='none'/%3E%3C/svg%3E"); background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' height='24' viewBox='0 0 24 24' width='24'%3E%3Cpath style='fill%3A%23000000%3Bfill-opacity%3A0.25' d='M7 10l5 5 5-5z'/%3E%3Cpath d='M0 0h24v24H0z' fill='none'/%3E%3C/svg%3E");
background-position: calc(100% - 8px) center;
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: 24px; background-size: 24px;
background-position: calc(100% - 8px) center; padding: 0px 40px 0px 8px;
background-clip: border-box; width: 100%;
line-height: 36px;
color: var(--foreground); color: var(--foreground);
margin-bottom: 8px;
} }
p { p {
@ -67,27 +67,9 @@ img {
} }
} }
i {
&.disabled {
opacity: 0.5;
cursor: default;
}
&.explicit_icon {
color: hsl(240, 5%, 59%);
margin-right: $explicit-separator;
margin-left: -3px;
&.explicit_icon--right {
margin-left: $explicit-separator;
margin-right: 0px;
}
}
}
.single-cover { .single-cover {
position: relative;
display: inline-block; display: inline-block;
position: relative;
color: white; color: white;
} }
@ -105,61 +87,42 @@ i {
cursor: pointer !important; cursor: pointer !important;
} }
.table--tracklist .clickable:hover,
.table--charts .clickable:hover {
text-decoration: underline;
}
.with_checkbox {
display: flex;
align-items: center;
[type='checkbox'] {
cursor: pointer;
}
.checkbox_text {
margin-left: 10px;
cursor: pointer;
user-select: none;
}
}
.coverart { .coverart {
background-color: var(--secondary-background); background-color: var(--secondary-background);
} }
// ? Maybe make a component?
.cover_container { .cover_container {
position: relative; position: relative;
.coverart { .coverart {
opacity: 1;
display: block; display: block;
backface-visibility: hidden;
transition: 0.5s ease;
opacity: 1;
width: 100%; width: 100%;
height: auto; height: auto;
transition: 0.5s ease;
backface-visibility: hidden;
} }
.download_overlay { .download_overlay {
transition: 0.5s ease;
opacity: 0;
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
text-align: center; transition: 0.5s ease;
background-color: #000000; opacity: 0;
border-radius: 50%;
min-width: 32px;
padding: 0px;
height: 44px;
border: 0px; border: 0px;
border-radius: 50%;
background-color: #000000;
padding: 0px;
min-width: 32px;
height: 44px;
text-align: center;
i { i {
color: white;
padding: 10px;
cursor: pointer; cursor: pointer;
padding: 10px;
color: white;
} }
&:focus { &:focus {
@ -178,45 +141,3 @@ i {
} }
} }
} }
// TODO Remove
.inline-flex {
display: flex;
align-items: center;
.right {
margin-left: auto;
}
}
// TODO Remove
.right {
float: right;
}
.hide {
display: none !important;
}
.changing-theme {
transition: all 200ms ease-in-out;
}
[v-cloak] {
display: none;
}
.material-icons {
@apply select-none;
$sizes: 18, 24, 36, 48;
@each $size in $sizes {
&.md-#{$size} {
font-size: $size * 1px;
}
}
&.mirrored {
transform: scaleX(-1);
}
}

View File

@ -228,9 +228,7 @@ $table-border-radius: 3px;
} }
} }
// @todo Remove .table--tracklist .clickable:hover,
.top-tracks-position { .table--charts .clickable:hover {
padding: 12px; text-decoration: underline;
text-align: center;
cursor: default;
} }

View File

@ -3,7 +3,6 @@
@import '~tailwindcss/utilities'; @import '~tailwindcss/utilities';
@import './base/base'; @import './base/base';
@import './base/variables';
html { html {
height: 100vh; height: 100vh;

View File

@ -1,47 +1,13 @@
#main_search {
.search_section {
float: none;
padding-top: 20px;
padding-bottom: 20px;
&:not(:first-child) {
border-top: 1px solid theme('colors.grayscale.500');
}
}
.top_result_header {
display: block;
cursor: default;
font-size: 2rem;
text-align: center;
}
}
.search_header {
display: inline-block;
cursor: pointer;
font-size: 1.75rem;
margin-bottom: 25px;
text-transform: capitalize;
&:not(.top_result_header) {
transition: color 200ms ease-in-out;
&:hover {
color: var(--primary-color);
}
}
}
/* Top Result */ /* Top Result */
.top_result { .top_result {
display: flex; @apply flex items-center flex-col;
align-items: center; // display: flex;
flex-direction: column; // align-items: center;
// flex-direction: column;
> .cover_container { > .cover_container {
width: 156px; width: 9.75rem;
height: 156px; height: 9.75rem;
} }
.info_box { .info_box {
@ -49,24 +15,24 @@
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
margin-top: 15px; margin-top: 0.9375rem;
.primary-text, .primary-text,
.secondary-text { .secondary-text {
font-size: 18px; font-size: 1.125rem;
text-align: center; text-align: center;
} }
.primary-text { .primary-text {
margin-bottom: 5px; margin-bottom: 0.3125rem;
} }
.secondary-text { .secondary-text {
margin-bottom: 10px; margin-bottom: 0.625rem;
} }
.tag { .tag {
width: 40px; width: 2.5rem;
text-align: center; text-align: center;
} }
} }
@ -76,17 +42,17 @@
.release { .release {
.primary-text, .primary-text,
.secondary-text { .secondary-text {
margin: 0px; margin: 0rem;
margin-bottom: 4px; margin-bottom: 0.25rem;
} }
.secondary-text { .secondary-text {
opacity: 0.75; opacity: 0.75;
font-size: 14px; font-size: 0.875rem;
.material-icons { .material-icons {
font-size: 17px !important; font-size: 1.0625rem !important;
margin-left: 4px; margin-left: 0.25rem;
} }
} }
} }
@ -99,7 +65,7 @@
&.firstrow_only { &.firstrow_only {
grid-template-rows: 1fr; grid-template-rows: 1fr;
grid-auto-rows: 0; grid-auto-rows: 0;
grid-row-gap: 0px; grid-row-gap: 0rem;
overflow-y: hidden; overflow-y: hidden;
} }
} }

View File

@ -78,6 +78,21 @@
} }
} }
.with-checkbox {
display: flex;
align-items: center;
[type='checkbox'] {
cursor: pointer;
}
.checkbox_text {
margin-left: 10px;
cursor: pointer;
user-select: none;
}
}
/* Input group */ /* Input group */
.input_group { .input_group {
margin-bottom: 25px; margin-bottom: 25px;
@ -86,7 +101,7 @@
margin-bottom: 7px; margin-bottom: 7px;
} }
.with_checkbox + & { .with-checkbox + & {
margin-top: 10px; margin-top: 10px;
} }
} }

View File

@ -6,7 +6,10 @@ import { socket } from '@/utils/socket'
const sharedOptions = { const sharedOptions = {
gravity: 'bottom', gravity: 'bottom',
position: 'left' position: 'left',
offset: {
x: '14rem'
}
} }
let toastsWithId = {} let toastsWithId = {}
@ -84,9 +87,6 @@ export const toast = function(msg, icon = null, dismiss = true, id = null) {
delete toastsWithId[id] delete toastsWithId[id]
} }
} }
},
offset: {
x: 'true' === localStorage.getItem('slimSidebar') ? '3rem': '14rem'
} }
}).showToast() }).showToast()
if (id) { if (id) {

View File

@ -95,6 +95,28 @@ export function copyToClipboard(text) {
ghostInput.remove() ghostInput.remove()
} }
export function getProperty(obj, ...props) {
for (const prop of props) {
// Example: this.is.an.example
let hasDotNotation = /\./.test(prop)
// Searching the properties in the object
let valueToTest = hasDotNotation
? prop.split('.').reduce((o, i) => {
if (o) {
return o[i]
}
}, obj)
: obj[prop]
if (!!valueToTest) {
return valueToTest
}
}
return null
}
export default { export default {
isValidURL, isValidURL,
convertDuration, convertDuration,

View File

@ -48,7 +48,8 @@ module.exports = {
} }
}, },
variants: { variants: {
textColor: ({ after }) => after(['group-hover']) textColor: ({ after }) => after(['group-hover']),
borderWidth: ['responsive', 'first', 'hover', 'focus']
}, },
corePlugins: { corePlugins: {
preflight: false preflight: false