Lint everything

This commit is contained in:
An Phan 2016-11-26 10:25:35 +07:00
parent 449fac97fc
commit adfebd0167
No known key found for this signature in database
GPG key ID: 05536BB4BCDC02A2
104 changed files with 2770 additions and 2764 deletions

2
.eslintignore Normal file
View file

@ -0,0 +1,2 @@
libs
tests

6
.eslintrc Normal file
View file

@ -0,0 +1,6 @@
{
"extends": "vue",
"rules": {
"no-multi-str": "off"
}
}

View file

@ -50,13 +50,16 @@
"browserify-hmr": "^0.3.1", "browserify-hmr": "^0.3.1",
"chai": "^3.4.1", "chai": "^3.4.1",
"chalk": "^1.1.3", "chalk": "^1.1.3",
"eslint": "^3.10.2",
"eslint-config-vue": "^2.0.1",
"eslint-plugin-vue": "^1.0.0",
"jsdom": "^9.2.1", "jsdom": "^9.2.1",
"mocha": "^2.3.4", "mocha": "^2.3.4",
"sinon": "^1.17.2" "sinon": "^1.17.2"
}, },
"scripts": { "scripts": {
"postinstall": "cross-env NODE_ENV=production && gulp --production", "postinstall": "cross-env NODE_ENV=production && gulp --production",
"test": "mocha --compilers js:babel-register --require resources/assets/js/tests/helper.js resources/assets/js/tests/**/*Test.js", "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" "e2e": "gulp e2e"
} }
} }

View file

@ -23,77 +23,79 @@
</template> </template>
<script> <script>
import Vue from 'vue'; import Vue from 'vue'
import $ from 'jquery'; import $ from 'jquery'
import siteHeader from './components/site-header/index.vue'; import siteHeader from './components/site-header/index.vue'
import siteFooter from './components/site-footer/index.vue'; import siteFooter from './components/site-footer/index.vue'
import mainWrapper from './components/main-wrapper/index.vue'; import mainWrapper from './components/main-wrapper/index.vue'
import overlay from './components/shared/overlay.vue'; import overlay from './components/shared/overlay.vue'
import loginForm from './components/auth/login-form.vue'; import loginForm from './components/auth/login-form.vue'
import editSongsForm from './components/modals/edit-songs-form.vue'; import editSongsForm from './components/modals/edit-songs-form.vue'
import { event, showOverlay, hideOverlay, forceReloadWindow, url } from './utils'; import { event, showOverlay, hideOverlay, forceReloadWindow } from './utils'
import { sharedStore, songStore, userStore, preferenceStore as preferences } from './stores'; import { sharedStore, userStore, preferenceStore as preferences } from './stores'
import { playback, ls } from './services'; import { playback, ls } from './services'
import { focusDirective, clickawayDirective } from './directives'; import { focusDirective, clickawayDirective } from './directives'
import router from './router'; import router from './router'
export default { export default {
components: { siteHeader, siteFooter, mainWrapper, overlay, loginForm, editSongsForm }, components: { siteHeader, siteFooter, mainWrapper, overlay, loginForm, editSongsForm },
data () { data () {
return { return {
authenticated: false, authenticated: false
}; }
}, },
mounted () { mounted () {
// The app has just been initialized, check if we can get the user data with an already existing token // The app has just been initialized, check if we can get the user data with an already existing token
const token = ls.get('jwt-token'); const token = ls.get('jwt-token')
if (token) { if (token) {
this.authenticated = true; this.authenticated = true
this.init(); this.init()
} }
// Create the element to be the ghost drag image. // Create the element to be the ghost drag image.
$('<div id="dragGhost"></div>').appendTo('body'); $('<div id="dragGhost"></div>').appendTo('body')
// And the textarea to copy stuff // And the textarea to copy stuff
$('<textarea id="copyArea"></textarea>').appendTo('body'); $('<textarea id="copyArea"></textarea>').appendTo('body')
// Add an ugly mac/non-mac class for OS-targeting styles. // Add an ugly mac/non-mac class for OS-targeting styles.
// I'm crying inside. // I'm crying inside.
$('html').addClass(navigator.userAgent.indexOf('Mac') !== -1 ? 'mac' : 'non-mac'); $('html').addClass(navigator.userAgent.indexOf('Mac') !== -1 ? 'mac' : 'non-mac')
}, },
methods: { methods: {
init () { init () {
showOverlay(); showOverlay()
// Make the most important HTTP request to get all necessary data from the server. // Make the most important HTTP request to get all necessary data from the server.
// Afterwards, init all mandatory stores and services. // Afterwards, init all mandatory stores and services.
sharedStore.init().then(() => { sharedStore.init().then(() => {
playback.init(); playback.init()
hideOverlay(); hideOverlay()
// Ask for user's notification permission. // Ask for user's notification permission.
this.requestNotifPermission(); this.requestNotifPermission()
// To confirm or not to confirm closing, it's a question. // To confirm or not to confirm closing, it's a question.
window.onbeforeunload = e => { window.onbeforeunload = e => {
if (!preferences.confirmClosing) { if (!preferences.confirmClosing) {
return; return
} }
// Notice that a custom message like this has ceased to be supported // Notice that a custom message like this has ceased to be supported
// starting from Chrome 51. // starting from Chrome 51.
return 'You asked Koel to confirm before closing, so here it is.'; return 'You asked Koel to confirm before closing, so here it is.'
}; }
// Let all other components know we're ready. // Let all other components know we're ready.
event.emit('koel:ready'); event.emit('koel:ready')
}).catch(() => this.authenticated = false); }).catch(() => {
this.authenticated = false
})
}, },
/** /**
@ -103,12 +105,12 @@ export default {
*/ */
togglePlayback (e) { togglePlayback (e) {
if ($(e.target).is('input,textarea,button,select')) { if ($(e.target).is('input,textarea,button,select')) {
return true; return true
} }
// Ah... Good ol' jQuery. Whatever play/pause control is there, we blindly click it. // Ah... Good ol' jQuery. Whatever play/pause control is there, we blindly click it.
$('#mainFooter .play:visible, #mainFooter .pause:visible').click(); $('#mainFooter .play:visible, #mainFooter .pause:visible').click()
e.preventDefault(); e.preventDefault()
}, },
/** /**
@ -118,11 +120,11 @@ export default {
*/ */
playPrev (e) { playPrev (e) {
if ($(e.target).is('input,textarea')) { if ($(e.target).is('input,textarea')) {
return true; return true
} }
playback.playPrev(); playback.playPrev()
e.preventDefault(); e.preventDefault()
}, },
/** /**
@ -132,11 +134,11 @@ export default {
*/ */
playNext (e) { playNext (e) {
if ($(e.target).is('input,textarea')) { if ($(e.target).is('input,textarea')) {
return true; return true
} }
playback.playNext(); playback.playNext()
e.preventDefault(); e.preventDefault()
}, },
/** /**
@ -146,25 +148,25 @@ export default {
*/ */
search (e) { search (e) {
if ($(e.target).is('input,textarea') || e.metaKey || e.ctrlKey) { if ($(e.target).is('input,textarea') || e.metaKey || e.ctrlKey) {
return true; return true
} }
$('#searchForm input[type="search"]').focus().select(); $('#searchForm input[type="search"]').focus().select()
e.preventDefault(); e.preventDefault()
}, },
/** /**
* Request for notification permission if it's not provided and the user is OK with notifs. * Request for notification permission if it's not provided and the user is OK with notifs.
*/ */
requestNotifPermission () { requestNotifPermission () {
if (window.Notification && preferences.notify && Notification.permission !== 'granted') { if (window.Notification && preferences.notify && window.Notification.permission !== 'granted') {
Notification.requestPermission(result => { window.Notification.requestPermission(result => {
if (result === 'denied') { if (result === 'denied') {
preferences.notify = false; preferences.notify = false
}
})
} }
});
} }
},
}, },
created () { created () {
@ -173,8 +175,8 @@ export default {
* When the user logs in, set the whole app to be "authenticated" and initialize it. * When the user logs in, set the whole app to be "authenticated" and initialize it.
*/ */
'user:loggedin': () => { 'user:loggedin': () => {
this.authenticated = true; this.authenticated = true
this.init(); this.init()
}, },
/** /**
@ -189,20 +191,20 @@ export default {
*/ */
logout () { logout () {
userStore.logout().then((r) => { userStore.logout().then((r) => {
ls.remove('jwt-token'); ls.remove('jwt-token')
forceReloadWindow(); forceReloadWindow()
}); })
}, },
/** /**
* Init our basic, custom router on ready to determine app state. * Init our basic, custom router on ready to determine app state.
*/ */
'koel:ready': () => { 'koel:ready': () => {
router.init(); router.init()
}, }
}); })
}, }
}; }
// Register our custom key codes // Register our custom key codes
Vue.config.keyCodes = { Vue.config.keyCodes = {
@ -213,11 +215,11 @@ Vue.config.keyCodes = {
mediaNext: 176, mediaNext: 176,
mediaPrev: 177, mediaPrev: 177,
mediaToggle: 179 mediaToggle: 179
}; }
// and the global directives // and the global directives
Vue.directive('koel-focus', focusDirective); Vue.directive('koel-focus', focusDirective)
Vue.directive('koel-clickaway',clickawayDirective); Vue.directive('koel-clickaway', clickawayDirective)
</script> </script>
<style lang="sass"> <style lang="sass">

View file

@ -7,33 +7,35 @@
</template> </template>
<script> <script>
import { userStore } from '../../stores'; import { userStore } from '../../stores'
import { event } from '../../utils'; import { event } from '../../utils'
export default { export default {
data () { data () {
return { return {
email: '', email: '',
password: '', password: '',
failed: false, failed: false
}; }
}, },
methods: { methods: {
login () { login () {
this.failed = false; this.failed = false
userStore.login(this.email, this.password).then(() => { userStore.login(this.email, this.password).then(() => {
this.failed = false; this.failed = false
// Reset the password so that the next login will have this field empty. // Reset the password so that the next login will have this field empty.
this.password = ''; this.password = ''
event.emit('user:loggedin'); event.emit('user:loggedin')
}).catch(() => this.failed = true); }).catch(() => {
}, this.failed = true
}, })
}; }
}
}
</script> </script>
<style lang="sass" scoped> <style lang="sass" scoped>

View file

@ -37,31 +37,31 @@
</template> </template>
<script> <script>
import { playback } from '../../../services'; import { playback } from '../../../services'
export default { export default {
props: ['album', 'mode'], props: ['album', 'mode'],
data () { data () {
return { return {
showingFullWiki: false, showingFullWiki: false
}; }
}, },
watch: { watch: {
album () { album () {
this.showingFullWiki = false; this.showingFullWiki = false
}, }
}, },
computed: { computed: {
showSummary () { showSummary () {
return this.mode !== 'full' && !this.showingFullWiki; return this.mode !== 'full' && !this.showingFullWiki
}, },
showFull () { showFull () {
return this.mode === 'full' || this.showingFullWiki; return this.mode === 'full' || this.showingFullWiki
}, }
}, },
methods: { methods: {
@ -69,10 +69,10 @@ export default {
* Shuffle all songs in the current album. * Shuffle all songs in the current album.
*/ */
shuffleAll () { shuffleAll () {
playback.playAllInAlbum(this.album); playback.playAllInAlbum(this.album)
}, }
}, }
}; }
</script> </script>
<style lang="sass"> <style lang="sass">

View file

@ -29,30 +29,30 @@
</template> </template>
<script> <script>
import { playback } from '../../../services'; import { playback } from '../../../services'
export default { export default {
props: ['artist', 'mode'], props: ['artist', 'mode'],
data () { data () {
return { return {
showingFullBio: false, showingFullBio: false
}; }
}, },
watch: { watch: {
artist () { artist () {
this.showingFullBio = false; this.showingFullBio = false
}, }
}, },
computed: { computed: {
showSummary () { showSummary () {
return this.mode !== 'full' && !this.showingFullBio; return this.mode !== 'full' && !this.showingFullBio
}, },
showFull () { showFull () {
return this.mode === 'full' || this.showingFullBio; return this.mode === 'full' || this.showingFullBio
} }
}, },
@ -61,10 +61,10 @@ export default {
* Shuffle all songs performed by the current song's artist. * Shuffle all songs performed by the current song's artist.
*/ */
shuffleAll () { shuffleAll () {
playback.playAllByArtist(this.artist); playback.playAllByArtist(this.artist)
}, }
}, }
}; }
</script> </script>
<style lang="sass"> <style lang="sass">

View file

@ -35,18 +35,17 @@
</template> </template>
<script> <script>
import isMobile from 'ismobilejs'; import isMobile from 'ismobilejs'
import { invokeMap } from 'lodash'; import $ from 'jquery'
import $ from 'jquery';
import { event } from '../../../utils'; import { event } from '../../../utils'
import { sharedStore, songStore, preferenceStore as preferences } from '../../../stores'; import { sharedStore, songStore, preferenceStore as preferences } from '../../../stores'
import { songInfo } from '../../../services'; import { songInfo } from '../../../services'
import lyrics from './lyrics.vue'; import lyrics from './lyrics.vue'
import artistInfo from './artist-info.vue'; import artistInfo from './artist-info.vue'
import albumInfo from './album-info.vue'; import albumInfo from './album-info.vue'
import youtube from './youtube.vue'; import youtube from './youtube.vue'
export default { export default {
name: 'main-wrapper--extra--index', name: 'main-wrapper--extra--index',
@ -57,8 +56,8 @@ export default {
song: songStore.stub, song: songStore.stub,
state: preferences.state, state: preferences.state,
sharedState: sharedStore.state, sharedState: sharedStore.state,
currentView: 'lyrics', currentView: 'lyrics'
}; }
}, },
watch: { watch: {
@ -67,25 +66,25 @@ export default {
* to/from the html tag. * to/from the html tag.
* Some element's CSS can then be controlled based on this class. * Some element's CSS can then be controlled based on this class.
*/ */
'state.showExtraPanel': function (newVal) { 'state.showExtraPanel' (newVal) {
if (newVal && !isMobile.any) { if (newVal && !isMobile.any) {
$('html').addClass('with-extra-panel'); $('html').addClass('with-extra-panel')
} else { } else {
$('html').removeClass('with-extra-panel'); $('html').removeClass('with-extra-panel')
}
} }
},
}, },
mounted () { mounted () {
// On ready, add 'with-extra-panel' class. // On ready, add 'with-extra-panel' class.
if (!isMobile.any) { if (!isMobile.any) {
$('html').addClass('with-extra-panel'); $('html').addClass('with-extra-panel')
} }
if (isMobile.phone) { if (isMobile.phone) {
// On a mobile device, we always hide the panel initially regardless of // On a mobile device, we always hide the panel initially regardless of
// the saved preference. // the saved preference.
preferences.showExtraPanel = false; preferences.showExtraPanel = false
} }
}, },
@ -94,9 +93,9 @@ export default {
* Reset all self and applicable child components' states. * Reset all self and applicable child components' states.
*/ */
resetState () { resetState () {
this.currentView = 'lyrics'; this.currentView = 'lyrics'
this.song = songStore.stub; this.song = songStore.stub
}, }
}, },
created () { created () {
@ -104,20 +103,20 @@ export default {
'main-content-view:load': view => { 'main-content-view:load': view => {
// Hide the panel away if a main view is triggered on mobile. // Hide the panel away if a main view is triggered on mobile.
if (isMobile.phone) { if (isMobile.phone) {
preferences.showExtraPanel = false; preferences.showExtraPanel = false
} }
}, },
'song:played': song => { 'song:played': song => {
songInfo.fetch(song).then(song => { songInfo.fetch(song).then(song => {
this.song = song; this.song = song
}); })
}, },
'koel:teardown': () => this.resetState(), 'koel:teardown': () => this.resetState()
}); })
}, }
}; }
</script> </script>
<style lang="sass"> <style lang="sass">

View file

@ -9,8 +9,8 @@
<script> <script>
export default { export default {
props: ['song'], props: ['song']
}; }
</script> </script>
<style lang="sass"> <style lang="sass">

View file

@ -18,8 +18,7 @@
</template> </template>
<script> <script>
import { event } from '../../../utils'; import { youtube as youtubeService } from '../../../services'
import { youtube as youtubeService } from '../../../services';
export default { export default {
name: 'main-wrapper--extra--youtube', name: 'main-wrapper--extra--youtube',
@ -28,33 +27,33 @@ export default {
data () { data () {
return { return {
loading: false, loading: false,
videos: [], videos: []
}; }
}, },
watch: { watch: {
song (val) { song (val) {
this.videos = val.youtube ? val.youtube.items : []; this.videos = val.youtube ? val.youtube.items : []
}, }
}, },
methods: { methods: {
playYouTube (id) { playYouTube (id) {
youtubeService.play(id); youtubeService.play(id)
}, },
/** /**
* Load more videos. * Load more videos.
*/ */
loadMore () { loadMore () {
this.loading = true; this.loading = true
youtubeService.searchVideosRelatedToSong(this.song, () => { youtubeService.searchVideosRelatedToSong(this.song, () => {
this.videos = this.song.youtube.items; this.videos = this.song.youtube.items
this.loading = false; this.loading = false
}); })
}, }
}, }
}; }
</script> </script>
<style lang="sass" scoped> <style lang="sass" scoped>

View file

@ -7,13 +7,13 @@
</template> </template>
<script> <script>
import sidebar from './sidebar/index.vue'; import sidebar from './sidebar/index.vue'
import mainContent from './main-content/index.vue'; import mainContent from './main-content/index.vue'
import extra from './extra/index.vue'; import extra from './extra/index.vue'
export default { export default {
components: { sidebar, mainContent, extra }, components: { sidebar, mainContent, extra }
}; }
</script> </script>
<style lang="sass"> <style lang="sass">

View file

@ -46,15 +46,13 @@
</template> </template>
<script> <script>
import isMobile from 'ismobilejs'; import { pluralize, event } from '../../../utils'
import { albumStore, artistStore, sharedStore } from '../../../stores'
import { pluralize, event } from '../../../utils'; import { playback, download, albumInfo as albumInfoService } from '../../../services'
import { albumStore, artistStore, sharedStore } from '../../../stores'; import router from '../../../router'
import { playback, download, albumInfo as albumInfoService } from '../../../services'; import hasSongList from '../../../mixins/has-song-list'
import router from '../../../router'; import albumInfo from '../extra/album-info.vue'
import hasSongList from '../../../mixins/has-song-list'; import soundBar from '../../shared/sound-bar.vue'
import albumInfo from '../extra/album-info.vue';
import soundBar from '../../shared/sound-bar.vue';
export default { export default {
name: 'main-wrapper--main-content--album', name: 'main-wrapper--main-content--album',
@ -68,16 +66,16 @@ export default {
album: albumStore.stub, album: albumStore.stub,
info: { info: {
showing: false, showing: false,
loading: true, loading: true
}, }
}; }
}, },
computed: { computed: {
isNormalArtist () { isNormalArtist () {
return !artistStore.isVariousArtists(this.album.artist) return !artistStore.isVariousArtists(this.album.artist) &&
&& !artistStore.isUnknownArtist(this.album.artist); !artistStore.isUnknownArtist(this.album.artist)
}, }
}, },
watch: { watch: {
@ -89,9 +87,9 @@ export default {
*/ */
'album.songs.length' (newVal) { 'album.songs.length' (newVal) {
if (!newVal) { if (!newVal) {
router.go('albums'); router.go('albums')
}
} }
},
}, },
created () { created () {
@ -104,10 +102,10 @@ export default {
*/ */
event.on('main-content-view:load', (view, album) => { event.on('main-content-view:load', (view, album) => {
if (view === 'album') { if (view === 'album') {
this.info.showing = false; this.info.showing = false
this.album = album; this.album = album
} }
}); })
}, },
methods: { methods: {
@ -115,27 +113,29 @@ export default {
* Shuffle the songs in the current album. * Shuffle the songs in the current album.
*/ */
shuffle () { shuffle () {
playback.queueAndPlay(this.album.songs, true); playback.queueAndPlay(this.album.songs, true)
}, },
/** /**
* Download all songs from the album. * Download all songs from the album.
*/ */
download () { download () {
download.fromAlbum(this.album); download.fromAlbum(this.album)
}, },
showInfo () { showInfo () {
this.info.showing = true; this.info.showing = true
if (!this.album.info) { if (!this.album.info) {
this.info.loading = true; this.info.loading = true
albumInfoService.fetch(this.album).then(() => this.info.loading = false); albumInfoService.fetch(this.album).then(() => {
this.info.loading = false
})
} else { } else {
this.info.loading = false; this.info.loading = false
}
}
}
} }
},
},
};
</script> </script>
<style lang="sass" scoped> <style lang="sass" scoped>

View file

@ -14,11 +14,11 @@
</template> </template>
<script> <script>
import { filterBy, limitBy, event } from '../../../utils'; import { filterBy, limitBy, event } from '../../../utils'
import { albumStore } from '../../../stores'; import { albumStore } from '../../../stores'
import albumItem from '../../shared/album-item.vue'; import albumItem from '../../shared/album-item.vue'
import viewModeSwitch from '../../shared/view-mode-switch.vue'; import viewModeSwitch from '../../shared/view-mode-switch.vue'
import infiniteScroll from '../../../mixins/infinite-scroll'; import infiniteScroll from '../../../mixins/infinite-scroll'
export default { export default {
mixins: [infiniteScroll], mixins: [infiniteScroll],
@ -29,8 +29,8 @@ export default {
perPage: 9, perPage: 9,
numOfItems: 9, numOfItems: 9,
q: '', q: '',
viewMode: null, viewMode: null
}; }
}, },
computed: { computed: {
@ -38,14 +38,14 @@ export default {
return limitBy( return limitBy(
filterBy(albumStore.all, this.q, 'name', 'artist.name'), filterBy(albumStore.all, this.q, 'name', 'artist.name'),
this.numOfItems this.numOfItems
); )
}, }
}, },
methods: { methods: {
changeViewMode (mode) { changeViewMode (mode) {
this.viewMode = mode; this.viewMode = mode
}, }
}, },
created () { created () {
@ -56,14 +56,16 @@ export default {
'koel:ready': () => this.displayMore(), 'koel:ready': () => this.displayMore(),
'koel:teardown': () => { 'koel:teardown': () => {
this.q = ''; this.q = ''
this.numOfItems = 9; this.numOfItems = 9
}, },
'filter:changed': q => this.q = q, 'filter:changed': q => {
}); this.q = q
}, }
}; })
}
}
</script> </script>
<style lang="sass"> <style lang="sass">

View file

@ -45,15 +45,13 @@
</template> </template>
<script> <script>
import isMobile from 'ismobilejs'; import { pluralize, event } from '../../../utils'
import { sharedStore, artistStore } from '../../../stores'
import { pluralize, event } from '../../../utils'; import { playback, download, artistInfo as artistInfoService } from '../../../services'
import { sharedStore, artistStore } from '../../../stores'; import router from '../../../router'
import { playback, download, artistInfo as artistInfoService } from '../../../services'; import hasSongList from '../../../mixins/has-song-list'
import router from '../../../router'; import artistInfo from '../extra/artist-info.vue'
import hasSongList from '../../../mixins/has-song-list'; import soundBar from '../../shared/sound-bar.vue'
import artistInfo from '../extra/artist-info.vue';
import soundBar from '../../shared/sound-bar.vue';
export default { export default {
name: 'main-wrapper--main-content--artist', name: 'main-wrapper--main-content--artist',
@ -67,9 +65,9 @@ export default {
artist: artistStore.stub, artist: artistStore.stub,
info: { info: {
showing: false, showing: false,
loading: true, loading: true
}, }
}; }
}, },
watch: { watch: {
@ -81,9 +79,9 @@ export default {
*/ */
'artist.albums.length' (newVal) { 'artist.albums.length' (newVal) {
if (!newVal) { if (!newVal) {
router.go('artists'); router.go('artists')
}
} }
},
}, },
created () { created () {
@ -96,10 +94,10 @@ export default {
*/ */
event.on('main-content-view:load', (view, artist) => { event.on('main-content-view:load', (view, artist) => {
if (view === 'artist') { if (view === 'artist') {
this.info.showing = false; this.info.showing = false
this.artist = artist; this.artist = artist
} }
}); })
}, },
methods: { methods: {
@ -107,27 +105,29 @@ export default {
* Shuffle the songs by the current artist. * Shuffle the songs by the current artist.
*/ */
shuffle () { shuffle () {
playback.queueAndPlay(this.artist.songs, true); playback.queueAndPlay(this.artist.songs, true)
}, },
/** /**
* Download all songs by the artist. * Download all songs by the artist.
*/ */
download () { download () {
download.fromArtist(this.artist); download.fromArtist(this.artist)
}, },
showInfo () { showInfo () {
this.info.showing = true; this.info.showing = true
if (!this.artist.info) { if (!this.artist.info) {
this.info.loading = true; this.info.loading = true
artistInfoService.fetch(this.artist).then(() => this.info.loading = false); artistInfoService.fetch(this.artist).then(() => {
this.info.loading = false
})
} else { } else {
this.info.loading = false; this.info.loading = false
}
}
}
} }
},
},
};
</script> </script>
<style lang="sass" scoped> <style lang="sass" scoped>

View file

@ -14,12 +14,12 @@
</template> </template>
<script> <script>
import { filterBy, limitBy, event } from '../../../utils'; import { filterBy, limitBy, event } from '../../../utils'
import { artistStore } from '../../../stores'; import { artistStore } from '../../../stores'
import artistItem from '../../shared/artist-item.vue'; import artistItem from '../../shared/artist-item.vue'
import viewModeSwitch from '../../shared/view-mode-switch.vue'; import viewModeSwitch from '../../shared/view-mode-switch.vue'
import infiniteScroll from '../../../mixins/infinite-scroll'; import infiniteScroll from '../../../mixins/infinite-scroll'
export default { export default {
mixins: [infiniteScroll], mixins: [infiniteScroll],
@ -31,8 +31,8 @@ export default {
perPage: 9, perPage: 9,
numOfItems: 9, numOfItems: 9,
q: '', q: '',
viewMode: null, viewMode: null
}; }
}, },
computed: { computed: {
@ -40,14 +40,14 @@ export default {
return limitBy( return limitBy(
filterBy(artistStore.all, this.q, 'name'), filterBy(artistStore.all, this.q, 'name'),
this.numOfItems this.numOfItems
); )
}, }
}, },
methods: { methods: {
changeViewMode (mode) { changeViewMode (mode) {
this.viewMode = mode; this.viewMode = mode
}, }
}, },
created () { created () {
@ -58,14 +58,16 @@ export default {
'koel:ready': () => this.displayMore(), 'koel:ready': () => this.displayMore(),
'koel:teardown': () => { 'koel:teardown': () => {
this.q = ''; this.q = ''
this.numOfItems = 9; this.numOfItems = 9
}, },
'filter:changed': q => this.q = q, 'filter:changed': q => {
}); this.q = q
}, }
}; })
}
}
</script> </script>
<style lang="sass"> <style lang="sass">

View file

@ -35,12 +35,10 @@
</template> </template>
<script> <script>
import isMobile from 'ismobilejs'; import { pluralize } from '../../../utils'
import { favoriteStore, sharedStore } from '../../../stores'
import { pluralize } from '../../../utils'; import { download } from '../../../services'
import { favoriteStore, sharedStore } from '../../../stores'; import hasSongList from '../../../mixins/has-song-list'
import { playback, download } from '../../../services';
import hasSongList from '../../../mixins/has-song-list';
export default { export default {
name: 'main-wrapper--main-content--favorites', name: 'main-wrapper--main-content--favorites',
@ -50,8 +48,8 @@ export default {
data () { data () {
return { return {
state: favoriteStore.state, state: favoriteStore.state,
sharedState: sharedStore.state, sharedState: sharedStore.state
}; }
}, },
methods: { methods: {
@ -59,10 +57,10 @@ export default {
* Download all favorite songs. * Download all favorite songs.
*/ */
download () { download () {
download.fromFavorites(); download.fromFavorites()
}, }
}, }
}; }
</script> </script>
<style lang="sass"> <style lang="sass">

View file

@ -74,15 +74,15 @@
</template> </template>
<script> <script>
import { sample } from 'lodash'; import { sample } from 'lodash'
import { event } from '../../../utils'; import { event } from '../../../utils'
import { songStore, albumStore, artistStore, userStore, preferenceStore } from '../../../stores'; import { songStore, albumStore, artistStore, userStore, preferenceStore } from '../../../stores'
import infiniteScroll from '../../../mixins/infinite-scroll'; import infiniteScroll from '../../../mixins/infinite-scroll'
import albumItem from '../../shared/album-item.vue'; import albumItem from '../../shared/album-item.vue'
import artistItem from '../../shared/artist-item.vue'; import artistItem from '../../shared/artist-item.vue'
import songItem from '../../shared/home-song-item.vue'; import songItem from '../../shared/home-song-item.vue'
export default { export default {
components: { albumItem, artistItem, songItem }, components: { albumItem, artistItem, songItem },
@ -103,31 +103,31 @@ export default {
'Sup, %s?', 'Sup, %s?',
'Hows life, %s?', 'Hows life, %s?',
'Hows your day, %s?', 'Hows your day, %s?',
'How have you been, %s?', 'How have you been, %s?'
], ],
recentSongs: [], recentSongs: [],
top: { top: {
songs: [], songs: [],
albums: [], albums: [],
artists: [], artists: []
}, },
recentlyAdded: { recentlyAdded: {
albums: [], albums: [],
songs: [], songs: []
}, },
preferences: preferenceStore.state, preferences: preferenceStore.state
}; }
}, },
computed: { computed: {
greeting () { greeting () {
return sample(this.greetings).replace('%s', userStore.current.name); return sample(this.greetings).replace('%s', userStore.current.name)
}, },
showRecentlyAddedSection () { showRecentlyAddedSection () {
return this.recentlyAdded.albums.length || this.recentlyAdded.songs.length; return this.recentlyAdded.albums.length || this.recentlyAdded.songs.length
}, }
}, },
methods: { methods: {
@ -135,23 +135,23 @@ export default {
* Refresh the dashboard with latest data. * Refresh the dashboard with latest data.
*/ */
refreshDashboard () { refreshDashboard () {
this.top.songs = songStore.getMostPlayed(7); this.top.songs = songStore.getMostPlayed(7)
this.top.albums = albumStore.getMostPlayed(6); this.top.albums = albumStore.getMostPlayed(6)
this.top.artists = artistStore.getMostPlayed(6); this.top.artists = artistStore.getMostPlayed(6)
this.recentlyAdded.albums = albumStore.getRecentlyAdded(6); this.recentlyAdded.albums = albumStore.getRecentlyAdded(6)
this.recentlyAdded.songs = songStore.getRecentlyAdded(10); this.recentlyAdded.songs = songStore.getRecentlyAdded(10)
this.recentSongs = songStore.getRecent(7); this.recentSongs = songStore.getRecent(7)
}, }
}, },
created () { created () {
event.on({ event.on({
'koel:ready': () => this.refreshDashboard(), 'koel:ready': () => this.refreshDashboard(),
'song:played': () => this.refreshDashboard(), 'song:played': () => this.refreshDashboard()
}); })
}, }
}; }
</script> </script>
<style lang="sass"> <style lang="sass">

View file

