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*
# Editor directories and files
# .vscode
.idea
.vscode
*.suo
*.ntvs*
*.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
}
}
]
}

3019
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -4,12 +4,12 @@
<head>
<meta charset="utf-8">
<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">
<meta name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=0">
<script>
if (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>
<div>
<div id="app">
<div class="app-container">
<TheSidebar />
@ -12,8 +12,8 @@
</div>
<BaseLoadingPlaceholder
:hidden="isSocketConnected"
text="Connecting to the server..."
:hidden="isSocketConnected"
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 TheTrackPreview from '@components/globals/TheTrackPreview.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 TheSearchBar from '@components/TheSearchBar.vue'
@ -65,8 +65,8 @@ export default {
TheQualityModal,
BaseLoadingPlaceholder,
TheContextMenu,
TheContent,
ConfirmModal
TheContent
// ConfirmModal
},
mounted() {
socket.on('connect', () => {

View file

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

View file

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

View file

@ -168,7 +168,8 @@ export default {
}
} else {
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
}

View file

@ -13,7 +13,7 @@
<span
v-if="hasFails"
class="inline-flex"
class="flex items-center"
:class="{ clickable: finishedWithFails }"
style="justify-content: center"
@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>
<script>

View file

@ -1,11 +1,10 @@
<template functional>
<div
:id="props.id"
class="flex justify-center items-center flex-col flex-1 h-full"
:class="props.additionalClasses"
v-show="!props.hidden"
>
<span class="mb-5">{{ props.text }}</span>
<span class="mb-5">{{ props.text || 'Loading...' }}</span>
<div class="lds-ring">
<div></div>
@ -60,27 +59,3 @@
}
</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>
</template>
<style lang="scss">
@import '../../styles/scss/base/_variables.scss';
.smallmodal {
position: fixed;
z-index: 1250;
@ -47,11 +45,11 @@
top: 50%;
transform: translateY(-50%);
@media only screen and (min-width: $small) {
@media only screen and (min-width: 601px) {
--modal-content-width: 85%;
}
@media only screen and (min-width: $medium) {
@media only screen and (min-width: 993px) {
--modal-content-width: 70%;
}
}

View file

@ -23,10 +23,10 @@
<a href="https://deemix.app" target="_blank">🌍 {{ $t('about.officialWebsite') }}</a>
</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>
<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>
<a href="https://www.reddit.com/r/deemix" target="_blank">🤖 {{ $t('about.officialSubreddit') }}</a>

View file

@ -1,7 +1,7 @@
<template>
<div id="artist_tab" class="relative image-header" ref="root">
<header
class="inline-flex"
class="flex items-center"
:style="{
'background-image':
'linear-gradient(to bottom, transparent 0%, var(--main-background) 100%), url(\'' + image + '\')'
@ -14,7 +14,7 @@
aria-label="download"
@click.stop="addToQueue"
: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>
</div>
@ -54,13 +54,13 @@
</thead>
<tbody>
<tr v-for="release in showTable" :key="release.id">
<router-link tag="td" class="inline-flex clickable" :to="{ name: 'Album', params: { id: release.id } }">
<router-link tag="td" class="flex items-center clickable" :to="{ name: 'Album', params: { id: release.id } }">
<img
class="rounded coverart"
:src="release.cover_small"
style="margin-right: 16px; width: 56px; height: 56px"
/>
<i v-if="release.explicit_lyrics" class="material-icons explicit_icon"> explicit </i>
<i v-if="release.explicit_lyrics" class="material-icons explicit-icon"> explicit </i>
{{ release.title }}
<i v-if="checkNewRelease(release.release_date)" class="material-icons" style="color: #ff7300">
fiber_new

View file

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

View file

@ -26,9 +26,7 @@
</ul>
<button class="btn btn-primary" v-if="!activeTabEmpty" style="margin-bottom: 2rem" @click="downloadAllOfType">
{{
$t('globals.download', { thing: $tc(`globals.listTabs.${activeTab}N`, getTabLenght() )})
}}
{{ $t('globals.downloadAll', { thing: $tc(`globals.listTabs.${activeTab}`, 2) }) }}
</button>
<div class="favorites_tabcontent" :class="{ 'favorites_tabcontent--active': activeTab === 'playlist' }">
@ -166,7 +164,7 @@
</div>
<table v-if="tracks.length > 0" class="table">
<tr v-for="track in tracks" class="track_row">
<td class="top-tracks-position" :class="{ first: track.position === 1 }">
<td class="p-3 text-center cursor-default" :class="{ first: track.position === 1 }">
{{ track.position }}
</td>
<td>
@ -389,12 +387,6 @@ export default {
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() {
let lovedTracks = this.playlists.filter(playlist => {
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">
<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') }}
</router-link>
</section>

View file

@ -17,7 +17,7 @@
<div v-else>
<header
class="inline-flex"
class="flex items-center"
:style="{
'background-image':
'linear-gradient(to bottom, transparent 0%, var(--main-background) 100%), url(\'' + image + '\')'
@ -68,7 +68,7 @@
@contextmenu.prevent="openQualityModal"
@click.stop="addToQueue"
: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>
</div>
@ -121,10 +121,11 @@
</table>
<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') }}
</router-link>
</div>
<div v-if="countries.length">
<p v-for="country in countries">{{ country[0] }} - {{ country[1] }}</p>
</div>

View file

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

View file

@ -1,5 +1,5 @@
<template>
<div id="settings_tab" class="fixed-footer " ref="root">
<div id="settings_tab" class="fixed-footer" ref="root">
<h1 class="mb-8 text-5xl">{{ $t('settings.title') }}</h1>
<div id="logged_in_info" v-if="isLoggedIn" ref="loggedInInfo">
@ -22,7 +22,7 @@
<h3 class="settings-group__header settings-group__header--with-icon">
<i class="material-icons">person</i>{{ $t('settings.login.title') }}
</h3>
<div class="inline-flex">
<div class="flex items-center">
<input
autocomplete="off"
type="password"
@ -68,11 +68,11 @@
<h3 class="settings-group__header settings-group__header--with-icon">
<i class="material-icons">web</i>{{ $t('settings.appearance.title') }}
</h3>
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="changeSlimDownloads" />
<span class="checkbox_text">{{ $t('settings.appearance.slimDownloadTab') }}</span>
</label>
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="changeSlimSidebar" />
<span class="checkbox_text">{{ $t('settings.appearance.slimSidebar') }}</span>
</label>
@ -82,7 +82,7 @@
<h3 class="settings-group__header settings-group__header--with-icon">
<i class="material-icons">folder</i>{{ $t('settings.downloadPath.title') }}
</h3>
<div class="inline-flex">
<div class="flex items-center">
<input autocomplete="off" type="text" v-model="settings.downloadLocation" />
<button
id="select_downloads_folder"
@ -116,7 +116,7 @@
</h3>
<div class="settings-container">
<div class="settings-container__third">
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="settings.createPlaylistFolder" />
<span class="checkbox_text">{{ $t('settings.folders.createPlaylistFolder') }}</span>
</label>
@ -126,7 +126,7 @@
</div>
</div>
<div class="settings-container__third">
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="settings.createArtistFolder" />
<span class="checkbox_text">{{ $t('settings.folders.createArtistFolder') }}</span>
</label>
@ -137,7 +137,7 @@
</div>
</div>
<div class="settings-container__third">
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="settings.createAlbumFolder" />
<span class="checkbox_text">{{ $t('settings.folders.createAlbumFolder') }}</span>
</label>
@ -149,17 +149,17 @@
</div>
</div>
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="settings.createCDFolder" />
<span class="checkbox_text">{{ $t('settings.folders.createCDFolder') }}</span>
</label>
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="settings.createStructurePlaylist" />
<span class="checkbox_text">{{ $t('settings.folders.createStructurePlaylist') }}</span>
</label>
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="settings.createSingleFolder" />
<span class="checkbox_text">{{ $t('settings.folders.createSingleFolder') }}</span>
</label>
@ -172,7 +172,7 @@
<div class="settings-container">
<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" />
<span class="checkbox_text">{{ $t('settings.trackTitles.padTracks') }}</span>
</label>
@ -224,34 +224,34 @@
<div class="settings-container">
<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" />
<span class="checkbox_text">{{ $t('settings.downloads.fallbackBitrate') }}</span>
</label>
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="settings.fallbackSearch" />
<span class="checkbox_text">{{ $t('settings.downloads.fallbackSearch') }}</span>
</label>
</div>
<div class="settings-container__third settings-container__third--only-checkbox">
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="settings.logErrors" />
<span class="checkbox_text">{{ $t('settings.downloads.logErrors') }}</span>
</label>
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="settings.logSearched" />
<span class="checkbox_text">{{ $t('settings.downloads.logSearched') }}</span>
</label>
</div>
<div class="settings-container__third settings-container__third--only-checkbox">
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="settings.syncedLyrics" />
<span class="checkbox_text">{{ $t('settings.downloads.syncedLyrics') }}</span>
</label>
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="settings.createM3U8File" />
<span class="checkbox_text">{{ $t('settings.downloads.createM3U8File') }}</span>
</label>
@ -263,7 +263,7 @@
<input type="text" v-model="settings.playlistFilenameTemplate" />
</div>
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="settings.saveDownloadQueue" />
<span class="checkbox_text">{{ $t('settings.downloads.saveDownloadQueue') }}</span>
</label>
@ -274,7 +274,7 @@
<i class="material-icons">album</i>{{ $t('settings.covers.title') }}
</h3>
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="settings.saveArtwork" />
<span class="checkbox_text">{{ $t('settings.covers.saveArtwork') }}</span>
</label>
@ -284,7 +284,7 @@
<input type="text" v-model="settings.coverImageTemplate" />
</div>
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="settings.saveArtworkArtist" />
<span class="checkbox_text">{{ $t('settings.covers.saveArtworkArtist') }}</span>
</label>
@ -319,7 +319,7 @@
</select>
</div>
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="settings.embeddedArtworkPNG" />
<span class="checkbox_text">{{ $t('settings.covers.embeddedArtworkPNG') }}</span>
</label>
@ -327,7 +327,7 @@
{{ $t('settings.covers.embeddedPNGWarning') }}
</p>
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.coverDescriptionUTF8" />
<span class="checkbox_text">{{ $t('settings.covers.coverDescriptionUTF8') }}</span>
</label>
@ -345,106 +345,106 @@
<div class="settings-container">
<div class="settings-container__half">
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.title" />
<span class="checkbox_text">{{ $t('settings.tags.title') }}</span>
</label>
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.artist" />
<span class="checkbox_text">{{ $t('settings.tags.artist') }}</span>
</label>
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.album" />
<span class="checkbox_text">{{ $t('settings.tags.album') }}</span>
</label>
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.cover" />
<span class="checkbox_text">{{ $t('settings.tags.cover') }}</span>
</label>
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.trackNumber" />
<span class="checkbox_text">{{ $t('settings.tags.trackNumber') }}</span>
</label>
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.trackTotal" />
<span class="checkbox_text">{{ $t('settings.tags.trackTotal') }}</span>
</label>
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.discNumber" />
<span class="checkbox_text">{{ $t('settings.tags.discNumber') }}</span>
</label>
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.discTotal" />
<span class="checkbox_text">{{ $t('settings.tags.discTotal') }}</span>
</label>
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.albumArtist" />
<span class="checkbox_text">{{ $t('settings.tags.albumArtist') }}</span>
</label>
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.genre" />
<span class="checkbox_text">{{ $t('settings.tags.genre') }}</span>
</label>
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.year" />
<span class="checkbox_text">{{ $t('settings.tags.year') }}</span>
</label>
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.date" />
<span class="checkbox_text">{{ $t('settings.tags.date') }}</span>
</label>
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.explicit" />
<span class="checkbox_text">{{ $t('settings.tags.explicit') }}</span>
</label>
</div>
<div class="settings-container__half">
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.isrc" />
<span class="checkbox_text">{{ $t('settings.tags.isrc') }}</span>
</label>
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.length" />
<span class="checkbox_text">{{ $t('settings.tags.length') }}</span>
</label>
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.barcode" />
<span class="checkbox_text">{{ $t('settings.tags.barcode') }}</span>
</label>
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.bpm" />
<span class="checkbox_text">{{ $t('settings.tags.bpm') }}</span>
</label>
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.replayGain" />
<span class="checkbox_text">{{ $t('settings.tags.replayGain') }}</span>
</label>
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.label" />
<span class="checkbox_text">{{ $t('settings.tags.label') }}</span>
</label>
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.lyrics" />
<span class="checkbox_text">{{ $t('settings.tags.lyrics') }}</span>
</label>
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.syncedLyrics" />
<span class="checkbox_text">{{ $t('settings.tags.syncedLyrics') }}</span>
</label>
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.copyright" />
<span class="checkbox_text">{{ $t('settings.tags.copyright') }}</span>
</label>
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.composer" />
<span class="checkbox_text">{{ $t('settings.tags.composer') }}</span>
</label>
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.involvedPeople" />
<span class="checkbox_text">{{ $t('settings.tags.involvedPeople') }}</span>
</label>
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.source" />
<span class="checkbox_text">{{ $t('settings.tags.source') }}</span>
</label>
@ -457,17 +457,17 @@
<i class="material-icons">list</i>{{ $t('settings.other.title') }}
</h3>
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.savePlaylistAsCompilation" />
<span class="checkbox_text">{{ $t('settings.other.savePlaylistAsCompilation') }}</span>
</label>
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.useNullSeparator" />
<span class="checkbox_text">{{ $t('settings.other.useNullSeparator') }}</span>
</label>
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.saveID3v1" />
<span class="checkbox_text">{{ $t('settings.other.saveID3v1') }}</span>
</label>
@ -488,22 +488,22 @@
</select>
</div>
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="settings.tags.singleAlbumArtist" />
<span class="checkbox_text">{{ $t('settings.other.singleAlbumArtist') }}</span>
</label>
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="settings.albumVariousArtists" />
<span class="checkbox_text">{{ $t('settings.other.albumVariousArtists') }}</span>
</label>
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="settings.removeAlbumVersion" />
<span class="checkbox_text">{{ $t('settings.other.removeAlbumVersion') }}</span>
</label>
<label class="with_checkbox">
<label class="with-checkbox">
<input type="checkbox" v-model="settings.removeDuplicateArtists" />
<span class="checkbox_text">{{ $t('settings.other.removeDuplicateArtists') }}</span>
</label>
@ -643,7 +643,7 @@
.locale-flag {
width: 60px;
display: inline-flex;
display: flex items-center;
justify-content: center;
align-items: center;
cursor: pointer;
@ -724,10 +724,6 @@ export default {
set(wantSlimSidebar) {
this.slimSidebar = 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)
}
},

View file

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

View file

@ -1,51 +1,79 @@
<template>
<div id="album_search" class="search_tabcontent">
<BaseLoadingPlaceholder v-if="!results.albumTab.loaded" />
<div v-else-if="results.albumTab.data.length == 0">
<h1>{{ $t('search.noResultsAlbum') }}</h1>
</div>
<div class="release_grid" v-if="results.albumTab.data.length > 0">
<router-link
tag="div"
v-for="release in results.albumTab.data"
:key="release.id"
class="release clickable"
:to="{ name: 'Album', params: { id: release.id } }"
>
<div class="cover_container">
<img aria-hidden="true" class="rounded coverart" :src="release.cover_medium" />
<button
role="button"
aria-label="download"
@click.stop="$emit('add-to-queue', $event)"
:data-link="release.link"
class="download_overlay"
tabindex="0"
>
<i class="material-icons" :title="$t('globals.download_hint')">get_app</i>
</button>
</div>
<p class="primary-text inline-flex">
<i v-if="release.explicit_lyrics" class="material-icons explicit_icon">explicit</i>
{{ release.title }}
</p>
<p class="secondary-text">
{{
$t('globals.by', { artist: release.artist.name }) +
' - ' +
$tc('globals.listTabs.trackN', release.nb_tracks)
}}
</p>
</router-link>
</div>
</div>
<section>
<BaseLoadingPlaceholder v-if="isLoading" />
<template v-else>
<div v-if="viewInfo.data.length === 0">
<h1>{{ $t('search.noResultsAlbum') }}</h1>
</div>
<div class="release_grid" v-else>
<router-link
tag="div"
v-for="release in viewInfo.data.slice(0, itemsToShow)"
:key="release.albumID"
class="release clickable"
:to="{ name: 'Album', params: { id: release.albumID } }"
>
<div class="cover_container">
<img aria-hidden="true" class="rounded coverart" :src="release.albumCoverMedium" />
<button
role="button"
aria-label="download"
@click.stop="$emit('add-to-queue', $event)"
:data-link="release.albumLink"
class="download_overlay"
tabindex="0"
>
<i class="material-icons" :title="$t('globals.download_hint')">get_app</i>
</button>
</div>
<p class="primary-text flex items-center">
<i v-if="release.isAlbumExplicit" class="material-icons explicit-icon">explicit</i>
{{ release.albumTitle }}
</p>
<p class="secondary-text">
{{
$t('globals.by', { artist: release.artistName }) +
' - ' +
$tc('globals.listTabs.trackN', release.albumTracks)
}}
</p>
</router-link>
</div>
</template>
</section>
</template>
<script>
import BaseLoadingPlaceholder from '@components/globals/BaseLoadingPlaceholder.vue'
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: {
BaseLoadingPlaceholder
}

View file

@ -1,221 +1,66 @@
<template>
<div id="main_search" class="search_tabcontent">
<template v-for="section in results.allTab.ORDER">
<section>
<div v-if="!thereAreResults">
<h1>{{ $t('search.noResults') }}</h1>
</div>
<template v-else>
<section
v-if="
(section != 'TOP_RESULT' && results.allTab[section].data.length > 0) || results.allTab[section].length > 0
"
class="search_section"
v-for="section in viewInfo.ORDER"
:key="section"
class="float-none py-5 border-grayscale-500 border-t first:border-t-0"
>
<h2
@click="$emit('change-search-tab', section)"
class="search_header"
:class="{ top_result_header: section === 'TOP_RESULT' }"
class="mb-6 capitalize"
: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) }}
</h2>
<!-- Top result -->
<router-link
tag="div"
v-if="section == 'TOP_RESULT'"
class="top_result clickable"
: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"
aria-label="download"
@click.stop="$emit('add-to-queue', $event)"
:data-link="results.allTab.TOP_RESULT[0].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">{{ 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">
<div class="table__cell-content table__cell-content--vertical-center">
<i v-if="track.EXPLICIT_LYRICS == 1" class="material-icons explicit_icon"> explicit </i>
{{ track.SNG_TITLE + (track.VERSION ? ' ' + track.VERSION : '') }}
</div>
</td>
<td class="table__cell table__cell--medium table__cell--center breakline">
<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"
aria-label="download"
@click.stop="$emit('add-to-queue', $event)"
:data-link="'https://deezer.com/artist/' + release.ART_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.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"
aria-label="download"
@click.stop="$emit('add-to-queue', $event)"
:data-link="'https://deezer.com/album/' + release.ALB_ID"
class="download_overlay"
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>
<TopResult
v-if="section === 'TOP_RESULT'"
:info="viewInfo.TOP_RESULT[0]"
@add-to-queue="$emit('add-to-queue', $event)"
/>
<ResultsTracks
v-else-if="section === 'TRACK'"
:viewInfo="reduceSearchResults(viewInfo.TRACK, formatSingleTrack)"
:itemsToShow="6"
@add-to-queue="$emit('add-to-queue', $event)"
/>
<ResultsAlbums
v-else-if="section == 'ALBUM'"
:viewInfo="reduceSearchResults(viewInfo.ALBUM, formatAlbums)"
:itemsToShow="6"
@add-to-queue="$emit('add-to-queue', $event)"
/>
<ResultsPlaylists
v-else-if="section == 'PLAYLIST'"
:viewInfo="reduceSearchResults(viewInfo.PLAYLIST, formatPlaylist)"
:itemsToShow="6"
@add-to-queue="$emit('add-to-queue', $event)"
/>
<ResultsArtists
v-else-if="section === 'ARTIST'"
:viewInfo="reduceSearchResults(viewInfo.ARTIST, formatArtist)"
:itemsToShow="6"
@add-to-queue="$emit('add-to-queue', $event)"
/>
</section>
</template>
<div v-if="noResults">
<h1>{{ $t('search.noResults') }}</h1>
</div>
</div>
</section>
</template>
<style lang="scss" scoped>
<style scoped>
.tag {
background-color: var(--tag-background);
border-radius: 2px;
@ -230,39 +75,66 @@
<script>
import { convertDuration } from '@/utils/utils'
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 {
props: ['results'],
components: {
TopResult,
ResultsTracks,
ResultsAlbums,
ResultsArtists,
ResultsPlaylists
},
props: {
viewInfo: {
type: Object,
required: false
}
},
computed: {
topResultType() {
return this.results.allTab.TOP_RESULT[0].type
},
noResults() {
return this.results.allTab.ORDER.every(section =>
section == 'TOP_RESULT'
? this.results.allTab[section].length == 0
: this.results.allTab[section].data.length == 0
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() {
let number
try {
number = this.$n(this.results.allTab.TOP_RESULT[0].nb_fan)
number = this.$n(this.viewInfo.TOP_RESULT[0].nb_fan)
} 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('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: {
convertDuration,
upperCaseFirstLowerCaseRest
upperCaseFirstLowerCaseRest,
reduceSearchResults,
formatSingleTrack,
formatAlbums,
formatArtist,
formatPlaylist
}
}
</script>

View file

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

View file

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

View file

@ -1,75 +1,77 @@
<template>
<div id="track_search" class="search_tabcontent">
<BaseLoadingPlaceholder v-if="!results.trackTab.loaded" />
<div v-else-if="results.trackTab.data.length == 0">
<h1>{{ $t('search.noResultsTrack') }}</h1>
</div>
<table class="table table--tracks" v-if="results.trackTab.data.length > 0">
<thead>
<tr>
<th colspan="2">{{ $tc('globals.listTabs.title', 1) }}</th>
<th>{{ $tc('globals.listTabs.artist', 1) }}</th>
<th>{{ $tc('globals.listTabs.album', 1) }}</th>
<th>
<i class="material-icons"> timer </i>
</th>
<th style="width: 56px"></th>
</tr>
</thead>
<tbody>
<tr v-for="track in results.trackTab.data">
<td class="table__icon table__icon--big">
<a
href="#"
@click="playPausePreview"
class="rounded"
:class="{ 'single-cover': !!track.preview }"
:data-preview="track.preview"
>
<PreviewControls v-if="track.preview" />
<section>
<BaseLoadingPlaceholder v-if="isLoading" />
<img class="rounded coverart" :src="track.album.cover_small" />
</a>
</td>
<td class="table__cell table__cell--large breakline">
<div class="table__cell-content table__cell-content--vertical-center">
<i v-if="track.explicit_lyrics" class="material-icons explicit_icon"> explicit </i>
{{
track.title +
(track.title_version && track.title.indexOf(track.title_version) == -1 ? ' ' + track.title_version : '')
}}
</div>
</td>
<router-link
tag="td"
class="table__cell table__cell--medium table__cell--center breakline clickable"
:to="{ name: 'Artist', params: { id: track.artist.id } }"
>
{{ track.artist.name }}
</router-link>
<router-link
tag="td"
class="table__cell table__cell--medium table__cell--center breakline clickable"
:to="{ name: 'Album', params: { id: track.album.id } }"
>
{{ track.album.title }}
</router-link>
<td class="table__cell table__cell--small 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="track.link"
role="button"
aria-label="download"
>
<i class="material-icons" :title="$t('globals.download_hint')"> get_app </i>
</td>
</tr>
</tbody>
</table>
</div>
<template v-else>
<div v-if="viewInfo.data.length === 0">
<h1>{{ $t('search.noResultsTrack') }}</h1>
</div>
<table v-else class="table table--tracks">
<thead v-if="wantHeaders">
<tr class="capitalize">
<th colspan="2">{{ $tc('globals.listTabs.title', 1) }}</th>
<th>{{ $tc('globals.listTabs.artist', 1) }}</th>
<th>{{ $tc('globals.listTabs.album', 1) }}</th>
<th>
<i class="material-icons">timer</i>
</th>
<th style="width: 3.5rem"></th>
</tr>
</thead>
<tbody>
<tr v-for="track in viewInfo.data.slice(0, itemsToShow)" :key="track.trackLink">
<td class="table__icon table__icon--big">
<a
href="#"
@click="playPausePreview"
class="rounded"
:class="{ 'single-cover': !!track.trackPreview }"
:data-preview="track.trackPreview"
>
<PreviewControls v-if="track.trackPreview" />
<img class="rounded coverart" :src="track.albumPicture" />
</a>
</td>
<td class="table__cell table__cell--large breakline">
<div class="table__cell-content table__cell-content--vertical-center">
<i v-if="track.isTrackExplicit" class="material-icons explicit-icon">explicit</i>
{{ getTitle(track) }}
</div>
</td>
<router-link
tag="td"
class="table__cell table__cell--medium table__cell--center breakline clickable"
:to="{ name: 'Artist', params: { id: track.artistID } }"
>
{{ track.artistName }}
</router-link>
<router-link
tag="td"
class="table__cell table__cell--medium table__cell--center breakline clickable"
:to="{ name: 'Album', params: { id: track.albumID } }"
>
{{ track.albumTitle }}
</router-link>
<td class="table__cell table__cell--small table__cell--center">
{{ convertDuration(track.trackDuration) }}
</td>
<td
class="table__cell--download table__cell--center clickable"
@click.stop="$emit('add-to-queue', $event)"
:data-link="track.trackLink"
role="button"
aria-label="download"
>
<i class="material-icons" :title="$t('globals.download_hint')">get_app</i>
</td>
</tr>
</tbody>
</table>
</template>
</section>
</template>
<script>
@ -80,15 +82,44 @@ import EventBus from '@/utils/EventBus'
import { convertDuration } from '@/utils/utils'
export default {
props: ['results'],
components: {
BaseLoadingPlaceholder,
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: {
convertDuration,
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',
loading: 'loading',
download: 'Download {thing}',
downloadAll: 'Download all {thing}',
by: 'by {artist}',
in: 'in {album}',
download_hint: 'Download',
@ -27,6 +28,7 @@ const en = {
single: 'single | singles',
title: 'title | titles',
track: 'track | tracks',
trackN: '0 tracks | {n} track | {n} tracks',
releaseN: '0 releases | {n} release | {n} releases',
playlist: 'playlist | playlists',
compile: 'compilation | compilations',
@ -36,11 +38,7 @@ const en = {
featured: 'Featured in',
spotifyPlaylist: 'spotify playlist | spotify playlists',
releaseDate: 'release date',
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',
error: 'error'
}
},
about: {

View file

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

View file

@ -4,6 +4,7 @@ const it = {
back: 'indietro',
loading: 'caricamento',
download: 'Scarica {thing}',
downloadAll: 'Scarica ogni {thing}',
by: 'di {artist}',
in: 'in {album}',
download_hint: 'Scarica',
@ -26,6 +27,7 @@ const it = {
single: 'singolo | singoli',
title: 'titolo | titoli',
track: 'brano | brani',
trackN: '0 brani | {n} brano | {n} brani',
releaseN: '0 dischi | {n} disco | {n} dischi',
playlist: 'playlist',
compile: 'compilation',
@ -36,11 +38,7 @@ const it = {
spotifyPlaylist: 'playlist spotify',
releaseDate: 'data di uscita',
error: 'errore',
empty: '',
trackN: '0 brani | {n} brano | {n} brani',
albumN: '{n} album',
artistN: '0 artisti | {n} artista | {n} artisti',
playlistN: '{n} playlist',
empty: ''
}
},
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';
-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);
}
}
@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='number'] {
appearance: none;
width: calc(100% - 16px);
margin-bottom: 8px;
border: 0px solid black;
line-height: 36px;
padding: 0px 8px;
border-radius: 4px;
background-color: var(--secondary-background);
padding: 0px 8px;
width: calc(100% - 16px);
line-height: 36px;
color: var(--foreground);
margin-bottom: 8px;
}
input[type='checkbox'] {
appearance: none;
background-color: none;
border: 2px solid gray;
opacity: 0.5;
border-radius: 2px;
padding: 7px;
margin: 3px;
display: inline-block;
position: relative;
opacity: 0.5;
margin: 3px;
border: 2px solid gray;
border-radius: 2px;
background-color: none;
padding: 7px;
&:checked {
opacity: 1;
margin: 3px;
border: 0px solid var(--primary-color);
border-radius: 2px;
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-position: center center;
border: 0px solid var(--primary-color);
border-radius: 2px;
padding: 9px;
margin: 3px;
color: var(--primary-text);
}
}
select {
appearance: none;
width: 100%;
margin-bottom: 8px;
border: 0px solid black;
line-height: 36px;
padding: 0px 40px 0px 8px;
border-radius: 4px;
background-clip: border-box;
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-position: calc(100% - 8px) center;
background-repeat: no-repeat;
background-size: 24px;
background-position: calc(100% - 8px) center;
background-clip: border-box;
padding: 0px 40px 0px 8px;
width: 100%;
line-height: 36px;
color: var(--foreground);
margin-bottom: 8px;
}
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 {
position: relative;
display: inline-block;
position: relative;
color: white;
}
@ -105,61 +87,42 @@ i {
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 {
background-color: var(--secondary-background);
}
// ? Maybe make a component?
.cover_container {
position: relative;
.coverart {
opacity: 1;
display: block;
backface-visibility: hidden;
transition: 0.5s ease;
opacity: 1;
width: 100%;
height: auto;
transition: 0.5s ease;
backface-visibility: hidden;
}
.download_overlay {
transition: 0.5s ease;
opacity: 0;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
background-color: #000000;
border-radius: 50%;
min-width: 32px;
padding: 0px;
height: 44px;
transition: 0.5s ease;
opacity: 0;
border: 0px;
border-radius: 50%;
background-color: #000000;
padding: 0px;
min-width: 32px;
height: 44px;
text-align: center;
i {
color: white;
padding: 10px;
cursor: pointer;
padding: 10px;
color: white;
}
&: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
.top-tracks-position {
padding: 12px;
text-align: center;
cursor: default;
.table--tracklist .clickable:hover,
.table--charts .clickable:hover {
text-decoration: underline;
}

View file

@ -3,7 +3,6 @@
@import '~tailwindcss/utilities';
@import './base/base';
@import './base/variables';
html {
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 {
display: flex;
align-items: center;
flex-direction: column;
@apply flex items-center flex-col;
// display: flex;
// align-items: center;
// flex-direction: column;
> .cover_container {
width: 156px;
height: 156px;
width: 9.75rem;
height: 9.75rem;
}
.info_box {
@ -49,24 +15,24 @@
flex-direction: column;
justify-content: center;
align-items: center;
margin-top: 15px;
margin-top: 0.9375rem;
.primary-text,
.secondary-text {
font-size: 18px;
font-size: 1.125rem;
text-align: center;
}
.primary-text {
margin-bottom: 5px;
margin-bottom: 0.3125rem;
}
.secondary-text {
margin-bottom: 10px;
margin-bottom: 0.625rem;
}
.tag {
width: 40px;
width: 2.5rem;
text-align: center;
}
}
@ -76,17 +42,17 @@
.release {
.primary-text,
.secondary-text {
margin: 0px;
margin-bottom: 4px;
margin: 0rem;
margin-bottom: 0.25rem;
}
.secondary-text {
opacity: 0.75;
font-size: 14px;
font-size: 0.875rem;
.material-icons {
font-size: 17px !important;
margin-left: 4px;
font-size: 1.0625rem !important;
margin-left: 0.25rem;
}
}
}
@ -99,7 +65,7 @@
&.firstrow_only {
grid-template-rows: 1fr;
grid-auto-rows: 0;
grid-row-gap: 0px;
grid-row-gap: 0rem;
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 {
margin-bottom: 25px;
@ -86,7 +101,7 @@
margin-bottom: 7px;
}
.with_checkbox + & {
.with-checkbox + & {
margin-top: 10px;
}
}

View file

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

View file

@ -95,6 +95,28 @@ export function copyToClipboard(text) {
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 {
isValidURL,
convertDuration,

View file

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