Remove jQuery

This commit is contained in:
An Phan 2016-12-20 23:44:47 +08:00
parent 7ea6700929
commit 9dc0ddebb5
No known key found for this signature in database
GPG key ID: 05536BB4BCDC02A2
33 changed files with 718 additions and 704 deletions

View file

@ -34,7 +34,7 @@ elixir(function (mix) {
.styles([
'resources/assets/css/**/*.css',
'node_modules/font-awesome/css/font-awesome.min.css',
'node_modules/rangeslider.js/dist/rangeslider.css',
'node_modules/nouislider/distribute/nouislider.min.css',
], 'public/css/vendors.css', './');
mix.version(['css/vendors.css', 'css/app.css', 'js/vendors.js', 'js/main.js']);

View file

@ -15,15 +15,15 @@
},
"dependencies": {
"alertify.js": "^1.0.12",
"axios": "^0.15.3",
"blueimp-md5": "^2.3.0",
"font-awesome": "^4.5.0",
"ismobilejs": "^0.4.0",
"jquery": "^3.1.1",
"local-storage": "^1.4.2",
"lodash": "^4.6.1",
"nouislider": "^9.1.0",
"nprogress": "^0.2.0",
"plyr": "1.5.x",
"rangeslider.js": "^2.2.1",
"raven-js": "^3.9.1",
"select": "^1.0.6",
"slugify": "^1.0.2",
@ -60,6 +60,7 @@
},
"scripts": {
"postinstall": "cross-env NODE_ENV=production && gulp --production",
"build": "cross-env NODE_ENV=production && gulp --production",
"test": "eslint resources/assets/js --ext=js,vue && mocha --compilers js:babel-register --require resources/assets/js/tests/helper.js resources/assets/js/tests/**/*Test.js",
"e2e": "gulp e2e",
"dev": "cross-env NODE_ENV=development && gulp watch"

View file

@ -24,7 +24,6 @@
<script>
import Vue from 'vue'
import $ from 'jquery'
import siteHeader from './components/site-header/index.vue'
import siteFooter from './components/site-footer/index.vue'
@ -33,7 +32,7 @@ import overlay from './components/shared/overlay.vue'
import loginForm from './components/auth/login-form.vue'
import editSongsForm from './components/modals/edit-songs-form.vue'
import { event, showOverlay, hideOverlay, forceReloadWindow } from './utils'
import { event, showOverlay, hideOverlay, forceReloadWindow, $ } from './utils'
import { sharedStore, userStore, preferenceStore as preferences } from './stores'
import { playback, ls } from './services'
import { focusDirective, clickawayDirective } from './directives'
@ -57,14 +56,18 @@ export default {
}
// Create the element to be the ghost drag image.
$('<div id="dragGhost"></div>').appendTo('body')
const dragGhost = document.createElement('div')
dragGhost.id = 'dragGhost'
document.body.appendChild(dragGhost)
// And the textarea to copy stuff
$('<textarea id="copyArea"></textarea>').appendTo('body')
const copyArea = document.createElement('textarea')
copyArea.id = 'copyArea'
document.body.appendChild(copyArea)
// Add an ugly mac/non-mac class for OS-targeting styles.
// I'm crying inside.
$('html').addClass(navigator.userAgent.indexOf('Mac') !== -1 ? 'mac' : 'non-mac')
$.addClass(document.documentElement, navigator.userAgent.indexOf('Mac') !== -1 ? 'mac' : 'non-mac')
},
methods: {
@ -104,12 +107,14 @@ export default {
* @param {Object} e The keydown event
*/
togglePlayback (e) {
if ($(e.target).is('input,textarea,button,select')) {
if ($.is(e.target, 'input,textarea,button,select')) {
return true
}
// Ah... Good ol' jQuery. Whatever play/pause control is there, we blindly click it.
$('#mainFooter .play:visible, #mainFooter .pause:visible').click()
// Whatever play/pause control is there, we blindly click it.
const play = document.querySelector('#mainFooter .play')
play ? play.click() : document.querySelector('#mainFooter .pause').click()
e.preventDefault()
},
@ -119,7 +124,7 @@ export default {
* @param {Object} e The keydown event
*/
playPrev (e) {
if ($(e.target).is('input,textarea')) {
if ($.is(e.target, 'input,textarea')) {
return true
}
@ -133,7 +138,7 @@ export default {
* @param {Object} e The keydown event
*/
playNext (e) {
if ($(e.target).is('input,textarea')) {
if ($.is(e.target, 'input,textarea')) {
return true
}
@ -147,11 +152,13 @@ export default {
* @param {Object} e The keydown event
*/
search (e) {
if ($(e.target).is('input,textarea') || e.metaKey || e.ctrlKey) {
if ($.is(e.target, 'input,textarea') || e.metaKey || e.ctrlKey) {
return true
}
$('#searchForm input[type="search"]').focus().select()
const selectBox = document.querySelector('#searchForm input[type="search"]')
selectBox.focus()
selectBox.select()
e.preventDefault()
},

View file

@ -36,9 +36,8 @@
<script>
import isMobile from 'ismobilejs'
import $ from 'jquery'
import { event } from '../../../utils'
import { event, $ } from '../../../utils'
import { sharedStore, songStore, preferenceStore as preferences } from '../../../stores'
import { songInfo } from '../../../services'
@ -68,9 +67,9 @@ export default {
*/
'state.showExtraPanel' (newVal) {
if (newVal && !isMobile.any) {
$('html').addClass('with-extra-panel')
$.addClass(document.documentElement, 'with-extra-panel')
} else {
$('html').removeClass('with-extra-panel')
$.removeClass(document.documentElement, 'with-extra-panel')
}
}
},
@ -78,7 +77,7 @@ export default {
mounted () {
// On ready, add 'with-extra-panel' class.
if (!isMobile.any) {
$('html').addClass('with-extra-panel')
$.addClass(document.documentElement, 'with-extra-panel')
}
if (isMobile.phone) {

View file

@ -47,7 +47,7 @@ export default {
*/
loadMore () {
this.loading = true
youtubeService.searchVideosRelatedToSong(this.song, () => {
youtubeService.searchVideosRelatedToSong(this.song).then(() => {
this.videos = this.song.youtube.items
this.loading = false
})

View file

@ -104,10 +104,8 @@
</template>
<script>
import $ from 'jquery'
import { userStore, preferenceStore, sharedStore } from '../../../stores'
import { forceReloadWindow } from '../../../utils'
import { forceReloadWindow, $ } from '../../../utils'
import { http, ls } from '../../../services'
export default {
@ -129,11 +127,13 @@ export default {
update () {
// A little validation put in a small place.
if ((this.pwd || this.confirmPwd) && this.pwd !== this.confirmPwd) {
$('#inputProfilePassword, #inputProfileConfirmPassword').addClass('error')
document.querySelectorAll('#inputProfilePassword, #inputProfileConfirmPassword')
.forEach(el => $.addClass(el, 'error'))
return
}
$('#inputProfilePassword, #inputProfileConfirmPassword').removeClass('error')
document.querySelectorAll('#inputProfilePassword, #inputProfileConfirmPassword')
.forEach(el => $.removeClass(el, 'error'))
userStore.updateProfile(this.pwd).then(() => {
this.pwd = ''

View file

@ -56,9 +56,8 @@
<script>
import isMobile from 'ismobilejs'
import $ from 'jquery'
import { event } from '../../../utils'
import { event, $ } from '../../../utils'
import { sharedStore, userStore, songStore, queueStore } from '../../../stores'
import playlists from './playlists.vue'
@ -87,7 +86,7 @@ export default {
* @param {Object} e The dragleave event.
*/
removeDroppableState (e) {
$(e.target).removeClass('droppable')
$.removeClass(e.target, 'droppable')
},
/**
@ -96,7 +95,7 @@ export default {
* @param {Object} e The dragover event.
*/
allowDrop (e) {
$(e.target).addClass('droppable')
$.addClass(e.target, 'droppable')
e.dataTransfer.dropEffect = 'move'
return false

View file

@ -20,9 +20,7 @@
</template>
<script>
import $ from 'jquery'
import { event } from '../../../utils'
import { event, $ } from '../../../utils'
import { songStore, playlistStore, favoriteStore } from '../../../stores'
export default {
@ -97,7 +95,7 @@ export default {
* @param {Object} e The dragleave event.
*/
removeDroppableState (e) {
$(e.target).removeClass('droppable')
$.removeClass(e.target, 'droppable')
},
/**
@ -107,7 +105,7 @@ export default {
* @param {Object} e The dragover event.
*/
allowDrop (e) {
$(e.target).addClass('droppable')
$.addClass(e.target, 'droppable')
e.dataTransfer.dropEffect = 'move'
return false

View file

@ -37,7 +37,6 @@
<script>
import { map } from 'lodash'
import $ from 'jquery'
import { pluralize } from '../../utils'
import { queueStore, artistStore, sharedStore } from '../../stores'
@ -97,8 +96,9 @@ export default {
e.dataTransfer.effectAllowed = 'move'
// Set a fancy drop image using our ghost element.
const $ghost = $('#dragGhost').text(`All ${pluralize(songIds.length, 'song')} in ${this.album.name}`)
e.dataTransfer.setDragImage($ghost[0], 0, 0)
const ghost = document.getElementById('dragGhost')
ghost.innerText = `All ${pluralize(songIds.length, 'song')} in ${this.album.name}`
e.dataTransfer.setDragImage(ghost, 0, 0)
}
}
}

View file

@ -29,7 +29,6 @@
<script>
import { map } from 'lodash'
import $ from 'jquery'
import { pluralize } from '../../utils'
import { artistStore, queueStore, sharedStore } from '../../stores'
@ -86,8 +85,9 @@ export default {
e.dataTransfer.effectAllowed = 'move'
// Set a fancy drop image using our ghost element.
const $ghost = $('#dragGhost').text(`All ${pluralize(songIds.length, 'song')} by ${this.artist.name}`)
e.dataTransfer.setDragImage($ghost[0], 0, 0)
const ghost = document.getElementById('dragGhost')
ghost.innerText = `All ${pluralize(songIds.length, 'song')} by ${this.artist.name}`
e.dataTransfer.setDragImage(ghost, 0, 0)
}
}
}

View file

@ -52,9 +52,8 @@
<script>
import { find, invokeMap, filter, map } from 'lodash'
import isMobile from 'ismobilejs'
import $ from 'jquery'
import { filterBy, orderBy, limitBy, event, pluralize } from '../../utils'
import { filterBy, orderBy, limitBy, event, pluralize, $ } from '../../utils'
import { playlistStore, queueStore, songStore, favoriteStore } from '../../stores'
import { playback } from '../../services'
import router from '../../router'
@ -310,10 +309,10 @@ export default {
selectRowsBetweenIndexes (indexes) {
indexes.sort((a, b) => a - b)
const rows = $(this.$refs.wrapper).find('tbody tr')
const rows = this.$refs.wrapper.querySelectorAll('tbody tr')
for (let i = indexes[0]; i <= indexes[1]; ++i) {
this.getComponentBySongId($(rows[i - 1]).data('song-id')).select()
this.getComponentBySongId(rows[i - 1].getAttribute('data-song-id')).select()
}
},
@ -347,8 +346,9 @@ export default {
e.dataTransfer.effectAllowed = 'move'
// Set a fancy drop image using our ghost element.
const $ghost = $('#dragGhost').text(`${pluralize(songIds.length, 'song')}`)
e.dataTransfer.setDragImage($ghost[0], 0, 0)
const ghost = document.getElementById('dragGhost')
ghost.innerText = `${pluralize(songIds.length, 'song')}`
e.dataTransfer.setDragImage(ghost, 0, 0)
})
},
@ -363,7 +363,7 @@ export default {
return
}
$(e.target).parents('tr').addClass('droppable')
$.addClass(e.target.parentNode, 'droppable')
e.dataTransfer.dropEffect = 'move'
return false
@ -401,7 +401,7 @@ export default {
* @param {Object} e
*/
removeDroppableState (e) {
return $(e.target).parents('tr').removeClass('droppable')
$.removeClass(e.target.parentNode, 'droppable')
},
openContextMenu (songId, e) {
@ -433,16 +433,15 @@ export default {
// Scroll the item into view if it's lost into oblivion.
if (this.type === 'queue') {
const $wrapper = $(this.$refs.wrapper)
const $row = $wrapper.find(`.song-item[data-song-id="${song.id}"]`)
const row = this.$refs.wrapper.querySelector(`.song-item[data-song-id="${song.id}"]`)
if (!$row.length) {
if (!row) {
return
}
if ($wrapper[0].getBoundingClientRect().top + $wrapper[0].getBoundingClientRect().height <
$row[0].getBoundingClientRect().top) {
$wrapper.scrollTop($wrapper.scrollTop() + $row.position().top)
const wrapperRec = this.$refs.wrapper.getBoundingClientRect()
if (wrapperRec.top + wrapperRec.height < row.getBoundingClientRect().top) {
this.$refs.wrapper.scrollTop = this.$refs.wrapper.scrollTop + row.offsetTop
}
}
},

View file

@ -30,8 +30,6 @@
</template>
<script>
import $ from 'jquery'
import songMenuMethods from '../../mixins/song-menu-methods'
import { event, isClipboardSupported, copyText } from '../../utils'
@ -79,15 +77,11 @@ export default {
this.$nextTick(() => {
// Make sure the menu isn't off-screen
if (this.$el.getBoundingClientRect().bottom > window.innerHeight) {
$(this.$el).css({
top: 'auto',
bottom: 0
})
this.$el.style.top = 'auto'
this.$el.style.bottom = 0
} else {
$(this.$el).css({
top: this.top,
bottom: 'auto'
})
this.$el.style.top = this.top
this.$el.style.bottom = 'auto'
}
this.$refs.menu.focus()
@ -160,25 +154,26 @@ export default {
* they don't appear off-screen.
*/
mounted () {
$(this.$el).find('.has-sub').hover(e => {
const $submenu = $(e.target).find('.submenu:first')
if (!$submenu.length) {
this.$el.querySelectorAll('.has-sub').forEach(item => {
const submenu = item.querySelector('.submenu')
if (!submenu) {
return
}
$submenu.show()
item.addEventListener('mouseenter', e => {
submenu.style.display = 'block'
// Make sure the submenu isn't off-screen
if ($submenu[0].getBoundingClientRect().bottom > window.innerHeight) {
$submenu.css({
top: 'auto',
bottom: 0
})
}
}, e => {
$(e.target).find('.submenu:first').hide().css({
top: 0,
bottom: 'auto'
// Make sure the submenu isn't off-screen
if (submenu.getBoundingClientRect().bottom > window.innerHeight) {
submenu.style.top = 'auto'
submenu.style.bottom = 0
}
})
item.addEventListener('mouseleave', e => {
submenu.style.top = 0
submenu.style.bottom = 'auto'
submenu.style.display = 'none'
})
})
}

View file

@ -21,8 +21,7 @@
</template>
<script>
import $ from 'jquery'
import { filterBy } from '../../utils'
import { filterBy, $ } from '../../utils'
export default {
props: ['options', 'value', 'items'],
@ -46,10 +45,16 @@ export default {
* Navigate down the result list.
*/
down (e) {
const selected = $(this.$el).find('.result li.selected')
const selected = this.$el.querySelector('.result li.selected')
if (!selected.length || !selected.removeClass('selected').next('li').addClass('selected').length) {
$(this.$el).find('.result li:first').addClass('selected')
if (!selected || !selected.nextElementSibling) {
// No item selected, or we're at the end of the list.
// Select the first item now.
$.addClass(this.$el.querySelector('.result li:first-child'), 'selected')
selected && $.removeClass(selected, 'selected')
} else {
$.removeClass(selected, 'selected')
$.addClass(selected.nextElementSibling, 'selected')
}
this.scrollSelectedIntoView(false)
@ -60,10 +65,14 @@ export default {
* Navigate up the result list.
*/
up (e) {
const selected = $(this.$el).find('.result li.selected')
const selected = this.$el.querySelector('.result li.selected')
if (!selected.length || !selected.removeClass('selected').prev('li').addClass('selected').length) {
$(this.$el).find('.result li:last').addClass('selected')
if (!selected || !selected.previousElementSibling) {
$.addClass(this.$el.querySelector('.result li:last-child'), 'selected')
selected && $.removeClass(selected, 'selected')
} else {
$.removeClass(selected, 'selected')
$.addClass(selected.previousElementSibling, 'selected')
}
this.scrollSelectedIntoView(true)
@ -108,16 +117,16 @@ export default {
},
resultClick (e) {
$(this.$el).find('.result li.selected').removeClass('selected')
$(e.target).addClass('selected')
const selected = this.$el.querySelector('.result li.selected')
$.removeClass(selected, 'selected')
$.addClass(e.target, 'selected')
this.apply()
this.showingResult = false
},
apply () {
this.mutatedValue = $(this.$el).find('.result li.selected').text().trim() || this.mutatedValue
// In Vue 2.0, we can use v-model on custom components like this.
this.mutatedValue = this.$el.querySelector('.result li.selected').innerText.trim() || this.mutatedValue
this.$emit('input', this.mutatedValue)
},
@ -127,7 +136,7 @@ export default {
* @param {boolean} alignTop Whether the item should be aligned to top of its container.
*/
scrollSelectedIntoView (alignTop) {
const elem = $(this.$el).find('.result li.selected')[0]
const elem = this.$el.querySelector('.result li.selected')
if (!elem) {
return
}

View file

@ -9,13 +9,7 @@
</div>
<div class="bands">
<span class="band preamp">
<input
type="range"
min="-20"
max="20"
step="0.01"
data-orientation="vertical"
v-model="preampGainValue">
<span class="slider"></span>
<label>Preamp</label>
</span>
@ -26,13 +20,7 @@
</span>
<span class="band amp" v-for="band in bands">
<input
type="range"
min="-20"
max="20"
step="0.01"
data-orientation="vertical"
:value="band.filter.gain.value">
<span class="slider"></span>
<label>{{ band.label }}</label>
</span>
</div>
@ -41,17 +29,14 @@
<script>
import { map, cloneDeep } from 'lodash'
import $ from 'jquery'
// eslint-disable-next-line no-unused-vars
import rangeslider from 'rangeslider.js'
import nouislider from 'nouislider'
import { isAudioContextSupported, event } from '../../utils'
import { isAudioContextSupported, event, $ } from '../../utils'
import { equalizerStore, preferenceStore as preferences } from '../../stores'
export default {
data () {
return {
idx: 0,
bands: [],
preampGainValue: 0,
selectedPresetIndex: -1
@ -90,8 +75,7 @@ export default {
methods: {
/**
* Init the equalizer.
*
* @param {Element} player The audio player's DOM.
* @param {Element} player The audio player's node.
*/
init (player) {
const settings = equalizerStore.get()
@ -113,8 +97,8 @@ export default {
let prevFilter = null
// Create 10 bands with the frequencies similar to those of Winamp and connect them together.
const freqs = [60, 170, 310, 600, 1000, 3000, 6000, 12000, 14000, 16000]
freqs.forEach((f, i) => {
const frequencies = [60, 170, 310, 600, 1000, 3000, 6000, 12000, 14000, 16000]
frequencies.forEach((frequency, i) => {
const filter = context.createBiquadFilter()
if (i === 0) {
@ -127,60 +111,60 @@ export default {
filter.gain.value = settings.gains[i] ? settings.gains[i] : 0
filter.Q.value = 1
filter.frequency.value = f
filter.frequency.value = frequency
prevFilter ? prevFilter.connect(filter) : this.preampGainNode.connect(filter)
prevFilter = filter
this.bands.push({
filter,
label: (f + '').replace('000', 'K')
label: (frequency + '').replace('000', 'K')
})
})
prevFilter.connect(context.destination)
this.$nextTick(this.createRangeSliders)
this.$nextTick(this.createSliders)
// Now we set this value to trigger the audio processing.
this.selectedPresetIndex = preferences.selectedPreset
},
/**
* Create the UI slider for both the preamp and the normal bands using rangeslider.js.
* Create the UI sliders for both the preamp and the normal bands.
*/
createRangeSliders () {
$('#equalizer input[type="range"]').each((i, el) => {
$(el).rangeslider({
/**
* Force the polyfill and its styles on all browsers.
*
* @type {Boolean}
*/
polyfill: false,
createSliders () {
const config = equalizerStore.get()
document.querySelectorAll('#equalizer .slider').forEach((el, i) => {
nouislider.create(el, {
connect: [false, true],
// the first element is the preamp. The rest are gains.
start: i === 0 ? config.preamp : config.gains[i - 1],
range: { min: -20, max: 20 },
orientation: 'vertical',
direction: 'rtl'
})
/**
* Change the gain/preamp value when the user drags the sliders.
*
* @param {Float} position
* @param {Float} value
*/
onSlide: (position, value) => {
if ($(el).parents('.band').is('.preamp')) {
this.changePreampGain(value)
} else {
this.changeFilterGain(this.bands[i - 1].filter, value)
}
},
/**
* Save the settings and set the preset index to -1 (which is None) on slideEnd.
*/
onSlideEnd: () => {
this.selectedPresetIndex = -1
this.save()
/**
* Update the audio effect upon sliding / tapping.
*/
el.noUiSlider.on('slide', (values, handle) => {
const value = values[handle]
if (el.parentNode.matches('.preamp')) {
this.changePreampGain(value)
} else {
this.changeFilterGain(this.bands[i - 1].filter, value)
}
})
/**
* Save the equalizer values after the change is done.
*/
el.noUiSlider.on('change', () => {
// User has customized the equalizer. No preset should be selected.
this.selectedPresetIndex = -1
this.save()
})
})
},
@ -208,21 +192,19 @@ export default {
* Load a preset when the user select it from the dropdown.
*/
loadPreset (preset) {
$('#equalizer input[type=range]').each((i, input) => {
document.querySelectorAll('#equalizer .slider').forEach((el, i) => {
// We treat our preamp slider differently.
if ($(input).parents('.band').is('.preamp')) {
if ($.is(el.parentNode, '.preamp')) {
this.changePreampGain(preset.preamp)
// Update the slider values into GUI.
el.noUiSlider.set(preset.preamp)
} else {
this.changeFilterGain(this.bands[i - 1].filter, preset.gains[i - 1])
input.value = preset.gains[i - 1]
// Update the slider values into GUI.
el.noUiSlider.set(preset.gains[i - 1])
}
})
this.$nextTick(() => {
// Update the slider values into GUI.
$('#equalizer input[type="range"]').rangeslider('update', true)
})
this.save()
},
@ -236,9 +218,7 @@ export default {
mounted () {
event.on('equalizer:init', player => {
if (isAudioContextSupported()) {
this.init(player)
}
isAudioContextSupported() && this.init(player)
})
}
}
@ -322,6 +302,10 @@ export default {
align-items: center;
}
.slider {
height: 100px;
}
.indicators {
height: 100px;
width: 20px;
@ -347,49 +331,56 @@ export default {
}
}
/**
* The range slider styles
*/
.rangeslider {
background: transparent;
box-shadow: none;
&--vertical {
min-height: 100px;
width: 16px;
&::before {
.noUi {
&-connect {
background: none;
box-shadow: none;
&::after {
content: " ";
position: absolute;
left: 7px;
width: 2px;
background: rgba(255, 255, 255, 0.2);
z-index: 1;
height: 100%;
pointer-events: none;
}
.rangeslider__fill {
width: 2px;
background: #fff;
position: absolute;
background: #333;
top: 0;
left: 7px;
box-shadow: none;
border-radius: 0;
}
}
.rangeslider__handle {
left: 0;
&-target {
background: transparent;
border-radius: 0;
border: 0;
box-shadow: none;
width: 16px;
&::after {
content: " ";
position: absolute;
width: 2px;
height: 100%;
background: #fff;
border: 0;
height: 2px;
width: 100%;
border-radius: 0;
box-shadow: none;
top: 0;
left: 7px;
}
}
&::after {
display: none;
}
&-handle {
border: 0;
border-radius: 0;
box-shadow: none;
cursor: pointer;
&::before, &::after {
display: none;
}
}
&-vertical {
.noUi-handle {
width: 16px;
height: 2px;
left: 0;
top: 0;
}
}
}

View file

@ -1,6 +1,4 @@
import $ from 'jquery'
import { event } from '../utils'
import { event, $ } from '../utils'
import toTopButton from '../components/shared/to-top-button.vue'
/**
@ -41,7 +39,7 @@ export default {
* Scroll to top of the wrapper.
*/
scrollToTop () {
$(this.$refs.wrapper).animate({ scrollTop: 0 }, 500)
$.scrollTo(this.$refs.wrapper, 0, 500)
this.showBackToTop = false
}
},

View file

@ -1,5 +1,4 @@
import $ from 'jquery'
import { $ } from '../utils'
import { queueStore, playlistStore, favoriteStore } from '../stores'
/**
@ -24,7 +23,9 @@ export default {
* Close all submenus.
*/
close () {
$(this.$el).find('.submenu').hide()
this.$el.querySelectorAll('.submenu').forEach(el => {
el.style.display = 'none'
})
this.shown = false
},

View file

@ -1,8 +1,8 @@
import $ from 'jquery'
import { map } from 'lodash'
import { map, reduce } from 'lodash'
import { playlistStore, favoriteStore } from '../stores'
import { ls } from '.'
import { $ } from '../utils'
export const download = {
/**
@ -12,10 +12,8 @@ export const download = {
*/
fromSongs (songs) {
songs = [].concat(songs)
const ids = map(songs, 'id')
const params = $.param({ songs: ids })
return this.trigger(`songs?${params}`)
const query = reduce(songs, (q, song) => `songs[]=${song.id}&${segment}`, '')
return this.trigger(`songs?${query}`)
},
/**
@ -73,8 +71,9 @@ export const download = {
*/
trigger (uri) {
const sep = uri.indexOf('?') === -1 ? '?' : '&'
const frameId = `downloader${Date.now()}`
$(`<iframe id="${frameId}" style="display:none"></iframe`).appendTo('body')
document.getElementById(frameId).src = `/api/download/${uri}${sep}jwt-token=${ls.get('jwt-token')}`
const iframe = document.createElement('iframe')
iframe.style.display = 'none'
iframe.setAttribute('src', `/api/download/${uri}${sep}jwt-token=${ls.get('jwt-token')}`)
document.body.appendChild(iframe)
}
}

View file

@ -1,4 +1,4 @@
import $ from 'jquery'
import axios from 'axios'
import NProgress from 'nprogress'
import { event } from '../utils'
@ -9,15 +9,11 @@ import { ls } from '../services'
*/
export const http = {
request (method, url, data, successCb = null, errorCb = null) {
return $.ajax({
axios.request({
url,
data,
dataType: 'json',
url: `/api/${url}`,
method: method.toUpperCase(),
headers: {
Authorization: `Bearer ${ls.get('jwt-token')}`
}
}).done(successCb).fail(errorCb)
method: method.toLowerCase()
}).then(successCb).catch(errorCb)
},
get (url, successCb = null, errorCb = null) {
@ -40,25 +36,37 @@ export const http = {
* Init the service.
*/
init () {
$(document).ajaxComplete((e, r, settings) => {
axios.defaults.baseURL = '/api'
// Intercept the request to make sure the token is injected into the header.
axios.interceptors.request.use(config => {
config.headers.Authorization = `Bearer ${ls.get('jwt-token')}`
return config
})
// Intercept the response and…
axios.interceptors.response.use(response => {
NProgress.done()
if (r.status === 400 || r.status === 401) {
if (!(settings.method === 'POST' && /\/api\/me\/?$/.test(settings.url))) {
// This is not a failed login. Log out then.
event.emit('logout')
return
}
}
const token = r.getResponseHeader('Authorization')
// …get the token from the header or response data if exists, and save it.
const token = response.headers['Authorization'] || response.data['token']
if (token) {
ls.set('jwt-token', token)
}
if (r.responseJSON && r.responseJSON.token && r.responseJSON.token.length > 10) {
ls.set('jwt-token', r.responseJSON.token)
return response
}, error => {
NProgress.done()
// Also, if we receive a Bad Request / Unauthorized error
if (error.response.status === 400 || error.response.status === 401) {
// and we're not trying to login
if (!(error.config.method === 'post' && /\/api\/me\/?$/.test(error.config.url))) {
// the token must have expired. Log out.
event.emit('logout')
}
}
return Promise.reject(error)
})
}
}

View file

@ -16,10 +16,10 @@ export const albumInfo = {
return
}
http.get(`album/${album.id}/info`, data => {
data && this.merge(album, data)
http.get(`album/${album.id}/info`, response => {
response.data && this.merge(album, response.data)
resolve(album)
}, r => reject(r))
}, error => reject(error))
})
},

View file

@ -13,10 +13,10 @@ export const artistInfo = {
return
}
http.get(`artist/${artist.id}/info`, data => {
data && this.merge(artist, data)
http.get(`artist/${artist.id}/info`, response => {
response.data && this.merge(artist, response.data)
resolve(artist)
}, r => reject(r))
}, error => reject(error))
})
},

View file

@ -14,14 +14,14 @@ export const songInfo = {
return
}
http.get(`${song.id}/info`, data => {
song.lyrics = data.lyrics
data.artist_info && artistInfo.merge(song.artist, data.artist_info)
data.album_info && albumInfo.merge(song.album, data.album_info)
song.youtube = data.youtube
http.get(`${song.id}/info`, response => {
song.lyrics = response.data.lyrics
response.data.artist_info && artistInfo.merge(song.artist, response.data.artist_info)
response.data.album_info && albumInfo.merge(song.album, response.data.album_info)
song.youtube = response.data.youtube
song.infoRetrieved = true
resolve(song)
}, r => reject(r))
}, error => reject(error))
})
}
}

View file

@ -1,5 +1,4 @@
import { shuffle, orderBy } from 'lodash'
import $ from 'jquery'
import plyr from 'plyr'
import Vue from 'vue'
@ -10,7 +9,7 @@ import router from '../router'
export const playback = {
player: null,
$volumeInput: null,
volumeInput: null,
repeatModes: ['NO_REPEAT', 'REPEAT_ALL', 'REPEAT_ONE'],
initialized: false,
@ -27,9 +26,8 @@ export const playback = {
controls: []
})[0]
this.audio = $('audio')
this.$volumeInput = $('#volumeRange')
this.audio = document.querySelector('audio')
this.volumeInput = document.getElementById('volumeRange')
/**
* Listen to 'error' event on the audio player and play the next song if any.
@ -69,8 +67,8 @@ export const playback = {
return
}
const $preloader = $('<audio>')
$preloader.attr('src', songStore.getSourceUrl(nextSong))
const preloader = document.createElement('audio')
preloader.setAttribute('src', songStore.getSourceUrl(nextSong))
nextSong.preloaded = true
})
@ -80,8 +78,8 @@ export const playback = {
* When user drags the volume control, this event will be triggered, and we
* update the volume on the plyr object.
*/
this.$volumeInput.on('input', e => {
this.setVolume($(e.target).val())
this.volumeInput.addEventListener('input', e => {
this.setVolume(e.target.value)
})
// On init, set the volume to the value found in the local storage.
@ -124,8 +122,8 @@ export const playback = {
// the audio media object and cause our equalizer to malfunction.
this.player.media.src = songStore.getSourceUrl(song)
$('title').text(`${song.title}${config.appTitle}`)
$('.plyr audio').attr('title', `${song.artist.name} - ${song.title}`)
document.title = `${song.title}${config.appTitle}`
document.querySelector('.plyr audio').setAttribute('title', `${song.artist.name} - ${song.title}`)
// We'll just "restart" playing the song, which will handle notification, scrobbling etc.
this.restart()
@ -273,7 +271,7 @@ export const playback = {
preferences.volume = volume
}
this.$volumeInput.val(volume)
this.volumeInput.value = volume
},
/**
@ -299,7 +297,7 @@ export const playback = {
* Completely stop playback.
*/
stop () {
$('title').text(config.appTitle)
document.title = config.appTitle
this.player.pause()
this.player.seek(0)

View file

@ -7,18 +7,19 @@ export const youtube = {
* Search for YouTube videos related to a song.
*
* @param {Object} song
* @param {Function} cb
*/
searchVideosRelatedToSong (song, cb = null) {
searchVideosRelatedToSong (song) {
if (!song.youtube) {
song.youtube = {}
}
const pageToken = song.youtube.nextPageToken || ''
http.get(`youtube/search/song/${song.id}?pageToken=${pageToken}`).then(data => {
song.youtube.nextPageToken = data.nextPageToken
song.youtube.items.push(...data.items)
cb && cb()
return new Promise((resolve, reject) => {
http.get(`youtube/search/song/${song.id}?pageToken=${pageToken}`, response => {
song.youtube.nextPageToken = response.data.nextPageToken
song.youtube.items.push(...response.data.items)
resolve()
}, error => reject(error))
})
},

View file

@ -44,10 +44,10 @@ export const favoriteStore = {
NProgress.start()
return new Promise((resolve, reject) => {
http.post('interaction/like', { song: song.id }, data => {
http.post('interaction/like', { song: song.id }, response => {
// We don't really need to notify just for one song.
resolve(data)
}, r => reject(r))
resolve(response.data)
}, error => reject(error))
})
},
@ -92,10 +92,10 @@ export const favoriteStore = {
NProgress.start()
return new Promise((resolve, reject) => {
http.post('interaction/batch/like', { songs: map(songs, 'id') }, data => {
http.post('interaction/batch/like', { songs: map(songs, 'id') }, response => {
alerts.success(`Added ${pluralize(songs.length, 'song')} into Favorites.`)
resolve(data)
}, r => reject(r))
resolve(response.data)
}, error => reject(error))
})
},
@ -113,10 +113,10 @@ export const favoriteStore = {
NProgress.start()
return new Promise((resolve, reject) => {
http.post('interaction/batch/unlike', { songs: map(songs, 'id') }, data => {
http.post('interaction/batch/unlike', { songs: map(songs, 'id') }, response => {
alerts.success(`Removed ${pluralize(songs.length, 'song')} from Favorites.`)
resolve(data)
}, r => reject(r))
resolve(response.data)
}, error => reject(error))
})
}
}

View file

@ -101,13 +101,14 @@ export const playlistStore = {
NProgress.start()
return new Promise((resolve, reject) => {
http.post('playlist', { name, songs }, playlist => {
http.post('playlist', { name, songs }, response => {
const playlist = response.data
playlist.songs = songs
this.objectifySongs(playlist)
this.add(playlist)
alerts.success(`Created playlist &quot;${playlist.name}&quot;.`)
resolve(playlist)
}, r => reject(r))
}, error => reject(error))
})
},
@ -120,11 +121,11 @@ export const playlistStore = {
NProgress.start()
return new Promise((resolve, reject) => {
http.delete(`playlist/${playlist.id}`, {}, data => {
http.delete(`playlist/${playlist.id}`, {}, response => {
this.remove(playlist)
alerts.success(`Deleted playlist &quot;${playlist.name}&quot;.`)
resolve(data)
}, r => reject(r))
resolve(response.data)
}, error => reject(error))
})
},
@ -146,13 +147,10 @@ export const playlistStore = {
NProgress.start()
http.put(`playlist/${playlist.id}/sync`, { songs: map(playlist.songs, 'id') },
data => {
alerts.success(`Added ${pluralize(songs.length, 'song')} into &quot;${playlist.name}&quot;.`)
resolve(playlist)
},
r => reject(r)
)
http.put(`playlist/${playlist.id}/sync`, { songs: map(playlist.songs, 'id') }, () => {
alerts.success(`Added ${pluralize(songs.length, 'song')} into &quot;${playlist.name}&quot;.`)
resolve(playlist)
}, error => reject(error))
})
},
@ -168,13 +166,10 @@ export const playlistStore = {
playlist.songs = difference(playlist.songs, songs)
return new Promise((resolve, reject) => {
http.put(`playlist/${playlist.id}/sync`, { songs: map(playlist.songs, 'id') },
data => {
alerts.success(`Removed ${pluralize(songs.length, 'song')} from &quot;${playlist.name}&quot;.`)
resolve(playlist)
},
r => reject(r)
)
http.put(`playlist/${playlist.id}/sync`, { songs: map(playlist.songs, 'id') }, () => {
alerts.success(`Removed ${pluralize(songs.length, 'song')} from &quot;${playlist.name}&quot;.`)
resolve(playlist)
}, error => reject(error))
})
},
@ -187,7 +182,7 @@ export const playlistStore = {
NProgress.start()
return new Promise((resolve, reject) => {
http.put(`playlist/${playlist.id}`, { name: playlist.name }, data => resolve(playlist), r => reject(r))
http.put(`playlist/${playlist.id}`, { name: playlist.name }, () => resolve(playlist), error => reject(error))
})
}
}

View file

@ -19,10 +19,10 @@ export const settingStore = {
update () {
return new Promise((resolve, reject) => {
http.post('settings', this.all, data => {
http.post('settings', this.all, response => {
alerts.success('Settings saved.')
resolve(data)
}, r => reject(r))
resolve(response.data)
}, error => reject(error))
})
}
}

View file

@ -30,11 +30,10 @@ export const sharedStore = {
this.reset()
return new Promise((resolve, reject) => {
http.get('data', data => {
http.get('data', response => {
assign(this.state, response.data)
// Don't allow downloading on mobile devices
data.allowDownload = data.allowDownload && !isMobile.any
assign(this.state, data)
this.state.allowDownload = this.state.allowDownload && !isMobile.any
// Always disable YouTube integration on mobile.
this.state.useYouTube = this.state.useYouTube && !isMobile.phone
@ -55,8 +54,8 @@ export const sharedStore = {
// Keep a copy of the media path. We'll need this to properly warn the user later.
this.state.originalMediaPath = this.state.settings.media_path
resolve(data)
}, r => reject(r))
resolve(this.state)
}, error => reject(error))
})
},

View file

@ -180,14 +180,14 @@ export const songStore = {
return new Promise((resolve, reject) => {
const oldCount = song.playCount
http.post('interaction/play', { song: song.id }, data => {
http.post('interaction/play', { song: song.id }, response => {
// Use the data from the server to make sure we don't miss a play from another device.
song.playCount = data.play_count
song.playCount = response.data.play_count
song.album.playCount += song.playCount - oldCount
song.artist.playCount += song.playCount - oldCount
resolve(data)
}, r => reject(r))
resolve(response.data)
}, error => reject(error))
})
},
@ -215,7 +215,9 @@ export const songStore = {
*/
scrobble (song) {
return new Promise((resolve, reject) => {
http.post(`${song.id}/scrobble/${song.playStartTime}`, {}, data => resolve(data), r => reject(r))
http.post(`${song.id}/scrobble/${song.playStartTime}`, {}, response => {
resolve(data.response)
}, error => reject(error))
})
},
@ -230,11 +232,12 @@ export const songStore = {
http.put('songs', {
data,
songs: map(songs, 'id')
}, songs => {
}, response => {
const songs = response.data
each(songs, song => this.syncUpdatedSong(song))
alerts.success(`Updated ${pluralize(songs.length, 'song')}.`)
resolve(songs)
}, r => reject(r))
}, error => reject(error))
})
},

View file

@ -105,7 +105,9 @@ export const userStore = {
NProgress.start()
return new Promise((resolve, reject) => {
http.post('me', { email, password }, data => resolve(data), r => reject(r))
http.post('me', { email, password }, response => {
resolve(response.data)
}, error => reject(error))
})
},
@ -114,7 +116,9 @@ export const userStore = {
*/
logout () {
return new Promise((resolve, reject) => {
http.delete('me', {}, data => resolve(data), r => reject(r))
http.delete('me', {}, response => {
resolve(response.data)
}, error => reject(error))
})
},
@ -136,7 +140,7 @@ export const userStore = {
alerts.success('Profile updated.')
resolve(this.current)
},
r => reject(r))
error => reject(error))
})
},
@ -151,12 +155,13 @@ export const userStore = {
NProgress.start()
return new Promise((resolve, reject) => {
http.post('user', { name, email, password }, user => {
http.post('user', { name, email, password }, response => {
const user = response.data
this.setAvatar(user)
this.all.unshift(user)
alerts.success(`New user &quot;${name}&quot; created.`)
resolve(user)
}, r => reject(r))
}, error => reject(error))
})
},
@ -179,7 +184,7 @@ export const userStore = {
user.password = ''
alerts.success('User profile updated.')
resolve(user)
}, r => reject(r))
}, error => reject(error))
})
},
@ -192,7 +197,7 @@ export const userStore = {
NProgress.start()
return new Promise((resolve, reject) => {
http.delete(`user/${user.id}`, {}, data => {
http.delete(`user/${user.id}`, {}, () => {
this.all = without(this.all, user)
alerts.success(`User &quot;${user.name}&quot; deleted.`)
@ -218,8 +223,8 @@ export const userStore = {
/**
* Brian May enters the stage.
*/
resolve(data)
}, r => reject(r))
resolve(response.data)
}, error => reject(error))
})
}
}

View file

@ -0,0 +1,49 @@
export const $ = {
is (el, selector) {
return (el.matches ||
el.matchesSelector ||
el.msMatchesSelector ||
el.mozMatchesSelector ||
el.webkitMatchesSelector ||
el.oMatchesSelector).call(el, selector)
},
addClass (el, className) {
if (!el) {
return
}
if (el.classList) {
el.classList.add(className)
} else {
el.className += ` ${className}`
}
},
removeClass (el, className) {
if (!el) {
return
}
if (el.classList) {
el.classList.remove(className)
} else {
el.className = el.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' ')
}
},
scrollTo (el, to, duration) {
if (duration <= 0 || !el) {
return
}
const difference = to - el.scrollTop
const perTick = difference / duration * 10
window.setTimeout(() => {
el.scrollTop = el.scrollTop + perTick
if (el.scrollTop === to) {
return
}
this.scrollTo(el, to, duration - 10);
}, 10)
}
}

View file

@ -3,3 +3,4 @@ export * from './filters'
export * from './formatters'
export * from './supports'
export * from './common'
export * from './$'

View file

@ -279,6 +279,7 @@ class SongTest extends TestCase
'compilationState' => 1,
],
])
->put('/api/songs', [
'songs' => [$song->id],
'data' => [

734
yarn.lock

File diff suppressed because it is too large Load diff