@ -18,22 +18,22 @@
</template> </template>
<script> <script>
import { event } from '../../../utils'; import { event } from '../../../utils'
import { albumStore, sharedStore } from '../../../stores'; import { albumStore, sharedStore } from '../../../stores'
import albums from './albums.vue'; import albums from './albums.vue'
import album from './album.vue'; import album from './album.vue'
import artists from './artists.vue'; import artists from './artists.vue'
import artist from './artist.vue'; import artist from './artist.vue'
import songs from './songs.vue'; import songs from './songs.vue'
import settings from './settings.vue'; import settings from './settings.vue'
import users from './users.vue'; import users from './users.vue'
import queue from './queue.vue'; import queue from './queue.vue'
import home from './home.vue'; import home from './home.vue'
import playlist from './playlist.vue'; import playlist from './playlist.vue'
import favorites from './favorites.vue'; import favorites from './favorites.vue'
import profile from './profile.vue'; import profile from './profile.vue'
import youtubePlayer from './youtube-player.vue'; import youtubePlayer from './youtube-player.vue'
export default { export default {
components: { albums, album, artists, artist, songs, settings, components: { albums, album, artists, artist, songs, settings,
@ -43,13 +43,15 @@ export default {
return { return {
view: 'home', // The default view view: 'home', // The default view
albumCover: null, albumCover: null,
sharedState: sharedStore.state, sharedState: sharedStore.state
}; }
}, },
created () { created () {
event.on({ event.on({
'main-content-view:load': view => this.view = view, 'main-content-view:load': view => {
this.view = view
},
/** /**
* When a new song is played, find its cover for the translucent effect. * When a new song is played, find its cover for the translucent effect.
@ -59,11 +61,11 @@ export default {
* @return {Boolean} * @return {Boolean}
*/ */
'song:played': song => { 'song:played': song => {
this.albumCover = song.album.cover === albumStore.stub.cover ? null : song.album.cover; this.albumCover = song.album.cover === albumStore.stub.cover ? null : song.album.cover
}, }
}); })
}, }
}; }
</script> </script>
<style lang="sass"> <style lang="sass">

View file

@ -37,14 +37,13 @@
</template> </template>
<script> <script>
import isMobile from 'ismobilejs'; import swal from 'sweetalert'
import swal from 'sweetalert';
import { pluralize, event } from '../../../utils'; import { pluralize, event } from '../../../utils'
import { playlistStore, sharedStore } from '../../../stores'; import { playlistStore, sharedStore } from '../../../stores'
import { playback, download } from '../../../services'; import { playback, download } from '../../../services'
import router from '../../../router'; import router from '../../../router'
import hasSongList from '../../../mixins/has-song-list'; import hasSongList from '../../../mixins/has-song-list'
export default { export default {
name: 'main-wrapper--main-content--playlist', name: 'main-wrapper--main-content--playlist',
@ -56,9 +55,9 @@ export default {
playlist: playlistStore.stub, playlist: playlistStore.stub,
sharedState: sharedStore.state, sharedState: sharedStore.state,
songListControlConfig: { songListControlConfig: {
deletePlaylist: true, deletePlaylist: true
}, }
}; }
}, },
created () { created () {
@ -71,9 +70,9 @@ export default {
*/ */
event.on('main-content-view:load', (view, playlist) => { event.on('main-content-view:load', (view, playlist) => {
if (view === 'playlist') { if (view === 'playlist') {
this.playlist = playlist; this.playlist = playlist
} }
}); })
}, },
methods: { methods: {
@ -82,7 +81,7 @@ export default {
* Overriding the mixin. * Overriding the mixin.
*/ */
shuffleAll () { shuffleAll () {
playback.queueAndPlay(this.playlist.songs, true); playback.queueAndPlay(this.playlist.songs, true)
}, },
/** /**
@ -91,9 +90,8 @@ export default {
confirmDelete () { confirmDelete () {
// If the playlist is empty, just go ahead and delete it. // If the playlist is empty, just go ahead and delete it.
if (!this.playlist.songs.length) { if (!this.playlist.songs.length) {
this.del(); this.del()
return
return;
} }
swal({ swal({
@ -101,8 +99,8 @@ export default {
text: 'Once its gone, its gone, and theres no turning back.', text: 'Once its gone, its gone, and theres no turning back.',
type: 'warning', type: 'warning',
showCancelButton: true, showCancelButton: true,
confirmButtonText: 'Yes, go ahead', confirmButtonText: 'Yes, go ahead'
}, this.del); }, this.del)
}, },
/** /**
@ -112,21 +110,21 @@ export default {
playlistStore.delete(this.playlist).then(() => { playlistStore.delete(this.playlist).then(() => {
// Reset the current playlist to our stub, so that we don't encounter // Reset the current playlist to our stub, so that we don't encounter
// any property reference error. // any property reference error.
this.playlist = playlistStore.stub; this.playlist = playlistStore.stub
// Switch back to Home screen // Switch back to Home screen
router.go('home'); router.go('home')
}); })
}, },
/** /**
* Download all songs in the current playlist. * Download all songs in the current playlist.
*/ */
download () { download () {
return download.fromPlaylist(this.playlist); return download.fromPlaylist(this.playlist)
}, }
}, }
}; }
</script> </script>
<style lang="sass"> <style lang="sass">

View file

@ -104,12 +104,12 @@
</template> </template>
<script> <script>
import $ from 'jquery'; import $ from 'jquery'
import swal from 'sweetalert'; import swal from 'sweetalert'
import { userStore, preferenceStore, sharedStore } from '../../../stores'; import { userStore, preferenceStore, sharedStore } from '../../../stores'
import { forceReloadWindow } from '../../../utils'; import { forceReloadWindow } from '../../../utils'
import { http, ls } from '../../../services'; import { http, ls } from '../../../services'
export default { export default {
data () { data () {
@ -119,8 +119,8 @@ export default {
pwd: '', pwd: '',
confirmPwd: '', confirmPwd: '',
prefs: preferenceStore.state, prefs: preferenceStore.state,
sharedState: sharedStore.state, sharedState: sharedStore.state
}; }
}, },
methods: { methods: {
@ -130,31 +130,30 @@ export default {
update () { update () {
// A little validation put in a small place. // A little validation put in a small place.
if ((this.pwd || this.confirmPwd) && this.pwd !== this.confirmPwd) { if ((this.pwd || this.confirmPwd) && this.pwd !== this.confirmPwd) {
$('#inputProfilePassword, #inputProfileConfirmPassword').addClass('error'); $('#inputProfilePassword, #inputProfileConfirmPassword').addClass('error')
return
return;
} }
$('#inputProfilePassword, #inputProfileConfirmPassword').removeClass('error'); $('#inputProfilePassword, #inputProfileConfirmPassword').removeClass('error')
userStore.updateProfile(this.pwd).then(() => { userStore.updateProfile(this.pwd).then(() => {
this.pwd = ''; this.pwd = ''
this.confirmPwd = ''; this.confirmPwd = ''
swal({ swal({
title: 'Done!', title: 'Done!',
text: 'Profile saved.', text: 'Profile saved.',
type: 'success', type: 'success',
allowOutsideClick: true, allowOutsideClick: true
}); })
}); })
}, },
/** /**
* Save the current user's preference. * Save the current user's preference.
*/ */
savePreference () { savePreference () {
this.$nextTick(() => preferenceStore.save()); this.$nextTick(() => preferenceStore.save())
}, },
/** /**
@ -167,7 +166,7 @@ export default {
`/api/lastfm/connect?jwt-token=${ls.get('jwt-token')}`, `/api/lastfm/connect?jwt-token=${ls.get('jwt-token')}`,
'_blank', '_blank',
'toolbar=no,titlebar=no,location=no,width=1024,height=640' 'toolbar=no,titlebar=no,location=no,width=1024,height=640'
); )
}, },
/** /**
@ -181,10 +180,10 @@ export default {
// - Nope. Users should be grown-ass adults who take responsibilty of their actions. // - Nope. Users should be grown-ass adults who take responsibilty of their actions.
// But one of my users is my new born kid! // But one of my users is my new born kid!
// - Then? Kids will fuck things up anyway. // - Then? Kids will fuck things up anyway.
http.delete('lastfm/disconnect', {}, forceReloadWindow); http.delete('lastfm/disconnect', {}, forceReloadWindow)
}, }
}, }
}; }
</script> </script>
<style lang="sass"> <style lang="sass">

View file

@ -34,10 +34,10 @@
</template> </template>
<script> <script>
import { pluralize } from '../../../utils'; import { pluralize } from '../../../utils'
import { queueStore, songStore } from '../../../stores'; import { queueStore, songStore } from '../../../stores'
import { playback } from '../../../services'; import { playback } from '../../../services'
import hasSongList from '../../../mixins/has-song-list'; import hasSongList from '../../../mixins/has-song-list'
export default { export default {
name: 'main-wrapper--main-content--queue', name: 'main-wrapper--main-content--queue',
@ -48,9 +48,9 @@ export default {
return { return {
state: queueStore.state, state: queueStore.state,
songListControlConfig: { songListControlConfig: {
clearQueue: true, clearQueue: true
}
} }
};
}, },
computed: { computed: {
@ -58,8 +58,8 @@ export default {
* Determine if we should display a "Shuffle All" link. * Determine if we should display a "Shuffle All" link.
*/ */
showShufflingAllOption () { showShufflingAllOption () {
return songStore.all.length; return songStore.all.length
}, }
}, },
methods: { methods: {
@ -68,17 +68,17 @@ export default {
* Overriding the mixin. * Overriding the mixin.
*/ */
shuffleAll () { shuffleAll () {
playback.queueAndPlay(this.state.songs.length ? this.state.songs : songStore.all, true); playback.queueAndPlay(this.state.songs.length ? this.state.songs : songStore.all, true)
}, },
/** /**
* Clear the queue. * Clear the queue.
*/ */
clearQueue () { clearQueue () {
queueStore.clear(); queueStore.clear()
}, }
}, }
}; }
</script> </script>
<style lang="sass"> <style lang="sass">

View file

@ -24,18 +24,18 @@
</template> </template>
<script> <script>
import swal from 'sweetalert'; import swal from 'sweetalert'
import { settingStore, sharedStore } from '../../../stores'; import { settingStore, sharedStore } from '../../../stores'
import { parseValidationError, forceReloadWindow, event, showOverlay, hideOverlay } from '../../../utils'; import { parseValidationError, forceReloadWindow, showOverlay, hideOverlay } from '../../../utils'
import router from '../../../router'; import router from '../../../router'
export default { export default {
data () { data () {
return { return {
state: settingStore.state, state: settingStore.state,
sharedState: sharedStore.state, sharedState: sharedStore.state
}; }
}, },
computed: { computed: {
@ -47,8 +47,8 @@ export default {
shouldWarn () { shouldWarn () {
// Warn the user if the media path is not empty and about to change. // Warn the user if the media path is not empty and about to change.
return this.sharedState.originalMediaPath && return this.sharedState.originalMediaPath &&
this.sharedState.originalMediaPath !== this.state.settings.media_path.trim(); this.sharedState.originalMediaPath !== this.state.settings.media_path.trim()
}, }
}, },
methods: { methods: {
@ -61,10 +61,10 @@ export default {
type: 'warning', type: 'warning',
showCancelButton: true, showCancelButton: true,
confirmButtonText: 'I know. Go ahead.', confirmButtonText: 'I know. Go ahead.',
confirmButtonColor: '#c34848', confirmButtonColor: '#c34848'
}, this.save); }, this.save)
} else { } else {
this.save(); this.save()
} }
}, },
@ -72,31 +72,31 @@ export default {
* Save the settings. * Save the settings.
*/ */
save () { save () {
showOverlay(); showOverlay()
settingStore.update().then(() => { settingStore.update().then(() => {
// Make sure we're back to home first. // Make sure we're back to home first.
router.go('home'); router.go('home')
forceReloadWindow(); forceReloadWindow()
}).catch(r => { }).catch(r => {
let msg = 'Unknown error.'; let msg = 'Unknown error.'
if (r.status === 422) { if (r.status === 422) {
msg = parseValidationError(r.responseJSON)[0]; msg = parseValidationError(r.responseJSON)[0]
} }
hideOverlay(); hideOverlay()
swal({ swal({
title: 'Something went wrong', title: 'Something went wrong',
text: msg, text: msg,
type: 'error', type: 'error',
allowOutsideClick: true, allowOutsideClick: true
}); })
}); })
}, }
}, }
}; }
</script> </script>
<style lang="sass"> <style lang="sass">

View file

@ -25,12 +25,9 @@
</template> </template>
<script> <script>
import isMobile from 'ismobilejs'; import { pluralize } from '../../../utils'
import { songStore } from '../../../stores'
import { pluralize } from '../../../utils'; import hasSongList from '../../../mixins/has-song-list'
import { songStore } from '../../../stores';
import { playback } from '../../../services';
import hasSongList from '../../../mixins/has-song-list';
export default { export default {
name: 'main-wrapper--main-content--songs', name: 'main-wrapper--main-content--songs',
@ -40,9 +37,9 @@ export default {
data () { data () {
return { return {
state: songStore.state state: songStore.state
}; }
}, }
}; }
</script> </script>
<style lang="sass"> <style lang="sass">

View file

@ -44,11 +44,11 @@
</template> </template>
<script> <script>
import { clone } from 'lodash'; import { clone } from 'lodash'
import isMobile from 'ismobilejs'; import isMobile from 'ismobilejs'
import { userStore } from '../../../stores'; import { userStore } from '../../../stores'
import userItem from '../../shared/user-item.vue'; import userItem from '../../shared/user-item.vue'
export default { export default {
components: { userItem }, components: { userItem },
@ -59,8 +59,8 @@ export default {
isPhone: isMobile.phone, isPhone: isMobile.phone,
showingControls: false, showingControls: false,
creating: false, creating: false,
newUser: {}, newUser: {}
}; }
}, },
methods: { methods: {
@ -69,12 +69,12 @@ export default {
*/ */
store () { store () {
userStore.store(this.newUser.name, this.newUser.email, this.newUser.password).then(u => { userStore.store(this.newUser.name, this.newUser.email, this.newUser.password).then(u => {
this.newUser = clone(userStore.stub); this.newUser = clone(userStore.stub)
this.creating = false; this.creating = false
}); })
}, }
}, }
}; }
</script> </script>
<style lang="sass"> <style lang="sass">

View file

@ -11,11 +11,11 @@
</template> </template>
<script> <script>
import { event } from '../../../utils'; import { event } from '../../../utils'
import { playback } from '../../../services'; import { playback } from '../../../services'
import YouTubePlayer from 'youtube-player'; import YouTubePlayer from 'youtube-player'
let player; let player
export default { export default {
name: 'main-wrapper--main-content--youtube-player', name: 'main-wrapper--main-content--youtube-player',
@ -28,32 +28,32 @@ export default {
if (!player) { if (!player) {
player = YouTubePlayer('player', { player = YouTubePlayer('player', {
width: '100%', width: '100%',
height: '100%', height: '100%'
}); })
player.on('stateChange', event => { player.on('stateChange', event => {
// Pause song playback when video is played // Pause song playback when video is played
event.data === 1 && playback.pause(); event.data === 1 && playback.pause()
}); })
}
} }
},
}, },
created () { created () {
event.on({ event.on({
'youtube:play': id => { 'youtube:play': id => {
this.initPlayer(); this.initPlayer()
player.loadVideoById(id); player.loadVideoById(id)
player.playVideo(); player.playVideo()
}, },
/** /**
* Stop video playback when a song is played/resumed. * Stop video playback when a song is played/resumed.
*/ */
'song:played': () => player && player.pauseVideo(), 'song:played': () => player && player.pauseVideo()
}); })
}, }
}; }
</script> </script>
<style lang="sass" scoped> <style lang="sass" scoped>

View file

@ -55,12 +55,12 @@
</template> </template>
<script> <script>
import isMobile from 'ismobilejs'; import isMobile from 'ismobilejs'
import $ from 'jquery'; import $ from 'jquery'
import { event } from '../../../utils'; import { event } from '../../../utils'
import { sharedStore, userStore, songStore, queueStore } from '../../../stores'; import { sharedStore, userStore, songStore, queueStore } from '../../../stores'
import playlists from './playlists.vue'; import playlists from './playlists.vue'
export default { export default {
components: { playlists }, components: { playlists },
@ -70,14 +70,14 @@ export default {
currentView: 'home', currentView: 'home',
user: userStore.state, user: userStore.state,
showing: !isMobile.phone, showing: !isMobile.phone,
sharedState: sharedStore.state, sharedState: sharedStore.state
}; }
}, },
computed: { computed: {
latestVersionUrl () { latestVersionUrl () {
return 'https://github.com/phanan/koel/releases/tag/' + this.sharedState.latestVersion; return 'https://github.com/phanan/koel/releases/tag/' + this.sharedState.latestVersion
}, }
}, },
methods: { methods: {
@ -87,7 +87,7 @@ export default {
* @param {Object} e The dragleave event. * @param {Object} e The dragleave event.
*/ */
removeDroppableState (e) { removeDroppableState (e) {
$(e.target).removeClass('droppable'); $(e.target).removeClass('droppable')
}, },
/** /**
@ -96,10 +96,10 @@ export default {
* @param {Object} e The dragover event. * @param {Object} e The dragover event.
*/ */
allowDrop (e) { allowDrop (e) {
$(e.target).addClass('droppable'); $(e.target).addClass('droppable')
e.dataTransfer.dropEffect = 'move'; e.dataTransfer.dropEffect = 'move'
return false; return false
}, },
/** /**
@ -110,41 +110,43 @@ export default {
* @return {Boolean} * @return {Boolean}
*/ */
handleDrop (e) { handleDrop (e) {
this.removeDroppableState(e); this.removeDroppableState(e)
if (!e.dataTransfer.getData('application/x-koel.text+plain')) { if (!e.dataTransfer.getData('application/x-koel.text+plain')) {
return false; return false
} }
const songs = songStore.byIds(e.dataTransfer.getData('application/x-koel.text+plain').split(',')); const songs = songStore.byIds(e.dataTransfer.getData('application/x-koel.text+plain').split(','))
if (!songs.length) { if (!songs.length) {
return false; return false
} }
queueStore.queue(songs); queueStore.queue(songs)
return false; return false
}, }
}, },
created () { created () {
event.on('main-content-view:load', view => { event.on('main-content-view:load', view => {
this.currentView = view; this.currentView = view
// Hide the sidebar if on mobile // Hide the sidebar if on mobile
if (isMobile.phone) { if (isMobile.phone) {
this.showing = false; this.showing = false
} }
}); })
/** /**
* Listen to sidebar:toggle event to show or hide the sidebar. * Listen to sidebar:toggle event to show or hide the sidebar.
* This should only be triggered on a mobile device. * This should only be triggered on a mobile device.
*/ */
event.on('sidebar:toggle', () => this.showing = !this.showing); event.on('sidebar:toggle', () => {
}, this.showing = !this.showing
}; })
}
}
</script> </script>
<style lang="sass"> <style lang="sass">

View file

@ -20,10 +20,10 @@
</template> </template>
<script> <script>
import $ from 'jquery'; import $ from 'jquery'
import { event } from '../../../utils'; import { event } from '../../../utils'
import { songStore, playlistStore, favoriteStore } from '../../../stores'; import { songStore, playlistStore, favoriteStore } from '../../../stores'
export default { export default {
props: ['playlist', 'type'], props: ['playlist', 'type'],
@ -32,8 +32,8 @@ export default {
return { return {
newName: '', newName: '',
editing: false, editing: false,
active: false, active: false
}; }
}, },
computed: { computed: {
@ -43,12 +43,12 @@ export default {
* @return {Boolean} * @return {Boolean}
*/ */
isFavorites () { isFavorites () {
return this.type === 'favorites'; return this.type === 'favorites'
}, },
playlistUrl () { playlistUrl () {
return this.isFavorites ? '/#!/favorites' : `/#!/playlist/${this.playlist.id}`; return this.isFavorites ? '/#!/favorites' : `/#!/playlist/${this.playlist.id}`
}, }
}, },
methods: { methods: {
@ -57,11 +57,11 @@ export default {
*/ */
edit () { edit () {
if (this.isFavorites) { if (this.isFavorites) {
return; return
} }
this.beforeEditCache = this.playlist.name; this.beforeEditCache = this.playlist.name
this.editing = true; this.editing = true
}, },
/** /**
@ -69,26 +69,26 @@ export default {
*/ */
update () { update () {
if (this.isFavorites || !this.editing) { if (this.isFavorites || !this.editing) {
return; return
} }
this.editing = false; this.editing = false
this.playlist.name = this.playlist.name.trim(); this.playlist.name = this.playlist.name.trim()
if (!this.playlist.name) { if (!this.playlist.name) {
this.playlist.name = this.beforeEditCache; this.playlist.name = this.beforeEditCache
return; return
} }
playlistStore.update(this.playlist); playlistStore.update(this.playlist)
}, },
/** /**
* Cancel editing. * Cancel editing.
*/ */
cancelEdit () { cancelEdit () {
this.editing = false; this.editing = false
this.playlist.name = this.beforeEditCache; this.playlist.name = this.beforeEditCache
}, },
/** /**
@ -97,7 +97,7 @@ export default {
* @param {Object} e The dragleave event. * @param {Object} e The dragleave event.
*/ */
removeDroppableState (e) { removeDroppableState (e) {
$(e.target).removeClass('droppable'); $(e.target).removeClass('droppable')
}, },
/** /**
@ -107,10 +107,10 @@ export default {
* @param {Object} e The dragover event. * @param {Object} e The dragover event.
*/ */
allowDrop (e) { allowDrop (e) {
$(e.target).addClass('droppable'); $(e.target).addClass('droppable')
e.dataTransfer.dropEffect = 'move'; e.dataTransfer.dropEffect = 'move'
return false; return false
}, },
/** /**
@ -121,40 +121,40 @@ export default {
* @return {Boolean} * @return {Boolean}
*/ */
handleDrop (e) { handleDrop (e) {
this.removeDroppableState(e); this.removeDroppableState(e)
if (!e.dataTransfer.getData('application/x-koel.text+plain')) { if (!e.dataTransfer.getData('application/x-koel.text+plain')) {
return false; return false
} }
const songs = songStore.byIds(e.dataTransfer.getData('application/x-koel.text+plain').split(',')); const songs = songStore.byIds(e.dataTransfer.getData('application/x-koel.text+plain').split(','))
if (!songs.length) { if (!songs.length) {
return false; return false
} }
if (this.type === 'favorites') { if (this.type === 'favorites') {
favoriteStore.like(songs); favoriteStore.like(songs)
} else { } else {
playlistStore.addSongs(this.playlist, songs); playlistStore.addSongs(this.playlist, songs)
} }
return false; return false
}, }
}, },
created () { created () {
event.on('main-content-view:load', (view, playlist) => { event.on('main-content-view:load', (view, playlist) => {
if (view === 'favorites') { if (view === 'favorites') {
this.active = this.isFavorites; this.active = this.isFavorites
} else if (view === 'playlist') { } else if (view === 'playlist') {
this.active = this.playlist === playlist; this.active = this.playlist === playlist
} else { } else {
this.active = false; this.active = false
}
})
}
} }
});
},
};
</script> </script>
<style lang="sass" scoped> <style lang="sass" scoped>

View file

@ -22,10 +22,10 @@
</template> </template>
<script> <script>
import { playlistStore, favoriteStore } from '../../../stores'; import { playlistStore, favoriteStore } from '../../../stores'
import router from '../../../router'; import router from '../../../router'
import playlistItem from './playlist-item.vue'; import playlistItem from './playlist-item.vue'
export default { export default {
name: 'sidebar--playlists', name: 'sidebar--playlists',
@ -37,8 +37,8 @@ export default {
playlistState: playlistStore.state, playlistState: playlistStore.state,
favoriteState: favoriteStore.state, favoriteState: favoriteStore.state,
creating: false, creating: false,
newName: '', newName: ''
}; }
}, },
methods: { methods: {
@ -46,16 +46,16 @@ export default {
* Store/create a new playlist. * Store/create a new playlist.
*/ */
store () { store () {
this.creating = false; this.creating = false
playlistStore.store(this.newName).then(p => { playlistStore.store(this.newName).then(p => {
this.newName = ''; this.newName = ''
// Activate the new playlist right away // Activate the new playlist right away
this.$nextTick(() => router.go(`playlist/${p.id}`)); this.$nextTick(() => router.go(`playlist/${p.id}`))
}); })
}, }
}, }
}; }
</script> </script>
<style lang="sass"> <style lang="sass">

View file

@ -76,20 +76,20 @@
</template> </template>
<script> <script>
import { every, filter } from 'lodash'; import { every, filter } from 'lodash'
import { br2nl } from '../../utils'; import { br2nl, forceReloadWindow } from '../../utils'
import { songInfo } from '../../services/info'; import { songInfo } from '../../services/info'
import { artistStore, albumStore, songStore } from '../../stores'; import { artistStore, albumStore, songStore } from '../../stores'
import soundBar from '../shared/sound-bar.vue'; import soundBar from '../shared/sound-bar.vue'
import typeahead from '../shared/typeahead.vue'; import typeahead from '../shared/typeahead.vue'
const COMPILATION_STATES = { const COMPILATION_STATES = {
NONE: 0, // No songs belong to a compilation album NONE: 0, // No songs belong to a compilation album
ALL: 1, // All songs belong to compilation album(s) ALL: 1, // All songs belong to compilation album(s)
SOME: 2, // Some of the songs belong to compilation album(s) SOME: 2 // Some of the songs belong to compilation album(s)
}; }
export default { export default {
components: { soundBar, typeahead }, components: { soundBar, typeahead },
@ -105,13 +105,13 @@
artistState: artistStore.state, artistState: artistStore.state,
artistTypeaheadOptions: { artistTypeaheadOptions: {
displayKey: 'name', displayKey: 'name',
filterKey: 'name', filterKey: 'name'
}, },
albumState: albumStore.state, albumState: albumStore.state,
albumTypeaheadOptions: { albumTypeaheadOptions: {
displayKey: 'name', displayKey: 'name',
filterKey: 'name', filterKey: 'name'
}, },
/** /**
@ -126,9 +126,9 @@
artistName: '', artistName: '',
lyrics: '', lyrics: '',
track: '', track: '',
compilationState: null, compilationState: null
}, }
}; }
}, },
computed: { computed: {
@ -138,7 +138,7 @@
* @return {boolean} * @return {boolean}
*/ */
editSingle () { editSingle () {
return this.songs.length === 1; return this.songs.length === 1
}, },
/** /**
@ -147,7 +147,7 @@
* @return {boolean} * @return {boolean}
*/ */
bySameArtist () { bySameArtist () {
return every(this.songs, song => song.artist.id === this.songs[0].artist.id); return every(this.songs, song => song.artist.id === this.songs[0].artist.id)
}, },
/** /**
@ -156,7 +156,7 @@
* @return {boolean} * @return {boolean}
*/ */
inSameAlbum () { inSameAlbum () {
return every(this.songs, song => song.album.id === this.songs[0].album.id); return every(this.songs, song => song.album.id === this.songs[0].album.id)
}, },
/** /**
@ -165,7 +165,7 @@
* @return {string} * @return {string}
*/ */
coverUrl () { coverUrl () {
return this.inSameAlbum ? this.songs[0].album.cover : '/public/img/covers/unknown-album.png'; return this.inSameAlbum ? this.songs[0].album.cover : '/public/img/covers/unknown-album.png'
}, },
/** /**
@ -179,12 +179,12 @@
if (!contributedSongs.length) { if (!contributedSongs.length) {
this.formData.compilationState = COMPILATION_STATES.NONE this.formData.compilationState = COMPILATION_STATES.NONE
} else if (contributedSongs.length === this.songs.length) { } else if (contributedSongs.length === this.songs.length) {
this.formData.compilationState = COMPILATION_STATES.ALL; this.formData.compilationState = COMPILATION_STATES.ALL
} else { } else {
this.formData.compilationState = COMPILATION_STATES.SOME; this.formData.compilationState = COMPILATION_STATES.SOME
} }
return this.formData.compilationState; return this.formData.compilationState
}, },
/** /**
@ -193,7 +193,7 @@
* @return {string} * @return {string}
*/ */
displayedTitle () { displayedTitle () {
return this.editSingle ? this.formData.title : `${this.songs.length} songs selected`; return this.editSingle ? this.formData.title : `${this.songs.length} songs selected`
}, },
/** /**
@ -203,9 +203,9 @@
*/ */
displayedAlbum () { displayedAlbum () {
if (this.editSingle) { if (this.editSingle) {
return this.formData.albumName; return this.formData.albumName
} else { } else {
return this.formData.albumName ? this.formData.albumName : 'Mixed Albums'; return this.formData.albumName ? this.formData.albumName : 'Mixed Albums'
} }
}, },
@ -216,46 +216,46 @@
*/ */
displayedArtist () { displayedArtist () {
if (this.editSingle) { if (this.editSingle) {
return this.formData.artistName; return this.formData.artistName
} else { } else {
return this.formData.artistName ? this.formData.artistName : 'Mixed Artists'; return this.formData.artistName ? this.formData.artistName : 'Mixed Artists'
}
} }
},
}, },
methods: { methods: {
open (songs) { open (songs) {
this.shown = true; this.shown = true
this.songs = songs; this.songs = songs
this.currentView = 'details'; this.currentView = 'details'
this.needsReload = false; this.needsReload = false
if (this.editSingle) { if (this.editSingle) {
this.formData.title = this.songs[0].title; this.formData.title = this.songs[0].title
this.formData.albumName = this.songs[0].album.name; this.formData.albumName = this.songs[0].album.name
this.formData.artistName = this.songs[0].artist.name; this.formData.artistName = this.songs[0].artist.name
// If we're editing only one song and the song's info (including lyrics) // If we're editing only one song and the song's info (including lyrics)
// hasn't been loaded, load it now. // hasn't been loaded, load it now.
if (!this.songs[0].infoRetrieved) { if (!this.songs[0].infoRetrieved) {
this.loading = true; this.loading = true
songInfo.fetch(this.songs[0]).then(r => { songInfo.fetch(this.songs[0]).then(r => {
this.loading = false; this.loading = false
this.formData.lyrics = br2nl(this.songs[0].lyrics); this.formData.lyrics = br2nl(this.songs[0].lyrics)
this.formData.track = this.songs[0].track; this.formData.track = this.songs[0].track
this.initCompilationStateCheckbox(); this.initCompilationStateCheckbox()
}); })
} else { } else {
this.formData.lyrics = br2nl(this.songs[0].lyrics); this.formData.lyrics = br2nl(this.songs[0].lyrics)
this.formData.track = this.songs[0].track; this.formData.track = this.songs[0].track
this.initCompilationStateCheckbox(); this.initCompilationStateCheckbox()
} }
} else { } else {
this.formData.albumName = this.inSameAlbum ? this.songs[0].album.name : ''; this.formData.albumName = this.inSameAlbum ? this.songs[0].album.name : ''
this.formData.artistName = this.bySameArtist ? this.songs[0].artist.name : ''; this.formData.artistName = this.bySameArtist ? this.songs[0].artist.name : ''
this.loading = false; this.loading = false
this.initCompilationStateCheckbox(); this.initCompilationStateCheckbox()
} }
}, },
@ -266,23 +266,23 @@
// This must be wrapped in a $nextTick callback, because the form is dynamically // This must be wrapped in a $nextTick callback, because the form is dynamically
// attached into DOM in conjunction with `this.loading` data binding. // attached into DOM in conjunction with `this.loading` data binding.
this.$nextTick(() => { this.$nextTick(() => {
const chk = this.$refs.compilationStateChk; const chk = this.$refs.compilationStateChk
switch (this.compilationState) { switch (this.compilationState) {
case COMPILATION_STATES.ALL: case COMPILATION_STATES.ALL:
chk.checked = true; chk.checked = true
chk.indeterminate = false; chk.indeterminate = false
break; break
case COMPILATION_STATES.NONE: case COMPILATION_STATES.NONE:
chk.checked = false; chk.checked = false
chk.indeterminate = false; chk.indeterminate = false
break; break
default: default:
chk.checked = false; chk.checked = false
chk.indeterminate = true; chk.indeterminate = true
break; break
} }
}); })
}, },
/** /**
@ -292,33 +292,33 @@
* once the user clicks the checkbox, there's no going back to indeterminate state. * once the user clicks the checkbox, there's no going back to indeterminate state.
*/ */
changeCompilationState (e) { changeCompilationState (e) {
this.formData.compilationState = e.target.checked ? COMPILATION_STATES.ALL : COMPILATION_STATES.NONE; this.formData.compilationState = e.target.checked ? COMPILATION_STATES.ALL : COMPILATION_STATES.NONE
this.needsReload = true; this.needsReload = true
}, },
/** /**
* Close the modal. * Close the modal.
*/ */
close () { close () {
this.shown = false; this.shown = false
}, },
/** /**
* Submit the form. * Submit the form.
*/ */
submit () { submit () {
this.loading = true; this.loading = true
songStore.update(this.songs, this.formData).then(r => { songStore.update(this.songs, this.formData).then(r => {
this.loading = false; this.loading = false
this.close(); this.close()
if (this.needsReload) { this.needsReload && forceReloadWindow()
forceReloadWindow(); }).catch(r => {
this.loading = false
})
}
}
} }
}).catch(r => this.loading = false);
},
},
};
</script> </script>
<style lang="sass"> <style lang="sass">

View file

