workflow: reorganized components' folder structure
This commit is contained in:
31
src/components/globals/BaseLoadingPlaceholder.vue
Normal file
31
src/components/globals/BaseLoadingPlaceholder.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template functional>
|
||||
<div :id="props.id" class="loading_placeholder" :class="{ 'loading_placeholder--hidden': props.hidden }">
|
||||
<span class="loading_placeholder__text">{{ props.text }}</span>
|
||||
<div class="lds-ring">
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
text: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'Loading...'
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
hidden: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
312
src/components/globals/TheContextMenu.vue
Normal file
312
src/components/globals/TheContextMenu.vue
Normal file
@@ -0,0 +1,312 @@
|
||||
<template>
|
||||
<div class="context-menu" v-show="menuOpen" ref="contextMenu" :style="{ top: yPos, left: xPos }">
|
||||
<button
|
||||
class="menu-option"
|
||||
v-for="option of sortedOptions"
|
||||
:key="option.label"
|
||||
v-show="option.show"
|
||||
@click.prevent="option.action"
|
||||
>
|
||||
<span class="menu-option__text">{{ option.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { sendAddToQueue } from '@/utils/downloads'
|
||||
import { generatePath, copyToClipboard } from '@/utils/utils'
|
||||
import { downloadQualities } from '@/data/qualities'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
menuOpen: false,
|
||||
xPos: 0,
|
||||
yPos: 0,
|
||||
deezerHref: '',
|
||||
generalHref: '',
|
||||
imgSrc: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
options() {
|
||||
// In the action property:
|
||||
// Use arrow functions to keep the Vue instance in 'this'
|
||||
// Use normal functions to keep the object instance in 'this'
|
||||
const options = {
|
||||
cut: {
|
||||
label: this.$t('globals.cut'),
|
||||
show: false,
|
||||
position: 1,
|
||||
action: () => {
|
||||
document.execCommand('Cut')
|
||||
}
|
||||
},
|
||||
copy: {
|
||||
label: this.$t('globals.copy'),
|
||||
show: false,
|
||||
position: 2,
|
||||
action: () => {
|
||||
document.execCommand('Copy')
|
||||
}
|
||||
},
|
||||
copyLink: {
|
||||
label: this.$t('globals.copyLink'),
|
||||
show: false,
|
||||
position: 3,
|
||||
action: () => {
|
||||
copyToClipboard(this.generalHref)
|
||||
}
|
||||
},
|
||||
copyImageLink: {
|
||||
label: this.$t('globals.copyImageLink'),
|
||||
show: false,
|
||||
position: 4,
|
||||
action: () => {
|
||||
copyToClipboard(this.imgSrc)
|
||||
}
|
||||
},
|
||||
copyDeezerLink: {
|
||||
label: this.$t('globals.copyDeezerLink'),
|
||||
show: false,
|
||||
position: 5,
|
||||
action: () => {
|
||||
copyToClipboard(this.deezerHref)
|
||||
}
|
||||
},
|
||||
paste: {
|
||||
label: this.$t('globals.paste'),
|
||||
show: false,
|
||||
position: 6,
|
||||
action: () => {
|
||||
// Paste does not always work
|
||||
if (clipboard in navigator) {
|
||||
navigator.clipboard.readText().then(text => {
|
||||
document.execCommand('insertText', undefined, text)
|
||||
})
|
||||
} else {
|
||||
document.execCommand('paste')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let nextValuePosition = Object.values(options).length + 1
|
||||
|
||||
downloadQualities.forEach((quality, index) => {
|
||||
options[quality.objName] = {
|
||||
label: `${this.$t('globals.download', { thing: quality.label })}`,
|
||||
show: false,
|
||||
position: nextValuePosition + index,
|
||||
action: sendAddToQueue.bind(null, this.deezerHref, quality.value)
|
||||
}
|
||||
})
|
||||
|
||||
return options
|
||||
},
|
||||
/**
|
||||
* This computed property is used for rendering the options in the wanted order
|
||||
* while keeping the options computed property an Object to make the properties
|
||||
* accessible via property name (es this.options.copyLink)
|
||||
*
|
||||
* @return {object[]} Options in order according to position property
|
||||
*/
|
||||
sortedOptions() {
|
||||
return Object.values(this.options).sort((first, second) => {
|
||||
return first.position < second.position ? -1 : 1
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
document.body.addEventListener('contextmenu', this.showMenu)
|
||||
document.body.addEventListener('click', this.hideMenu)
|
||||
},
|
||||
methods: {
|
||||
showMenu(contextMenuEvent) {
|
||||
const { pageX, pageY, target: elementClicked } = contextMenuEvent
|
||||
const path = generatePath(elementClicked)
|
||||
let deezerLink = null
|
||||
|
||||
// Searching for the first element with a data-link attribute
|
||||
// let deezerLink = this.searchForDataLink(...)
|
||||
for (let i = 0; i < path.length; i++) {
|
||||
if (path[i] == document) break
|
||||
|
||||
if (path[i].matches('[data-link]')) {
|
||||
deezerLink = path[i].dataset.link
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const isLink = elementClicked.matches('a')
|
||||
const isImage = elementClicked.matches('img')
|
||||
const hasDeezerLink = !!deezerLink
|
||||
|
||||
if (!isLink && !isImage && !hasDeezerLink) return
|
||||
|
||||
contextMenuEvent.preventDefault()
|
||||
this.menuOpen = true
|
||||
this.positionMenu(pageX, pageY)
|
||||
|
||||
if (isLink) {
|
||||
// Show 'Copy Link' option
|
||||
this.generalHref = elementClicked.href
|
||||
this.options.copyLink.show = true
|
||||
}
|
||||
|
||||
if (isImage) {
|
||||
// Show 'Copy Image Link' option
|
||||
this.imgSrc = elementClicked.src
|
||||
this.options.copyImageLink.show = true
|
||||
}
|
||||
|
||||
if (deezerLink) {
|
||||
// Show 'Copy Deezer Link' option
|
||||
this.deezerHref = deezerLink
|
||||
this.showDeezerOptions()
|
||||
}
|
||||
},
|
||||
hideMenu() {
|
||||
if (!this.menuOpen) return
|
||||
|
||||
// Finish all operations before closing (may be not necessary)
|
||||
this.$nextTick()
|
||||
.then(() => {
|
||||
this.menuOpen = false
|
||||
|
||||
this.options.copyLink.show = false
|
||||
this.options.copyDeezerLink.show = false
|
||||
this.options.copyImageLink.show = false
|
||||
|
||||
downloadQualities.forEach(quality => {
|
||||
this.options[quality.objName].show = false
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err)
|
||||
})
|
||||
},
|
||||
positionMenu(newX, newY) {
|
||||
this.xPos = `${newX}px`
|
||||
this.yPos = `${newY}px`
|
||||
|
||||
this.$nextTick().then(() => {
|
||||
const { innerHeight, innerWidth } = window
|
||||
const menuXOffest = newX + this.$refs.contextMenu.getBoundingClientRect().width
|
||||
const menuYOffest = newY + this.$refs.contextMenu.getBoundingClientRect().height
|
||||
|
||||
if (menuXOffest > innerWidth) {
|
||||
const difference = menuXOffest - innerWidth + 15
|
||||
this.xPos = `${newX - difference}px`
|
||||
}
|
||||
|
||||
if (menuYOffest > innerHeight) {
|
||||
const difference = menuYOffest - innerHeight + 15
|
||||
this.yPos = `${newY - difference}px`
|
||||
}
|
||||
})
|
||||
},
|
||||
showDeezerOptions() {
|
||||
this.options.copyDeezerLink.show = true
|
||||
|
||||
downloadQualities.forEach(quality => {
|
||||
this.options[quality.objName].show = true
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.context-menu {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
min-width: 100px;
|
||||
border-radius: 7px;
|
||||
background: var(--foreground-inverted);
|
||||
box-shadow: 4px 10px 18px 0px hsla(0, 0%, 0%, 0.15);
|
||||
overflow: hidden;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.menu-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
color: var(--foreground);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: var(--table-highlight);
|
||||
filter: brightness(150%);
|
||||
}
|
||||
|
||||
&__text {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
|
||||
// Resetting buttons only for this component (because the style is scoped)
|
||||
button {
|
||||
color: var(--accent-text);
|
||||
color: unset;
|
||||
background-color: var(--accent-color);
|
||||
background-color: unset;
|
||||
min-width: unset;
|
||||
position: unset;
|
||||
border: unset;
|
||||
border-radius: unset;
|
||||
font-family: unset;
|
||||
font-weight: unset;
|
||||
font-size: unset;
|
||||
padding: unset;
|
||||
margin-right: unset;
|
||||
height: unset;
|
||||
text-transform: unset;
|
||||
cursor: unset;
|
||||
transition: unset;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
background-color: unset;
|
||||
color: unset;
|
||||
opacity: unset;
|
||||
}
|
||||
|
||||
&.selective {
|
||||
background-color: unset;
|
||||
color: unset;
|
||||
|
||||
&.active {
|
||||
background-color: unset;
|
||||
color: unset;
|
||||
}
|
||||
}
|
||||
|
||||
&.with_icon {
|
||||
display: unset;
|
||||
align-items: unset;
|
||||
|
||||
i {
|
||||
margin-left: unset;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: unset;
|
||||
transform: unset;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: unset;
|
||||
border: unset;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
85
src/components/globals/TheQualityModal.vue
Normal file
85
src/components/globals/TheQualityModal.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<div id="modal_quality" class="smallmodal" v-show="open" @click="tryToDownloadTrack($event)" ref="modal">
|
||||
<div class="smallmodal-content">
|
||||
<button class="quality-button" data-quality-value="9">{{ $t('globals.download', {thing: 'FLAC'}) }}</button>
|
||||
<button class="quality-button" data-quality-value="3">{{ $t('globals.download', {thing: 'MP3 320kbps'}) }}</button>
|
||||
<button class="quality-button" data-quality-value="1">{{ $t('globals.download', {thing: 'MP3 128kbps'}) }}</button>
|
||||
<button class="quality-button" data-quality-value="15">
|
||||
{{ $t('globals.download', {thing: '360 Reality Audio [HQ]'}) }}
|
||||
</button>
|
||||
<button class="quality-button" data-quality-value="14">
|
||||
{{ $t('globals.download', {thing: '360 Reality Audio [MQ]'}) }}
|
||||
</button>
|
||||
<button class="quality-button" data-quality-value="13">
|
||||
{{ $t('globals.download', {thing: '360 Reality Audio [LQ]'}) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style>
|
||||
.smallmodal {
|
||||
position: fixed;
|
||||
z-index: 1250;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
background-color: hsla(0, 0%, 0%, 0.4);
|
||||
animation-duration: 0.3s;
|
||||
}
|
||||
|
||||
.smallmodal-content {
|
||||
background-color: transparent;
|
||||
margin: auto;
|
||||
width: var(--modal-content-width);
|
||||
position: relative;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.smallmodal-content button {
|
||||
width: 100%;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
import Downloads from '@/utils/downloads'
|
||||
|
||||
export default {
|
||||
data: () => ({
|
||||
open: false,
|
||||
url: ''
|
||||
}),
|
||||
mounted() {
|
||||
this.$root.$on('QualityModal:open', this.openModal)
|
||||
this.$refs.modal.addEventListener('webkitAnimationEnd', this.handleAnimationEnd)
|
||||
},
|
||||
methods: {
|
||||
tryToDownloadTrack(event) {
|
||||
const { target } = event
|
||||
|
||||
this.$refs.modal.classList.add('animated', 'fadeOut')
|
||||
|
||||
// If true, the click did not happen on a button but outside
|
||||
if (!target.matches('.quality-button')) return
|
||||
|
||||
Downloads.sendAddToQueue(this.url, target.dataset.qualityValue)
|
||||
},
|
||||
openModal(link) {
|
||||
this.url = link
|
||||
this.open = true
|
||||
this.$refs.modal.classList.add('animated', 'fadeIn')
|
||||
},
|
||||
handleAnimationEnd(event) {
|
||||
const { animationName } = event
|
||||
|
||||
this.$refs.modal.classList.remove('animated', animationName)
|
||||
|
||||
if (animationName === 'fadeIn') return
|
||||
|
||||
this.open = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
144
src/components/globals/TheTrackPreview.vue
Normal file
144
src/components/globals/TheTrackPreview.vue
Normal file
@@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<audio id="preview-track" @canplay="onCanPlay" @timeupdate="onTimeUpdate" ref="preview">
|
||||
<source id="preview-track_source" src="" type="audio/mpeg" />
|
||||
</audio>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// import $ from 'jquery'
|
||||
import EventBus from '@/utils/EventBus'
|
||||
|
||||
import { adjustVolume } from '@/utils/adjust-volume'
|
||||
|
||||
export default {
|
||||
data: () => ({
|
||||
previewStopped: false
|
||||
}),
|
||||
mounted() {
|
||||
this.$refs.preview.volume = 1
|
||||
|
||||
EventBus.$on('trackPreview:playPausePreview', this.playPausePreview)
|
||||
EventBus.$on('trackPreview:stopStackedTabsPreview', this.stopStackedTabsPreview)
|
||||
EventBus.$on('trackPreview:previewMouseEnter', this.previewMouseEnter)
|
||||
EventBus.$on('trackPreview:previewMouseLeave', this.previewMouseLeave)
|
||||
},
|
||||
methods: {
|
||||
async onCanPlay() {
|
||||
await this.$refs.preview.play()
|
||||
|
||||
this.previewStopped = false
|
||||
|
||||
await adjustVolume(this.$refs.preview, window.vol.preview_max_volume / 100, { duration: 500 })
|
||||
},
|
||||
async onTimeUpdate() {
|
||||
// Prevents first time entering in this function
|
||||
if (isNaN(this.$refs.preview.duration)) return
|
||||
|
||||
let duration = this.$refs.preview.duration
|
||||
|
||||
if (!isFinite(duration)) {
|
||||
duration = 30
|
||||
}
|
||||
|
||||
if (duration - this.$refs.preview.currentTime >= 1) return
|
||||
if (this.previewStopped) return
|
||||
|
||||
await adjustVolume(this.$refs.preview, 0, { duration: 800 })
|
||||
|
||||
this.previewStopped = true
|
||||
|
||||
document.querySelectorAll('a[playing] > .preview_controls').forEach(control => {
|
||||
control.style.opacity = 0
|
||||
})
|
||||
|
||||
document.querySelectorAll('*').forEach(el => {
|
||||
el.removeAttribute('playing')
|
||||
})
|
||||
|
||||
document.querySelectorAll('.preview_controls, .preview_playlist_controls').forEach(el => {
|
||||
el.textContent = 'play_arrow'
|
||||
})
|
||||
},
|
||||
async playPausePreview(e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
const { currentTarget: obj } = event
|
||||
|
||||
var icon = obj.tagName == 'I' ? obj : obj.querySelector('i')
|
||||
|
||||
if (obj.hasAttribute('playing')) {
|
||||
if (this.$refs.preview.paused) {
|
||||
this.$refs.preview.play()
|
||||
this.previewStopped = false
|
||||
|
||||
icon.innerText = 'pause'
|
||||
|
||||
await adjustVolume(this.$refs.preview, window.vol.preview_max_volume / 100, { duration: 500 })
|
||||
} else {
|
||||
this.previewStopped = true
|
||||
|
||||
icon.innerText = 'play_arrow'
|
||||
|
||||
await adjustVolume(this.$refs.preview, 0, { duration: 250 })
|
||||
|
||||
this.$refs.preview.pause()
|
||||
}
|
||||
} else {
|
||||
document.querySelectorAll('*').forEach(el => {
|
||||
el.removeAttribute('playing')
|
||||
})
|
||||
obj.setAttribute('playing', true)
|
||||
|
||||
document.querySelectorAll('.preview_controls, .preview_playlist_controls').forEach(el => {
|
||||
el.textContent = 'play_arrow'
|
||||
})
|
||||
|
||||
document.querySelectorAll('.preview_controls').forEach(el => {
|
||||
el.style.opacity = 0
|
||||
})
|
||||
|
||||
icon.innerText = 'pause'
|
||||
icon.style.opacity = 1
|
||||
|
||||
this.previewStopped = false
|
||||
|
||||
await adjustVolume(this.$refs.preview, 0, { duration: 250 })
|
||||
this.$refs.preview.pause()
|
||||
|
||||
document.getElementById('preview-track_source').src = obj.getAttribute('data-preview')
|
||||
|
||||
this.$refs.preview.load()
|
||||
}
|
||||
},
|
||||
async stopStackedTabsPreview() {
|
||||
let controls = Array.prototype.slice.call(document.querySelectorAll('.preview_playlist_controls[playing]'))
|
||||
|
||||
if (controls.length === 0) return
|
||||
|
||||
await adjustVolume(this.$refs.preview, 0, { duration: 800 })
|
||||
|
||||
this.previewStopped = true
|
||||
|
||||
controls.forEach(control => {
|
||||
control.removeAttribute('playing')
|
||||
control.innerText = 'play_arrow'
|
||||
})
|
||||
},
|
||||
previewMouseEnter(e) {
|
||||
e.currentTarget.style.opacity = 1
|
||||
},
|
||||
previewMouseLeave(event) {
|
||||
const { currentTarget: obj } = event
|
||||
const parentIsPlaying = obj.parentElement.hasAttribute('playing')
|
||||
|
||||
if ((parentIsPlaying && this.previewStopped) || !parentIsPlaying) {
|
||||
obj.style.opacity = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
Reference in New Issue
Block a user