@ -27,12 +27,10 @@
</template> </template>
<script> <script>
import { assign } from 'lodash'; import { pluralize } from '../../utils'
import { playlistStore } from '../../stores'
import { pluralize, event } from '../../utils'; import router from '../../router'
import { playlistStore } from '../../stores'; import songMenuMethods from '../../mixins/song-menu-methods'
import router from '../../router';
import songMenuMethods from '../../mixins/song-menu-methods';
export default { export default {
name: 'shared--add-to-menu', name: 'shared--add-to-menu',
@ -43,16 +41,16 @@ export default {
data () { data () {
return { return {
newPlaylistName: '', newPlaylistName: '',
playlistState: playlistStore.state, playlistState: playlistStore.state
}; }
}, },
watch: { watch: {
songs () { songs () {
if (!this.songs.length) { if (!this.songs.length) {
this.close(); this.close()
}
} }
},
}, },
methods: { methods: {
@ -61,26 +59,26 @@ export default {
* As of current we don't have selective save. * As of current we don't have selective save.
*/ */
createNewPlaylistFromSongs () { createNewPlaylistFromSongs () {
this.newPlaylistName = this.newPlaylistName.trim(); this.newPlaylistName = this.newPlaylistName.trim()
if (!this.newPlaylistName) { if (!this.newPlaylistName) {
return; return
} }
playlistStore.store(this.newPlaylistName, this.songs).then(p => { playlistStore.store(this.newPlaylistName, this.songs).then(p => {
this.newPlaylistName = ''; this.newPlaylistName = ''
// Activate the new playlist right away // Activate the new playlist right away
this.$nextTick(() => router.go(`playlist/${p.id}`)); this.$nextTick(() => router.go(`playlist/${p.id}`))
}); })
this.close(); this.close()
}, },
close () { close () {
this.$parent.closeAddToMenu(); this.$parent.closeAddToMenu()
}, }
}, }
}; }
</script> </script>
<style lang="sass" scoped> <style lang="sass" scoped>

View file

@ -36,12 +36,12 @@
</template> </template>
<script> <script>
import { map } from 'lodash'; import { map } from 'lodash'
import $ from 'jquery'; import $ from 'jquery'
import { pluralize } from '../../utils'; import { pluralize } from '../../utils'
import { queueStore, artistStore, sharedStore } from '../../stores'; import { queueStore, artistStore, sharedStore } from '../../stores'
import { playback, download } from '../../services'; import { playback, download } from '../../services'
export default { export default {
name: 'shared--album-item', name: 'shared--album-item',
@ -50,15 +50,15 @@ export default {
data () { data () {
return { return {
sharedState: sharedStore.state, sharedState: sharedStore.state
}; }
}, },
computed: { computed: {
isNormalArtist () { isNormalArtist () {
return !artistStore.isVariousArtists(this.album.artist) return !artistStore.isVariousArtists(this.album.artist) &&
&& !artistStore.isUnknownArtist(this.album.artist); !artistStore.isUnknownArtist(this.album.artist)
}, }
}, },
methods: { methods: {
@ -68,9 +68,9 @@ export default {
*/ */
play (e) { play (e) {
if (e.metaKey || e.ctrlKey) { if (e.metaKey || e.ctrlKey) {
queueStore.queue(this.album.songs); queueStore.queue(this.album.songs)
} else { } else {
playback.playAllInAlbum(this.album, false); playback.playAllInAlbum(this.album, false)
} }
}, },
@ -78,30 +78,30 @@ export default {
* Shuffle all songs in album. * Shuffle all songs in album.
*/ */
shuffle () { shuffle () {
playback.playAllInAlbum(this.album, true); playback.playAllInAlbum(this.album, true)
}, },
/** /**
* Download all songs in album. * Download all songs in album.
*/ */
download () { download () {
download.fromAlbum(this.album); download.fromAlbum(this.album)
}, },
/** /**
* Allow dragging the album (actually, its songs). * Allow dragging the album (actually, its songs).
*/ */
dragStart (e) { dragStart (e) {
const songIds = map(this.album.songs, 'id'); const songIds = map(this.album.songs, 'id')
e.dataTransfer.setData('application/x-koel.text+plain', songIds); e.dataTransfer.setData('application/x-koel.text+plain', songIds)
e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.effectAllowed = 'move'
// Set a fancy drop image using our ghost element. // Set a fancy drop image using our ghost element.
const $ghost = $('#dragGhost').text(`All ${songIds.length} song${songIds.length === 1 ? '' : 's'} in ${this.album.name}`); const $ghost = $('#dragGhost').text(`All ${songIds.length} song${songIds.length === 1 ? '' : 's'} in ${this.album.name}`)
e.dataTransfer.setDragImage($ghost[0], 0, 0); e.dataTransfer.setDragImage($ghost[0], 0, 0)
}, }
}, }
}; }
</script> </script>
<style lang="sass"> <style lang="sass">

View file

@ -28,12 +28,12 @@
</template> </template>
<script> <script>
import { map } from 'lodash'; import { map } from 'lodash'
import $ from 'jquery'; import $ from 'jquery'
import { pluralize } from '../../utils'; import { pluralize } from '../../utils'
import { artistStore, queueStore, sharedStore } from '../../stores'; import { artistStore, queueStore, sharedStore } from '../../stores'
import { playback, download } from '../../services'; import { playback, download } from '../../services'
export default { export default {
name: 'shared--artist-item', name: 'shared--artist-item',
@ -42,8 +42,8 @@ export default {
data () { data () {
return { return {
sharedState: sharedStore.state, sharedState: sharedStore.state
}; }
}, },
computed: { computed: {
@ -54,7 +54,7 @@ export default {
* @return {Boolean} * @return {Boolean}
*/ */
showing () { showing () {
return this.artist.songCount && !artistStore.isVariousArtists(this.artist); return this.artist.songCount && !artistStore.isVariousArtists(this.artist)
} }
}, },
@ -64,9 +64,9 @@ export default {
*/ */
play (e) { play (e) {
if (e.metaKey || e.ctrlKey) { if (e.metaKey || e.ctrlKey) {
queueStore.queue(this.artist.songs); queueStore.queue(this.artist.songs)
} else { } else {
playback.playAllByArtist(this.artist); playback.playAllByArtist(this.artist)
} }
}, },
@ -74,23 +74,23 @@ export default {
* Download all songs by artist. * Download all songs by artist.
*/ */
download () { download () {
download.fromArtist(this.artist); download.fromArtist(this.artist)
}, },
/** /**
* Allow dragging the artist (actually, their songs). * Allow dragging the artist (actually, their songs).
*/ */
dragStart (e) { dragStart (e) {
const songIds = map(this.artist.songs, 'id'); const songIds = map(this.artist.songs, 'id')
e.dataTransfer.setData('application/x-koel.text+plain', songIds); e.dataTransfer.setData('application/x-koel.text+plain', songIds)
e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.effectAllowed = 'move'
// Set a fancy drop image using our ghost element. // Set a fancy drop image using our ghost element.
const $ghost = $('#dragGhost').text(`All ${songIds.length} song${songIds.length === 1 ? '' : 's'} by ${this.artist.name}`); const $ghost = $('#dragGhost').text(`All ${songIds.length} song${songIds.length === 1 ? '' : 's'} by ${this.artist.name}`)
e.dataTransfer.setDragImage($ghost[0], 0, 0); e.dataTransfer.setDragImage($ghost[0], 0, 0)
}, }
}, }
}; }
</script> </script>
<style lang="sass"> <style lang="sass">

View file

@ -21,9 +21,9 @@
</template> </template>
<script> <script>
import { pluralize } from '../../utils'; import { pluralize } from '../../utils'
import { queueStore } from '../../stores'; import { queueStore } from '../../stores'
import { playback } from '../../services'; import { playback } from '../../services'
export default { export default {
name: 'shared--home-song-item', name: 'shared--home-song-item',
@ -32,30 +32,30 @@ export default {
computed: { computed: {
showPlayCount () { showPlayCount () {
return this.topPlayCount && this.song.playCount; return this.topPlayCount && this.song.playCount
}, }
}, },
methods: { methods: {
play () { play () {
if (!queueStore.contains(this.song)) { if (!queueStore.contains(this.song)) {
queueStore.queueAfterCurrent(this.song); queueStore.queueAfterCurrent(this.song)
} }
playback.play(this.song); playback.play(this.song)
}, },
changeSongState () { changeSongState () {
if (this.song.playbackState === 'stopped') { if (this.song.playbackState === 'stopped') {
this.play(this.song); this.play(this.song)
} else if (this.song.playbackState === 'paused') { } else if (this.song.playbackState === 'paused') {
playback.resume(); playback.resume()
} else { } else {
playback.pause(); playback.pause()
}
}
}
} }
},
},
};
</script> </script>
<style lang="sass" scoped> <style lang="sass" scoped>

View file

@ -15,10 +15,10 @@
</template> </template>
<script> <script>
import { assign } from 'lodash'; import { assign } from 'lodash'
import { event } from '../../utils'; import { event } from '../../utils'
import soundBar from './sound-bar.vue'; import soundBar from './sound-bar.vue'
export default { export default {
components: { soundBar }, components: { soundBar },
@ -35,9 +35,9 @@ export default {
* @type {String} * @type {String}
*/ */
type: 'loading', type: 'loading',
message: '', message: ''
}, }
}; }
}, },
methods: { methods: {
@ -49,15 +49,15 @@ export default {
* @param {Boolean} dismissable Whether to show the Close button * @param {Boolean} dismissable Whether to show the Close button
*/ */
show (options) { show (options) {
assign(this.state, options); assign(this.state, options)
this.state.showing = true; this.state.showing = true
}, },
/** /**
* Hide the overlay. * Hide the overlay.
*/ */
hide () { hide () {
this.state.showing = false; this.state.showing = false
}, },
/** /**
@ -67,17 +67,17 @@ export default {
* @param {Boolean} dismissable * @param {Boolean} dismissable
*/ */
setDimissable (dismissable = true) { setDimissable (dismissable = true) {
this.state.dismissable = dismissable; this.state.dismissable = dismissable
}, }
}, },
created () { created () {
event.on({ event.on({
'overlay:show': options => this.show(options), 'overlay:show': options => this.show(options),
'overlay:hide': () => this.hide(), 'overlay:hide': () => this.hide()
}); })
}, }
}; }
</script> </script>
<style lang="sass"> <style lang="sass">

View file

@ -25,22 +25,22 @@
</template> </template>
<script> <script>
import { playback } from '../../services'; import { playback } from '../../services'
import { queueStore } from '../../stores'; import { queueStore } from '../../stores'
export default { export default {
props: ['song'], props: ['song'],
data () { data () {
return { return {
selected: false, selected: false
}; }
}, },
computed: { computed: {
playing () { playing () {
return this.song.playbackState === 'playing' || this.song.playbackState === 'paused'; return this.song.playbackState === 'playing' || this.song.playbackState === 'paused'
}, }
}, },
methods: { methods: {
@ -49,10 +49,10 @@ export default {
*/ */
playRightAwayyyyyyy () { playRightAwayyyyyyy () {
if (!queueStore.contains(this.song)) { if (!queueStore.contains(this.song)) {
queueStore.queueAfterCurrent(this.song); queueStore.queueAfterCurrent(this.song)
} }
playback.play(this.song); playback.play(this.song)
}, },
/** /**
@ -61,37 +61,37 @@ export default {
doPlayback () { doPlayback () {
switch (this.song.playbackState) { switch (this.song.playbackState) {
case 'playing': case 'playing':
playback.pause(); playback.pause()
break; break
case 'paused': case 'paused':
playback.resume(); playback.resume()
break; break
default: default:
this.playRightAwayyyyyyy(); this.playRightAwayyyyyyy()
break; break
} }
}, },
clicked ($e) { clicked ($e) {
this.$emit('itemClicked', this.song.id, $e); this.$emit('itemClicked', this.song.id, $e)
}, },
select () { select () {
this.selected = true; this.selected = true
}, },
deselect () { deselect () {
this.selected = false; this.selected = false
}, },
/** /**
* Toggle the "selected" state of the current component. * Toggle the "selected" state of the current component.
*/ */
toggleSelectedState () { toggleSelectedState () {
this.selected = !this.selected; this.selected = !this.selected
}, }
}, }
}; }
</script> </script>
<style lang="sass"> <style lang="sass">

View file

@ -6,7 +6,7 @@
</template> </template>
<script> <script>
import isMobile from 'ismobilejs'; import isMobile from 'ismobilejs'
export default { export default {
name: 'shared--song-list-controls-toggler', name: 'shared--song-list-controls-toggler',
@ -20,7 +20,7 @@ export default {
methods: { methods: {
toggleControls () { toggleControls () {
this.$emit('toggleControls'); this.$emit('toggleControls')
} }
} }
} }

View file

@ -39,9 +39,8 @@
</template> </template>
<script> <script>
import { assign } from 'lodash'; import { assign } from 'lodash'
import { queueStore } from '../../stores'; import addToMenu from './add-to-menu.vue'
import addToMenu from './add-to-menu.vue';
export default { export default {
name: 'shared--song-list-controls', name: 'shared--song-list-controls',
@ -57,52 +56,52 @@ export default {
queue: true, queue: true,
favorites: true, favorites: true,
playlists: true, playlists: true,
newPlaylist: true, newPlaylist: true
}, },
clearQueue: false, clearQueue: false,
deletePlaylist: false deletePlaylist: false
}, },
showingAddToMenu: false, showingAddToMenu: false,
numberOfQueuedSongs: 0 numberOfQueuedSongs: 0
}; }
}, },
computed: { computed: {
showClearQueueButton () { showClearQueueButton () {
return this.fullConfig.clearQueue; return this.fullConfig.clearQueue
}, },
showDeletePlaylistButton () { showDeletePlaylistButton () {
return this.fullConfig.deletePlaylist; return this.fullConfig.deletePlaylist
}, }
}, },
mounted () { mounted () {
assign(this.fullConfig, this.config); assign(this.fullConfig, this.config)
}, },
methods: { methods: {
shuffle () { shuffle () {
this.$emit('shuffleAll'); this.$emit('shuffleAll')
}, },
shuffleSelected () { shuffleSelected () {
this.$emit('shuffleSelected'); this.$emit('shuffleSelected')
}, },
clearQueue () { clearQueue () {
this.$emit('clearQueue'); this.$emit('clearQueue')
}, },
deletePlaylist () { deletePlaylist () {
this.$emit('deletePlaylist'); this.$emit('deletePlaylist')
}, },
closeAddToMenu () { closeAddToMenu () {
this.showingAddToMenu = false; this.showingAddToMenu = false
}
} }
} }
};
</script> </script>
<style lang="sass"></style> <style lang="sass"></style>

View file

@ -50,17 +50,17 @@
</template> </template>
<script> <script>
import { find, invokeMap, filter, map } from 'lodash'; import { find, invokeMap, filter, map } from 'lodash'
import isMobile from 'ismobilejs'; import isMobile from 'ismobilejs'
import $ from 'jquery'; import $ from 'jquery'
import { filterBy, orderBy, limitBy, event } from '../../utils'; import { filterBy, orderBy, limitBy, event } from '../../utils'
import { playlistStore, queueStore, songStore, favoriteStore } from '../../stores'; import { playlistStore, queueStore, songStore, favoriteStore } from '../../stores'
import { playback } from '../../services'; import { playback } from '../../services'
import router from '../../router'; import router from '../../router'
import songItem from './song-item.vue'; import songItem from './song-item.vue'
import songMenu from './song-menu.vue'; import songMenu from './song-menu.vue'
import infiniteScroll from '../../mixins/infinite-scroll'; import infiniteScroll from '../../mixins/infinite-scroll'
export default { export default {
name: 'song-list', name: 'song-list',
@ -77,8 +77,8 @@ export default {
sortingByAlbum: false, sortingByAlbum: false,
sortingByArtist: false, sortingByArtist: false,
selectedSongs: [], selectedSongs: [],
mutatedItems: [], mutatedItems: []
}; }
}, },
watch: { watch: {
@ -87,21 +87,21 @@ export default {
*/ */
items () { items () {
if (this.sortable === false) { if (this.sortable === false) {
this.sortKey = ''; this.sortKey = ''
} }
this.mutatedItems = this.items; this.mutatedItems = this.items
// Update the song count and duration status on parent. // Update the song count and duration status on parent.
this.$parent.updateMeta({ this.$parent.updateMeta({
songCount: this.items.length, songCount: this.items.length,
totalLength: songStore.getLength(this.items, true), totalLength: songStore.getLength(this.items, true)
}); })
}, },
selectedSongs (val) { selectedSongs (val) {
this.$parent.setSelectedSongs(val); this.$parent.setSelectedSongs(val)
}, }
}, },
computed: { computed: {
@ -113,8 +113,8 @@ export default {
'title', 'album.name', 'artist.name' 'title', 'album.name', 'artist.name'
), ),
this.numOfItems this.numOfItems
); )
}, }
}, },
methods: { methods: {
@ -125,41 +125,41 @@ export default {
*/ */
sort (key) { sort (key) {
if (this.sortable === false) { if (this.sortable === false) {
return; return
} }
this.sortKey = key; this.sortKey = key
this.order = 0 - this.order; this.order = 0 - this.order
this.sortingByAlbum = Array.isArray(this.sortKey) && this.sortKey[0] === 'album.name'; this.sortingByAlbum = Array.isArray(this.sortKey) && this.sortKey[0] === 'album.name'
this.sortingByArtist = Array.isArray(this.sortKey) && this.sortKey[0] === 'album.artist.name'; this.sortingByArtist = Array.isArray(this.sortKey) && this.sortKey[0] === 'album.artist.name'
this.mutatedItems = orderBy(this.items, this.sortKey, this.order); this.mutatedItems = orderBy(this.items, this.sortKey, this.order)
}, },
/** /**
* Execute the corresponding reaction(s) when the user presses Delete. * Execute the corresponding reaction(s) when the user presses Delete.
*/ */
handleDelete () { handleDelete () {
const songs = this.selectedSongs; const songs = this.selectedSongs
if (!songs.length) { if (!songs.length) {
return; return
} }
switch (this.type) { switch (this.type) {
case 'queue': case 'queue':
queueStore.unqueue(songs); queueStore.unqueue(songs)
break; break
case 'favorites': case 'favorites':
favoriteStore.unlike(songs); favoriteStore.unlike(songs)
break; break
case 'playlist': case 'playlist':
playlistStore.removeSongs(this.playlist, songs); playlistStore.removeSongs(this.playlist, songs)
break; break
default: default:
break; break
} }
this.clearSelection(); this.clearSelection()
}, },
/** /**
@ -168,24 +168,24 @@ export default {
* @param {Object} e The keydown event. * @param {Object} e The keydown event.
*/ */
handleEnter (e) { handleEnter (e) {
const songs = this.selectedSongs; const songs = this.selectedSongs
if (!songs.length) { if (!songs.length) {
return; return
} }
if (songs.length === 1) { if (songs.length === 1) {
// Just play the song // Just play the song
playback.play(songs[0]); playback.play(songs[0])
return; return
} }
switch (this.type) { switch (this.type) {
case 'queue': case 'queue':
// Play the first song selected if we're in Queue screen. // Play the first song selected if we're in Queue screen.
playback.play(songs[0]); playback.play(songs[0])
break; break
case 'favorites': case 'favorites':
case 'playlist': case 'playlist':
default: default:
@ -201,20 +201,20 @@ export default {
// Also, if there's only one song selected, play it right away. // Also, if there's only one song selected, play it right away.
// -------------------------------------------------------------------- // --------------------------------------------------------------------
// //
queueStore.queue(songs, false, e.shiftKey); queueStore.queue(songs, false, e.shiftKey)
this.$nextTick(() => { this.$nextTick(() => {
router.go('queue'); router.go('queue')
if (e.ctrlKey || e.metaKey || songs.length === 1) { if (e.ctrlKey || e.metaKey || songs.length === 1) {
playback.play(songs[0]); playback.play(songs[0])
} }
}); })
break; break
} }
this.clearSelection(); this.clearSelection()
}, },
/** /**
@ -225,7 +225,7 @@ export default {
* @return {Object} The Vue compoenent * @return {Object} The Vue compoenent
*/ */
getComponentBySongId (id) { getComponentBySongId (id) {
return find(this.$refs.rows, { song: { id } }); return find(this.$refs.rows, { song: { id }})
}, },
/** /**
@ -235,11 +235,11 @@ export default {
*/ */
handleA (e) { handleA (e) {
if (!e.metaKey && !e.ctrlKey) { if (!e.metaKey && !e.ctrlKey) {
return; return
} }
invokeMap(this.$refs.rows, 'select'); invokeMap(this.$refs.rows, 'select')
this.gatherSelected(); this.gatherSelected()
}, },
/** /**
@ -248,10 +248,10 @@ export default {
* @return {Array.<Object>} An array of Song objects * @return {Array.<Object>} An array of Song objects
*/ */
gatherSelected () { gatherSelected () {
const selectedRows = filter(this.$refs.rows, { selected: true }); const selectedRows = filter(this.$refs.rows, { selected: true })
const ids = map(selectedRows, row => row.song.id); const ids = map(selectedRows, row => row.song.id)
this.selectedSongs = songStore.byIds(ids); this.selectedSongs = songStore.byIds(ids)
}, },
/** /**
@ -269,32 +269,32 @@ export default {
* @param {Object} e * @param {Object} e
*/ */
itemClicked (songId, e) { itemClicked (songId, e) {
const row = this.getComponentBySongId(songId); const row = this.getComponentBySongId(songId)
// If we're on a touch device, or if Ctrl/Cmd key is pressed, just toggle selection. // If we're on a touch device, or if Ctrl/Cmd key is pressed, just toggle selection.
if (isMobile.any) { if (isMobile.any) {
this.toggleRow(row); this.toggleRow(row)
this.gatherSelected(); this.gatherSelected()
return; return
} }
if (e.ctrlKey || e.metaKey) { if (e.ctrlKey || e.metaKey) {
this.toggleRow(row); this.toggleRow(row)
} }
if (e.button === 0) { if (e.button === 0) {
if (!e.ctrlKey && !e.metaKey && !e.shiftKey) { if (!e.ctrlKey && !e.metaKey && !e.shiftKey) {
this.clearSelection(); this.clearSelection()
this.toggleRow(row); this.toggleRow(row)
} }
if (e.shiftKey && this.lastSelectedRow && this.lastSelectedRow.$el) { if (e.shiftKey && this.lastSelectedRow && this.lastSelectedRow.$el) {
this.selectRowsBetweenIndexes([this.lastSelectedRow.$el.rowIndex, row.$el.rowIndex]); this.selectRowsBetweenIndexes([this.lastSelectedRow.$el.rowIndex, row.$el.rowIndex])
} }
} }
this.gatherSelected(); this.gatherSelected()
}, },
/** /**
@ -303,17 +303,17 @@ export default {
* @param {Object} row The song-item component * @param {Object} row The song-item component
*/ */
toggleRow (row) { toggleRow (row) {
row.toggleSelectedState(); row.toggleSelectedState()
this.lastSelectedRow = row; this.lastSelectedRow = row
}, },
selectRowsBetweenIndexes (indexes) { selectRowsBetweenIndexes (indexes) {
indexes.sort((a, b) => a - b); indexes.sort((a, b) => a - b)
const rows = $(this.$refs.wrapper).find('tbody tr'); const rows = $(this.$refs.wrapper).find('tbody tr')
for (let i = indexes[0]; i <= indexes[1]; ++i) { for (let i = indexes[0]; i <= indexes[1]; ++i) {
this.getComponentBySongId($(rows[i - 1]).data('song-id')).select(); this.getComponentBySongId($(rows[i - 1]).data('song-id')).select()
} }
}, },
@ -321,8 +321,8 @@ export default {
* Clear the current selection on this song list. * Clear the current selection on this song list.
*/ */
clearSelection () { clearSelection () {
invokeMap(this.$refs.rows, 'deselect'); invokeMap(this.$refs.rows, 'deselect')
this.gatherSelected(); this.gatherSelected()
}, },
/** /**
@ -334,22 +334,22 @@ export default {
*/ */
dragStart (songId, e) { dragStart (songId, e) {
// If the user is dragging an unselected row, clear the current selection. // If the user is dragging an unselected row, clear the current selection.
const currentRow = this.getComponentBySongId(songId); const currentRow = this.getComponentBySongId(songId)
if (!currentRow.selected) { if (!currentRow.selected) {
this.clearSelection(); this.clearSelection()
currentRow.select(); currentRow.select()
this.gatherSelected(); this.gatherSelected()
} }
this.$nextTick(() => { this.$nextTick(() => {
const songIds = map(this.selectedSongs, 'id'); const songIds = map(this.selectedSongs, 'id')
e.dataTransfer.setData('application/x-koel.text+plain', songIds); e.dataTransfer.setData('application/x-koel.text+plain', songIds)
e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.effectAllowed = 'move'
// Set a fancy drop image using our ghost element. // Set a fancy drop image using our ghost element.
const $ghost = $('#dragGhost').text(`${songIds.length} song${songIds.length === 1 ? '' : 's'}`); const $ghost = $('#dragGhost').text(`${songIds.length} song${songIds.length === 1 ? '' : 's'}`)
e.dataTransfer.setDragImage($ghost[0], 0, 0); e.dataTransfer.setDragImage($ghost[0], 0, 0)
}); })
}, },
/** /**
@ -360,13 +360,13 @@ export default {
*/ */
allowDrop (songId, e) { allowDrop (songId, e) {
if (this.type !== 'queue') { if (this.type !== 'queue') {
return; return
} }
$(e.target).parents('tr').addClass('droppable'); $(e.target).parents('tr').addClass('droppable')
e.dataTransfer.dropEffect = 'move'; e.dataTransfer.dropEffect = 'move'
return false; return false
}, },
/** /**
@ -376,25 +376,23 @@ export default {
* @param {Object} e * @param {Object} e
*/ */
handleDrop (songId, e) { handleDrop (songId, e) {
console.log('dropping into', songId);
if (this.type !== 'queue') { if (this.type !== 'queue') {
return this.removeDroppableState(e) && false; return this.removeDroppableState(e) && false
} }
if (!e.dataTransfer.getData('application/x-koel.text+plain')) { if (!e.dataTransfer.getData('application/x-koel.text+plain')) {
return this.removeDroppableState(e) && false; return this.removeDroppableState(e) && false
} }
const songs = this.selectedSongs; const songs = this.selectedSongs
console.log('selected songs after drop:', songs);
if (!songs.length) { if (!songs.length) {
return this.removeDroppableState(e) && false; return this.removeDroppableState(e) && false
} }
queueStore.move(songs, songStore.byId(songId)); queueStore.move(songs, songStore.byId(songId))
return this.removeDroppableState(e) && false; return this.removeDroppableState(e) && false
}, },
/** /**
@ -403,21 +401,21 @@ export default {
* @param {Object} e * @param {Object} e
*/ */
removeDroppableState (e) { removeDroppableState (e) {
return $(e.target).parents('tr').removeClass('droppable'); return $(e.target).parents('tr').removeClass('droppable')
}, },
openContextMenu (songId, e) { openContextMenu (songId, e) {
// If the user is right-clicking an unselected row, // If the user is right-clicking an unselected row,
// clear the current selection and select it instead. // clear the current selection and select it instead.
const currentRow = this.getComponentBySongId(songId); const currentRow = this.getComponentBySongId(songId)
if (!currentRow.selected) { if (!currentRow.selected) {
this.clearSelection(); this.clearSelection()
currentRow.select(); currentRow.select()
this.gatherSelected(); this.gatherSelected()
} }
this.$nextTick(() => this.$refs.contextMenu.open(e.pageY, e.pageX)); this.$nextTick(() => this.$refs.contextMenu.open(e.pageY, e.pageX))
}, }
}, },
created () { created () {
@ -430,21 +428,21 @@ export default {
'song:played': song => { 'song:played': song => {
// If the song is at the end of the current displayed items, load more. // If the song is at the end of the current displayed items, load more.
if (this.type === 'queue' && this.items.indexOf(song) >= this.numOfItems) { if (this.type === 'queue' && this.items.indexOf(song) >= this.numOfItems) {
this.displayMore(); this.displayMore()
} }
// Scroll the item into view if it's lost into oblivion. // Scroll the item into view if it's lost into oblivion.
if (this.type === 'queue') { if (this.type === 'queue') {
const $wrapper = $(this.$refs.wrapper); const $wrapper = $(this.$refs.wrapper)
const $row = $wrapper.find(`.song-item[data-song-id="${song.id}"]`); const $row = $wrapper.find(`.song-item[data-song-id="${song.id}"]`)
if (!$row.length) { if (!$row.length) {
return; return
} }
if ($wrapper[0].getBoundingClientRect().top + $wrapper[0].getBoundingClientRect().height < if ($wrapper[0].getBoundingClientRect().top + $wrapper[0].getBoundingClientRect().height <
$row[0].getBoundingClientRect().top) { $row[0].getBoundingClientRect().top) {
$wrapper.scrollTop($wrapper.scrollTop() + $row.position().top); $wrapper.scrollTop($wrapper.scrollTop() + $row.position().top)
} }
} }
}, },
@ -452,7 +450,9 @@ export default {
/** /**
* Listen to 'filter:changed' event to filter the current list. * Listen to 'filter:changed' event to filter the current list.
*/ */
'filter:changed': q => this.q = q, 'filter:changed': q => {
this.q = q
},
/** /**
* Clears the current list's selection if the user has switched to another view. * Clears the current list's selection if the user has switched to another view.
@ -463,10 +463,10 @@ export default {
* Listen to 'song:selection-clear' (often broadcasted from the direct parent) * Listen to 'song:selection-clear' (often broadcasted from the direct parent)
* to clear the selected songs. * to clear the selected songs.
*/ */
'song:selection-clear': () => this.clearSelection(), 'song:selection-clear': () => this.clearSelection()
}); })
}, }
}; }
</script> </script>
<style lang="sass"> <style lang="sass">

View file

@ -30,14 +30,14 @@
</template> </template>
<script> <script>
import $ from 'jquery'; import $ from 'jquery'
import songMenuMethods from '../../mixins/song-menu-methods'; import songMenuMethods from '../../mixins/song-menu-methods'
import { event, isClipboardSupported, copyText } from '../../utils'; import { event, isClipboardSupported, copyText } from '../../utils'
import { sharedStore, songStore, queueStore, userStore, playlistStore } from '../../stores'; import { sharedStore, songStore, queueStore, userStore, playlistStore } from '../../stores'
import { playback, download } from '../../services'; import { playback, download } from '../../services'
import router from '../../router'; import router from '../../router'
export default { export default {
name: 'song-menu', name: 'song-menu',
@ -48,50 +48,50 @@ export default {
return { return {
playlistState: playlistStore.state, playlistState: playlistStore.state,
sharedState: sharedStore.state, sharedState: sharedStore.state,
copyable: isClipboardSupported(), copyable: isClipboardSupported()
}; }
}, },
computed: { computed: {
onlyOneSongSelected () { onlyOneSongSelected () {
return this.songs.length === 1; return this.songs.length === 1
}, },
firstSongPlaying () { firstSongPlaying () {
return this.songs[0] ? this.songs[0].playbackState === 'playing' : false; return this.songs[0] ? this.songs[0].playbackState === 'playing' : false
}, },
isAdmin () { isAdmin () {
return userStore.current.is_admin; return userStore.current.is_admin
}, }
}, },
methods: { methods: {
open (top = 0, left = 0) { open (top = 0, left = 0) {
if (!this.songs.length) { if (!this.songs.length) {
return; return
} }
this.top = top; this.top = top
this.left = left; this.left = left
this.shown = true; this.shown = true
this.$nextTick(() => { this.$nextTick(() => {
// Make sure the menu isn't off-screen // Make sure the menu isn't off-screen
if (this.$el.getBoundingClientRect().bottom > window.innerHeight) { if (this.$el.getBoundingClientRect().bottom > window.innerHeight) {
$(this.$el).css({ $(this.$el).css({
top: 'auto', top: 'auto',
bottom: 0, bottom: 0
}); })
} else { } else {
$(this.$el).css({ $(this.$el).css({
top: this.top, top: this.top,
bottom: 'auto', bottom: 'auto'
}); })
} }
this.$refs.menu.focus(); this.$refs.menu.focus()
}); })
}, },
/** /**
@ -100,21 +100,21 @@ export default {
doPlayback () { doPlayback () {
switch (this.songs[0].playbackState) { switch (this.songs[0].playbackState) {
case 'playing': case 'playing':
playback.pause(); playback.pause()
break; break
case 'paused': case 'paused':
playback.resume(); playback.resume()
break; break
default: default:
if (!queueStore.contains(this.songs[0])) { if (!queueStore.contains(this.songs[0])) {
queueStore.queueAfterCurrent(this.songs[0]); queueStore.queueAfterCurrent(this.songs[0])
} }
playback.play(this.songs[0]); playback.play(this.songs[0])
break; break
} }
this.close(); this.close()
}, },
/** /**
@ -122,36 +122,36 @@ export default {
*/ */
openEditForm () { openEditForm () {
if (this.songs.length) { if (this.songs.length) {
event.emit('songs:edit', this.songs); event.emit('songs:edit', this.songs)
} }
this.close(); this.close()
}, },
/** /**
* Load the album details screen. * Load the album details screen.
*/ */
viewAlbumDetails (album) { viewAlbumDetails (album) {
router.go(`album/${album.id}`); router.go(`album/${album.id}`)
this.close(); this.close()
}, },
/** /**
* Load the artist details screen. * Load the artist details screen.
*/ */
viewArtistDetails (artist) { viewArtistDetails (artist) {
router.go(`artist/${artist.id}`); router.go(`artist/${artist.id}`)
this.close(); this.close()
}, },
download () { download () {
download.fromSongs(this.songs); download.fromSongs(this.songs)
this.close(); this.close()
}, },
copyUrl () { copyUrl () {
copyText(songStore.getShareableUrl(this.songs[0])); copyText(songStore.getShareableUrl(this.songs[0]))
}, }
}, },
/** /**
@ -161,28 +161,28 @@ export default {
*/ */
mounted () { mounted () {
$(this.$el).find('.has-sub').hover(e => { $(this.$el).find('.has-sub').hover(e => {
const $submenu = $(e.target).find('.submenu:first'); const $submenu = $(e.target).find('.submenu:first')
if (!$submenu.length) { if (!$submenu.length) {
return; return
} }
$submenu.show(); $submenu.show()
// Make sure the submenu isn't off-screen // Make sure the submenu isn't off-screen
if ($submenu[0].getBoundingClientRect().bottom > window.innerHeight) { if ($submenu[0].getBoundingClientRect().bottom > window.innerHeight) {
$submenu.css({ $submenu.css({
top: 'auto', top: 'auto',
bottom: 0, bottom: 0
}); })
} }
}, e => { }, e => {
$(e.target).find('.submenu:first').hide().css({ $(e.target).find('.submenu:first').hide().css({
top: 0, top: 0,
bottom: 'auto', bottom: 'auto'
}); })
}); })
}, }
}; }
</script> </script>
<style lang="sass" scoped> <style lang="sass" scoped>

View file

@ -42,7 +42,7 @@ export default {
// They call the Rising Sun // They call the Rising Sun
// And it's been the ruin of many a poor boy // And it's been the ruin of many a poor boy
// And God, I know I'm one. // And God, I know I'm one.
}; }
</script> </script>
<style lang="sass"> <style lang="sass">

View file

@ -9,8 +9,8 @@
<script> <script>
export default { export default {
props: ['showing'], props: ['showing']
}; }
</script> </script>
<style lang="sass"> <style lang="sass">

View file

@ -21,8 +21,8 @@
</template> </template>
<script> <script>
import $ from 'jquery'; import $ from 'jquery'
import { filterBy } from '../../utils'; import { filterBy } from '../../utils'
export default { export default {
props: ['options', 'value', 'items'], props: ['options', 'value', 'items'],
@ -31,14 +31,14 @@ export default {
return { return {
filter: '', filter: '',
showingResult: false, showingResult: false,
mutatedValue: this.value, mutatedValue: this.value
}; }
}, },
computed: { computed: {
displayedItems () { displayedItems () {
return filterBy(this.items, this.filter, this.options.filterKey); return filterBy(this.items, this.filter, this.options.filterKey)
}, }
}, },
methods: { methods: {
@ -46,36 +46,36 @@ export default {
* Navigate down the result list. * Navigate down the result list.
*/ */
down (e) { down (e) {
const selected = $(this.$el).find('.result li.selected'); const selected = $(this.$el).find('.result li.selected')
if (!selected.length || !selected.removeClass('selected').next('li').addClass('selected').length) { if (!selected.length || !selected.removeClass('selected').next('li').addClass('selected').length) {
$(this.$el).find('.result li:first').addClass('selected'); $(this.$el).find('.result li:first').addClass('selected')
} }
this.scrollSelectedIntoView(false); this.scrollSelectedIntoView(false)
this.apply(); this.apply()
}, },
/** /**
* Navigate up the result list. * Navigate up the result list.
*/ */
up (e) { up (e) {
const selected = $(this.$el).find('.result li.selected'); const selected = $(this.$el).find('.result li.selected')
if (!selected.length || !selected.removeClass('selected').prev('li').addClass('selected').length) { if (!selected.length || !selected.removeClass('selected').prev('li').addClass('selected').length) {
$(this.$el).find('.result li:last').addClass('selected'); $(this.$el).find('.result li:last').addClass('selected')
} }
this.scrollSelectedIntoView(true); this.scrollSelectedIntoView(true)
this.apply(); this.apply()
}, },
/** /**
* Handle ENTER or TAB keydown events. * Handle ENTER or TAB keydown events.
*/ */
enter () { enter () {
this.apply(); this.apply()
this.showingResult = false; this.showingResult = false
}, },
keyup (e) { keyup (e) {
@ -84,41 +84,41 @@ export default {
* The actually result navigation is handled by this.up() and this.down(). * The actually result navigation is handled by this.up() and this.down().
*/ */
if (e.keyCode === 38 || e.keyCode === 40) { if (e.keyCode === 38 || e.keyCode === 40) {
e.stopPropagation(); e.stopPropagation()
e.preventDefault(); e.preventDefault()
return; return
} }
// If it's an ENTER or TAB key, don't do anything. // If it's an ENTER or TAB key, don't do anything.
// We've handled ENTER & TAB on keydown. // We've handled ENTER & TAB on keydown.
if (e.keyCode === 13 || e.keyCode === 9) { if (e.keyCode === 13 || e.keyCode === 9) {
return; return
} }
// Hide the typeahead results and reset the value if ESC is pressed. // Hide the typeahead results and reset the value if ESC is pressed.
if (e.keyCode === 27) { if (e.keyCode === 27) {
this.showingResult = false; this.showingResult = false
return; return
} }
this.filter = this.mutatedValue; this.filter = this.mutatedValue
this.showingResult = true; this.showingResult = true
}, },
resultClick (e) { resultClick (e) {
$(this.$el).find('.result li.selected').removeClass('selected'); $(this.$el).find('.result li.selected').removeClass('selected')
$(e.target).addClass('selected'); $(e.target).addClass('selected')
this.apply(); this.apply()
this.showingResult = false; this.showingResult = false
}, },
apply () { apply () {
this.mutatedValue = $(this.$el).find('.result li.selected').text().trim() || this.mutatedValue; 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. // In Vue 2.0, we can use v-model on custom components like this.
this.$emit('input', this.mutatedValue); this.$emit('input', this.mutatedValue)
}, },
/** /**
@ -127,24 +127,24 @@ export default {
* @param {boolean} alignTop Whether the item should be aligned to top of its container. * @param {boolean} alignTop Whether the item should be aligned to top of its container.
*/ */
scrollSelectedIntoView (alignTop) { scrollSelectedIntoView (alignTop) {
const elem = $(this.$el).find('.result li.selected')[0]; const elem = $(this.$el).find('.result li.selected')[0]
if (!elem) { if (!elem) {
return; return
} }
const elemRect = elem.getBoundingClientRect(); const elemRect = elem.getBoundingClientRect()
const containerRect = elem.offsetParent.getBoundingClientRect(); const containerRect = elem.offsetParent.getBoundingClientRect()
if (elemRect.bottom > containerRect.bottom || elemRect.top < containerRect.top) { if (elemRect.bottom > containerRect.bottom || elemRect.top < containerRect.top) {
elem.scrollIntoView(alignTop); elem.scrollIntoView(alignTop)
} }
}, },
hideResults () { hideResults () {
this.showingResult = false; this.showingResult = false
}, }
}, }
}; }
</script> </script>
<style lang="sass" scoped> <style lang="sass" scoped>

View file

@ -43,11 +43,11 @@
</template> </template>
<script> <script>
import { clone, assign } from 'lodash'; import { clone, assign } from 'lodash'
import swal from 'sweetalert'; import swal from 'sweetalert'
import { userStore } from '../../stores'; import { userStore } from '../../stores'
import router from '../../router'; import router from '../../router'
export default { export default {
props: ['user'], props: ['user'],
@ -56,8 +56,8 @@ export default {
return { return {
editing: false, editing: false,
confirmingDelete: false, confirmingDelete: false,
cached: {}, cached: {}
}; }
}, },
computed: { computed: {
@ -67,8 +67,8 @@ export default {
* @return {Boolean} * @return {Boolean}
*/ */
isCurrentUser () { isCurrentUser () {
return this.user.id === userStore.current.id; return this.user.id === userStore.current.id
}, }
}, },
methods: { methods: {
@ -78,14 +78,14 @@ export default {
*/ */
edit () { edit () {
if (this.isCurrentUser) { if (this.isCurrentUser) {
router.go('profile'); router.go('profile')
return; return
} }
// Keep a cached version of the user for rolling back. // Keep a cached version of the user for rolling back.
this.cached = clone(this.user); this.cached = clone(this.user)
this.editing = true; this.editing = true
}, },
/** /**
@ -93,8 +93,8 @@ export default {
*/ */
cancelEdit () { cancelEdit () {
// Restore the original user's properties // Restore the original user's properties
assign(this.user, this.cached); assign(this.user, this.cached)
this.editing = false; this.editing = false
}, },
/** /**
@ -102,8 +102,8 @@ export default {
*/ */
update () { update () {
userStore.update(this.user, this.user.name, this.user.email, this.user.password).then(u => { userStore.update(this.user, this.user.name, this.user.email, this.user.password).then(u => {
this.editing = false; this.editing = false
}); })
}, },
/** /**
@ -116,15 +116,15 @@ export default {
type: 'warning', type: 'warning',
showCancelButton: true, showCancelButton: true,
confirmButtonText: 'Certainly', confirmButtonText: 'Certainly',
cancelButtonText: 'Oops', cancelButtonText: 'Oops'
}, () => { }, () => {
userStore.destroy(this.user).then(() => { userStore.destroy(this.user).then(() => {
this.$destroy(true); this.$destroy(true)
}); })
}); })
}, }
}, }
}; }
</script> </script>
<style lang="sass"> <style lang="sass">

View file

@ -10,18 +10,18 @@
</template> </template>
<script> <script>
import isMobile from 'ismobilejs'; import isMobile from 'ismobilejs'
import { event } from '../../utils'; import { event } from '../../utils'
import { preferenceStore as preferences } from '../../stores'; import { preferenceStore as preferences } from '../../stores'
export default { export default {
props: ['mode', 'for'], props: ['mode', 'for'],
data () { data () {
return { return {
mutatedMode: this.mode, mutatedMode: this.mode
}; }
}, },
computed: { computed: {
@ -31,32 +31,32 @@ export default {
* @return {string} * @return {string}
*/ */
preferenceKey () { preferenceKey () {
return `${this.for}ViewMode`; return `${this.for}ViewMode`
}, }
}, },
methods: { methods: {
setMode (mode) { setMode (mode) {
preferences[this.preferenceKey] = this.mutatedMode = mode; preferences[this.preferenceKey] = this.mutatedMode = mode
this.$parent.changeViewMode(mode); this.$parent.changeViewMode(mode)
}, }
}, },
created () { created () {
event.on('koel:ready', () => { event.on('koel:ready', () => {
this.mutatedMode = preferences[this.preferenceKey]; this.mutatedMode = preferences[this.preferenceKey]
// If the value is empty, we set a default mode. // If the value is empty, we set a default mode.
// On mobile, the mode should be 'listing'. // On mobile, the mode should be 'listing'.
// For desktop, 'thumbnails'. // For desktop, 'thumbnails'.
if (!this.mutatedMode) { if (!this.mutatedMode) {
this.mutatedMode = isMobile.phone ? 'list' : 'thumbnails'; this.mutatedMode = isMobile.phone ? 'list' : 'thumbnails'
} }
this.setMode(this.mutatedMode); this.setMode(this.mutatedMode)
}); })
}, }
}; }
</script> </script>
<style lang="sass" scoped> <style lang="sass" scoped>

View file

@ -40,12 +40,13 @@
</template> </template>
<script> <script>
import { map, cloneDeep } from 'lodash'; import { map, cloneDeep } from 'lodash'
import $ from 'jquery'; import $ from 'jquery'
import rangeslider from 'rangeslider.js'; // eslint-disable-next-line no-unused-vars
import rangeslider from 'rangeslider.js'
import { isAudioContextSupported, event } from '../../utils'; import { isAudioContextSupported, event } from '../../utils'
import { equalizerStore, preferenceStore as preferences } from '../../stores'; import { equalizerStore, preferenceStore as preferences } from '../../stores'
export default { export default {
data () { data () {
@ -53,20 +54,20 @@ export default {
idx: 0, idx: 0,
bands: [], bands: [],
preampGainValue: 0, preampGainValue: 0,
selectedPresetIndex: -1, selectedPresetIndex: -1
}; }
}, },
computed: { computed: {
presets () { presets () {
let clonedPreset = cloneDeep(equalizerStore.presets); const clonedPreset = cloneDeep(equalizerStore.presets)
// Prepend an empty option for instruction purpose. // Prepend an empty option for instruction purpose.
clonedPreset.unshift({ clonedPreset.unshift({
id: -1, id: -1,
name: 'Preset', name: 'Preset'
}); })
return clonedPreset; return clonedPreset
}, }
}, },
watch: { watch: {
@ -78,12 +79,12 @@ export default {
/** /**
* Save the selected preset (index) into local storage every time the value's changed. * Save the selected preset (index) into local storage every time the value's changed.
*/ */
preferences.selectedPreset = val; preferences.selectedPreset = val
if (~~val !== -1) { if (~~val !== -1) {
this.loadPreset(equalizerStore.getPresetById(val)); this.loadPreset(equalizerStore.getPresetById(val))
}
} }
},
}, },
methods: { methods: {
@ -93,55 +94,56 @@ export default {
* @param {Element} player The audio player's DOM. * @param {Element} player The audio player's DOM.
*/ */
init (player) { init (player) {
const settings = equalizerStore.get(); const settings = equalizerStore.get()
const AudioContext = window.AudioContext || const AudioContext = window.AudioContext ||
window.webkitAudioContext || window.webkitAudioContext ||
window.mozAudioContext || window.mozAudioContext ||
window.oAudioContext || window.oAudioContext ||
window.msAudioContext; window.msAudioContext
const context = new AudioContext(); const context = new AudioContext()
this.preampGainNode = context.createGain(); this.preampGainNode = context.createGain()
this.changePreampGain(settings.preamp); this.changePreampGain(settings.preamp)
const source = context.createMediaElementSource(player); const source = context.createMediaElementSource(player)
source.connect(this.preampGainNode); source.connect(this.preampGainNode)
let prevFilter = null; let prevFilter = null
// Create 10 bands with the frequencies similar to those of Winamp and connect them together. // Create 10 bands with the frequencies similar to those of Winamp and connect them together.
[60, 170, 310, 600, 1000, 3000, 6000, 12000, 14000, 16000].forEach((f, i) => { const freqs = [60, 170, 310, 600, 1000, 3000, 6000, 12000, 14000, 16000]
const filter = context.createBiquadFilter(); freqs.forEach((f, i) => {
const filter = context.createBiquadFilter()
if (i === 0) { if (i === 0) {
filter.type = 'lowshelf'; filter.type = 'lowshelf'
} else if (i === 9) { } else if (i === 9) {
filter.type = 'highshelf'; filter.type = 'highshelf'
} else { } else {
filter.type = 'peaking'; filter.type = 'peaking'
} }
filter.gain.value = settings.gains[i] ? settings.gains[i] : 0; filter.gain.value = settings.gains[i] ? settings.gains[i] : 0
filter.Q.value = 1; filter.Q.value = 1
filter.frequency.value = f; filter.frequency.value = f
prevFilter ? prevFilter.connect(filter) : this.preampGainNode.connect(filter); prevFilter ? prevFilter.connect(filter) : this.preampGainNode.connect(filter)
prevFilter = filter; prevFilter = filter
this.bands.push({ this.bands.push({
filter, filter,
label: (f + '').replace('000', 'K'), label: (f + '').replace('000', 'K')
}); })
}); })
prevFilter.connect(context.destination); prevFilter.connect(context.destination)
this.$nextTick(this.createRangeSliders); this.$nextTick(this.createRangeSliders)
// Now we set this value to trigger the audio processing. // Now we set this value to trigger the audio processing.
this.selectedPresetIndex = preferences.selectedPreset; this.selectedPresetIndex = preferences.selectedPreset
}, },
/** /**
@ -165,9 +167,9 @@ export default {
*/ */
onSlide: (position, value) => { onSlide: (position, value) => {
if ($(el).parents('.band').is('.preamp')) { if ($(el).parents('.band').is('.preamp')) {
this.changePreampGain(value); this.changePreampGain(value)
} else { } else {
this.changeFilterGain(this.bands[i - 1].filter, value); this.changeFilterGain(this.bands[i - 1].filter, value)
} }
}, },
@ -175,11 +177,11 @@ export default {
* Save the settings and set the preset index to -1 (which is None) on slideEnd. * Save the settings and set the preset index to -1 (which is None) on slideEnd.
*/ */
onSlideEnd: () => { onSlideEnd: () => {
this.selectedPresetIndex = -1; this.selectedPresetIndex = -1
this.save(); this.save()
}, }
}); })
}); })
}, },
/** /**
@ -188,8 +190,8 @@ export default {
* @param {Number} dbValue The value of the gain, in dB. * @param {Number} dbValue The value of the gain, in dB.
*/ */
changePreampGain (dbValue) { changePreampGain (dbValue) {
this.preampGainValue = dbValue; this.preampGainValue = dbValue
this.preampGainNode.gain.value = Math.pow(10, dbValue / 20); this.preampGainNode.gain.value = Math.pow(10, dbValue / 20)
}, },
/** /**
@ -199,7 +201,7 @@ export default {
* @param {Object} value Value of the gain, in dB. * @param {Object} value Value of the gain, in dB.
*/ */
changeFilterGain (filter, value) { changeFilterGain (filter, value) {
filter.gain.value = value; filter.gain.value = value
}, },
/** /**
@ -209,37 +211,37 @@ export default {
$('#equalizer input[type=range]').each((i, input) => { $('#equalizer input[type=range]').each((i, input) => {
// We treat our preamp slider differently. // We treat our preamp slider differently.
if ($(input).parents('.band').is('.preamp')) { if ($(input).parents('.band').is('.preamp')) {
this.changePreampGain(preset.preamp); this.changePreampGain(preset.preamp)
} else { } else {
this.changeFilterGain(this.bands[i - 1].filter, preset.gains[i - 1]); this.changeFilterGain(this.bands[i - 1].filter, preset.gains[i - 1])
input.value = preset.gains[i - 1]; input.value = preset.gains[i - 1]
} }
}); })
this.$nextTick(() => { this.$nextTick(() => {
// Update the slider values into GUI. // Update the slider values into GUI.
$('#equalizer input[type="range"]').rangeslider('update', true); $('#equalizer input[type="range"]').rangeslider('update', true)
}); })
this.save(); this.save()
}, },
/** /**
* Save the current user's equalizer preferences into local storage. * Save the current user's equalizer preferences into local storage.
*/ */
save () { save () {
equalizerStore.set(this.preampGainValue, map(this.bands, 'filter.gain.value')); equalizerStore.set(this.preampGainValue, map(this.bands, 'filter.gain.value'))
}, }
}, },
mounted () { mounted () {
event.on('equalizer:init', player => { event.on('equalizer:init', player => {
if (isAudioContextSupported()) { if (isAudioContextSupported()) {
this.init(player); this.init(player)
}
})
}
} }
});
},
};
</script> </script>
<style lang="sass"> <style lang="sass">

View file

@ -56,14 +56,13 @@
</template> </template>
<script> <script>
import config from '../../config'; import { playback } from '../../services'
import { playback } from '../../services'; import { isAudioContextSupported, event } from '../../utils'
import { isAudioContextSupported, event } from '../../utils'; import { songStore, favoriteStore, preferenceStore } from '../../stores'
import { songStore, favoriteStore, preferenceStore } from '../../stores';
import soundBar from '../shared/sound-bar.vue'; import soundBar from '../shared/sound-bar.vue'
import equalizer from './equalizer.vue'; import equalizer from './equalizer.vue'
import volume from './volume.vue'; import volume from './volume.vue'
export default { export default {
data () { data () {
@ -80,8 +79,8 @@ export default {
* *
* @type {Boolean} * @type {Boolean}
*/ */
useEqualizer: isAudioContextSupported(), useEqualizer: isAudioContextSupported()
}; }
}, },
components: { soundBar, equalizer, volume }, components: { soundBar, equalizer, volume },
@ -93,7 +92,7 @@ export default {
* @return {?Object} * @return {?Object}
*/ */
prev () { prev () {
return playback.previous; return playback.previous
}, },
/** /**
@ -102,8 +101,8 @@ export default {
* @return {?Object} * @return {?Object}
*/ */
next () { next () {
return playback.next; return playback.next
}, }
}, },
methods: { methods: {
@ -111,14 +110,14 @@ export default {
* Play the previous song in queue. * Play the previous song in queue.
*/ */
playPrev () { playPrev () {
return playback.playPrev(); return playback.playPrev()
}, },
/** /**
* Play the next song in queue. * Play the next song in queue.
*/ */
playNext () { playNext () {
return playback.playNext(); return playback.playNext()
}, },
/** /**
@ -127,24 +126,24 @@ export default {
*/ */
resume () { resume () {
if (!this.song.id) { if (!this.song.id) {
return playback.playFirstInQueue(); return playback.playFirstInQueue()
} }
playback.resume(); playback.resume()
}, },
/** /**
* Pause the playback. * Pause the playback.
*/ */
pause () { pause () {
playback.pause(); playback.pause()
}, },
/** /**
* Change the repeat mode. * Change the repeat mode.
*/ */
changeRepeatMode () { changeRepeatMode () {
return playback.changeRepeatMode(); return playback.changeRepeatMode()
}, },
/** /**
@ -152,22 +151,22 @@ export default {
*/ */
like () { like () {
if (!this.song.id) { if (!this.song.id) {
return; return
} }
favoriteStore.toggleOne(this.song); favoriteStore.toggleOne(this.song)
}, },
/** /**
* Toggle hide or show the extra panel. * Toggle hide or show the extra panel.
*/ */
toggleExtraPanel () { toggleExtraPanel () {
preferenceStore.set('showExtraPanel', !this.prefs.showExtraPanel); preferenceStore.set('showExtraPanel', !this.prefs.showExtraPanel)
}, },
closeEqualizer () { closeEqualizer () {
this.showEqualizer = false; this.showEqualizer = false
}, }
}, },
created () { created () {
@ -180,20 +179,24 @@ export default {
* @return {Boolean} * @return {Boolean}
*/ */
'song:played': song => { 'song:played': song => {
this.song = song; this.song = song
this.cover = this.song.album.cover; this.cover = this.song.album.cover
}, },
/** /**
* Listen to main-content-view:load event and highlight the Queue icon if * Listen to main-content-view:load event and highlight the Queue icon if
* the Queue screen is being loaded. * the Queue screen is being loaded.
*/ */
'main-content-view:load': view => this.viewingQueue = view === 'queue', 'main-content-view:load': view => {
this.viewingQueue = view === 'queue'
'koel:teardown': () => this.song = songStore.stub,
});
}, },
};
'koel:teardown': () => {
this.song = songStore.stub
}
})
}
}
</script> </script>
<style lang="sass"> <style lang="sass">

View file

@ -7,13 +7,13 @@
</template> </template>
<script> <script>
import { playback } from '../../services'; import { playback } from '../../services'
export default { export default {
data () { data () {
return { return {
muted: false, muted: false
}; }
}, },
methods: { methods: {
@ -21,21 +21,19 @@ export default {
* Mute the volume. * Mute the volume.
*/ */
mute () { mute () {
this.muted = true; this.muted = true
return playback.mute()
return playback.mute();
}, },
/** /**
* Unmute the volume. * Unmute the volume.
*/ */
unmute () { unmute () {
this.muted = false; this.muted = false
return playback.unmute()
return playback.unmute(); }
}, }
}, }
};
</script> </script>
<style lang="sass"> <style lang="sass">

View file

@ -14,18 +14,18 @@
</template> </template>
<script> <script>
import config from '../../config'; import config from '../../config'
import { event } from '../../utils'; import { event } from '../../utils'
import searchForm from './search-form.vue'; import searchForm from './search-form.vue'
import userBadge from './user-badge.vue'; import userBadge from './user-badge.vue'
export default { export default {
components: { searchForm, userBadge }, components: { searchForm, userBadge },
data () { data () {
return { return {
appTitle: config.appTitle, appTitle: config.appTitle
}; }
}, },
methods: { methods: {
@ -33,17 +33,17 @@ export default {
* No I'm not documenting this. * No I'm not documenting this.
*/ */
toggleSidebar () { toggleSidebar () {
event.emit('sidebar:toggle'); event.emit('sidebar:toggle')
}, },
/** /**
* or this. * or this.
*/ */
toggleSearchForm () { toggleSearchForm () {
event.emit('search:toggle'); event.emit('search:toggle')
}, }
}, }
}; }
</script> </script>
<style lang="sass"> <style lang="sass">

View file

@ -10,10 +10,10 @@
</template> </template>
<script> <script>
import isMobile from 'ismobilejs'; import isMobile from 'ismobilejs'
import { debounce } from 'lodash'; import { debounce } from 'lodash'
import { event } from '../../utils'; import { event } from '../../utils'
export default { export default {
name: 'site-header--search-form', name: 'site-header--search-form',
@ -21,8 +21,8 @@ export default {
data () { data () {
return { return {
q: '', q: '',
showing: !isMobile.phone, showing: !isMobile.phone
}; }
}, },
methods: { methods: {
@ -30,21 +30,21 @@ export default {
* Limit the filter's execution rate using lodash's debounce. * Limit the filter's execution rate using lodash's debounce.
*/ */
filter: debounce(function () { filter: debounce(function () {
event.emit('filter:changed', this.q); event.emit('filter:changed', this.q)
}, 200), }, 200)
}, },
created () { created () {
event.on('search:toggle', () => { event.on('search:toggle', () => {
this.showing = !this.showing; this.showing = !this.showing
}); })
event.on('koel:teardown', () => { event.on('koel:teardown', () => {
this.q = ''; this.q = ''
this.filter(); this.filter()
}) })
}, }
}; }
</script> </script>
<style lang="sass"> <style lang="sass">

View file

@ -10,24 +10,24 @@
</template> </template>
<script> <script>
import { userStore } from '../../stores'; import { userStore } from '../../stores'
import { event } from '../../utils'; import { event } from '../../utils'
export default { export default {
name: 'site-header--user-badge', name: 'site-header--user-badge',
data () { data () {
return { return {
state: userStore.state, state: userStore.state
}; }
}, },
methods: { methods: {
logout () { logout () {
event.emit('logout'); event.emit('logout')
}, }
}, }
}; }
</script> </script>
<style lang="sass"> <style lang="sass">

View file

@ -1,4 +1,4 @@
export default { export default {
unknownCover: (typeof window !== 'undefined' ? window.location.href : '/') + 'public/img/covers/unknown-album.png', unknownCover: (typeof window !== 'undefined' ? window.location.href : '/') + 'public/img/covers/unknown-album.png',
appTitle: 'Koel', appTitle: 'Koel'
}; }

View file

@ -6,14 +6,14 @@
export const clickawayDirective = { export const clickawayDirective = {
bind (el, { value }) { bind (el, { value }) {
if (typeof value !== 'function') { if (typeof value !== 'function') {
console.warn(`Expect a function, got ${value}`); console.warn(`Expect a function, got ${value}`)
return; return
} }
document.addEventListener('click', e => { document.addEventListener('click', e => {
if (!el.contains(e.target)) { if (!el.contains(e.target)) {
value(); value()
}
})
} }
});
},
} }

View file

@ -1,10 +1,8 @@
import Vue from 'vue';
/** /**
* A simple directive to set focus into an input field when it's shown. * A simple directive to set focus into an input field when it's shown.
*/ */
export const focusDirective = { export const focusDirective = {
inserted (el) { inserted (el) {
el.focus(); el.focus()
}, }
}; }

View file

@ -1,2 +1,2 @@
export * from './focus'; export * from './focus'
export * from './clickaway'; export * from './clickaway'

View file

@ -1,7 +1,7 @@
import Vue from 'vue'; import Vue from 'vue'
import { event } from './utils'; import { event } from './utils'
import { http } from './services'; import { http } from './services'
/** /**
* For Ancelot, the ancient cross of war * For Ancelot, the ancient cross of war
* for the holy town of Gods * for the holy town of Gods
@ -12,7 +12,7 @@ new Vue({
el: '#app', el: '#app',
render: h => h(require('./app.vue')), render: h => h(require('./app.vue')),
created () { created () {
event.init(); event.init()
http.init(); http.init()
}, }
}); })

View file

@ -2,13 +2,13 @@
* Add necessary functionalities into a view that contains a song-list component. * Add necessary functionalities into a view that contains a song-list component.
*/ */
import { assign } from 'lodash'; import { assign } from 'lodash'
import isMobile from 'ismobilejs'; import isMobile from 'ismobilejs'
import { playback } from '../services'; import { playback } from '../services'
import songList from '../components/shared/song-list.vue'; import songList from '../components/shared/song-list.vue'
import songListControls from '../components/shared/song-list-controls.vue'; import songListControls from '../components/shared/song-list-controls.vue'
import controlsToggler from '../components/shared/song-list-controls-toggler.vue'; import controlsToggler from '../components/shared/song-list-controls-toggler.vue'
export default { export default {
components: { songList, songListControls, controlsToggler }, components: { songList, songListControls, controlsToggler },
@ -18,34 +18,34 @@ export default {
state: null, state: null,
meta: { meta: {
songCount: 0, songCount: 0,
totalLength: '00:00', totalLength: '00:00'
}, },
selectedSongs: [], selectedSongs: [],
showingControls: false, showingControls: false,
songListControlConfig: {}, songListControlConfig: {},
isPhone: isMobile.phone, isPhone: isMobile.phone
}; }
}, },
methods: { methods: {
setSelectedSongs (songs) { setSelectedSongs (songs) {
this.selectedSongs = songs; this.selectedSongs = songs
}, },
updateMeta (meta) { updateMeta (meta) {
this.meta = assign(this.meta, meta); this.meta = assign(this.meta, meta)
}, },
shuffleAll () { shuffleAll () {
playback.queueAndPlay(this.state.songs, true); playback.queueAndPlay(this.state.songs, true)
}, },
shuffleSelected () { shuffleSelected () {
playback.queueAndPlay(this.selectedSongs, true); playback.queueAndPlay(this.selectedSongs, true)
}, },
toggleControls () { toggleControls () {
this.showingControls = !this.showingControls; this.showingControls = !this.showingControls
}
}
} }
},
};

View file

@ -1,7 +1,7 @@
import $ from 'jquery'; import $ from 'jquery'
import { event } from '../utils'; import { event } from '../utils'
import toTopButton from '../components/shared/to-top-button.vue'; import toTopButton from '../components/shared/to-top-button.vue'
/** /**
* Add a "infinite scroll" functionality to any component using this mixin. * Add a "infinite scroll" functionality to any component using this mixin.
@ -15,8 +15,8 @@ export default {
return { return {
numOfItems: 30, // Number of currently loaded and displayed items numOfItems: 30, // Number of currently loaded and displayed items
perPage: 30, // Number of items to be loaded per "page" perPage: 30, // Number of items to be loaded per "page"
showBackToTop: false, showBackToTop: false
}; }
}, },
methods: { methods: {
@ -24,32 +24,32 @@ export default {
// Here we check if the user has scrolled to the end of the wrapper (or 32px to the end). // Here we check if the user has scrolled to the end of the wrapper (or 32px to the end).
// If that's true, load more items. // If that's true, load more items.
if (e.target.scrollTop + e.target.clientHeight >= e.target.scrollHeight - 32) { if (e.target.scrollTop + e.target.clientHeight >= e.target.scrollHeight - 32) {
this.displayMore(); this.displayMore()
} }
this.showBackToTop = e.target.scrollTop > 64; this.showBackToTop = e.target.scrollTop > 64
}, },
/** /**
* Load and display more items into the scrollable area. * Load and display more items into the scrollable area.
*/ */
displayMore () { displayMore () {
this.numOfItems += this.perPage; this.numOfItems += this.perPage
}, },
/** /**
* Scroll to top of the wrapper. * Scroll to top of the wrapper.
*/ */
scrollToTop () { scrollToTop () {
$(this.$refs.wrapper).animate({ scrollTop: 0 }, 500); $(this.$refs.wrapper).animate({ scrollTop: 0 }, 500)
this.showBackToTop = false; this.showBackToTop = false
}, }
}, },
created () { created () {
event.on('koel:teardown', () => { event.on('koel:teardown', () => {
this.numOfItems = 30; this.numOfItems = 30
this.showBackToTop = false; this.showBackToTop = false
}); })
}, }
}; }

View file

@ -1,6 +1,6 @@
import $ from 'jquery'; import $ from 'jquery'
import { queueStore, playlistStore, favoriteStore } from '../stores'; import { queueStore, playlistStore, favoriteStore } from '../stores'
/** /**
* Includes the methods triggerable on a song (context) menu. * Includes the methods triggerable on a song (context) menu.
@ -13,8 +13,8 @@ export default {
return { return {
shown: false, shown: false,
top: 0, top: 0,
left: 0, left: 0
}; }
}, },
methods: { methods: {
@ -24,41 +24,40 @@ export default {
* Close all submenus. * Close all submenus.
*/ */
close () { close () {
$(this.$el).find('.submenu').hide(); $(this.$el).find('.submenu').hide()
this.shown = false
this.shown = false;
}, },
/** /**
* Queue select songs after the current song. * Queue select songs after the current song.
*/ */
queueSongsAfterCurrent () { queueSongsAfterCurrent () {
queueStore.queueAfterCurrent(this.songs); queueStore.queueAfterCurrent(this.songs)
this.close(); this.close()
}, },
/** /**
* Queue selected songs to bottom of queue. * Queue selected songs to bottom of queue.
*/ */
queueSongsToBottom () { queueSongsToBottom () {
queueStore.queue(this.songs); queueStore.queue(this.songs)
this.close(); this.close()
}, },
/** /**
* Queue selected songs to top of queue. * Queue selected songs to top of queue.
*/ */
queueSongsToTop () { queueSongsToTop () {
queueStore.queue(this.songs, false, true); queueStore.queue(this.songs, false, true)
this.close(); this.close()
}, },
/** /**
* Add the selected songs into Favorites. * Add the selected songs into Favorites.
*/ */
addSongsToFavorite () { addSongsToFavorite () {
favoriteStore.like(this.songs); favoriteStore.like(this.songs)
this.close(); this.close()
}, },
/** /**
@ -67,8 +66,8 @@ export default {
* @param {Object} playlist The playlist. * @param {Object} playlist The playlist.
*/ */
addSongsToExistingPlaylist (playlist) { addSongsToExistingPlaylist (playlist) {
playlistStore.addSongs(playlist, this.songs); playlistStore.addSongs(playlist, this.songs)
this.close(); this.close()
}, }
}, }
}; }

View file

@ -1,66 +1,66 @@
import isMobile from 'ismobilejs'; import isMobile from 'ismobilejs'
import { loadMainView } from './utils'; import { loadMainView } from './utils'
import { artistStore, albumStore, songStore, queueStore, playlistStore, userStore } from './stores'; import { artistStore, albumStore, songStore, queueStore, playlistStore, userStore } from './stores'
import { playback } from './services'; import { playback } from './services'
export default { export default {
routes: { routes: {
'/home' () { '/home' () {
loadMainView('home'); loadMainView('home')
}, },
'/queue' () { '/queue' () {
loadMainView('queue'); loadMainView('queue')
}, },
'/songs' () { '/songs' () {
loadMainView('songs'); loadMainView('songs')
}, },
'/albums' () { '/albums' () {
loadMainView('albums'); loadMainView('albums')
}, },
'/album/(\\d+)' (id) { '/album/(\\d+)' (id) {
const album = albumStore.byId(~~id); const album = albumStore.byId(~~id)
if (album) { if (album) {
loadMainView('album', album); loadMainView('album', album)
} }
}, },
'/artists' () { '/artists' () {
loadMainView('artists'); loadMainView('artists')
}, },
'/artist/(\\d+)' (id) { '/artist/(\\d+)' (id) {
const artist = artistStore.byId(~~id); const artist = artistStore.byId(~~id)
if (artist) { if (artist) {
loadMainView('artist', artist); loadMainView('artist', artist)
} }
}, },
'/favorites' () { '/favorites' () {
loadMainView('favorites'); loadMainView('favorites')
}, },
'/playlist/(\\d+)' (id) { '/playlist/(\\d+)' (id) {
const playlist = playlistStore.byId(~~id); const playlist = playlistStore.byId(~~id)
if (playlist) { if (playlist) {
loadMainView('playlist', playlist); loadMainView('playlist', playlist)
} }
}, },
'/settings' () { '/settings' () {
userStore.current.is_admin && loadMainView('settings'); userStore.current.is_admin && loadMainView('settings')
}, },
'/users' () { '/users' () {
userStore.current.is_admin && loadMainView('users'); userStore.current.is_admin && loadMainView('users')
}, },
'/profile' () { '/profile' () {
loadMainView('profile'); loadMainView('profile')
}, },
'/login' () { '/login' () {
@ -68,41 +68,41 @@ export default {
}, },
'/song/([a-z0-9]{32})' (id) { '/song/([a-z0-9]{32})' (id) {
const song = songStore.byId(id); const song = songStore.byId(id)
if (!song) return; if (!song) return
if (isMobile.apple.device) { if (isMobile.apple.device) {
// Mobile Safari doesn't allow autoplay, so we just queue. // Mobile Safari doesn't allow autoplay, so we just queue.
queueStore.queue(song); queueStore.queue(song)
loadMainView('queue'); loadMainView('queue')
} else { } else {
playback.queueAndPlay(song); playback.queueAndPlay(song)
} }
}, },
'/youtube' () { '/youtube' () {
loadMainView('youtubePlayer'); loadMainView('youtubePlayer')
}, }
}, },
init () { init () {
this.loadState(); this.loadState()
window.addEventListener('popstate', () => this.loadState(), true); window.addEventListener('popstate', () => this.loadState(), true)
}, },
loadState () { loadState () {
if (!window.location.hash) { if (!window.location.hash) {
return this.go('home'); return this.go('home')
} }
Object.keys(this.routes).forEach(route => { Object.keys(this.routes).forEach(route => {
const matches = window.location.hash.match(new RegExp(`^#!${route}$`)); const matches = window.location.hash.match(new RegExp(`^#!${route}$`))
if (matches) { if (matches) {
const [, ...params] = matches; const [, ...params] = matches
this.routes[route](...params); this.routes[route](...params)
return false; return false
} }
}); })
}, },
/** /**
@ -112,13 +112,13 @@ export default {
*/ */
go (path) { go (path) {
if (path[0] !== '/') { if (path[0] !== '/') {
path = `/${path}`; path = `/${path}`
} }
if (path.indexOf('/#!') !== 0) { if (path.indexOf('/#!') !== 0) {
path = `/#!${path}`; path = `/#!${path}`
} }
document.location.href = `${document.location.origin}${path}`; document.location.href = `${document.location.origin}${path}`
}, }
}; }

View file

@ -1,8 +1,8 @@
import $ from 'jquery'; import $ from 'jquery'
import { map } from 'lodash'; import { map } from 'lodash'
import { playlistStore, favoriteStore } from '../stores'; import { playlistStore, favoriteStore } from '../stores'
import { ls } from '.'; import { ls } from '.'
export const download = { export const download = {
/** /**
@ -11,11 +11,11 @@ export const download = {
* @param {Array.<Object>|Object} songs * @param {Array.<Object>|Object} songs
*/ */
fromSongs (songs) { fromSongs (songs) {
songs = [].concat(songs); songs = [].concat(songs)
const ids = map(songs, 'id'); const ids = map(songs, 'id')
const params = $.param({ songs: ids }); const params = $.param({ songs: ids })
return this.trigger(`songs?${params}`); return this.trigger(`songs?${params}`)
}, },
/** /**
@ -24,7 +24,7 @@ export const download = {
* @param {Object} album * @param {Object} album
*/ */
fromAlbum (album) { fromAlbum (album) {
return this.trigger(`album/${album.id}`); return this.trigger(`album/${album.id}`)
}, },
/** /**
@ -36,7 +36,7 @@ export const download = {
// It's safe to assume an artist always has songs. // It's safe to assume an artist always has songs.
// After all, what's an artist without her songs? // After all, what's an artist without her songs?
// (See what I did there? Yes, I'm advocating for women's rights). // (See what I did there? Yes, I'm advocating for women's rights).
return this.trigger(`artist/${artist.id}`); return this.trigger(`artist/${artist.id}`)
}, },
/** /**
@ -46,11 +46,11 @@ export const download = {
*/ */
fromPlaylist (playlist) { fromPlaylist (playlist) {
if (!playlistStore.getSongs(playlist).length) { if (!playlistStore.getSongs(playlist).length) {
console.warn('Empty playlist.'); console.warn('Empty playlist.')
return; return
} }
return this.trigger(`playlist/${playlist.id}`); return this.trigger(`playlist/${playlist.id}`)
}, },
/** /**
@ -58,11 +58,11 @@ export const download = {
*/ */
fromFavorites () { fromFavorites () {
if (!favoriteStore.all.length) { if (!favoriteStore.all.length) {
console.warn("You don't like any song? Come on, don't be that grumpy."); console.warn("You don't like any song? Come on, don't be that grumpy.")
return; return
} }
return this.trigger('favorites'); return this.trigger('favorites')
}, },
/** /**
@ -72,9 +72,9 @@ export const download = {
* artist, playlist, or album. * artist, playlist, or album.
*/ */
trigger (uri) { trigger (uri) {
const sep = uri.indexOf('?') === -1 ? '?' : '&'; const sep = uri.indexOf('?') === -1 ? '?' : '&'
const frameId = `downloader${Date.now()}`; const frameId = `downloader${Date.now()}`
$(`<iframe id="${frameId}" style="display:none"></iframe`).appendTo('body'); $(`<iframe id="${frameId}" style="display:none"></iframe`).appendTo('body')
document.getElementById(frameId).src = `/api/download/${uri}${sep}jwt-token=${ls.get('jwt-token')}`; document.getElementById(frameId).src = `/api/download/${uri}${sep}jwt-token=${ls.get('jwt-token')}`
}, }
} }

View file

@ -1,8 +1,8 @@
import $ from 'jquery'; import $ from 'jquery'
import NProgress from 'nprogress'; import NProgress from 'nprogress'
import { event } from '../utils'; import { event } from '../utils'
import { ls } from '../services'; import { ls } from '../services'
/** /**
* Responsible for all HTTP requests. * Responsible for all HTTP requests.
@ -15,25 +15,25 @@ export const http = {
url: `/api/${url}`, url: `/api/${url}`,
method: method.toUpperCase(), method: method.toUpperCase(),
headers: { headers: {
Authorization: `Bearer ${ls.get('jwt-token')}`, Authorization: `Bearer ${ls.get('jwt-token')}`
} }
}).done(successCb).fail(errorCb); }).done(successCb).fail(errorCb)
}, },
get (url, successCb = null, errorCb = null) { get (url, successCb = null, errorCb = null) {
return this.request('get', url, {}, successCb, errorCb); return this.request('get', url, {}, successCb, errorCb)
}, },
post (url, data, successCb = null, errorCb = null) { post (url, data, successCb = null, errorCb = null) {
return this.request('post', url, data, successCb, errorCb); return this.request('post', url, data, successCb, errorCb)
}, },
put (url, data, successCb = null, errorCb = null) { put (url, data, successCb = null, errorCb = null) {
return this.request('put', url, data, successCb, errorCb); return this.request('put', url, data, successCb, errorCb)
}, },
delete (url, data = {}, successCb = null, errorCb = null) { delete (url, data = {}, successCb = null, errorCb = null) {
return this.request('delete', url, data, successCb, errorCb); return this.request('delete', url, data, successCb, errorCb)
}, },
/** /**
@ -41,24 +41,24 @@ export const http = {
*/ */
init () { init () {
$(document).ajaxComplete((e, r, settings) => { $(document).ajaxComplete((e, r, settings) => {
NProgress.done(); NProgress.done()
if (r.status === 400 || r.status === 401) { if (r.status === 400 || r.status === 401) {
if (!(settings.method === 'POST' && /\/api\/me\/?$/.test(settings.url))) { if (!(settings.method === 'POST' && /\/api\/me\/?$/.test(settings.url))) {
// This is not a failed login. Log out then. // This is not a failed login. Log out then.
event.emit('logout'); event.emit('logout')
return; return
} }
} }
const token = r.getResponseHeader('Authorization'); const token = r.getResponseHeader('Authorization')
if (token) { if (token) {
ls.set('jwt-token', token); ls.set('jwt-token', token)
} }
if (r.responseJSON && r.responseJSON.token && r.responseJSON.token.length > 10) { if (r.responseJSON && r.responseJSON.token && r.responseJSON.token.length > 10) {
ls.set('jwt-token', r.responseJSON.token); ls.set('jwt-token', r.responseJSON.token)
}
})
}
} }
});
},
};

View file

@ -1,6 +1,6 @@
export * from './info'; export * from './info'
export * from './download'; export * from './download'
export * from './http'; export * from './http'
export * from './ls'; export * from './ls'
export * from './playback'; export * from './playback'
export * from './youtube'; export * from './youtube'

View file

@ -1,7 +1,7 @@
import { each } from 'lodash'; import { each } from 'lodash'
import { secondsToHis } from '../../utils'; import { secondsToHis } from '../../utils'
import { http } from '..'; import { http } from '..'
export const albumInfo = { export const albumInfo = {
/** /**
@ -12,15 +12,15 @@ export const albumInfo = {
fetch (album) { fetch (album) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (album.info) { if (album.info) {
resolve(album); resolve(album)
return; return
} }
http.get(`album/${album.id}/info`, data => { http.get(`album/${album.id}/info`, data => {
data && this.merge(album, data); data && this.merge(album, data)
resolve(album); resolve(album)
}, r => reject(r)); }, r => reject(r))
}); })
}, },
/** /**
@ -31,18 +31,20 @@ export const albumInfo = {
*/ */
merge (album, info) { merge (album, info) {
// Convert the duration into i:s // Convert the duration into i:s
info.tracks && each(info.tracks, track => track.fmtLength = secondsToHis(track.length)); info.tracks && each(info.tracks, track => {
track.fmtLength = secondsToHis(track.length)
})
// If the album cover is not in a nice form, discard. // If the album cover is not in a nice form, discard.
if (typeof info.image !== 'string') { if (typeof info.image !== 'string') {
info.image = null; info.image = null
} }
// Set the album cover on the client side to the retrieved image from server. // Set the album cover on the client side to the retrieved image from server.
if (info.cover) { if (info.cover) {
album.cover = info.cover; album.cover = info.cover
} }
album.info = info; album.info = info
}, }
}; }

View file

@ -1,4 +1,4 @@
import { http } from '..'; import { http } from '..'
export const artistInfo = { export const artistInfo = {
/** /**
@ -9,15 +9,15 @@ export const artistInfo = {
fetch (artist) { fetch (artist) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (artist.info) { if (artist.info) {
resolve(artist); resolve(artist)
return; return
} }
http.get(`artist/${artist.id}/info`, data => { http.get(`artist/${artist.id}/info`, data => {
data && this.merge(artist, data); data && this.merge(artist, data)
resolve(artist); resolve(artist)
}, r => reject(r)); }, r => reject(r))
}); })
}, },
/** /**
@ -29,14 +29,14 @@ export const artistInfo = {
merge (artist, info) { merge (artist, info) {
// If the artist image is not in a nice form, discard. // If the artist image is not in a nice form, discard.
if (typeof info.image !== 'string') { if (typeof info.image !== 'string') {
info.image = null; info.image = null
} }
// Set the artist image on the client side to the retrieved image from server. // Set the artist image on the client side to the retrieved image from server.
if (info.image) { if (info.image) {
artist.image = info.image; artist.image = info.image
} }
artist.info = info; artist.info = info
}, }
}; }

View file

@ -1,3 +1,3 @@
export * from './album'; export * from './album'
export * from './artist'; export * from './artist'
export * from './song'; export * from './song'

View file

@ -1,4 +1,4 @@
import { http, albumInfo, artistInfo } from '..'; import { http, albumInfo, artistInfo } from '..'
export const songInfo = { export const songInfo = {
/** /**
@ -10,18 +10,18 @@ export const songInfo = {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// Check if the song's info has been retrieved before. // Check if the song's info has been retrieved before.
if (song.infoRetrieved) { if (song.infoRetrieved) {
resolve(song); resolve(song)
return; return
} }
http.get(`${song.id}/info`, data => { http.get(`${song.id}/info`, data => {
song.lyrics = data.lyrics; song.lyrics = data.lyrics
data.artist_info && artistInfo.merge(song.artist, data.artist_info); data.artist_info && artistInfo.merge(song.artist, data.artist_info)
data.album_info && albumInfo.merge(song.album, data.album_info); data.album_info && albumInfo.merge(song.album, data.album_info)
song.youtube = data.youtube; song.youtube = data.youtube
song.infoRetrieved = true; song.infoRetrieved = true
resolve(song); resolve(song)
}, r => reject(r)); }, r => reject(r))
}); })
}, }
}; }

View file

@ -1,17 +1,17 @@
import localStore from 'local-storage'; import localStore from 'local-storage'
export const ls = { export const ls = {
get (key, defaultVal = null) { get (key, defaultVal = null) {
const val = localStore(key); const val = localStore(key)
return val ? val : defaultVal; return val || defaultVal
}, },
set (key, val) { set (key, val) {
return localStore(key, val); return localStore(key, val)
}, },
remove (key) { remove (key) {
return localStore.remove(key); return localStore.remove(key)
}, }
}; }

View file

@ -1,12 +1,12 @@
import { shuffle, orderBy } from 'lodash'; import { shuffle, orderBy } from 'lodash'
import $ from 'jquery'; import $ from 'jquery'
import plyr from 'plyr'; import plyr from 'plyr'
import Vue from 'vue'; import Vue from 'vue'
import { event } from '../utils'; import { event } from '../utils'
import { queueStore, sharedStore, userStore, songStore, artistStore, preferenceStore as preferences } from '../stores'; import { queueStore, sharedStore, userStore, songStore, preferenceStore as preferences } from '../stores'
import config from '../config'; import config from '../config'
import router from '../router'; import router from '../router'
export const playback = { export const playback = {
player: null, player: null,
@ -20,60 +20,60 @@ export const playback = {
init () { init () {
// We don't need to init this service twice, or the media events will be duplicated. // We don't need to init this service twice, or the media events will be duplicated.
if (this.initialized) { if (this.initialized) {
return; return
} }
this.player = plyr.setup({ this.player = plyr.setup({
controls: [], controls: []
})[0]; })[0]
this.audio = $('audio'); this.audio = $('audio')
this.$volumeInput = $('#volumeRange'); this.$volumeInput = $('#volumeRange')
/** /**
* Listen to 'error' event on the audio player and play the next song if any. * Listen to 'error' event on the audio player and play the next song if any.
*/ */
document.querySelector('.plyr').addEventListener('error', e => { document.querySelector('.plyr').addEventListener('error', e => {
this.playNext(); this.playNext()
}, true); }, true)
/** /**
* Listen to 'ended' event on the audio player and play the next song in the queue. * Listen to 'ended' event on the audio player and play the next song in the queue.
*/ */
document.querySelector('.plyr').addEventListener('ended', e => { document.querySelector('.plyr').addEventListener('ended', e => {
if (sharedStore.state.useLastfm && userStore.current.preferences.lastfm_session_key) { if (sharedStore.state.useLastfm && userStore.current.preferences.lastfm_session_key) {
songStore.scrobble(queueStore.current); songStore.scrobble(queueStore.current)
} }
if (preferences.repeatMode === 'REPEAT_ONE') { if (preferences.repeatMode === 'REPEAT_ONE') {
this.restart(); this.restart()
return; return
} }
this.playNext(); this.playNext()
}); })
/** /**
* Attempt to preload the next song if the current song is about to end. * Attempt to preload the next song if the current song is about to end.
*/ */
document.querySelector('.plyr').addEventListener('timeupdate', e => { document.querySelector('.plyr').addEventListener('timeupdate', e => {
if (!this.player.media.duration || this.player.media.currentTime + 10 < this.player.media.duration) { if (!this.player.media.duration || this.player.media.currentTime + 10 < this.player.media.duration) {
return; return
} }
// The current song has only 10 seconds left to play. // The current song has only 10 seconds left to play.
const nextSong = queueStore.next; const nextSong = queueStore.next
if (!nextSong || nextSong.preloaded) { if (!nextSong || nextSong.preloaded) {
return; return
} }
const $preloader = $('<audio>'); const $preloader = $('<audio>')
$preloader.attr('src', songStore.getSourceUrl(nextSong)); $preloader.attr('src', songStore.getSourceUrl(nextSong))
nextSong.preloaded = true; nextSong.preloaded = true
}); })
/** /**
* Listen to 'input' event on the volume range control. * Listen to 'input' event on the volume range control.
@ -81,16 +81,16 @@ export const playback = {
* update the volume on the plyr object. * update the volume on the plyr object.
*/ */
this.$volumeInput.on('input', e => { this.$volumeInput.on('input', e => {
this.setVolume($(e.target).val()); this.setVolume($(e.target).val())
}); })
// On init, set the volume to the value found in the local storage. // On init, set the volume to the value found in the local storage.
this.setVolume(preferences.volume); this.setVolume(preferences.volume)
// Init the equalizer if supported. // Init the equalizer if supported.
event.emit('equalizer:init', this.player.media); event.emit('equalizer:init', this.player.media)
this.initialized = true; this.initialized = true
}, },
/** /**
@ -105,64 +105,64 @@ export const playback = {
*/ */
play (song) { play (song) {
if (!song) { if (!song) {
return; return
} }
if (queueStore.current) { if (queueStore.current) {
queueStore.current.playbackState = 'stopped'; queueStore.current.playbackState = 'stopped'
} }
song.playbackState = 'playing'; song.playbackState = 'playing'
// Set the song as the current song // Set the song as the current song
queueStore.current = song; queueStore.current = song
// Add it into the "recent" list // Add it into the "recent" list
songStore.addRecent(song); songStore.addRecent(song)
// Manually set the `src` attribute of the audio to prevent plyr from resetting // Manually set the `src` attribute of the audio to prevent plyr from resetting
// the audio media object and cause our equalizer to malfunction. // the audio media object and cause our equalizer to malfunction.
this.player.media.src = songStore.getSourceUrl(song); this.player.media.src = songStore.getSourceUrl(song)
$('title').text(`${song.title}${config.appTitle}`); $('title').text(`${song.title}${config.appTitle}`)
$('.plyr audio').attr('title', `${song.artist.name} - ${song.title}`); $('.plyr audio').attr('title', `${song.artist.name} - ${song.title}`)
// We'll just "restart" playing the song, which will handle notification, scrobbling etc. // We'll just "restart" playing the song, which will handle notification, scrobbling etc.
this.restart(); this.restart()
}, },
/** /**
* Restart playing a song. * Restart playing a song.
*/ */
restart () { restart () {
const song = queueStore.current; const song = queueStore.current
// Record the UNIX timestamp the song start playing, for scrobbling purpose // Record the UNIX timestamp the song start playing, for scrobbling purpose
song.playStartTime = Math.floor(Date.now() / 1000); song.playStartTime = Math.floor(Date.now() / 1000)
event.emit('song:played', song); event.emit('song:played', song)
this.player.restart(); this.player.restart()
this.player.play(); this.player.play()
// Register the play to the server // Register the play to the server
songStore.registerPlay(song); songStore.registerPlay(song)
// Show the notification if we're allowed to // Show the notification if we're allowed to
if (!window.Notification || !preferences.notify) { if (!window.Notification || !preferences.notify) {
return; return
} }
try { try {
const notification = new Notification(`${song.title}`, { const notif = new window.Notification(`${song.title}`, {
icon: song.album.cover, icon: song.album.cover,
body: `${song.album.name} ${song.artist.name}` body: `${song.album.name} ${song.artist.name}`
}); })
notification.onclick = () => window.focus(); notif.onclick = () => window.focus()
// Close the notif after 5 secs. // Close the notif after 5 secs.
window.setTimeout(() => notification.close(), 5000); window.setTimeout(() => notif.close(), 5000)
} catch (e) { } catch (e) {
// Notification fails. // Notification fails.
// @link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification // @link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification
@ -176,14 +176,14 @@ export const playback = {
* @return {Object} The song * @return {Object} The song
*/ */
get next () { get next () {
const next = queueStore.next; const next = queueStore.next
if (next) { if (next) {
return next; return next
} }
if (preferences.repeatMode === 'REPEAT_ALL') { if (preferences.repeatMode === 'REPEAT_ALL') {
return queueStore.first; return queueStore.first
} }
}, },
@ -194,14 +194,14 @@ export const playback = {
* @return {Object} The song * @return {Object} The song
*/ */
get previous () { get previous () {
const prev = queueStore.previous; const prev = queueStore.previous
if (prev) { if (prev) {
return prev; return prev
} }
if (preferences.repeatMode === 'REPEAT_ALL') { if (preferences.repeatMode === 'REPEAT_ALL') {
return queueStore.last; return queueStore.last
} }
}, },
@ -210,13 +210,13 @@ export const playback = {
* The selected mode will be stored into local storage as well. * The selected mode will be stored into local storage as well.
*/ */
changeRepeatMode () { changeRepeatMode () {
let idx = this.repeatModes.indexOf(preferences.repeatMode) + 1; let idx = this.repeatModes.indexOf(preferences.repeatMode) + 1
if (idx >= this.repeatModes.length) { if (idx >= this.repeatModes.length) {
idx = 0; idx = 0
} }
preferences.repeatMode = this.repeatModes[idx]; preferences.repeatMode = this.repeatModes[idx]
}, },
/** /**
@ -227,20 +227,20 @@ export const playback = {
// If the song's duration is greater than 5 seconds and we've passed 5 seconds into it, // If the song's duration is greater than 5 seconds and we've passed 5 seconds into it,
// restart playing instead. // restart playing instead.
if (this.player.media.currentTime > 5 && queueStore.current.length > 5) { if (this.player.media.currentTime > 5 && queueStore.current.length > 5) {
this.player.restart(); this.player.restart()
return; return
} }
const prev = this.previous; const prev = this.previous
if (!prev && preferences.repeatMode === 'NO_REPEAT') { if (!prev && preferences.repeatMode === 'NO_REPEAT') {
this.stop(); this.stop()
return; return
} }
this.play(prev); this.play(prev)
}, },
/** /**
@ -248,16 +248,16 @@ export const playback = {
* If the next song is not found and the current mode is NO_REPEAT, we stop completely. * If the next song is not found and the current mode is NO_REPEAT, we stop completely.
*/ */
playNext () { playNext () {
const next = this.next; const next = this.next
if (!next && preferences.repeatMode === 'NO_REPEAT') { if (!next && preferences.repeatMode === 'NO_REPEAT') {
// Nothing lasts forever, even cold November rain. // Nothing lasts forever, even cold November rain.
this.stop(); this.stop()
return; return
} }
this.play(next); this.play(next)
}, },
/** /**
@ -267,20 +267,20 @@ export const playback = {
* @param {Boolean=true} persist Whether the volume should be saved into local storage * @param {Boolean=true} persist Whether the volume should be saved into local storage
*/ */
setVolume (volume, persist = true) { setVolume (volume, persist = true) {
this.player.setVolume(volume); this.player.setVolume(volume)
if (persist) { if (persist) {
preferences.volume = volume; preferences.volume = volume
} }
this.$volumeInput.val(volume); this.$volumeInput.val(volume)
}, },
/** /**
* Mute playback. * Mute playback.
*/ */
mute () { mute () {
this.setVolume(0, false); this.setVolume(0, false)
}, },
/** /**
@ -289,22 +289,22 @@ export const playback = {
unmute () { unmute () {
// If the saved volume is 0, we unmute to the default level (7). // If the saved volume is 0, we unmute to the default level (7).
if (preferences.volume === '0' || preferences.volume === 0) { if (preferences.volume === '0' || preferences.volume === 0) {
preferences.volume = 7; preferences.volume = 7
} }
this.setVolume(preferences.volume); this.setVolume(preferences.volume)
}, },
/** /**
* Completely stop playback. * Completely stop playback.
*/ */
stop () { stop () {
$('title').text(config.appTitle); $('title').text(config.appTitle)
this.player.pause(); this.player.pause()
this.player.seek(0); this.player.seek(0)
if (queueStore.current) { if (queueStore.current) {
queueStore.current.playbackState = 'stopped'; queueStore.current.playbackState = 'stopped'
} }
}, },
@ -312,17 +312,17 @@ export const playback = {
* Pause playback. * Pause playback.
*/ */
pause () { pause () {
this.player.pause(); this.player.pause()
queueStore.current.playbackState = 'paused'; queueStore.current.playbackState = 'paused'
}, },
/** /**
* Resume playback. * Resume playback.
*/ */
resume () { resume () {
this.player.play(); this.player.play()
queueStore.current.playbackState = 'playing'; queueStore.current.playbackState = 'playing'
event.emit('song:played', queueStore.current); event.emit('song:played', queueStore.current)
}, },
/** /**
@ -333,25 +333,25 @@ export const playback = {
*/ */
queueAndPlay (songs = null, shuffled = false) { queueAndPlay (songs = null, shuffled = false) {
if (!songs) { if (!songs) {
songs = songStore.all; songs = songStore.all
} }
if (!songs.length) { if (!songs.length) {
return; return
} }
if (shuffled) { if (shuffled) {
songs = shuffle(songs); songs = shuffle(songs)
} }
queueStore.queue(songs, true); queueStore.queue(songs, true)
// Wrap this inside a nextTick() to wait for the DOM to complete updating // Wrap this inside a nextTick() to wait for the DOM to complete updating
// and then play the first song in the queue. // and then play the first song in the queue.
Vue.nextTick(() => { Vue.nextTick(() => {
router.go('queue'); router.go('queue')
this.play(queueStore.first); this.play(queueStore.first)
}); })
}, },
/** /**
@ -360,12 +360,12 @@ export const playback = {
*/ */
playFirstInQueue () { playFirstInQueue () {
if (!queueStore.all.length) { if (!queueStore.all.length) {
this.queueAndPlay(); this.queueAndPlay()
return; return
} }
this.play(queueStore.first); this.play(queueStore.first)
}, },
/** /**
@ -375,7 +375,7 @@ export const playback = {
* @param {Boolean=true} shuffled Whether to shuffle the songs * @param {Boolean=true} shuffled Whether to shuffle the songs
*/ */
playAllByArtist (artist, shuffled = true) { playAllByArtist (artist, shuffled = true) {
this.queueAndPlay(artist.songs, shuffled); this.queueAndPlay(artist.songs, shuffled)
}, },
/** /**
@ -386,11 +386,10 @@ export const playback = {
*/ */
playAllInAlbum (album, shuffled = true) { playAllInAlbum (album, shuffled = true) {
if (!shuffled) { if (!shuffled) {
this.queueAndPlay(orderBy(album.songs, 'track')); this.queueAndPlay(orderBy(album.songs, 'track'))
return
return;
} }
this.queueAndPlay(album.songs, true); this.queueAndPlay(album.songs, true)
}, }
}; }

View file

@ -1,7 +1,6 @@
import { http, playback } from '.'; import { http } from '.'
import { assign } from 'lodash'; import { event } from '../utils'
import { event, loadMainView } from '../utils'; import router from '../router'
import router from '../router';
export const youtube = { export const youtube = {
/** /**
@ -12,15 +11,15 @@ export const youtube = {
*/ */
searchVideosRelatedToSong (song, cb = null) { searchVideosRelatedToSong (song, cb = null) {
if (!song.youtube) { if (!song.youtube) {
song.youtube = {}; song.youtube = {}
} }
const pageToken = song.youtube.nextPageToken || ''; const pageToken = song.youtube.nextPageToken || ''
http.get(`youtube/search/song/${song.id}?pageToken=${pageToken}`).then(data => { http.get(`youtube/search/song/${song.id}?pageToken=${pageToken}`).then(data => {
song.youtube.nextPageToken = data.nextPageToken; song.youtube.nextPageToken = data.nextPageToken
song.youtube.items.push(...data.items); song.youtube.items.push(...data.items)
cb && cb(); cb && cb()
}); })
}, },
/** /**
@ -29,7 +28,7 @@ export const youtube = {
* @param {string} id The video ID * @param {string} id The video ID
*/ */
play (id) { play (id) {
event.emit('youtube:play', id); event.emit('youtube:play', id)
router.go('youtube'); router.go('youtube')
}, }
}; }

View file

@ -1,15 +1,15 @@
import Vue from 'vue'; import Vue from 'vue'
import { reduce, each, find, union, difference, take, filter, orderBy } from 'lodash'; import { reduce, each, find, union, difference, take, filter, orderBy } from 'lodash'
import { secondsToHis } from '../utils'; import { secondsToHis } from '../utils'
import stub from '../stubs/album'; import stub from '../stubs/album'
import { songStore, artistStore } from '.'; import { songStore, artistStore } from '.'
export const albumStore = { export const albumStore = {
stub, stub,
state: { state: {
albums: [stub], albums: [stub]
}, },
/** /**
@ -23,23 +23,23 @@ export const albumStore = {
// While we're doing so, for each album, we get its length // While we're doing so, for each album, we get its length
// and keep a back reference to the artist too. // and keep a back reference to the artist too.
each(artist.albums, album => { each(artist.albums, album => {
this.setupAlbum(album, artist); this.setupAlbum(album, artist)
}); })
return albums.concat(artist.albums); return albums.concat(artist.albums)
}, []); }, [])
// Then we init the song store. // Then we init the song store.
songStore.init(this.all); songStore.init(this.all)
}, },
setupAlbum (album, artist) { setupAlbum (album, artist) {
Vue.set(album, 'playCount', 0); Vue.set(album, 'playCount', 0)
Vue.set(album, 'artist', artist); Vue.set(album, 'artist', artist)
Vue.set(album, 'info', null); Vue.set(album, 'info', null)
this.getLength(album); this.getLength(album)
return album; return album
}, },
/** /**
@ -48,7 +48,7 @@ export const albumStore = {
* @return {Array.<Object>} * @return {Array.<Object>}
*/ */
get all () { get all () {
return this.state.albums; return this.state.albums
}, },
/** /**
@ -57,11 +57,11 @@ export const albumStore = {
* @param {Array.<Object>} value * @param {Array.<Object>} value
*/ */
set all (value) { set all (value) {
this.state.albums = value; this.state.albums = value
}, },
byId (id) { byId (id) {
return find(this.all, { id }); return find(this.all, { id })
}, },
/** /**
@ -73,10 +73,10 @@ export const albumStore = {
* @return {String} The H:i:s format of the album length. * @return {String} The H:i:s format of the album length.
*/ */
getLength (album) { getLength (album) {
Vue.set(album, 'length', reduce(album.songs, (length, song) => length + song.length, 0)); Vue.set(album, 'length', reduce(album.songs, (length, song) => length + song.length, 0))
Vue.set(album, 'fmtLength', secondsToHis(album.length)); Vue.set(album, 'fmtLength', secondsToHis(album.length))
return album.fmtLength; return album.fmtLength
}, },
/** /**
@ -85,13 +85,13 @@ export const albumStore = {
* @param {Array.<Object>|Object} albums * @param {Array.<Object>|Object} albums
*/ */
add (albums) { add (albums) {
albums = [].concat(albums); albums = [].concat(albums)
each(albums, a => { each(albums, a => {
this.setupAlbum(a, a.artist) this.setupAlbum(a, a.artist)
a.playCount = reduce(a.songs, (count, song) => count + song.playCount, 0); a.playCount = reduce(a.songs, (count, song) => count + song.playCount, 0)
}); })
this.all = union(this.all, albums); this.all = union(this.all, albums)
}, },
/** /**
@ -101,17 +101,17 @@ export const albumStore = {
* @param {Array.<Object>|Object} song * @param {Array.<Object>|Object} song
*/ */
addSongsIntoAlbum (album, songs) { addSongsIntoAlbum (album, songs) {
songs = [].concat(songs); songs = [].concat(songs)
album.songs = union(album.songs ? album.songs : [], songs); album.songs = union(album.songs ? album.songs : [], songs)
each(songs, song => { each(songs, song => {
song.album_id = album.id; song.album_id = album.id
song.album = album; song.album = album
}); })
album.playCount = reduce(album.songs, (count, song) => count + song.playCount, 0); album.playCount = reduce(album.songs, (count, song) => count + song.playCount, 0)
this.getLength(album); this.getLength(album)
}, },
/** /**
@ -121,9 +121,9 @@ export const albumStore = {
* @param {Array.<Object>|Object} songs * @param {Array.<Object>|Object} songs
*/ */
removeSongsFromAlbum (album, songs) { removeSongsFromAlbum (album, songs) {
album.songs = difference(album.songs, [].concat(songs)); album.songs = difference(album.songs, [].concat(songs))
album.playCount = reduce(album.songs, (count, song) => count + song.playCount, 0); album.playCount = reduce(album.songs, (count, song) => count + song.playCount, 0)
this.getLength(album); this.getLength(album)
}, },
/** /**
@ -134,7 +134,7 @@ export const albumStore = {
* @return {boolean} * @return {boolean}
*/ */
isAlbumEmpty (album) { isAlbumEmpty (album) {
return !album.songs.length; return !album.songs.length
}, },
/** /**
@ -143,13 +143,13 @@ export const albumStore = {
* @param {Array.<Object>|Object} albums * @param {Array.<Object>|Object} albums
*/ */
remove (albums) { remove (albums) {
albums = [].concat(albums); albums = [].concat(albums)
this.all = difference(this.all, albums); this.all = difference(this.all, albums)
// Remove from the artist as well // Remove from the artist as well
each(albums, album => { each(albums, album => {
artistStore.removeAlbumsFromArtist(album.artist, album); artistStore.removeAlbumsFromArtist(album.artist, album)
}); })
}, },
/** /**
@ -162,10 +162,10 @@ export const albumStore = {
getMostPlayed (n = 6) { getMostPlayed (n = 6) {
// Only non-unknown albums with actually play count are applicable. // Only non-unknown albums with actually play count are applicable.
const applicable = filter(this.all, album => { const applicable = filter(this.all, album => {
return album.playCount && album.id !== 1; return album.playCount && album.id !== 1
}); })
return take(orderBy(applicable, 'playCount', 'desc'), n); return take(orderBy(applicable, 'playCount', 'desc'), n)
}, },
/** /**
@ -176,8 +176,8 @@ export const albumStore = {
* @return {Array.<Object>} * @return {Array.<Object>}
*/ */
getRecentlyAdded (n = 6) { getRecentlyAdded (n = 6) {
const applicable = filter(this.all, album => album.id !== 1); const applicable = filter(this.all, album => album.id !== 1)
return take(orderBy(applicable, 'created_at', 'desc'), n); return take(orderBy(applicable, 'created_at', 'desc'), n)
}, }
}; }

View file

@ -1,18 +1,18 @@
import Vue from 'vue'; import Vue from 'vue'
import { reduce, each, find, union, difference, take, filter, orderBy } from 'lodash'; import { reduce, each, find, union, difference, take, filter, orderBy } from 'lodash'
import config from '../config'; import config from '../config'
import stub from '../stubs/artist'; import stub from '../stubs/artist'
import { albumStore } from '.'; import { albumStore } from '.'
const UNKNOWN_ARTIST_ID = 1; const UNKNOWN_ARTIST_ID = 1
const VARIOUS_ARTISTS_ID = 2; const VARIOUS_ARTISTS_ID = 2
export const artistStore = { export const artistStore = {
stub, stub,
state: { state: {
artists: [], artists: []
}, },
/** /**
@ -21,14 +21,14 @@ export const artistStore = {
* @param {Array.<Object>} artists The array of artists we got from the server. * @param {Array.<Object>} artists The array of artists we got from the server.
*/ */
init (artists) { init (artists) {
this.all = artists; this.all = artists
albumStore.init(this.all); albumStore.init(this.all)
// Traverse through artists array to get the cover and number of songs for each. // Traverse through artists array to get the cover and number of songs for each.
each(this.all, artist => { each(this.all, artist => {
this.setupArtist(artist); this.setupArtist(artist)
}); })
}, },
/** /**
@ -37,8 +37,8 @@ export const artistStore = {
* @param {Object} artist * @param {Object} artist
*/ */
setupArtist (artist) { setupArtist (artist) {
this.getImage(artist); this.getImage(artist)
Vue.set(artist, 'playCount', 0); Vue.set(artist, 'playCount', 0)
// Here we build a list of songs performed by the artist, so that we don't need to traverse // Here we build a list of songs performed by the artist, so that we don't need to traverse
// down the "artist > albums > items" route later. // down the "artist > albums > items" route later.
@ -46,18 +46,18 @@ export const artistStore = {
Vue.set(artist, 'songs', reduce(artist.albums, (songs, album) => { Vue.set(artist, 'songs', reduce(artist.albums, (songs, album) => {
// If the album is compilation, we cater for the songs contributed by this artist only. // If the album is compilation, we cater for the songs contributed by this artist only.
if (album.is_compilation) { if (album.is_compilation) {
return songs.concat(filter(album.songs, { contributing_artist_id: artist.id })); return songs.concat(filter(album.songs, { contributing_artist_id: artist.id }))
} }
// Otherwise, just use all songs in the album. // Otherwise, just use all songs in the album.
return songs.concat(album.songs); return songs.concat(album.songs)
}, [])); }, []))
Vue.set(artist, 'songCount', artist.songs.length); Vue.set(artist, 'songCount', artist.songs.length)
Vue.set(artist, 'info', null); Vue.set(artist, 'info', null)
return artist; return artist
}, },
/** /**
@ -66,7 +66,7 @@ export const artistStore = {
* @return {Array.<Object>} * @return {Array.<Object>}
*/ */
get all () { get all () {
return this.state.artists; return this.state.artists
}, },
/** /**
@ -75,7 +75,7 @@ export const artistStore = {
* @param {Array.<Object>} value * @param {Array.<Object>} value
*/ */
set all (value) { set all (value) {
this.state.artists = value; this.state.artists = value
}, },
/** /**
@ -84,7 +84,7 @@ export const artistStore = {
* @param {Number} id * @param {Number} id
*/ */
byId (id) { byId (id) {
return find(this.all, { id }); return find(this.all, { id })
}, },
/** /**
@ -93,10 +93,10 @@ export const artistStore = {
* @param {Array.<Object>|Object} artists * @param {Array.<Object>|Object} artists
*/ */
add (artists) { add (artists) {
artists = [].concat(artists); artists = [].concat(artists)
each(artists, a => this.setupArtist(a)); each(artists, a => this.setupArtist(a))
this.all = union(this.all, artists); this.all = union(this.all, artists)
}, },
/** /**
@ -105,7 +105,7 @@ export const artistStore = {
* @param {Array.<Object>|Object} artists * @param {Array.<Object>|Object} artists
*/ */
remove (artists) { remove (artists) {
this.all = difference(this.all, [].concat(artists)); this.all = difference(this.all, [].concat(artists))
}, },
/** /**
@ -116,15 +116,15 @@ export const artistStore = {
* *
*/ */
addAlbumsIntoArtist (artist, albums) { addAlbumsIntoArtist (artist, albums) {
albums = [].concat(albums); albums = [].concat(albums)
artist.albums = union(artist.albums ? artist.albums : [], albums); artist.albums = union(artist.albums ? artist.albums : [], albums)
each(albums, album => { each(albums, album => {
album.artist_id = artist.id; album.artist_id = artist.id
album.artist = artist; album.artist = artist
artist.playCount += album.playCount; artist.playCount += album.playCount
}); })
}, },
/** /**
@ -134,9 +134,11 @@ export const artistStore = {
* @param {Array.<Object>|Object} albums * @param {Array.<Object>|Object} albums
*/ */
removeAlbumsFromArtist (artist, albums) { removeAlbumsFromArtist (artist, albums) {
albums = [].concat(albums); albums = [].concat(albums)
artist.albums = difference(artist.albums, albums); artist.albums = difference(artist.albums, albums)
each(albums, album => artist.playCount -= album.playCount); each(albums, album => {
artist.playCount -= album.playCount
})
}, },
/** /**
@ -147,7 +149,7 @@ export const artistStore = {
* @return {boolean} * @return {boolean}
*/ */
isArtistEmpty (artist) { isArtistEmpty (artist) {
return !artist.albums.length; return !artist.albums.length
}, },
/** /**
@ -158,7 +160,7 @@ export const artistStore = {
* @return {Boolean} * @return {Boolean}
*/ */
isVariousArtists (artist) { isVariousArtists (artist) {
return artist.id === VARIOUS_ARTISTS_ID; return artist.id === VARIOUS_ARTISTS_ID
}, },
/** /**
@ -169,7 +171,7 @@ export const artistStore = {
* @return {Boolean} * @return {Boolean}
*/ */
isUnknownArtist (artist) { isUnknownArtist (artist) {
return artist.id === UNKNOWN_ARTIST_ID; return artist.id === UNKNOWN_ARTIST_ID
}, },
/** /**
@ -180,7 +182,7 @@ export const artistStore = {
* @return {Array.<Object>} * @return {Array.<Object>}
*/ */
getSongsByArtist (artist) { getSongsByArtist (artist) {
return artist.songs; return artist.songs
}, },
/** /**
@ -193,20 +195,20 @@ export const artistStore = {
getImage (artist) { getImage (artist) {
if (!artist.image) { if (!artist.image) {
// Try to get an image from one of the albums. // Try to get an image from one of the albums.
artist.image = config.unknownCover; artist.image = config.unknownCover
artist.albums.every(album => { artist.albums.every(album => {
// If there's a "real" cover, use it. // If there's a "real" cover, use it.
if (album.image !== config.unknownCover) { if (album.image !== config.unknownCover) {
artist.image = album.cover; artist.image = album.cover
// I want to break free. // I want to break free.
return false; return false
} }
}); })
} }
return artist.image; return artist.image
}, },
/** /**
@ -220,11 +222,11 @@ export const artistStore = {
// Only non-unknown artists with actually play count are applicable. // Only non-unknown artists with actually play count are applicable.
// Also, "Various Artists" doesn't count. // Also, "Various Artists" doesn't count.
const applicable = filter(this.all, artist => { const applicable = filter(this.all, artist => {
return artist.playCount return artist.playCount &&
&& !this.isUnknownArtist(artist) !this.isUnknownArtist(artist) &&
&& !this.isVariousArtists(artist); !this.isVariousArtists(artist)
}); })
return take(orderBy(applicable, 'playCount', 'desc'), n); return take(orderBy(applicable, 'playCount', 'desc'), n)
}, }
}; }

View file

@ -1,6 +1,6 @@
import { find } from 'lodash'; import { find } from 'lodash'
import { preferenceStore } from '.'; import { preferenceStore } from '.'
export const equalizerStore = { export const equalizerStore = {
presets: [ presets: [
@ -8,25 +8,25 @@ export const equalizerStore = {
id: 0, id: 0,
name: 'Default', name: 'Default',
preamp: 0, preamp: 0,
gains: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], gains: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
}, },
{ {
id: 1, id: 1,
name: 'Classical', name: 'Classical',
preamp: -1, preamp: -1,
gains: [-1, -1, -1, -1, -1, -1, -7, -7, -7, -9], gains: [-1, -1, -1, -1, -1, -1, -7, -7, -7, -9]
}, },
{ {
id: 2, id: 2,
name: 'Club', name: 'Club',
preamp: -6.7, preamp: -6.7,
gains: [-1, -1, 8, 5, 5, 5, 3, -1, -1, -1], gains: [-1, -1, 8, 5, 5, 5, 3, -1, -1, -1]
}, },
{ {
id: 3, id: 3,
name: 'Dance', name: 'Dance',
preamp: -4.3, preamp: -4.3,
gains: [9, 7, 2, -1, -1, -5, -7, -7, -1, -1], gains: [9, 7, 2, -1, -1, -5, -7, -7, -1, -1]
}, },
{ {
id: 4, id: 4,
@ -50,48 +50,48 @@ export const equalizerStore = {
id: 7, id: 7,
name: 'Large Hall', name: 'Large Hall',
preamp: -7.2, preamp: -7.2,
gains: [10, 10, 5, 5, -1, -4, -4, -4, -1, -1], gains: [10, 10, 5, 5, -1, -4, -4, -4, -1, -1]
}, },
{ {
id: 8, id: 8,
name: 'Live', name: 'Live',
preamp: -5.3, preamp: -5.3,
gains: [-4, -1, 4, 5, 5, 5, 4, 2, 2, 2], gains: [-4, -1, 4, 5, 5, 5, 4, 2, 2, 2]
}, },
{ {
id: 9, id: 9,
name: 'Pop', name: 'Pop',
preamp: -6.2, preamp: -6.2,
gains: [-1, 4, 7, 8, 5, -1, -2, -2, -1, -1], gains: [-1, 4, 7, 8, 5, -1, -2, -2, -1, -1]
}, },
{ {
id: 10, id: 10,
name: 'Reggae', name: 'Reggae',
preamp: -8.2, preamp: -8.2,
gains: [-1, -1, -1, -5, -1, 6, 6, -1, -1, -1], gains: [-1, -1, -1, -5, -1, 6, 6, -1, -1, -1]
}, },
{ {
id: 11, id: 11,
name: 'Rock', name: 'Rock',
preamp: -10, preamp: -10,
gains: [8, 4, -5, -8, -3, 4, 8, 11, 11, 11], gains: [8, 4, -5, -8, -3, 4, 8, 11, 11, 11]
}, },
{ {
id: 12, id: 12,
name: 'Soft Rock', name: 'Soft Rock',
preamp: -5.3, preamp: -5.3,
gains: [4, 4, 2, -1, -4, -5, -3, -1, 2, 8], gains: [4, 4, 2, -1, -4, -5, -3, -1, 2, 8]
}, },
{ {
id: 13, id: 13,
name: 'Techno', name: 'Techno',
preamp: -7.7, preamp: -7.7,
gains: [8, 5, -1, -5, -4, -1, 8, 9, 9, 8], gains: [8, 5, -1, -5, -4, -1, 8, 9, 9, 8]
}, }
], ],
getPresetById (id) { getPresetById (id) {
return find(this.presets, { id }); return find(this.presets, { id })
}, },
/** /**
@ -101,7 +101,7 @@ export const equalizerStore = {
*/ */
get () { get () {
if (!this.presets[preferenceStore.selectedPreset]) { if (!this.presets[preferenceStore.selectedPreset]) {
return preferenceStore.equalizer; return preferenceStore.equalizer
} }
// If the user chose a preset (instead of customizing one), just return it. // If the user chose a preset (instead of customizing one), just return it.
@ -115,6 +115,6 @@ export const equalizerStore = {
* @param {Array.<Number>} gains The band's gain value (dB) * @param {Array.<Number>} gains The band's gain value (dB)
*/ */
set (preamp, gains) { set (preamp, gains) {
preferenceStore.equalizer = { preamp, gains }; preferenceStore.equalizer = { preamp, gains }
}, }
}; }

View file

@ -1,12 +1,12 @@
import { each, map, difference, union } from 'lodash'; import { each, map, difference, union } from 'lodash'
import { http } from '../services'; import { http } from '../services'
export const favoriteStore = { export const favoriteStore = {
state: { state: {
songs: [], songs: [],
length: 0, length: 0,
fmtLength: '', fmtLength: ''
}, },
/** /**
@ -15,7 +15,7 @@ export const favoriteStore = {
* @return {Array.<Object>} * @return {Array.<Object>}
*/ */
get all () { get all () {
return this.state.songs; return this.state.songs
}, },
/** /**
@ -24,7 +24,7 @@ export const favoriteStore = {
* @param {Array.<Object>} value * @param {Array.<Object>} value
*/ */
set all (value) { set all (value) {
this.state.songs = value; this.state.songs = value
}, },
/** /**
@ -36,12 +36,12 @@ export const favoriteStore = {
toggleOne (song) { toggleOne (song) {
// Don't wait for the HTTP response to update the status, just toggle right away. // Don't wait for the HTTP response to update the status, just toggle right away.
// This may cause a minor problem if the request fails somehow, but do we care? // This may cause a minor problem if the request fails somehow, but do we care?
song.liked = !song.liked; song.liked = !song.liked
song.liked ? this.add(song) : this.remove(song); song.liked ? this.add(song) : this.remove(song)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http.post('interaction/like', { song: song.id }, data => resolve(data), r => reject(r)); http.post('interaction/like', { song: song.id }, data => resolve(data), r => reject(r))
}); })
}, },
/** /**
@ -50,7 +50,7 @@ export const favoriteStore = {
* @param {Array.<Object>|Object} songs * @param {Array.<Object>|Object} songs
*/ */
add (songs) { add (songs) {
this.all = union(this.all, [].concat(songs)); this.all = union(this.all, [].concat(songs))
}, },
/** /**
@ -59,14 +59,14 @@ export const favoriteStore = {
* @param {Array.<Object>|Object} songs * @param {Array.<Object>|Object} songs
*/ */
remove (songs) { remove (songs) {
this.all = difference(this.all, [].concat(songs)); this.all = difference(this.all, [].concat(songs))
}, },
/** /**
* Remove all favorites. * Remove all favorites.
*/ */
clear () { clear () {
this.all = []; this.all = []
}, },
/** /**
@ -77,12 +77,14 @@ export const favoriteStore = {
like (songs) { like (songs) {
// Don't wait for the HTTP response to update the status, just set them to Liked right away. // Don't wait for the HTTP response to update the status, just set them to Liked right away.
// This may cause a minor problem if the request fails somehow, but do we care? // This may cause a minor problem if the request fails somehow, but do we care?
each(songs, song => song.liked = true); each(songs, song => {
this.add(songs); song.liked = true
})
this.add(songs)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http.post('interaction/batch/like', { songs: map(songs, 'id') }, data => resolve(data), r => reject(r)); http.post('interaction/batch/like', { songs: map(songs, 'id') }, data => resolve(data), r => reject(r))
}); })
}, },
/** /**
@ -91,11 +93,13 @@ export const favoriteStore = {
* @param {Array.<Object>} songs * @param {Array.<Object>} songs
*/ */
unlike (songs) { unlike (songs) {
each(songs, song => song.liked = false); each(songs, song => {
this.remove(songs); song.liked = false
})
this.remove(songs)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http.post('interaction/batch/unlike', { songs: map(songs, 'id') }, data => resolve(data), r => reject(r)); http.post('interaction/batch/unlike', { songs: map(songs, 'id') }, data => resolve(data), r => reject(r))
}); })
}, }
}; }

View file

@ -1,11 +1,11 @@
export * from './album'; export * from './album'
export * from './artist'; export * from './artist'
export * from './equalizer'; export * from './equalizer'
export * from './favorite'; export * from './favorite'
export * from './playlist'; export * from './playlist'
export * from './preference'; export * from './preference'
export * from './queue'; export * from './queue'
export * from './setting'; export * from './setting'
export * from './shared'; export * from './shared'
export * from './song'; export * from './song'
export * from './user'; export * from './user'

View file

@ -1,20 +1,20 @@
import { each, find, map, difference, union, without } from 'lodash'; import { each, find, map, difference, union } from 'lodash'
import NProgress from 'nprogress'; import NProgress from 'nprogress'
import stub from '../stubs/playlist'; import stub from '../stubs/playlist'
import { http } from '../services'; import { http } from '../services'
import { songStore } from '.'; import { songStore } from '.'
export const playlistStore = { export const playlistStore = {
stub, stub,
state: { state: {
playlists: [], playlists: []
}, },
init (playlists) { init (playlists) {
this.all = playlists; this.all = playlists
each(this.all, this.objectifySongs); each(this.all, this.objectifySongs)
}, },
/** /**
@ -23,7 +23,7 @@ export const playlistStore = {
* @return {Array.<Object>} * @return {Array.<Object>}
*/ */
get all () { get all () {
return this.state.playlists; return this.state.playlists
}, },
/** /**
@ -32,7 +32,7 @@ export const playlistStore = {
* @param {Array.<Object>} value * @param {Array.<Object>} value
*/ */
set all (value) { set all (value) {
this.state.playlists = value; this.state.playlists = value
}, },
/** /**
@ -43,7 +43,7 @@ export const playlistStore = {
* @return {Object} * @return {Object}
*/ */
byId (id) { byId (id) {
return find(this.all, { id }); return find(this.all, { id })
}, },
/** /**
@ -53,7 +53,7 @@ export const playlistStore = {
* @param {Object} playlist * @param {Object} playlist
*/ */
objectifySongs (playlist) { objectifySongs (playlist) {
playlist.songs = songStore.byIds(playlist.songs); playlist.songs = songStore.byIds(playlist.songs)
}, },
/** /**
@ -64,7 +64,7 @@ export const playlistStore = {
* return {Array.<Object>} * return {Array.<Object>}
*/ */
getSongs (playlist) { getSongs (playlist) {
return playlist.songs; return playlist.songs
}, },
/** /**
@ -73,7 +73,7 @@ export const playlistStore = {
* @param {Array.<Object>|Object} playlists * @param {Array.<Object>|Object} playlists
*/ */
add (playlists) { add (playlists) {
this.all = union(this.all, [].concat(playlists)); this.all = union(this.all, [].concat(playlists))
}, },
/** /**
@ -82,7 +82,7 @@ export const playlistStore = {
* @param {Array.<Object>|Object} playlist * @param {Array.<Object>|Object} playlist
*/ */
remove (playlists) { remove (playlists) {
this.all = difference(this.all, [].concat(playlists)); this.all = difference(this.all, [].concat(playlists))
}, },
/** /**
@ -94,19 +94,19 @@ export const playlistStore = {
store (name, songs = []) { store (name, songs = []) {
if (songs.length) { if (songs.length) {
// Extract the IDs from the song objects. // Extract the IDs from the song objects.
songs = map(songs, 'id'); songs = map(songs, 'id')
} }
NProgress.start(); NProgress.start()
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http.post('playlist', { name, songs }, playlist => { http.post('playlist', { name, songs }, playlist => {
playlist.songs = songs; playlist.songs = songs
this.objectifySongs(playlist); this.objectifySongs(playlist)
this.add(playlist); this.add(playlist)
resolve(playlist); resolve(playlist)
}, r => reject(r)); }, r => reject(r))
}); })
}, },
/** /**
@ -115,14 +115,14 @@ export const playlistStore = {
* @param {Object} playlist * @param {Object} playlist
*/ */
delete (playlist) { delete (playlist) {
NProgress.start(); NProgress.start()
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http.delete(`playlist/${playlist.id}`, {}, data => { http.delete(`playlist/${playlist.id}`, {}, data => {
this.remove(playlist); this.remove(playlist)
resolve(data); resolve(data)
}, r => reject(r)); }, r => reject(r))
}); })
}, },
/** /**
@ -133,18 +133,18 @@ export const playlistStore = {
*/ */
addSongs (playlist, songs) { addSongs (playlist, songs) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const count = playlist.songs.length; const count = playlist.songs.length
playlist.songs = union(playlist.songs, songs); playlist.songs = union(playlist.songs, songs)
if (count === playlist.songs.length) { if (count === playlist.songs.length) {
resolve(playlist); resolve(playlist)
return; return
} }
http.put(`playlist/${playlist.id}/sync`, { songs: map(playlist.songs, 'id') }, http.put(`playlist/${playlist.id}/sync`, { songs: map(playlist.songs, 'id') },
data => resolve(playlist), data => resolve(playlist),
r => reject(r) r => reject(r)
); )
}) })
}, },
@ -155,13 +155,13 @@ export const playlistStore = {
* @param {Array.<Object>} songs * @param {Array.<Object>} songs
*/ */
removeSongs (playlist, songs) { removeSongs (playlist, songs) {
playlist.songs = difference(playlist.songs, songs); playlist.songs = difference(playlist.songs, songs)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http.put(`playlist/${playlist.id}/sync`, { songs: map(playlist.songs, 'id') }, http.put(`playlist/${playlist.id}/sync`, { songs: map(playlist.songs, 'id') },
data => resolve(playlist), data => resolve(playlist),
r => reject(r) r => reject(r)
); )
}) })
}, },
@ -171,10 +171,10 @@ export const playlistStore = {
* @param {Object} playlist * @param {Object} playlist
*/ */
update (playlist) { update (playlist) {
NProgress.start(); NProgress.start()
return new Promise((resolve, reject) => { 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 }, data => resolve(playlist), r => reject(r))
}); })
}, }
}; }

View file

@ -1,7 +1,7 @@
import { extend, has, each } from 'lodash'; import { extend, has, each } from 'lodash'
import { userStore } from '.'; import { userStore } from '.'
import { ls } from '../services'; import { ls } from '../services'
export const preferenceStore = { export const preferenceStore = {
storeKey: '', storeKey: '',
@ -14,11 +14,11 @@ export const preferenceStore = {
confirmClosing: false, confirmClosing: false,
equalizer: { equalizer: {
preamp: 0, preamp: 0,
gains: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], gains: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
}, },
artistsViewMode: null, artistsViewMode: null,
albumsViewMode: null, albumsViewMode: null,
selectedPreset: -1, selectedPreset: -1
}, },
/** /**
@ -28,12 +28,12 @@ export const preferenceStore = {
*/ */
init (user = null) { init (user = null) {
if (!user) { if (!user) {
user = userStore.current; user = userStore.current
} }
this.storeKey = `preferences_${user.id}`; this.storeKey = `preferences_${user.id}`
extend(this.state, ls.get(this.storeKey, this.state)); extend(this.state, ls.get(this.storeKey, this.state))
this.setupProxy(); this.setupProxy()
}, },
/** /**
@ -44,24 +44,24 @@ export const preferenceStore = {
Object.defineProperty(this, key, { Object.defineProperty(this, key, {
get: () => this.state[key], get: () => this.state[key],
set: (value) => { set: (value) => {
this.state[key] = value; this.state[key] = value
this.save(); this.save()
}, },
configurable: true, configurable: true
}); })
}); })
}, },
set (key, val) { set (key, val) {
this.state[key] = val; this.state[key] = val
this.save(); this.save()
}, },
get (key) { get (key) {
return has(this.state, key) ? this.state[key] : null; return has(this.state, key) ? this.state[key] : null
}, },
save () { save () {
ls.set(this.storeKey, this.state); ls.set(this.storeKey, this.state)
}, }
}; }

View file

@ -1,9 +1,9 @@
import { head, last, each, includes, union, difference, map, shuffle as _shuffle, first } from 'lodash'; import { head, last, each, includes, union, difference, map, shuffle as _shuffle, first } from 'lodash'
export const queueStore = { export const queueStore = {
state: { state: {
songs: [], songs: [],
current: null, current: null
}, },
init () { init () {
@ -37,7 +37,7 @@ export const queueStore = {
* @return {Array.<Object>} * @return {Array.<Object>}
*/ */
get all () { get all () {
return this.state.songs; return this.state.songs
}, },
/** /**
@ -46,7 +46,7 @@ export const queueStore = {
* @param {Array.<Object>} * @param {Array.<Object>}
*/ */
set all (songs) { set all (songs) {
this.state.songs = songs; this.state.songs = songs
}, },
/** /**
@ -55,7 +55,7 @@ export const queueStore = {
* @return {?Object} * @return {?Object}
*/ */
get first () { get first () {
return head(this.all); return head(this.all)
}, },
/** /**
@ -64,7 +64,7 @@ export const queueStore = {
* @return {?Object} * @return {?Object}
*/ */
get last () { get last () {
return last(this.all); return last(this.all)
}, },
/** /**
@ -75,7 +75,7 @@ export const queueStore = {
* @return {Boolean} * @return {Boolean}
*/ */
contains (song) { contains (song) {
return includes(this.all, song); return includes(this.all, song)
}, },
/** /**
@ -87,12 +87,12 @@ export const queueStore = {
* @param {Boolean} toTop Whether to prepend or append to the queue * @param {Boolean} toTop Whether to prepend or append to the queue
*/ */
queue (songs, replace = false, toTop = false) { queue (songs, replace = false, toTop = false) {
songs = [].concat(songs); songs = [].concat(songs)
if (replace) { if (replace) {
this.all = songs; this.all = songs
} else { } else {
this.all = toTop ? union(songs, this.all) : union(this.all, songs); this.all = toTop ? union(songs, this.all) : union(this.all, songs)
} }
}, },
@ -102,17 +102,17 @@ export const queueStore = {
* @param {Array.<Object>|Object} songs * @param {Array.<Object>|Object} songs
*/ */
queueAfterCurrent (songs) { queueAfterCurrent (songs) {
songs = [].concat(songs); songs = [].concat(songs)
if (!this.current || !this.all.length) { if (!this.current || !this.all.length) {
return this.queue(songs); return this.queue(songs)
} }
// First we unqueue the songs to make sure there are no duplicates. // First we unqueue the songs to make sure there are no duplicates.
this.unqueue(songs); this.unqueue(songs)
const head = this.all.splice(0, this.indexOf(this.current) + 1); const head = this.all.splice(0, this.indexOf(this.current) + 1)
this.all = head.concat(songs, this.all); this.all = head.concat(songs, this.all)
}, },
/** /**
@ -121,7 +121,7 @@ export const queueStore = {
* @param {Object|String|Array.<Object>} songs The song(s) to unqueue * @param {Object|String|Array.<Object>} songs The song(s) to unqueue
*/ */
unqueue (songs) { unqueue (songs) {
this.all = difference(this.all, [].concat(songs)); this.all = difference(this.all, [].concat(songs))
}, },
/** /**
@ -131,20 +131,20 @@ export const queueStore = {
* @param {Object} target The target song object * @param {Object} target The target song object
*/ */
move (songs, target) { move (songs, target) {
const $targetIndex = this.indexOf(target); const $targetIndex = this.indexOf(target)
each(songs, song => { each(songs, song => {
this.all.splice(this.indexOf(song), 1); this.all.splice(this.indexOf(song), 1)
this.all.splice($targetIndex, 0, song); this.all.splice($targetIndex, 0, song)
}); })
}, },
/** /**
* Clear the current queue. * Clear the current queue.
*/ */
clear () { clear () {
this.all = []; this.all = []
this.current = null; this.current = null
}, },
/** /**
@ -155,7 +155,7 @@ export const queueStore = {
* @return {?Integer} * @return {?Integer}
*/ */
indexOf (song) { indexOf (song) {
return this.all.indexOf(song); return this.all.indexOf(song)
}, },
/** /**
@ -165,12 +165,12 @@ export const queueStore = {
*/ */
get next () { get next () {
if (!this.current) { if (!this.current) {
return first(this.all); return first(this.all)
} }
const idx = map(this.all, 'id').indexOf(this.current.id) + 1; const idx = map(this.all, 'id').indexOf(this.current.id) + 1
return idx >= this.all.length ? null : this.all[idx]; return idx >= this.all.length ? null : this.all[idx]
}, },
/** /**
@ -180,12 +180,12 @@ export const queueStore = {
*/ */
get previous () { get previous () {
if (!this.current) { if (!this.current) {
return last(this.all); return last(this.all)
} }
const idx = map(this.all, 'id').indexOf(this.current.id) - 1; const idx = map(this.all, 'id').indexOf(this.current.id) - 1
return idx < 0 ? null : this.all[idx]; return idx < 0 ? null : this.all[idx]
}, },
/** /**
@ -194,7 +194,7 @@ export const queueStore = {
* @return {Object} * @return {Object}
*/ */
get current () { get current () {
return this.state.current; return this.state.current
}, },
/** /**
@ -205,7 +205,8 @@ export const queueStore = {
* @return {Object} The queued song. * @return {Object} The queued song.
*/ */
set current (song) { set current (song) {
return this.state.current = song; this.state.current = song
return this.state.current
}, },
/** /**
@ -214,6 +215,7 @@ export const queueStore = {
* @return {Array.<Object>} The shuffled array of song objects * @return {Array.<Object>} The shuffled array of song objects
*/ */
shuffle () { shuffle () {
return this.all = _shuffle(this.all); this.all = _shuffle(this.all)
}, return this.all
}; }
}

View file

@ -1,24 +1,24 @@
import { http } from '../services'; import { http } from '../services'
import stub from '../stubs/settings'; import stub from '../stubs/settings'
export const settingStore = { export const settingStore = {
stub, stub,
state: { state: {
settings: [], settings: []
}, },
init (settings) { init (settings) {
this.state.settings = settings; this.state.settings = settings
}, },
get all () { get all () {
return this.state.settings; return this.state.settings
}, },
update () { update () {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http.post('settings', this.all, data => resolve(data), r => reject(r)); http.post('settings', this.all, data => resolve(data), r => reject(r))
}); })
}, }
}; }

View file

@ -1,8 +1,8 @@
import { assign } from 'lodash'; import { assign } from 'lodash'
import isMobile from 'ismobilejs'; import isMobile from 'ismobilejs'
import { http } from '../services'; import { http } from '../services'
import { userStore, preferenceStore, artistStore, songStore, playlistStore, queueStore, settingStore } from '.'; import { userStore, preferenceStore, artistStore, songStore, playlistStore, queueStore, settingStore } from '.'
export const sharedStore = { export const sharedStore = {
state: { state: {
@ -22,58 +22,58 @@ export const sharedStore = {
currentVersion: '', currentVersion: '',
latestVersion: '', latestVersion: '',
cdnUrl: '', cdnUrl: '',
originalMediaPath: '', originalMediaPath: ''
}, },
init () { init () {
this.reset(); this.reset()
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http.get('data', data => { http.get('data', data => {
// Don't allow downloading on mobile devices // Don't allow downloading on mobile devices
data.allowDownload = data.allowDownload && !isMobile.any; data.allowDownload = data.allowDownload && !isMobile.any
assign(this.state, data); assign(this.state, data)
// Always disable YouTube integration on mobile. // Always disable YouTube integration on mobile.
this.state.useYouTube = this.state.useYouTube && !isMobile.phone; this.state.useYouTube = this.state.useYouTube && !isMobile.phone
// If this is a new user, initialize his preferences to be an empty object. // If this is a new user, initialize his preferences to be an empty object.
if (!this.state.currentUser.preferences) { if (!this.state.currentUser.preferences) {
this.state.currentUser.preferences = {}; this.state.currentUser.preferences = {}
} }
userStore.init(this.state.users, this.state.currentUser); userStore.init(this.state.users, this.state.currentUser)
preferenceStore.init(this.state.preferences); preferenceStore.init(this.state.preferences)
artistStore.init(this.state.artists); // This will init album and song stores as well. artistStore.init(this.state.artists) // This will init album and song stores as well.
songStore.initInteractions(this.state.interactions); songStore.initInteractions(this.state.interactions)
playlistStore.init(this.state.playlists); playlistStore.init(this.state.playlists)
queueStore.init(); queueStore.init()
settingStore.init(this.state.settings); settingStore.init(this.state.settings)
// Keep a copy of the media path. We'll need this to properly warn the user later. // 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; this.state.originalMediaPath = this.state.settings.media_path
resolve(data) resolve(data)
}, r => reject(r)); }, r => reject(r))
}); })
}, },
reset () { reset () {
this.state.songs = []; this.state.songs = []
this.state.albums = []; this.state.albums = []
this.state.artists = []; this.state.artists = []
this.state.favorites = []; this.state.favorites = []
this.state.queued = []; this.state.queued = []
this.state.interactions = []; this.state.interactions = []
this.state.users = []; this.state.users = []
this.state.settings = []; this.state.settings = []
this.state.currentUser = null; this.state.currentUser = null
this.state.playlists = []; this.state.playlists = []
this.state.useLastfm = false; this.state.useLastfm = false
this.state.allowDownload = false; this.state.allowDownload = false
this.state.currentVersion = ''; this.state.currentVersion = ''
this.state.latestVersion = ''; this.state.latestVersion = ''
this.state.cdnUrl = ''; this.state.cdnUrl = ''
}, }
}; }

View file

@ -1,10 +1,10 @@
import Vue from 'vue'; import Vue from 'vue'
import { without, map, take, remove, orderBy, each, union } from 'lodash'; import { without, map, take, remove, orderBy, each, union } from 'lodash'
import { secondsToHis } from '../utils'; import { secondsToHis } from '../utils'
import { http, ls } from '../services'; import { http, ls } from '../services'
import { sharedStore, favoriteStore, userStore, albumStore, artistStore } from '.'; import { sharedStore, favoriteStore, albumStore, artistStore } from '.'
import stub from '../stubs/song'; import stub from '../stubs/song'
export const songStore = { export const songStore = {
stub, stub,
@ -24,7 +24,7 @@ export const songStore = {
* *
* @type {Array} * @type {Array}
*/ */
recent: [], recent: []
}, },
/** /**
@ -37,34 +37,34 @@ export const songStore = {
// While doing so, we populate some other information into the songs as well. // While doing so, we populate some other information into the songs as well.
this.all = albums.reduce((songs, album) => { this.all = albums.reduce((songs, album) => {
each(album.songs, song => { each(album.songs, song => {
this.setupSong(song, album); this.setupSong(song, album)
}); })
return songs.concat(album.songs); return songs.concat(album.songs)
}, []); }, [])
}, },
setupSong (song, album) { setupSong (song, album) {
song.fmtLength = secondsToHis(song.length); song.fmtLength = secondsToHis(song.length)
// Manually set these additional properties to be reactive // Manually set these additional properties to be reactive
Vue.set(song, 'playCount', 0); Vue.set(song, 'playCount', 0)
Vue.set(song, 'album', album); Vue.set(song, 'album', album)
Vue.set(song, 'liked', false); Vue.set(song, 'liked', false)
Vue.set(song, 'lyrics', null); Vue.set(song, 'lyrics', null)
Vue.set(song, 'playbackState', 'stopped'); Vue.set(song, 'playbackState', 'stopped')
if (song.contributing_artist_id) { if (song.contributing_artist_id) {
const artist = artistStore.byId(song.contributing_artist_id); const artist = artistStore.byId(song.contributing_artist_id)
artist.albums = union(artist.albums, [album]); artist.albums = union(artist.albums, [album])
artistStore.setupArtist(artist); artistStore.setupArtist(artist)
Vue.set(song, 'artist', artist); Vue.set(song, 'artist', artist)
} else { } else {
Vue.set(song, 'artist', artistStore.byId(song.album.artist.id)); Vue.set(song, 'artist', artistStore.byId(song.album.artist.id))
} }
// Cache the song, so that byId() is faster // Cache the song, so that byId() is faster
this.cache[song.id] = song; this.cache[song.id] = song
}, },
/** /**
@ -73,24 +73,24 @@ export const songStore = {
* @param {Array.<Object>} interactions The array of interactions of the current user * @param {Array.<Object>} interactions The array of interactions of the current user
*/ */
initInteractions (interactions) { initInteractions (interactions) {
favoriteStore.clear(); favoriteStore.clear()
each(interactions, interaction => { each(interactions, interaction => {
const song = this.byId(interaction.song_id); const song = this.byId(interaction.song_id)
if (!song) { if (!song) {
return; return
} }
song.liked = interaction.liked; song.liked = interaction.liked
song.playCount = interaction.play_count; song.playCount = interaction.play_count
song.album.playCount += song.playCount; song.album.playCount += song.playCount
song.artist.playCount += song.playCount; song.artist.playCount += song.playCount
if (song.liked) { if (song.liked) {
favoriteStore.add(song); favoriteStore.add(song)
} }
}); })
}, },
/** /**
@ -102,9 +102,9 @@ export const songStore = {
* @return {Float|String} * @return {Float|String}
*/ */
getLength (songs, toHis) { getLength (songs, toHis) {
const duration = songs.reduce((length, song) => length + song.length, 0); const duration = songs.reduce((length, song) => length + song.length, 0)
return toHis ? secondsToHis(duration) : duration; return toHis ? secondsToHis(duration) : duration
}, },
/** /**
@ -113,7 +113,7 @@ export const songStore = {
* @return {Array.<Object>} * @return {Array.<Object>}
*/ */
get all () { get all () {
return this.state.songs; return this.state.songs
}, },
/** /**
@ -122,7 +122,7 @@ export const songStore = {
* @param {Array.<Object>} value * @param {Array.<Object>} value
*/ */
set all (value) { set all (value) {
this.state.songs = value; this.state.songs = value
}, },
/** /**
@ -133,7 +133,7 @@ export const songStore = {
* @return {Object} * @return {Object}
*/ */
byId (id) { byId (id) {
return this.cache[id]; return this.cache[id]
}, },
/** /**
@ -144,7 +144,7 @@ export const songStore = {
* @return {Array.<Object>} * @return {Array.<Object>}
*/ */
byIds (ids) { byIds (ids) {
return ids.map(id => this.byId(id)); return ids.map(id => this.byId(id))
}, },
/** /**
@ -154,17 +154,17 @@ export const songStore = {
*/ */
registerPlay (song) { registerPlay (song) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const oldCount = song.playCount; const oldCount = song.playCount
http.post('interaction/play', { song: song.id }, data => { http.post('interaction/play', { song: song.id }, data => {
// Use the data from the server to make sure we don't miss a play from another device. // 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 = data.play_count
song.album.playCount += song.playCount - oldCount; song.album.playCount += song.playCount - oldCount
song.artist.playCount += song.playCount - oldCount; song.artist.playCount += song.playCount - oldCount
resolve(data); resolve(data)
}, r => reject(r)); }, r => reject(r))
}); })
}, },
/** /**
@ -174,10 +174,10 @@ export const songStore = {
*/ */
addRecent (song) { addRecent (song) {
// First we make sure that there's no duplicate. // First we make sure that there's no duplicate.
this.state.recent = without(this.state.recent, song); this.state.recent = without(this.state.recent, song)
// Then we prepend the song into the list. // Then we prepend the song into the list.
this.state.recent.unshift(song); this.state.recent.unshift(song)
}, },
/** /**
@ -187,7 +187,7 @@ export const songStore = {
*/ */
scrobble (song) { scrobble (song) {
return new Promise((resolve, reject) => { 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}`, {}, data => resolve(data), r => reject(r))
}) })
}, },
@ -201,12 +201,12 @@ export const songStore = {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http.put('songs', { http.put('songs', {
data, data,
songs: map(songs, 'id'), songs: map(songs, 'id')
}, songs => { }, songs => {
each(songs, song => this.syncUpdatedSong(song)); each(songs, song => this.syncUpdatedSong(song))
resolve(songs); resolve(songs)
}, r => reject(r)); }, r => reject(r))
}); })
}, },
/** /**
@ -229,77 +229,77 @@ export const songStore = {
// 2.b. Artist changes as well. Note that an artist might have been created. // 2.b. Artist changes as well. Note that an artist might have been created.
// Find the original song, // Find the original song,
const originalSong = this.byId(updatedSong.id); const originalSong = this.byId(updatedSong.id)
if (!originalSong) { if (!originalSong) {
return; return
} }
// and keep track of original album/artist. // and keep track of original album/artist.
const originalAlbumId = originalSong.album.id; const originalAlbumId = originalSong.album.id
const originalArtistId = originalSong.artist.id; const originalArtistId = originalSong.artist.id
// First, we update the title, lyrics, and track # // First, we update the title, lyrics, and track #
originalSong.title = updatedSong.title; originalSong.title = updatedSong.title
originalSong.lyrics = updatedSong.lyrics; originalSong.lyrics = updatedSong.lyrics
originalSong.track = updatedSong.track; originalSong.track = updatedSong.track
if (updatedSong.album.id === originalAlbumId) { // case 1 if (updatedSong.album.id === originalAlbumId) { // case 1
// Nothing to do // Nothing to do
} else { // case 2 } else { // case 2
// First, remove it from its old album // First, remove it from its old album
albumStore.removeSongsFromAlbum(originalSong.album, originalSong); albumStore.removeSongsFromAlbum(originalSong.album, originalSong)
const existingAlbum = albumStore.byId(updatedSong.album.id); const existingAlbum = albumStore.byId(updatedSong.album.id)
const newAlbumCreated = !existingAlbum; const newAlbumCreated = !existingAlbum
if (!newAlbumCreated) { if (!newAlbumCreated) {
// The song changed to an existing album. We now add it to such album. // The song changed to an existing album. We now add it to such album.
albumStore.addSongsIntoAlbum(existingAlbum, originalSong); albumStore.addSongsIntoAlbum(existingAlbum, originalSong)
} else { } else {
// A new album was created. We: // A new album was created. We:
// - Add the new album into our collection // - Add the new album into our collection
// - Add the song into it // - Add the song into it
albumStore.addSongsIntoAlbum(updatedSong.album, originalSong); albumStore.addSongsIntoAlbum(updatedSong.album, originalSong)
albumStore.add(updatedSong.album); albumStore.add(updatedSong.album)
} }
if (updatedSong.album.artist.id === originalArtistId) { // case 2.a if (updatedSong.album.artist.id === originalArtistId) { // case 2.a
// Same artist, but what if the album is new? // Same artist, but what if the album is new?
if (newAlbumCreated) { if (newAlbumCreated) {
artistStore.addAlbumsIntoArtist(artistStore.byId(originalArtistId), updatedSong.album); artistStore.addAlbumsIntoArtist(artistStore.byId(originalArtistId), updatedSong.album)
} }
} else { // case 2.b } else { // case 2.b
// The artist changes. // The artist changes.
const existingArtist = artistStore.byId(updatedSong.album.artist.id); const existingArtist = artistStore.byId(updatedSong.album.artist.id)
if (existingArtist) { if (existingArtist) {
originalSong.artist = existingArtist; originalSong.artist = existingArtist
} else { } else {
// New artist created. We: // New artist created. We:
// - Add the album into it, because now it MUST BE a new album // - Add the album into it, because now it MUST BE a new album
// (there's no "new artist with existing album" in our system). // (there's no "new artist with existing album" in our system).
// - Add the new artist into our collection // - Add the new artist into our collection
artistStore.addAlbumsIntoArtist(updatedSong.album.artist, updatedSong.album); artistStore.addAlbumsIntoArtist(updatedSong.album.artist, updatedSong.album)
artistStore.add(updatedSong.album.artist); artistStore.add(updatedSong.album.artist)
originalSong.artist = updatedSong.album.artist; originalSong.artist = updatedSong.album.artist
} }
} }
// As a last step, we purify our library of empty albums/artists. // As a last step, we purify our library of empty albums/artists.
if (albumStore.isAlbumEmpty(albumStore.byId(originalAlbumId))) { if (albumStore.isAlbumEmpty(albumStore.byId(originalAlbumId))) {
albumStore.remove(albumStore.byId(originalAlbumId)); albumStore.remove(albumStore.byId(originalAlbumId))
} }
if (artistStore.isArtistEmpty(artistStore.byId(originalArtistId))) { if (artistStore.isArtistEmpty(artistStore.byId(originalArtistId))) {
artistStore.remove(artistStore.byId(originalArtistId)); artistStore.remove(artistStore.byId(originalArtistId))
} }
// Now we make sure the next call to info() get the refreshed, correct info. // Now we make sure the next call to info() get the refreshed, correct info.
originalSong.infoRetrieved = false; originalSong.infoRetrieved = false
} }
return originalSong; return originalSong
}, },
/** /**
@ -310,7 +310,7 @@ export const songStore = {
* @return {string} The source URL, with JWT token appended. * @return {string} The source URL, with JWT token appended.
*/ */
getSourceUrl (song) { getSourceUrl (song) {
return `${sharedStore.state.cdnUrl}api/${song.id}/play?jwt-token=${ls.get('jwt-token')}`; return `${sharedStore.state.cdnUrl}api/${song.id}/play?jwt-token=${ls.get('jwt-token')}`
}, },
/** /**
@ -322,7 +322,7 @@ export const songStore = {
* @return {string} * @return {string}
*/ */
getShareableUrl (song) { getShareableUrl (song) {
return `${window.location.origin}/#!/song/${song.id}`; return `${window.location.origin}/#!/song/${song.id}`
}, },
/** /**
@ -333,7 +333,7 @@ export const songStore = {
* @return {Array.<Object>} * @return {Array.<Object>}
*/ */
getRecent (n = 10) { getRecent (n = 10) {
return take(this.state.recent, n); return take(this.state.recent, n)
}, },
/** /**
@ -344,12 +344,12 @@ export const songStore = {
* @return {Array.<Object>} * @return {Array.<Object>}
*/ */
getMostPlayed (n = 10) { getMostPlayed (n = 10) {
const songs = take(orderBy(this.all, 'playCount', 'desc'), n); const songs = take(orderBy(this.all, 'playCount', 'desc'), n)
// Remove those with playCount=0 // Remove those with playCount=0
remove(songs, song => !song.playCount); remove(songs, song => !song.playCount)
return songs; return songs
}, },
/** /**
@ -358,7 +358,7 @@ export const songStore = {
* @return {Array.<Object>} * @return {Array.<Object>}
*/ */
getRecentlyAdded (n = 10) { getRecentlyAdded (n = 10) {
return take(orderBy(this.all, 'created_at', 'desc'), n); return take(orderBy(this.all, 'created_at', 'desc'), n)
}, },
/** /**
@ -366,6 +366,6 @@ export const songStore = {
* Reset stuff. * Reset stuff.
*/ */
teardown () { teardown () {
this.state.recent = []; this.state.recent = []
}, }
}; }

View file

@ -1,17 +1,17 @@
import { each, find, without } from 'lodash'; import { each, find, without } from 'lodash'
import md5 from 'blueimp-md5'; import md5 from 'blueimp-md5'
import Vue from 'vue'; import Vue from 'vue'
import NProgress from 'nprogress'; import NProgress from 'nprogress'
import { http } from '../services'; import { http } from '../services'
import stub from '../stubs/user'; import stub from '../stubs/user'
export const userStore = { export const userStore = {
stub, stub,
state: { state: {
users: [], users: [],
current: stub, current: stub
}, },
/** /**
@ -21,14 +21,14 @@ export const userStore = {
* @param {Object} currentUser The current user. * @param {Object} currentUser The current user.
*/ */
init (users, currentUser) { init (users, currentUser) {
this.all = users; this.all = users
this.current = currentUser; this.current = currentUser
// Set the avatar for each of the users… // Set the avatar for each of the users…
each(this.all, this.setAvatar); each(this.all, this.setAvatar)
// …and the current user as well. // …and the current user as well.
this.setAvatar(); this.setAvatar()
}, },
/** /**
@ -37,7 +37,7 @@ export const userStore = {
* @return {Array.<Object>} * @return {Array.<Object>}
*/ */
get all () { get all () {
return this.state.users; return this.state.users
}, },
/** /**
@ -46,7 +46,7 @@ export const userStore = {
* @param {Array.<Object>} value * @param {Array.<Object>} value
*/ */
set all (value) { set all (value) {
this.state.users = value; this.state.users = value
}, },
/** /**
@ -57,7 +57,7 @@ export const userStore = {
* @return {Object} * @return {Object}
*/ */
byId (id) { byId (id) {
return find(this.all, { id }); return find(this.all, { id })
}, },
/** /**
@ -66,7 +66,7 @@ export const userStore = {
* @return {Object} * @return {Object}
*/ */
get current () { get current () {
return this.state.current; return this.state.current
}, },
/** /**
@ -77,7 +77,8 @@ export const userStore = {
* @return {Object} * @return {Object}
*/ */
set current (user) { set current (user) {
return this.state.current = user; this.state.current = user
return this.state.current
}, },
/** /**
@ -87,10 +88,10 @@ export const userStore = {
*/ */
setAvatar (user = null) { setAvatar (user = null) {
if (!user) { if (!user) {
user = this.current; user = this.current
} }
Vue.set(user, 'avatar', `https://www.gravatar.com/avatar/${md5(user.email)}?s=256`); Vue.set(user, 'avatar', `https://www.gravatar.com/avatar/${md5(user.email)}?s=256`)
}, },
/** /**
@ -100,11 +101,11 @@ export const userStore = {
* @param {String} password * @param {String} password
*/ */
login (email, password) { login (email, password) {
NProgress.start(); NProgress.start()
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http.post('me', { email, password }, data => resolve(data), r => reject(r)); http.post('me', { email, password }, data => resolve(data), r => reject(r))
}); })
}, },
/** /**
@ -112,8 +113,8 @@ export const userStore = {
*/ */
logout () { logout () {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http.delete('me', {}, data => resolve(data), r => reject(r)); http.delete('me', {}, data => resolve(data), r => reject(r))
}); })
}, },
/** /**
@ -122,7 +123,7 @@ export const userStore = {
* @param {string} password Can be an empty string if the user is not changing his password. * @param {string} password Can be an empty string if the user is not changing his password.
*/ */
updateProfile (password) { updateProfile (password) {
NProgress.start(); NProgress.start()
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http.put('me', { http.put('me', {
@ -130,11 +131,11 @@ export const userStore = {
name: this.current.name, name: this.current.name,
email: this.current.email email: this.current.email
}, () => { }, () => {
this.setAvatar(); this.setAvatar()
resolve(this.current) resolve(this.current)
}, r => reject(r) },
); r => reject(r))
}); })
}, },
/** /**
@ -145,15 +146,15 @@ export const userStore = {
* @param {string} password * @param {string} password
*/ */
store (name, email, password) { store (name, email, password) {
NProgress.start(); NProgress.start()
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http.post('user', { name, email, password }, user => { http.post('user', { name, email, password }, user => {
this.setAvatar(user); this.setAvatar(user)
this.all.unshift(user); this.all.unshift(user)
resolve(user); resolve(user)
}, r => reject(r)); }, r => reject(r))
}); })
}, },
/** /**
@ -165,15 +166,15 @@ export const userStore = {
* @param {String} password * @param {String} password
*/ */
update (user, name, email, password) { update (user, name, email, password) {
NProgress.start(); NProgress.start()
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http.put(`user/${user.id}`, { name, email, password }, () => { http.put(`user/${user.id}`, { name, email, password }, () => {
this.setAvatar(user); this.setAvatar(user)
user.password = ''; user.password = ''
resolve(user); resolve(user)
}, r => reject(r)); }, r => reject(r))
}); })
}, },
/** /**
@ -182,11 +183,11 @@ export const userStore = {
* @param {Object} user * @param {Object} user
*/ */
destroy (user) { destroy (user) {
NProgress.start(); NProgress.start()
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http.delete(`user/${user.id}`, {}, data => { http.delete(`user/${user.id}`, {}, data => {
this.all = without(this.all, user); this.all = without(this.all, user)
// Mama, just killed a man // Mama, just killed a man
// Put a gun against his head // Put a gun against his head
@ -210,8 +211,8 @@ export const userStore = {
/** /**
* Brian May enters the stage. * Brian May enters the stage.
*/ */
resolve(data); resolve(data)
}, r => reject(r)); }, r => reject(r))
}); })
}, }
}; }

View file

@ -1,5 +1,5 @@
import config from '../config'; import config from '../config'
import artist from './artist'; import artist from './artist'
export default { export default {
artist, artist,
@ -10,5 +10,5 @@ export default {
playCount: 0, playCount: 0,
length: 0, length: 0,
fmtLength: '00:00', fmtLength: '00:00',
songs: [], songs: []
}; }

View file

@ -4,5 +4,5 @@ export default {
image: null, image: null,
playCount: 0, playCount: 0,
albums: [], albums: [],
songs: [], songs: []
}; }

View file

@ -1,4 +1,4 @@
export default { export default {
name: '', name: '',
songs: [], songs: []
}; }

View file

@ -1,3 +1,3 @@
export default { export default {
media_path: '', media_path: ''
}; }

View file

@ -1,5 +1,5 @@
import album from './album'; import album from './album'
import artist from './artist'; import artist from './artist'
export default { export default {
album, album,
@ -12,5 +12,5 @@ export default {
lyrics: '', lyrics: '',
liked: false, liked: false,
playCount: 0, playCount: 0,
playbackState: 'stopped', playbackState: 'stopped'
}; }

View file

@ -3,5 +3,5 @@ export default {
name: '', name: '',
email: '', email: '',
avatar: '', avatar: '',
is_admin: false, is_admin: false
}; }

View file

@ -1,32 +1,32 @@
export default [ export default [
{ {
id: 1, id: 1,
song_id: "7900ab518f51775fe6cf06092c074ee5", song_id: '7900ab518f51775fe6cf06092c074ee5',
liked: false, liked: false,
play_count: 1 play_count: 1
}, },
{ {
id: 2, id: 2,
song_id: "95c0ffc33c08c8c14ea5de0a44d5df3c", song_id: '95c0ffc33c08c8c14ea5de0a44d5df3c',
liked: false, liked: false,
play_count: 2 play_count: 2
}, },
{ {
id: 3, id: 3,
song_id: "c83b201502eb36f1084f207761fa195c", song_id: 'c83b201502eb36f1084f207761fa195c',
liked: false, liked: false,
play_count: 1 play_count: 1
}, },
{ {
id: 4, id: 4,
song_id: "cb7edeac1f097143e65b1b2cde102482", song_id: 'cb7edeac1f097143e65b1b2cde102482',
liked: true, liked: true,
play_count: 3 play_count: 3
}, },
{ {
id: 5, id: 5,
song_id: "ccc38cc14bb95aefdf6da4b34adcf548", song_id: 'ccc38cc14bb95aefdf6da4b34adcf548',
liked: false, liked: false,
play_count: 4 play_count: 4
} }
]; ]

View file

@ -1,18 +1,18 @@
export default [ export default [
{ {
id: 1, id: 1,
name: "All-4-One", name: 'All-4-One',
albums: [ albums: [
{ {
id: 1193, id: 1193,
artist_id: 1, artist_id: 1,
name: "All-4-One", name: 'All-4-One',
cover: "/public/img/covers/565c0f7067425.jpeg", cover: '/public/img/covers/565c0f7067425.jpeg',
songs: [ songs: [
{ {
id: "39189f4545f9d5671fb3dc964f0080a0", id: '39189f4545f9d5671fb3dc964f0080a0',
album_id: 1193, album_id: 1193,
title: "I Swear", title: 'I Swear',
length: 259.92, length: 259.92,
playCount: 4 playCount: 4
} }
@ -21,13 +21,13 @@ export default [
{ {
id: 1194, id: 1194,
artist_id: 1, artist_id: 1,
name: "And The Music Speaks", name: 'And The Music Speaks',
cover: "/public/img/covers/unknown-album.png", cover: '/public/img/covers/unknown-album.png',
songs: [ songs: [
{ {
id: "a6a550f7d950d2a2520f9bf1a60f025a", id: 'a6a550f7d950d2a2520f9bf1a60f025a',
album_id: 1194, album_id: 1194,
title: "I can love you like that", title: 'I can love you like that',
length: 262.61, length: 262.61,
playCount: 2 playCount: 2
} }
@ -36,13 +36,13 @@ export default [
{ {
id: 1195, id: 1195,
artist_id: 1, artist_id: 1,
name: "Space Jam", name: 'Space Jam',
cover: "/public/img/covers/565c0f7115e0f.png", cover: '/public/img/covers/565c0f7115e0f.png',
songs: [ songs: [
{ {
id: "d86c30fd34f13c1aff8db59b7fc9c610", id: 'd86c30fd34f13c1aff8db59b7fc9c610',
album_id: 1195, album_id: 1195,
title: "I turn to you", title: 'I turn to you',
length: 293.04 length: 293.04
} }
] ]
@ -51,18 +51,18 @@ export default [
}, },
{ {
id: 2, id: 2,
name: "Bob Dylan", name: 'Bob Dylan',
albums: [ albums: [
{ {
id: 1217, id: 1217,
artist_id: 2, artist_id: 2,
name: "Highway 61 Revisited", name: 'Highway 61 Revisited',
cover: "/public/img/covers/565c0f76dc6e8.jpeg", cover: '/public/img/covers/565c0f76dc6e8.jpeg',
songs: [ songs: [
{ {
id: "e6d3977f3ffa147801ca5d1fdf6fa55e", id: 'e6d3977f3ffa147801ca5d1fdf6fa55e',
album_id: 1217, album_id: 1217,
title: "Like a rolling stone", title: 'Like a rolling stone',
length: 373.63 length: 373.63
} }
] ]
@ -70,13 +70,13 @@ export default [
{ {
id: 1218, id: 1218,
artist_id: 2, artist_id: 2,
name: "Pat Garrett & Billy the Kid", name: 'Pat Garrett & Billy the Kid',
cover: "/public/img/covers/unknown-album.png", cover: '/public/img/covers/unknown-album.png',
songs: [ songs: [
{ {
id: "aa16bbef6a9710eb9a0f41ecc534fad5", id: 'aa16bbef6a9710eb9a0f41ecc534fad5',
album_id: 1218, album_id: 1218,
title: "Knockin' on heaven's door", title: 'Knockin\' on heaven\'s door',
length: 151.9 length: 151.9
} }
] ]
@ -84,13 +84,13 @@ export default [
{ {
id: 1219, id: 1219,
artist_id: 2, artist_id: 2,
name: "The Times They Are A-Changin'", name: 'The Times They Are A-Changin\'',
cover: "/public/img/covers/unknown-album.png", cover: '/public/img/covers/unknown-album.png',
songs: [ songs: [
{ {
id: "cb7edeac1f097143e65b1b2cde102482", id: 'cb7edeac1f097143e65b1b2cde102482',
album_id: 1219, album_id: 1219,
title: "The times they are a-changin'", title: 'The times they are a-changin\'',
length: 196 length: 196
} }
] ]
@ -99,116 +99,116 @@ export default [
}, },
{ {
id: 3, id: 3,
name: "James Blunt", name: 'James Blunt',
albums: [ albums: [
{ {
id: 1268, id: 1268,
artist_id: 3, artist_id: 3,
name: "Back To Bedlam", name: 'Back To Bedlam',
cover: "/public/img/covers/unknown-album.png", cover: '/public/img/covers/unknown-album.png',
songs: [ songs: [
{ {
id: "0ba9fb128427b32683b9eb9140912a70", id: '0ba9fb128427b32683b9eb9140912a70',
album_id: 1268, album_id: 1268,
title: "No bravery", title: 'No bravery',
length: 243.12 length: 243.12
}, },
{ {
id: "123fd1ad32240ecab28a4e86ed5173", id: '123fd1ad32240ecab28a4e86ed5173',
album_id: 1268, album_id: 1268,
title: "So long, Jimmy", title: 'So long, Jimmy',
length: 265.04 length: 265.04
}, },
{ {
id: "6a54c674d8b16732f26df73f59c63e21", id: '6a54c674d8b16732f26df73f59c63e21',
album_id: 1268, album_id: 1268,
title: "Wisemen", title: 'Wisemen',
length: 223.14 length: 223.14
}, },
{ {
id: "6df7d82a9a8701e40d1c291cf14a16bc", id: '6df7d82a9a8701e40d1c291cf14a16bc',
album_id: 1268, album_id: 1268,
title: "Goodbye my lover", title: 'Goodbye my lover',
length: 258.61 length: 258.61
}, },
{ {
id: "74a2000d343e4587273d3ad14e2fd741", id: '74a2000d343e4587273d3ad14e2fd741',
album_id: 1268, album_id: 1268,
title: "High", title: 'High',
length: 245.86 length: 245.86
}, },
{ {
id: "7900ab518f51775fe6cf06092c074ee5", id: '7900ab518f51775fe6cf06092c074ee5',
album_id: 1268, album_id: 1268,
title: "You're beautiful", title: 'You\'re beautiful',
length: 213.29 length: 213.29
}, },
{ {
id: "803910a51f9893347e087af851e38777", id: '803910a51f9893347e087af851e38777',
album_id: 1268, album_id: 1268,
title: "Cry", title: 'Cry',
length: 246.91 length: 246.91
}, },
{ {
id: "d82b0d4d4803ebbcb61000a5b6a868f5", id: 'd82b0d4d4803ebbcb61000a5b6a868f5',
album_id: 1268, album_id: 1268,
title: "Tears and rain", title: 'Tears and rain',
length: 244.45 length: 244.45
} }
] ]
} }
] ]
} }
]; ]
export const singleAlbum = { export const singleAlbum = {
id: 9999, id: 9999,
artist_id: 99, artist_id: 99,
name: "Foo bar", name: 'Foo bar',
cover: "/foo.jpg", cover: '/foo.jpg',
songs: [ songs: [
{ {
id: "39189f4545f0d5671fc3dc964f0080a0", id: '39189f4545f0d5671fc3dc964f0080a0',
album_id: 9999, album_id: 9999,
title: "A Foo Song", title: 'A Foo Song',
length: 100, length: 100,
playCount: 4 playCount: 4
}, { }, {
id: "39189f4545f9d5671fc3dc96cf1080a0", id: '39189f4545f9d5671fc3dc96cf1080a0',
album_id: 9999, album_id: 9999,
title: "A Bar Song", title: 'A Bar Song',
length: 200, length: 200,
playCount: 7 playCount: 7
} }
] ]
}; }
export const singleArtist = { export const singleArtist = {
id: 999, id: 999,
name: "John Cena", name: 'John Cena',
albums: [ albums: [
{ {
id: 9991, id: 9991,
artist_id: 999, artist_id: 999,
name: "It's John Cena!!!!", name: 'It\'s John Cena!!!!',
cover: "/tmp/john.jpg", cover: '/tmp/john.jpg',
songs: [ songs: [
{ {
id: "e6d3977f3ffa147801ca5d1fdf6fa55f", id: 'e6d3977f3ffa147801ca5d1fdf6fa55f',
album_id: 9991, album_id: 9991,
title: "John Cena to the Rescue", title: 'John Cena to the Rescue',
length: 300 length: 300
} }
] ]
} }
] ]
}; }
export const singleSong = { export const singleSong = {
id: "dccb0d4d4803ebbcb61000a5b6a868f5", id: 'dccb0d4d4803ebbcb61000a5b6a868f5',
album_id: 1193, album_id: 1193,
title: "Foo and Bar", title: 'Foo and Bar',
length: 100, length: 100,
playCount: 4, playCount: 4,
lyrics: '' lyrics: ''
}; }

View file

@ -3,7 +3,7 @@ export default {
id: 1, id: 1,
name: 'Phan An', name: 'Phan An',
email: 'me@phanan.net', email: 'me@phanan.net',
is_admin: true, is_admin: true
}, },
users: [ users: [
@ -11,13 +11,13 @@ export default {
id: 1, id: 1,
name: 'Phan An', name: 'Phan An',
email: 'me@phanan.net', email: 'me@phanan.net',
is_admin: true, is_admin: true
}, },
{ {
id: 2, id: 2,
name: 'John Doe', name: 'John Doe',
email: 'john@doe.tld', email: 'john@doe.tld',
is_admin: false, is_admin: false
}, }
] ]
}; }

View file

@ -1,13 +1,13 @@
import { jsdom } from 'jsdom'; import { jsdom } from 'jsdom'
const doc = jsdom('<!doctype html><html><body></body></html>'); const doc = jsdom('<!doctype html><html><body></body></html>')
const win = doc.defaultView; const win = doc.defaultView
global.document = doc; global.document = doc
global.window = win; global.window = win
Object.keys(window).forEach((key) => { Object.keys(window).forEach((key) => {
if (!(key in global)) { if (!(key in global)) {
global[key] = window[key]; global[key] = window[key]
} }
}); })

View file

@ -1,34 +1,35 @@
require('chai').should(); require('chai').should()
import localStorage from 'local-storage'; import localStorage from 'local-storage'
import { ls } from '../../services'; import { ls } from '../../services'
describe('services/ls', () => { describe('services/ls', () => {
beforeEach(() => localStorage.remove('foo')); beforeEach(() => localStorage.remove('foo'))
describe('#get', () => { describe('#get', () => {
it('correctly gets an existing item from local storage', () => { it('correctly gets an existing item from local storage', () => {
localStorage('foo', 'bar'); localStorage('foo', 'bar')
ls.get('foo').should.equal('bar'); ls.get('foo').should.equal('bar')
}); })
it('correctly returns the default value for a non exising item', () => { it('correctly returns the default value for a non exising item', () => {
ls.get('baz', 'qux').should.equal('qux'); ls.get('baz', 'qux').should.equal('qux')
}); })
}); })
describe('#set', () => { describe('#set', () => {
it('correctly sets an item into local storage', () => { it('correctly sets an item into local storage', () => {
ls.set('foo', 'bar'); ls.set('foo', 'bar')
localStorage('foo').should.equal('bar'); localStorage('foo').should.equal('bar')
}); })
}); })
describe('#remove', () => { describe('#remove', () => {
it('correctly removes an item from local storage', () => { it('correctly removes an item from local storage', () => {
localStorage('foo', 'bar'); localStorage('foo', 'bar')
ls.remove('foo'); ls.remove('foo')
(localStorage('foo') === null).should.be.true; var result = localStorage('foo') === null
}); result.should.be.true
}); })
}); })
})

View file

@ -1,102 +1,102 @@
require('chai').should(); require('chai').should()
import { cloneDeep, last } from 'lodash'; import { cloneDeep, last } from 'lodash'
import { albumStore, artistStore } from '../../stores'; import { albumStore, artistStore } from '../../stores'
import { default as artists, singleAlbum, singleSong } from '../blobs/media'; import { default as artists, singleAlbum, singleSong } from '../blobs/media'
describe('stores/album', () => { describe('stores/album', () => {
beforeEach(() => albumStore.init(cloneDeep(artists))); beforeEach(() => albumStore.init(cloneDeep(artists)))
afterEach(() => albumStore.state.albums = []); afterEach(() => albumStore.state.albums = [])
describe('#init', () => { describe('#init', () => {
it('correctly gathers albums', () => { it('correctly gathers albums', () => {
albumStore.state.albums.length.should.equal(7); albumStore.state.albums.length.should.equal(7)
}); })
it('correctly sets albums length', () => { it('correctly sets albums length', () => {
albumStore.state.albums[0].length.should.equal(259.92); albumStore.state.albums[0].length.should.equal(259.92)
}); })
it('correctly sets album artists', () => { it('correctly sets album artists', () => {
albumStore.state.albums[0].artist.id.should.equal(1); albumStore.state.albums[0].artist.id.should.equal(1)
}); })
}); })
describe('#all', () => { describe('#all', () => {
it('correctly returns all songs', () => { it('correctly returns all songs', () => {
albumStore.all.length.should.equal(7); albumStore.all.length.should.equal(7)
}); })
}); })
describe('#getLength', () => { describe('#getLength', () => {
it('correctly calculates an albums length', () => { it('correctly calculates an albums length', () => {
albumStore.getLength(albumStore.state.albums[6]); albumStore.getLength(albumStore.state.albums[6])
albumStore.state.albums[6].length.should.equal(1940.42); // I'm sorry… albumStore.state.albums[6].length.should.equal(1940.42); // I'm sorry…
}); })
}); })
describe('#add', () => { describe('#add', () => {
beforeEach(() => { beforeEach(() => {
albumStore.add(cloneDeep(singleAlbum)); albumStore.add(cloneDeep(singleAlbum))
}); })
it('correctly adds a new album into the state', () => { it('correctly adds a new album into the state', () => {
last(albumStore.state.albums).id.should.equal(9999); last(albumStore.state.albums).id.should.equal(9999)
}); })
it('correctly recalculates the length', () => { it('correctly recalculates the length', () => {
last(albumStore.state.albums).length.should.equal(300); last(albumStore.state.albums).length.should.equal(300)
}); })
it('correctly recalculates the play count', () => { it('correctly recalculates the play count', () => {
last(albumStore.state.albums).playCount.should.equal(11); last(albumStore.state.albums).playCount.should.equal(11)
}); })
}); })
describe('#remove', () => { describe('#remove', () => {
beforeEach(() => { beforeEach(() => {
albumStore.remove(albumStore.state.albums[0]); // ID 1193 albumStore.remove(albumStore.state.albums[0]); // ID 1193
}); })
it('correctly removes an album', () => { it('correctly removes an album', () => {
albumStore.state.albums.length.should.equal(6); albumStore.state.albums.length.should.equal(6)
}); })
}); })
describe('#addSongsIntoAlbum', () => { describe('#addSongsIntoAlbum', () => {
beforeEach(() => { beforeEach(() => {
albumStore.addSongsIntoAlbum(albumStore.state.albums[0], cloneDeep(singleSong)); albumStore.addSongsIntoAlbum(albumStore.state.albums[0], cloneDeep(singleSong))
}); })
it('correctly adds a song into an album', () => { it('correctly adds a song into an album', () => {
albumStore.state.albums[0].songs.length.should.equal(2); albumStore.state.albums[0].songs.length.should.equal(2)
}); })
it('correctly recalculates the play count', () => { it('correctly recalculates the play count', () => {
albumStore.state.albums[0].playCount.should.equal(4); albumStore.state.albums[0].playCount.should.equal(4)
}); })
it ('correctly recalculates album length', () => { it ('correctly recalculates album length', () => {
albumStore.state.albums[0].length.should.equal(359.92); albumStore.state.albums[0].length.should.equal(359.92)
}); })
}); })
describe('#removeSongsFromAlbum', () => { describe('#removeSongsFromAlbum', () => {
beforeEach(() => { beforeEach(() => {
albumStore.removeSongsFromAlbum(albumStore.state.albums[0], albumStore.state.albums[0].songs[0]); albumStore.removeSongsFromAlbum(albumStore.state.albums[0], albumStore.state.albums[0].songs[0])
}); })
it('correctly removes a song from an album', () => { it('correctly removes a song from an album', () => {
albumStore.state.albums[0].songs.length.should.equal(0); albumStore.state.albums[0].songs.length.should.equal(0)
}); })
it('correctly recalculates the play count', () => { it('correctly recalculates the play count', () => {
albumStore.state.albums[0].playCount.should.equal(0); albumStore.state.albums[0].playCount.should.equal(0)
}); })
it('correctly recalculates the length', () => { it('correctly recalculates the length', () => {
albumStore.state.albums[0].length.should.equal(0); albumStore.state.albums[0].length.should.equal(0)
}); })
}); })
}); })

View file

@ -1,73 +1,73 @@
require('chai').should(); require('chai').should()
import { cloneDeep, last } from 'lodash'; import { cloneDeep, last } from 'lodash'
import { artistStore } from '../../stores'; import { artistStore } from '../../stores'
import { default as artists, singleAlbum, singleArtist } from '../blobs/media'; import { default as artists, singleAlbum, singleArtist } from '../blobs/media'
describe('stores/artist', () => { describe('stores/artist', () => {
beforeEach(() => artistStore.init(cloneDeep(artists))); beforeEach(() => artistStore.init(cloneDeep(artists)))
afterEach(() => artistStore.state.artists = []); afterEach(() => artistStore.state.artists = [])
describe('#init', () => { describe('#init', () => {
it('correctly gathers artists', () => { it('correctly gathers artists', () => {
artistStore.state.artists.length.should.equal(3); artistStore.state.artists.length.should.equal(3)
}); })
it('correctly gets artist images', () => { it('correctly gets artist images', () => {
artistStore.state.artists[0].image.should.equal('/public/img/covers/565c0f7067425.jpeg'); artistStore.state.artists[0].image.should.equal('/public/img/covers/565c0f7067425.jpeg')
}); })
it('correctly counts songs by artists', () => { it('correctly counts songs by artists', () => {
artistStore.state.artists[0].songCount = 3; artistStore.state.artists[0].songCount = 3
}); })
}); })
describe('#getImage', () => { describe('#getImage', () => {
it('correctly gets an artists image', () => { it('correctly gets an artists image', () => {
artistStore.getImage(artistStore.state.artists[0]).should.equal('/public/img/covers/565c0f7067425.jpeg'); artistStore.getImage(artistStore.state.artists[0]).should.equal('/public/img/covers/565c0f7067425.jpeg')
}); })
}); })
describe('#add', () => { describe('#add', () => {
beforeEach(() => artistStore.add(cloneDeep(singleArtist))); beforeEach(() => artistStore.add(cloneDeep(singleArtist)))
it('correctly adds an artist', () => { it('correctly adds an artist', () => {
last(artistStore.state.artists).name.should.equal('John Cena'); last(artistStore.state.artists).name.should.equal('John Cena')
}); })
}); })
describe('#remove', () => { describe('#remove', () => {
beforeEach(() => artistStore.remove(artistStore.state.artists[0])); beforeEach(() => artistStore.remove(artistStore.state.artists[0]))
it('correctly removes an artist', () => { it('correctly removes an artist', () => {
artistStore.state.artists.length.should.equal(2); artistStore.state.artists.length.should.equal(2)
artistStore.state.artists[0].name.should.equal('Bob Dylan'); artistStore.state.artists[0].name.should.equal('Bob Dylan')
}); })
}); })
describe('#addAlbumsIntoArtist', () => { describe('#addAlbumsIntoArtist', () => {
beforeEach(() => { beforeEach(() => {
artistStore.addAlbumsIntoArtist(artistStore.state.artists[0], cloneDeep(singleAlbum)); artistStore.addAlbumsIntoArtist(artistStore.state.artists[0], cloneDeep(singleAlbum))
}); })
it('correctly adds albums into an artist', () => { it('correctly adds albums into an artist', () => {
artistStore.state.artists[0].albums.length.should.equal(4); artistStore.state.artists[0].albums.length.should.equal(4)
}); })
it('correctly sets the album artist', () => { it('correctly sets the album artist', () => {
const addedAlbum = last(artistStore.state.artists[0].albums); const addedAlbum = last(artistStore.state.artists[0].albums)
addedAlbum.artist.should.equal(artistStore.state.artists[0]); addedAlbum.artist.should.equal(artistStore.state.artists[0])
addedAlbum.artist_id.should.equal(artistStore.state.artists[0].id); addedAlbum.artist_id.should.equal(artistStore.state.artists[0].id)
}); })
}); })
describe('#removeAlbumsFromArtist', () => { describe('#removeAlbumsFromArtist', () => {
beforeEach(() => { beforeEach(() => {
artistStore.removeAlbumsFromArtist(artistStore.state.artists[0], artistStore.state.artists[0].albums[0]); artistStore.removeAlbumsFromArtist(artistStore.state.artists[0], artistStore.state.artists[0].albums[0])
}); })
it('correctly removes an album from an artist', () => { it('correctly removes an album from an artist', () => {
artistStore.state.artists[0].albums.length.should.equal(2); artistStore.state.artists[0].albums.length.should.equal(2)
}); })
}); })
}); })

View file

@ -1,37 +1,37 @@
require('chai').should(); require('chai').should()
import localStorage from 'local-storage'; import localStorage from 'local-storage'
import { preferenceStore } from '../../stores'; import { preferenceStore } from '../../stores'
const user = { id: 0 }; const user = { id: 0 }
const preferences = { const preferences = {
volume: 8, volume: 8,
notify: false, notify: false
}; }
describe('stores/preference', () => { describe('stores/preference', () => {
beforeEach(() => { beforeEach(() => {
localStorage.set(`preferences_${user.id}`, preferences); localStorage.set(`preferences_${user.id}`, preferences)
preferenceStore.init(user); preferenceStore.init(user)
}); })
describe("#set", () => { describe("#set", () => {
it('correctly sets preferences', () => { it('correctly sets preferences', () => {
preferenceStore.set('volume', 5); preferenceStore.set('volume', 5)
localStorage.get(`preferences_${user.id}`).volume.should.equal(5); localStorage.get(`preferences_${user.id}`).volume.should.equal(5)
// Test the proxy // Test the proxy
preferenceStore.volume = 6; preferenceStore.volume = 6
localStorage.get(`preferences_${user.id}`).volume.should.equal(6); localStorage.get(`preferences_${user.id}`).volume.should.equal(6)
}); })
}); })
describe("#get", () => { describe("#get", () => {
it('returns correct preference values', () => { it('returns correct preference values', () => {
preferenceStore.get('volume').should.equal(8); preferenceStore.get('volume').should.equal(8)
// Test the proxy // Test the proxy
preferenceStore.volume.should.equal(8); preferenceStore.volume.should.equal(8)
}); })
}); })
}); })

View file

@ -1,107 +1,109 @@
require('chai').should(); require('chai').should()
import { queueStore } from '../../stores'; import { queueStore } from '../../stores'
import artists from '../blobs/media'; import artists from '../blobs/media'
const songs = artists[2].albums[0].songs; const songs = artists[2].albums[0].songs
describe('stores/queue', () => { describe('stores/queue', () => {
beforeEach(() => { beforeEach(() => {
queueStore.state.songs = songs; queueStore.state.songs = songs
queueStore.state.current = songs[1]; queueStore.state.current = songs[1]
}); })
describe('#all', () => { describe('#all', () => {
it('correctly returns all queued songs', () => { it('correctly returns all queued songs', () => {
queueStore.all.should.equal(songs); queueStore.all.should.equal(songs)
}); })
}); })
describe('#first', () => { describe('#first', () => {
it('correctly returns the first queued song', () => { it('correctly returns the first queued song', () => {
queueStore.first.title.should.equal('No bravery'); queueStore.first.title.should.equal('No bravery')
}); })
}); })
describe('#last', () => { describe('#last', () => {
it('correctly returns the last queued song', () => { it('correctly returns the last queued song', () => {
queueStore.last.title.should.equal('Tears and rain'); queueStore.last.title.should.equal('Tears and rain')
}); })
}); })
describe('#queue', () => { describe('#queue', () => {
beforeEach(() => queueStore.state.songs = songs); beforeEach(() => queueStore.state.songs = songs)
const song = artists[0].albums[0].songs[0]; const song = artists[0].albums[0].songs[0]
it('correctly appends a song to end of the queue', () => { it('correctly appends a song to end of the queue', () => {
queueStore.queue(song); queueStore.queue(song)
queueStore.last.title.should.equal('I Swear'); queueStore.last.title.should.equal('I Swear')
}); })
it('correctly prepends a song to top of the queue', () => { it('correctly prepends a song to top of the queue', () => {
queueStore.queue(song, false, true); queueStore.queue(song, false, true)
queueStore.first.title.should.equal('I Swear'); queueStore.first.title.should.equal('I Swear')
}); })
it('correctly replaces the whole queue', () => { it('correctly replaces the whole queue', () => {
queueStore.queue(song, true); queueStore.queue(song, true)
queueStore.all.length.should.equal(1); queueStore.all.length.should.equal(1)
queueStore.first.title.should.equal('I Swear'); queueStore.first.title.should.equal('I Swear')
}); })
}); })
describe('#unqueue', () => { describe('#unqueue', () => {
beforeEach(() => queueStore.state.songs = songs); beforeEach(() => queueStore.state.songs = songs)
it('correctly removes a song from queue', () => { it('correctly removes a song from queue', () => {
queueStore.unqueue(queueStore.state.songs[0]); queueStore.unqueue(queueStore.state.songs[0])
queueStore.first.title.should.equal('So long, Jimmy'); // Oh the irony. queueStore.first.title.should.equal('So long, Jimmy'); // Oh the irony.
}); })
it('correctly removes mutiple songs from queue', () => { it('correctly removes mutiple songs from queue', () => {
queueStore.unqueue([queueStore.state.songs[0], queueStore.state.songs[1]]); queueStore.unqueue([queueStore.state.songs[0], queueStore.state.songs[1]])
queueStore.first.title.should.equal('Wisemen'); queueStore.first.title.should.equal('Wisemen')
}); })
}); })
describe('#clear', () => { describe('#clear', () => {
it('correctly clears all songs from queue', () => { it('correctly clears all songs from queue', () => {
queueStore.clear(); queueStore.clear()
queueStore.state.songs.length.should.equal(0); queueStore.state.songs.length.should.equal(0)
}); })
}); })
describe('#current', () => { describe('#current', () => {
it('returns the correct current song', () => { it('returns the correct current song', () => {
queueStore.current.title.should.equal('So long, Jimmy'); queueStore.current.title.should.equal('So long, Jimmy')
}); })
it('successfully sets the current song', () => { it('successfully sets the current song', () => {
queueStore.current = queueStore.state.songs[0]; queueStore.current = queueStore.state.songs[0]
queueStore.current.title.should.equal('No bravery'); queueStore.current.title.should.equal('No bravery')
}); })
}); })
describe('#getNextSong', () => { describe('#getNextSong', () => {
it('correctly gets the next song in queue', () => { it('correctly gets the next song in queue', () => {
queueStore.next.title.should.equal('Wisemen'); queueStore.next.title.should.equal('Wisemen')
}); })
it('correctly returns null if at end of queue', () => { it('correctly returns null if at end of queue', () => {
queueStore.current = queueStore.state.songs[queueStore.state.songs.length - 1]; queueStore.current = queueStore.state.songs[queueStore.state.songs.length - 1]
(queueStore.next === null).should.be.true; var result = queueStore.next === null
}); result.should.be.true
}); })
})
describe('#getPrevSong', () => { describe('#getPrevSong', () => {
it('correctly gets the previous song in queue', () => { it('correctly gets the previous song in queue', () => {
queueStore.previous.title.should.equal('No bravery'); queueStore.previous.title.should.equal('No bravery')
}); })
it('correctly returns null if at end of queue', () => { it('correctly returns null if at end of queue', () => {
queueStore.current = queueStore.state.songs[0]; queueStore.current = queueStore.state.songs[0]
(queueStore.previous === null).should.be.true; var result = queueStore.previous === null
}); result.should.be.true
}); })
}); })
})

View file

@ -1,61 +1,61 @@
require('chai').should(); require('chai').should()
import { cloneDeep, last } from 'lodash'; import { cloneDeep, last } from 'lodash'
import { songStore, albumStore, artistStore } from '../../stores'; import { songStore, albumStore, artistStore } from '../../stores'
import artists from '../blobs/media'; import artists from '../blobs/media'
import interactions from '../blobs/interactions'; import interactions from '../blobs/interactions'
describe('stores/song', () => { describe('stores/song', () => {
beforeEach(() => { beforeEach(() => {
artistStore.init(artists); artistStore.init(artists)
}); })
describe('#init', () => { describe('#init', () => {
it('correctly gathers all songs', () => { it('correctly gathers all songs', () => {
songStore.state.songs.length.should.equal(14); songStore.state.songs.length.should.equal(14)
}); })
it ('coverts lengths to formatted lengths', () => { it ('coverts lengths to formatted lengths', () => {
songStore.state.songs[0].fmtLength.should.be.a.string; songStore.state.songs[0].fmtLength.should.be.a.string
}); })
it('correctly sets albums', () => { it('correctly sets albums', () => {
songStore.state.songs[0].album.id.should.equal(1193); songStore.state.songs[0].album.id.should.equal(1193)
}); })
}); })
describe('#all', () => { describe('#all', () => {
it('correctly returns all songs', () => { it('correctly returns all songs', () => {
songStore.all.length.should.equal(14); songStore.all.length.should.equal(14)
}); })
}); })
describe('#byId', () => { describe('#byId', () => {
it('correctly gets a song by ID', () => { it('correctly gets a song by ID', () => {
songStore.byId('e6d3977f3ffa147801ca5d1fdf6fa55e').title.should.equal('Like a rolling stone'); songStore.byId('e6d3977f3ffa147801ca5d1fdf6fa55e').title.should.equal('Like a rolling stone')
}); })
}); })
describe('#byIds', () => { describe('#byIds', () => {
it('correctly gets multiple songs by IDs', () => { it('correctly gets multiple songs by IDs', () => {
const songs = songStore.byIds(['e6d3977f3ffa147801ca5d1fdf6fa55e', 'aa16bbef6a9710eb9a0f41ecc534fad5']); const songs = songStore.byIds(['e6d3977f3ffa147801ca5d1fdf6fa55e', 'aa16bbef6a9710eb9a0f41ecc534fad5'])
songs[0].title.should.equal('Like a rolling stone'); songs[0].title.should.equal('Like a rolling stone')
songs[1].title.should.equal("Knockin' on heaven's door"); songs[1].title.should.equal("Knockin' on heaven's door")
}); })
}); })
describe('#initInteractions', () => { describe('#initInteractions', () => {
beforeEach(() => songStore.initInteractions(interactions)); beforeEach(() => songStore.initInteractions(interactions))
it('correctly sets interaction status', () => { it('correctly sets interaction status', () => {
const song = songStore.byId('cb7edeac1f097143e65b1b2cde102482'); const song = songStore.byId('cb7edeac1f097143e65b1b2cde102482')
song.liked.should.be.true; song.liked.should.be.true
song.playCount.should.equal(3); song.playCount.should.equal(3)
}); })
}); })
describe('#syncUpdatedSong', () => { describe('#syncUpdatedSong', () => {
beforeEach(() => artistStore.init(artists)); beforeEach(() => artistStore.init(artists))
const updatedSong = { const updatedSong = {
id: "39189f4545f9d5671fb3dc964f0080a0", id: "39189f4545f9d5671fb3dc964f0080a0",
@ -66,19 +66,19 @@ describe('stores/song', () => {
arist_id: 1, arist_id: 1,
artist: { artist: {
id: 1, id: 1,
name: 'All-4-One', name: 'All-4-One'
}, }
}, }
}; }
it ('correctly syncs an updated song with no album changes', () => { it ('correctly syncs an updated song with no album changes', () => {
songStore.syncUpdatedSong(cloneDeep(updatedSong)); songStore.syncUpdatedSong(cloneDeep(updatedSong))
songStore.byId(updatedSong.id).title.should.equal('I Swear A Lot'); songStore.byId(updatedSong.id).title.should.equal('I Swear A Lot')
}); })
it ('correctly syncs an updated song into an existing album of same artist', () => { it ('correctly syncs an updated song into an existing album of same artist', () => {
const song = cloneDeep(updatedSong); const song = cloneDeep(updatedSong)
song.album_id = 1194; song.album_id = 1194
song.album = { song.album = {
id: 1194, id: 1194,
artist_id: 1, artist_id: 1,
@ -86,62 +86,62 @@ describe('stores/song', () => {
id: 1, id: 1,
name: 'All-4-One', name: 'All-4-One',
}, },
}; }
songStore.syncUpdatedSong(song); songStore.syncUpdatedSong(song)
songStore.byId(song.id).album.name.should.equal('And The Music Speaks'); songStore.byId(song.id).album.name.should.equal('And The Music Speaks')
}); })
it ('correctly syncs an updated song into a new album of same artist', () => { it ('correctly syncs an updated song into a new album of same artist', () => {
const song = cloneDeep(updatedSong); const song = cloneDeep(updatedSong)
song.album_id = 9999; song.album_id = 9999
song.album = { song.album = {
id: 9999, id: 9999,
artist_id: 1, artist_id: 1,
name: 'Brand New Album from All-4-One', name: 'Brand New Album from All-4-One',
artist: { artist: {
id: 1, id: 1,
name: 'All-4-One', name: 'All-4-One'
}, }
}; }
songStore.syncUpdatedSong(song); songStore.syncUpdatedSong(song)
// A new album should be created... // A new album should be created...
last(albumStore.all).name.should.equal('Brand New Album from All-4-One'); last(albumStore.all).name.should.equal('Brand New Album from All-4-One')
// ...and assigned with the song. // ...and assigned with the song.
songStore.byId(song.id).album.name.should.equal('Brand New Album from All-4-One'); songStore.byId(song.id).album.name.should.equal('Brand New Album from All-4-One')
}); })
it ('correctly syncs an updated song into a new album of a new artist', () => { it ('correctly syncs an updated song into a new album of a new artist', () => {
const song = cloneDeep(updatedSong); const song = cloneDeep(updatedSong)
song.album_id = 10000; song.album_id = 10000
song.album = { song.album = {
id: 10000, id: 10000,
name: "It's... John Cena!!!", name: "It's... John Cena!!!",
artist_id: 10000, artist_id: 10000,
artist: { artist: {
id: 10000, id: 10000,
name: 'John Cena', name: 'John Cena'
}, }
}; }
songStore.syncUpdatedSong(song); songStore.syncUpdatedSong(song)
// A new artist should be created... // A new artist should be created...
const lastArtist = last(artistStore.all); const lastArtist = last(artistStore.all)
lastArtist.name.should.equal('John Cena'); lastArtist.name.should.equal('John Cena')
// A new album should be created // A new album should be created
const lastAlbum = last(albumStore.all); const lastAlbum = last(albumStore.all)
lastAlbum.name.should.equal("It's... John Cena!!!"); lastAlbum.name.should.equal("It's... John Cena!!!")
// The album must belong to John Cena of course! // The album must belong to John Cena of course!
last(lastArtist.albums).should.equal(lastAlbum); last(lastArtist.albums).should.equal(lastAlbum)
// And the song belongs to the album. // And the song belongs to the album.
songStore.byId(song.id).album.should.equal(lastAlbum); songStore.byId(song.id).album.should.equal(lastAlbum)
}); })
}); })
}); })

View file

@ -1,66 +1,50 @@
require('chai').should(); require('chai').should()
import { userStore } from '../../stores'; import { userStore } from '../../stores'
import data from '../blobs/users'; import data from '../blobs/users'
describe('stores/user', () => { describe('stores/user', () => {
beforeEach(() => userStore.init(data.users, data.currentUser)); beforeEach(() => userStore.init(data.users, data.currentUser))
describe('#init', () => { describe('#init', () => {
it('correctly sets data state', () => { it('correctly sets data state', () => {
userStore.state.users.should.equal(data.users); userStore.state.users.should.equal(data.users)
userStore.state.current.should.equal(data.currentUser); userStore.state.current.should.equal(data.currentUser)
}); })
}); })
describe('#all', () => { describe('#all', () => {
it('correctly returns all users', () => { it('correctly returns all users', () => {
userStore.all.should.equal(data.users); userStore.all.should.equal(data.users)
}); })
}); })
describe('#byId', () => { describe('#byId', () => {
it('correctly gets a user by ID', () => { it('correctly gets a user by ID', () => {
userStore.byId(1).should.equal(data.users[0]); userStore.byId(1).should.equal(data.users[0])
}); })
}); })
describe('#current', () => { describe('#current', () => {
it('correctly gets the current user', () => { it('correctly gets the current user', () => {
userStore.current.id.should.equal(1); userStore.current.id.should.equal(1)
}); })
it('correctly sets the current user', () => { it('correctly sets the current user', () => {
userStore.current = data.users[1]; userStore.current = data.users[1]
userStore.current.id.should.equal(2); userStore.current.id.should.equal(2)
}); })
}); })
describe('#setAvatar', () => { describe('#setAvatar', () => {
it('correctly sets the current users avatar', () => { it('correctly sets the current users avatar', () => {
userStore.setAvatar(); userStore.setAvatar()
userStore.current.avatar.should.equal('https://www.gravatar.com/avatar/b9611f1bba1aacbe6f5de5856695a202?s=256'); userStore.current.avatar.should.equal('https://www.gravatar.com/avatar/b9611f1bba1aacbe6f5de5856695a202?s=256')
}); })
it('correctly sets a users avatar', () => { it('correctly sets a users avatar', () => {
userStore.setAvatar(data.users[1]); userStore.setAvatar(data.users[1])
data.users[1].avatar.should.equal('https://www.gravatar.com/avatar/5024672cfe53f113b746e1923e373058?s=256'); data.users[1].avatar.should.equal('https://www.gravatar.com/avatar/5024672cfe53f113b746e1923e373058?s=256')
}); })
}); })
})
describe('#updateProfile', () => {
});
describe('#store', () => {
});
describe('#update', () => {
});
describe('#destroy', () => {
});
});

View file

@ -1,34 +1,34 @@
require('chai').should(); require('chai').should()
import { secondsToHis, parseValidationError } from '../../utils'; import { secondsToHis, parseValidationError } from '../../utils'
describe('services/utils', () => { describe('services/utils', () => {
describe('#secondsToHis', () => { describe('#secondsToHis', () => {
it('correctly formats a duration to H:i:s', () => { it('correctly formats a duration to H:i:s', () => {
secondsToHis(7547).should.equal('02:05:47'); secondsToHis(7547).should.equal('02:05:47')
}); })
it('ommits hours from short duration when formats to H:i:s', () => { it('ommits hours from short duration when formats to H:i:s', () => {
secondsToHis(314).should.equal('05:14'); secondsToHis(314).should.equal('05:14')
}); })
}); })
describe('#parseValidationError', () => { describe('#parseValidationError', () => {
it('correctly parses single-level validation error', () => { it('correctly parses single-level validation error', () => {
const error = { const error = {
err_1: ['Foo'], err_1: ['Foo']
}; }
parseValidationError(error).should.eql(['Foo']); parseValidationError(error).should.eql(['Foo'])
}); })
it('correctly parses multi-level validation error', () => { it('correctly parses multi-level validation error', () => {
const error = { const error = {
err_1: ['Foo', 'Bar'], err_1: ['Foo', 'Bar'],
err_2: ['Baz', 'Qux'], err_2: ['Baz', 'Qux']
}; }
parseValidationError(error).should.eql(['Foo', 'Bar', 'Baz', 'Qux']); parseValidationError(error).should.eql(['Foo', 'Bar', 'Baz', 'Qux'])
}); })
}); })
}); })

View file

@ -1,7 +1,7 @@
/** /**
* Other common methods. * Other common methods.
*/ */
import select from 'select'; import select from 'select'
import { event } from '../utils' import { event } from '../utils'
/** /**
@ -11,17 +11,17 @@ import { event } from '../utils'
* @param {...*} Extra data to attach to the view. * @param {...*} Extra data to attach to the view.
*/ */
export function loadMainView (view, ...args) { export function loadMainView (view, ...args) {
event.emit('main-content-view:load', view, ...args); event.emit('main-content-view:load', view, ...args)
}; }
/** /**
* Force reloading window regardless of "Confirm before reload" setting. * Force reloading window regardless of "Confirm before reload" setting.
* This is handy for certain cases, for example Last.fm connect/disconnect. * This is handy for certain cases, for example Last.fm connect/disconnect.
*/ */
export function forceReloadWindow () { export function forceReloadWindow () {
window.onbeforeunload = function() {}; window.onbeforeunload = function () {}
window.location.reload(); window.location.reload()
}; }
/** /**
* Show the overlay. * Show the overlay.
@ -31,15 +31,15 @@ export function forceReloadWindow() {
* @param {Boolean} dismissable * @param {Boolean} dismissable
*/ */
export function showOverlay (message = 'Just a little patience…', type = 'loading', dismissable = false) { export function showOverlay (message = 'Just a little patience…', type = 'loading', dismissable = false) {
event.emit('overlay:show', { message, type, dismissable }); event.emit('overlay:show', { message, type, dismissable })
}; }
/** /**
* Hide the overlay. * Hide the overlay.
*/ */
export function hideOverlay () { export function hideOverlay () {
event.emit('overlay:hide'); event.emit('overlay:hide')
}; }
/** /**
* Copy a text into clipboard. * Copy a text into clipboard.
@ -47,9 +47,9 @@ export function hideOverlay() {
* @param {string} txt * @param {string} txt
*/ */
export function copyText (txt) { export function copyText (txt) {
const copyArea = document.querySelector('#copyArea'); const copyArea = document.querySelector('#copyArea')
copyArea.style.top = (window.pageYOffset || document.documentElement.scrollTop) + 'px'; copyArea.style.top = (window.pageYOffset || document.documentElement.scrollTop) + 'px'
copyArea.value = txt; copyArea.value = txt
select(copyArea); select(copyArea)
document.execCommand('copy'); document.execCommand('copy')
}; }

Some files were not shown because too many files have changed in this diff Show more