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",
"chai": "^3.4.1",
"chalk": "^1.1.3",
"eslint": "^3.10.2",
"eslint-config-vue": "^2.0.1",
"eslint-plugin-vue": "^1.0.0",
"jsdom": "^9.2.1",
"mocha": "^2.3.4",
"sinon": "^1.17.2"
},
"scripts": {
"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"
}
}

View file

@ -23,77 +23,79 @@
</template>
<script>
import Vue from 'vue';
import $ from 'jquery';
import Vue from 'vue'
import $ from 'jquery'
import siteHeader from './components/site-header/index.vue';
import siteFooter from './components/site-footer/index.vue';
import mainWrapper from './components/main-wrapper/index.vue';
import overlay from './components/shared/overlay.vue';
import loginForm from './components/auth/login-form.vue';
import editSongsForm from './components/modals/edit-songs-form.vue';
import siteHeader from './components/site-header/index.vue'
import siteFooter from './components/site-footer/index.vue'
import mainWrapper from './components/main-wrapper/index.vue'
import overlay from './components/shared/overlay.vue'
import loginForm from './components/auth/login-form.vue'
import editSongsForm from './components/modals/edit-songs-form.vue'
import { event, showOverlay, hideOverlay, forceReloadWindow, url } from './utils';
import { sharedStore, songStore, userStore, preferenceStore as preferences } from './stores';
import { playback, ls } from './services';
import { focusDirective, clickawayDirective } from './directives';
import router from './router';
import { event, showOverlay, hideOverlay, forceReloadWindow } from './utils'
import { sharedStore, userStore, preferenceStore as preferences } from './stores'
import { playback, ls } from './services'
import { focusDirective, clickawayDirective } from './directives'
import router from './router'
export default {
components: { siteHeader, siteFooter, mainWrapper, overlay, loginForm, editSongsForm },
data() {
data () {
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
const token = ls.get('jwt-token');
const token = ls.get('jwt-token')
if (token) {
this.authenticated = true;
this.init();
this.authenticated = true
this.init()
}
// 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
$('<textarea id="copyArea"></textarea>').appendTo('body');
$('<textarea id="copyArea"></textarea>').appendTo('body')
// Add an ugly mac/non-mac class for OS-targeting styles.
// I'm crying inside.
$('html').addClass(navigator.userAgent.indexOf('Mac') !== -1 ? 'mac' : 'non-mac');
$('html').addClass(navigator.userAgent.indexOf('Mac') !== -1 ? 'mac' : 'non-mac')
},
methods: {
init() {
showOverlay();
init () {
showOverlay()
// Make the most important HTTP request to get all necessary data from the server.
// Afterwards, init all mandatory stores and services.
sharedStore.init().then(() => {
playback.init();
hideOverlay();
playback.init()
hideOverlay()
// Ask for user's notification permission.
this.requestNotifPermission();
this.requestNotifPermission()
// To confirm or not to confirm closing, it's a question.
window.onbeforeunload = e => {
if (!preferences.confirmClosing) {
return;
return
}
// Notice that a custom message like this has ceased to be supported
// 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.
event.emit('koel:ready');
}).catch(() => this.authenticated = false);
event.emit('koel:ready')
}).catch(() => {
this.authenticated = false
})
},
/**
@ -101,14 +103,14 @@ export default {
*
* @param {Object} e The keydown event
*/
togglePlayback(e) {
togglePlayback (e) {
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.
$('#mainFooter .play:visible, #mainFooter .pause:visible').click();
e.preventDefault();
$('#mainFooter .play:visible, #mainFooter .pause:visible').click()
e.preventDefault()
},
/**
@ -116,13 +118,13 @@ export default {
*
* @param {Object} e The keydown event
*/
playPrev(e) {
playPrev (e) {
if ($(e.target).is('input,textarea')) {
return true;
return true
}
playback.playPrev();
e.preventDefault();
playback.playPrev()
e.preventDefault()
},
/**
@ -130,13 +132,13 @@ export default {
*
* @param {Object} e The keydown event
*/
playNext(e) {
playNext (e) {
if ($(e.target).is('input,textarea')) {
return true;
return true
}
playback.playNext();
e.preventDefault();
playback.playNext()
e.preventDefault()
},
/**
@ -144,37 +146,37 @@ export default {
*
* @param {Object} e The keydown event
*/
search(e) {
search (e) {
if ($(e.target).is('input,textarea') || e.metaKey || e.ctrlKey) {
return true;
return true
}
$('#searchForm input[type="search"]').focus().select();
e.preventDefault();
$('#searchForm input[type="search"]').focus().select()
e.preventDefault()
},
/**
* Request for notification permission if it's not provided and the user is OK with notifs.
*/
requestNotifPermission() {
if (window.Notification && preferences.notify && Notification.permission !== 'granted') {
Notification.requestPermission(result => {
requestNotifPermission () {
if (window.Notification && preferences.notify && window.Notification.permission !== 'granted') {
window.Notification.requestPermission(result => {
if (result === 'denied') {
preferences.notify = false;
preferences.notify = false
}
});
})
}
},
}
},
created() {
created () {
event.on({
/**
* When the user logs in, set the whole app to be "authenticated" and initialize it.
*/
'user:loggedin': () => {
this.authenticated = true;
this.init();
'user:loggedin': () => {
this.authenticated = true
this.init()
},
/**
@ -187,22 +189,22 @@ export default {
/**
* Log the current user out and reset the application state.
*/
logout() {
logout () {
userStore.logout().then((r) => {
ls.remove('jwt-token');
forceReloadWindow();
});
ls.remove('jwt-token')
forceReloadWindow()
})
},
/**
* Init our basic, custom router on ready to determine app state.
*/
'koel:ready': () => {
router.init();
},
});
},
};
router.init()
}
})
}
}
// Register our custom key codes
Vue.config.keyCodes = {
@ -213,11 +215,11 @@ Vue.config.keyCodes = {
mediaNext: 176,
mediaPrev: 177,
mediaToggle: 179
};
}
// and the global directives
Vue.directive('koel-focus', focusDirective);
Vue.directive('koel-clickaway',clickawayDirective);
Vue.directive('koel-focus', focusDirective)
Vue.directive('koel-clickaway', clickawayDirective)
</script>
<style lang="sass">

View file

@ -7,33 +7,35 @@
</template>
<script>
import { userStore } from '../../stores';
import { event } from '../../utils';
import { userStore } from '../../stores'
import { event } from '../../utils'
export default {
data() {
data () {
return {
email: '',
password: '',
failed: false,
};
failed: false
}
},
methods: {
login() {
this.failed = false;
login () {
this.failed = false
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.
this.password = '';
this.password = ''
event.emit('user:loggedin');
}).catch(() => this.failed = true);
},
},
};
event.emit('user:loggedin')
}).catch(() => {
this.failed = true
})
}
}
}
</script>
<style lang="sass" scoped>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -14,41 +14,41 @@
</template>
<script>
import { filterBy, limitBy, event } from '../../../utils';
import { albumStore } from '../../../stores';
import albumItem from '../../shared/album-item.vue';
import viewModeSwitch from '../../shared/view-mode-switch.vue';
import infiniteScroll from '../../../mixins/infinite-scroll';
import { filterBy, limitBy, event } from '../../../utils'
import { albumStore } from '../../../stores'
import albumItem from '../../shared/album-item.vue'
import viewModeSwitch from '../../shared/view-mode-switch.vue'
import infiniteScroll from '../../../mixins/infinite-scroll'
export default {
mixins: [infiniteScroll],
components: { albumItem, viewModeSwitch },
data() {
data () {
return {
perPage: 9,
numOfItems: 9,
q: '',
viewMode: null,
};
viewMode: null
}
},
computed: {
displayedItems() {
displayedItems () {
return limitBy(
filterBy(albumStore.all, this.q, 'name', 'artist.name'),
this.numOfItems
);
},
)
}
},
methods: {
changeViewMode(mode) {
this.viewMode = mode;
},
changeViewMode (mode) {
this.viewMode = mode
}
},
created() {
created () {
event.on({
/**
* When the application is ready, load the first batch of items.
@ -56,14 +56,16 @@ export default {
'koel:ready': () => this.displayMore(),
'koel:teardown': () => {
this.q = '';
this.numOfItems = 9;
this.q = ''
this.numOfItems = 9
},
'filter:changed': q => this.q = q,
});
},
};
'filter:changed': q => {
this.q = q
}
})
}
}
</script>
<style lang="sass">

View file

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

View file

@ -14,43 +14,43 @@
</template>
<script>
import { filterBy, limitBy, event } from '../../../utils';
import { artistStore } from '../../../stores';
import { filterBy, limitBy, event } from '../../../utils'
import { artistStore } from '../../../stores'
import artistItem from '../../shared/artist-item.vue';
import viewModeSwitch from '../../shared/view-mode-switch.vue';
import infiniteScroll from '../../../mixins/infinite-scroll';
import artistItem from '../../shared/artist-item.vue'
import viewModeSwitch from '../../shared/view-mode-switch.vue'
import infiniteScroll from '../../../mixins/infinite-scroll'
export default {
mixins: [infiniteScroll],
components: { artistItem, viewModeSwitch },
data() {
data () {
return {
perPage: 9,
numOfItems: 9,
q: '',
viewMode: null,
};
viewMode: null
}
},
computed: {
displayedItems() {
displayedItems () {
return limitBy(
filterBy(artistStore.all, this.q, 'name'),
this.numOfItems
);
},
)
}
},
methods: {
changeViewMode(mode) {
this.viewMode = mode;
},
changeViewMode (mode) {
this.viewMode = mode
}
},
created() {
created () {
event.on({
/**
* When the application is ready, load the first batch of items.
@ -58,14 +58,16 @@ export default {
'koel:ready': () => this.displayMore(),
'koel:teardown': () => {
this.q = '';
this.numOfItems = 9;
this.q = ''
this.numOfItems = 9
},
'filter:changed': q => this.q = q,
});
},
};
'filter:changed': q => {
this.q = q
}
})
}
}
</script>
<style lang="sass">

View file

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

View file

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

View file

@ -18,38 +18,40 @@
</template>
<script>
import { event } from '../../../utils';
import { albumStore, sharedStore } from '../../../stores';
import { event } from '../../../utils'
import { albumStore, sharedStore } from '../../../stores'
import albums from './albums.vue';
import album from './album.vue';
import artists from './artists.vue';
import artist from './artist.vue';
import songs from './songs.vue';
import settings from './settings.vue';
import users from './users.vue';
import queue from './queue.vue';
import home from './home.vue';
import playlist from './playlist.vue';
import favorites from './favorites.vue';
import profile from './profile.vue';
import youtubePlayer from './youtube-player.vue';
import albums from './albums.vue'
import album from './album.vue'
import artists from './artists.vue'
import artist from './artist.vue'
import songs from './songs.vue'
import settings from './settings.vue'
import users from './users.vue'
import queue from './queue.vue'
import home from './home.vue'
import playlist from './playlist.vue'
import favorites from './favorites.vue'
import profile from './profile.vue'
import youtubePlayer from './youtube-player.vue'
export default {
components: { albums, album, artists, artist, songs, settings,
users, home, queue, playlist, favorites, profile, youtubePlayer },
data() {
data () {
return {
view: 'home', // The default view
albumCover: null,
sharedState: sharedStore.state,
};
sharedState: sharedStore.state
}
},
created() {
created () {
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.
@ -59,11 +61,11 @@ export default {
* @return {Boolean}
*/
'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>
<style lang="sass">

View file

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

View file

@ -104,57 +104,56 @@
</template>
<script>
import $ from 'jquery';
import swal from 'sweetalert';
import $ from 'jquery'
import swal from 'sweetalert'
import { userStore, preferenceStore, sharedStore } from '../../../stores';
import { forceReloadWindow } from '../../../utils';
import { http, ls } from '../../../services';
import { userStore, preferenceStore, sharedStore } from '../../../stores'
import { forceReloadWindow } from '../../../utils'
import { http, ls } from '../../../services'
export default {
data() {
data () {
return {
state: userStore.state,
cache: userStore.stub,
pwd: '',
confirmPwd: '',
prefs: preferenceStore.state,
sharedState: sharedStore.state,
};
sharedState: sharedStore.state
}
},
methods: {
/**
* Update the current user's profile.
*/
update() {
update () {
// A little validation put in a small place.
if ((this.pwd || this.confirmPwd) && this.pwd !== this.confirmPwd) {
$('#inputProfilePassword, #inputProfileConfirmPassword').addClass('error');
return;
$('#inputProfilePassword, #inputProfileConfirmPassword').addClass('error')
return
}
$('#inputProfilePassword, #inputProfileConfirmPassword').removeClass('error');
$('#inputProfilePassword, #inputProfileConfirmPassword').removeClass('error')
userStore.updateProfile(this.pwd).then(() => {
this.pwd = '';
this.confirmPwd = '';
this.pwd = ''
this.confirmPwd = ''
swal({
title: 'Done!',
text: 'Profile saved.',
type: 'success',
allowOutsideClick: true,
});
});
allowOutsideClick: true
})
})
},
/**
* Save the current user's preference.
*/
savePreference() {
this.$nextTick(() => preferenceStore.save());
savePreference () {
this.$nextTick(() => preferenceStore.save())
},
/**
@ -162,29 +161,29 @@ export default {
* This method opens a new window.
* Koel will reload once the connection is successful.
*/
connectToLastfm() {
connectToLastfm () {
window.open(
`/api/lastfm/connect?jwt-token=${ls.get('jwt-token')}`,
'_blank',
'toolbar=no,titlebar=no,location=no,width=1024,height=640'
);
)
},
/**
* Disconnect the current user from Last.fm.
* Oh God why.
*/
disconnectFromLastfm() {
disconnectFromLastfm () {
// Should we use userStore?
// - We shouldn't. This doesn't have anything to do with stores.
// Should we confirm the user?
// - Nope. Users should be grown-ass adults who take responsibilty of their actions.
// But one of my users is my new born kid!
// - Then? Kids will fuck things up anyway.
http.delete('lastfm/disconnect', {}, forceReloadWindow);
},
},
};
http.delete('lastfm/disconnect', {}, forceReloadWindow)
}
}
}
</script>
<style lang="sass">

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -55,29 +55,29 @@
</template>
<script>
import isMobile from 'ismobilejs';
import $ from 'jquery';
import isMobile from 'ismobilejs'
import $ from 'jquery'
import { event } from '../../../utils';
import { sharedStore, userStore, songStore, queueStore } from '../../../stores';
import playlists from './playlists.vue';
import { event } from '../../../utils'
import { sharedStore, userStore, songStore, queueStore } from '../../../stores'
import playlists from './playlists.vue'
export default {
components: { playlists },
data() {
data () {
return {
currentView: 'home',
user: userStore.state,
showing: !isMobile.phone,
sharedState: sharedStore.state,
};
sharedState: sharedStore.state
}
},
computed: {
latestVersionUrl() {
return 'https://github.com/phanan/koel/releases/tag/' + this.sharedState.latestVersion;
},
latestVersionUrl () {
return 'https://github.com/phanan/koel/releases/tag/' + this.sharedState.latestVersion
}
},
methods: {
@ -86,8 +86,8 @@ export default {
*
* @param {Object} e The dragleave event.
*/
removeDroppableState(e) {
$(e.target).removeClass('droppable');
removeDroppableState (e) {
$(e.target).removeClass('droppable')
},
/**
@ -95,11 +95,11 @@ export default {
*
* @param {Object} e The dragover event.
*/
allowDrop(e) {
$(e.target).addClass('droppable');
e.dataTransfer.dropEffect = 'move';
allowDrop (e) {
$(e.target).addClass('droppable')
e.dataTransfer.dropEffect = 'move'
return false;
return false
},
/**
@ -109,42 +109,44 @@ export default {
*
* @return {Boolean}
*/
handleDrop(e) {
this.removeDroppableState(e);
handleDrop (e) {
this.removeDroppableState(e)
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) {
return false;
return false
}
queueStore.queue(songs);
queueStore.queue(songs)
return false;
},
return false
}
},
created() {
created () {
event.on('main-content-view:load', view => {
this.currentView = view;
this.currentView = view
// Hide the sidebar if on mobile
if (isMobile.phone) {
this.showing = false;
this.showing = false
}
});
})
/**
* Listen to sidebar:toggle event to show or hide the sidebar.
* 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>
<style lang="sass">

View file

@ -20,20 +20,20 @@
</template>
<script>
import $ from 'jquery';
import $ from 'jquery'
import { event } from '../../../utils';
import { songStore, playlistStore, favoriteStore } from '../../../stores';
import { event } from '../../../utils'
import { songStore, playlistStore, favoriteStore } from '../../../stores'
export default {
props: ['playlist', 'type'],
data() {
data () {
return {
newName: '',
editing: false,
active: false,
};
active: false
}
},
computed: {
@ -42,53 +42,53 @@ export default {
*
* @return {Boolean}
*/
isFavorites() {
return this.type === 'favorites';
isFavorites () {
return this.type === 'favorites'
},
playlistUrl() {
return this.isFavorites ? '/#!/favorites' : `/#!/playlist/${this.playlist.id}`;
},
playlistUrl () {
return this.isFavorites ? '/#!/favorites' : `/#!/playlist/${this.playlist.id}`
}
},
methods: {
/**
* Show the form to edit the playlist.
*/
edit() {
edit () {
if (this.isFavorites) {
return;
return
}
this.beforeEditCache = this.playlist.name;
this.editing = true;
this.beforeEditCache = this.playlist.name
this.editing = true
},
/**
* Update the playlist's name.
*/
update() {
update () {
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) {
this.playlist.name = this.beforeEditCache;
return;
this.playlist.name = this.beforeEditCache
return
}
playlistStore.update(this.playlist);
playlistStore.update(this.playlist)
},
/**
* Cancel editing.
*/
cancelEdit() {
this.editing = false;
this.playlist.name = this.beforeEditCache;
cancelEdit () {
this.editing = false
this.playlist.name = this.beforeEditCache
},
/**
@ -96,8 +96,8 @@ export default {
*
* @param {Object} e The dragleave event.
*/
removeDroppableState(e) {
$(e.target).removeClass('droppable');
removeDroppableState (e) {
$(e.target).removeClass('droppable')
},
/**
@ -106,11 +106,11 @@ export default {
*
* @param {Object} e The dragover event.
*/
allowDrop(e) {
$(e.target).addClass('droppable');
e.dataTransfer.dropEffect = 'move';
allowDrop (e) {
$(e.target).addClass('droppable')
e.dataTransfer.dropEffect = 'move'
return false;
return false
},
/**
@ -120,41 +120,41 @@ export default {
*
* @return {Boolean}
*/
handleDrop(e) {
this.removeDroppableState(e);
handleDrop (e) {
this.removeDroppableState(e)
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) {
return false;
return false
}
if (this.type === 'favorites') {
favoriteStore.like(songs);
favoriteStore.like(songs)
} 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) => {
if (view === 'favorites') {
this.active = this.isFavorites;
this.active = this.isFavorites
} else if (view === 'playlist') {
this.active = this.playlist === playlist;
this.active = this.playlist === playlist
} else {
this.active = false;
this.active = false
}
});
},
};
})
}
}
</script>
<style lang="sass" scoped>

View file

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

View file

@ -76,25 +76,25 @@
</template>
<script>
import { every, filter } from 'lodash';
import { every, filter } from 'lodash'
import { br2nl } from '../../utils';
import { songInfo } from '../../services/info';
import { artistStore, albumStore, songStore } from '../../stores';
import { br2nl, forceReloadWindow } from '../../utils'
import { songInfo } from '../../services/info'
import { artistStore, albumStore, songStore } from '../../stores'
import soundBar from '../shared/sound-bar.vue';
import typeahead from '../shared/typeahead.vue';
import soundBar from '../shared/sound-bar.vue'
import typeahead from '../shared/typeahead.vue'
const COMPILATION_STATES = {
NONE: 0, // No songs belong to a compilation album
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 {
components: { soundBar, typeahead },
data() {
data () {
return {
shown: false,
songs: [],
@ -105,13 +105,13 @@
artistState: artistStore.state,
artistTypeaheadOptions: {
displayKey: 'name',
filterKey: 'name',
filterKey: 'name'
},
albumState: albumStore.state,
albumTypeaheadOptions: {
displayKey: 'name',
filterKey: 'name',
filterKey: 'name'
},
/**
@ -126,9 +126,9 @@
artistName: '',
lyrics: '',
track: '',
compilationState: null,
},
};
compilationState: null
}
}
},
computed: {
@ -137,8 +137,8 @@
*
* @return {boolean}
*/
editSingle() {
return this.songs.length === 1;
editSingle () {
return this.songs.length === 1
},
/**
@ -146,8 +146,8 @@
*
* @return {boolean}
*/
bySameArtist() {
return every(this.songs, song => song.artist.id === this.songs[0].artist.id);
bySameArtist () {
return every(this.songs, song => song.artist.id === this.songs[0].artist.id)
},
/**
@ -155,8 +155,8 @@
*
* @return {boolean}
*/
inSameAlbum() {
return every(this.songs, song => song.album.id === this.songs[0].album.id);
inSameAlbum () {
return every(this.songs, song => song.album.id === this.songs[0].album.id)
},
/**
@ -164,8 +164,8 @@
*
* @return {string}
*/
coverUrl() {
return this.inSameAlbum ? this.songs[0].album.cover : '/public/img/covers/unknown-album.png';
coverUrl () {
return this.inSameAlbum ? this.songs[0].album.cover : '/public/img/covers/unknown-album.png'
},
/**
@ -173,18 +173,18 @@
*
* @return {Number}
*/
compilationState() {
compilationState () {
const contributedSongs = filter(this.songs, song => song.contributing_artist_id)
if (!contributedSongs.length) {
this.formData.compilationState = COMPILATION_STATES.NONE
} else if (contributedSongs.length === this.songs.length) {
this.formData.compilationState = COMPILATION_STATES.ALL;
this.formData.compilationState = COMPILATION_STATES.ALL
} else {
this.formData.compilationState = COMPILATION_STATES.SOME;
this.formData.compilationState = COMPILATION_STATES.SOME
}
return this.formData.compilationState;
return this.formData.compilationState
},
/**
@ -192,8 +192,8 @@
*
* @return {string}
*/
displayedTitle() {
return this.editSingle ? this.formData.title : `${this.songs.length} songs selected`;
displayedTitle () {
return this.editSingle ? this.formData.title : `${this.songs.length} songs selected`
},
/**
@ -201,11 +201,11 @@
*
* @return {string}
*/
displayedAlbum() {
displayedAlbum () {
if (this.editSingle) {
return this.formData.albumName;
return this.formData.albumName
} else {
return this.formData.albumName ? this.formData.albumName : 'Mixed Albums';
return this.formData.albumName ? this.formData.albumName : 'Mixed Albums'
}
},
@ -214,75 +214,75 @@
*
* @return {string}
*/
displayedArtist() {
displayedArtist () {
if (this.editSingle) {
return this.formData.artistName;
return this.formData.artistName
} else {
return this.formData.artistName ? this.formData.artistName : 'Mixed Artists';
return this.formData.artistName ? this.formData.artistName : 'Mixed Artists'
}
},
}
},
methods: {
open(songs) {
this.shown = true;
this.songs = songs;
this.currentView = 'details';
this.needsReload = false;
open (songs) {
this.shown = true
this.songs = songs
this.currentView = 'details'
this.needsReload = false
if (this.editSingle) {
this.formData.title = this.songs[0].title;
this.formData.albumName = this.songs[0].album.name;
this.formData.artistName = this.songs[0].artist.name;
this.formData.title = this.songs[0].title
this.formData.albumName = this.songs[0].album.name
this.formData.artistName = this.songs[0].artist.name
// If we're editing only one song and the song's info (including lyrics)
// hasn't been loaded, load it now.
if (!this.songs[0].infoRetrieved) {
this.loading = true;
this.loading = true
songInfo.fetch(this.songs[0]).then(r => {
this.loading = false;
this.formData.lyrics = br2nl(this.songs[0].lyrics);
this.formData.track = this.songs[0].track;
this.initCompilationStateCheckbox();
});
this.loading = false
this.formData.lyrics = br2nl(this.songs[0].lyrics)
this.formData.track = this.songs[0].track
this.initCompilationStateCheckbox()
})
} else {
this.formData.lyrics = br2nl(this.songs[0].lyrics);
this.formData.track = this.songs[0].track;
this.initCompilationStateCheckbox();
this.formData.lyrics = br2nl(this.songs[0].lyrics)
this.formData.track = this.songs[0].track
this.initCompilationStateCheckbox()
}
} else {
this.formData.albumName = this.inSameAlbum ? this.songs[0].album.name : '';
this.formData.artistName = this.bySameArtist ? this.songs[0].artist.name : '';
this.loading = false;
this.initCompilationStateCheckbox();
this.formData.albumName = this.inSameAlbum ? this.songs[0].album.name : ''
this.formData.artistName = this.bySameArtist ? this.songs[0].artist.name : ''
this.loading = false
this.initCompilationStateCheckbox()
}
},
/**
* Initialize the compilation state's checkbox of the editing songs' album(s).
*/
initCompilationStateCheckbox() {
initCompilationStateCheckbox () {
// This must be wrapped in a $nextTick callback, because the form is dynamically
// attached into DOM in conjunction with `this.loading` data binding.
this.$nextTick(() => {
const chk = this.$refs.compilationStateChk;
const chk = this.$refs.compilationStateChk
switch (this.compilationState) {
case COMPILATION_STATES.ALL:
chk.checked = true;
chk.indeterminate = false;
break;
chk.checked = true
chk.indeterminate = false
break
case COMPILATION_STATES.NONE:
chk.checked = false;
chk.indeterminate = false;
break;
chk.checked = false
chk.indeterminate = false
break
default:
chk.checked = false;
chk.indeterminate = true;
break;
chk.checked = false
chk.indeterminate = true
break
}
});
})
},
/**
@ -291,34 +291,34 @@
* Also, following iTunes style, we don't support circular switching of the states -
* once the user clicks the checkbox, there's no going back to indeterminate state.
*/
changeCompilationState(e) {
this.formData.compilationState = e.target.checked ? COMPILATION_STATES.ALL : COMPILATION_STATES.NONE;
this.needsReload = true;
changeCompilationState (e) {
this.formData.compilationState = e.target.checked ? COMPILATION_STATES.ALL : COMPILATION_STATES.NONE
this.needsReload = true
},
/**
* Close the modal.
*/
close() {
this.shown = false;
close () {
this.shown = false
},
/**
* Submit the form.
*/
submit() {
this.loading = true;
submit () {
this.loading = true
songStore.update(this.songs, this.formData).then(r => {
this.loading = false;
this.close();
if (this.needsReload) {
forceReloadWindow();
}
}).catch(r => this.loading = false);
},
},
};
this.loading = false
this.close()
this.needsReload && forceReloadWindow()
}).catch(r => {
this.loading = false
})
}
}
}
</script>
<style lang="sass">

View file

@ -27,12 +27,10 @@
</template>
<script>
import { assign } from 'lodash';
import { pluralize, event } from '../../utils';
import { playlistStore } from '../../stores';
import router from '../../router';
import songMenuMethods from '../../mixins/song-menu-methods';
import { pluralize } from '../../utils'
import { playlistStore } from '../../stores'
import router from '../../router'
import songMenuMethods from '../../mixins/song-menu-methods'
export default {
name: 'shared--add-to-menu',
@ -40,19 +38,19 @@ export default {
mixins: [songMenuMethods],
filters: { pluralize },
data() {
data () {
return {
newPlaylistName: '',
playlistState: playlistStore.state,
};
playlistState: playlistStore.state
}
},
watch: {
songs() {
songs () {
if (!this.songs.length) {
this.close();
this.close()
}
},
}
},
methods: {
@ -60,27 +58,27 @@ export default {
* Save the selected songs as a playlist.
* As of current we don't have selective save.
*/
createNewPlaylistFromSongs() {
this.newPlaylistName = this.newPlaylistName.trim();
createNewPlaylistFromSongs () {
this.newPlaylistName = this.newPlaylistName.trim()
if (!this.newPlaylistName) {
return;
return
}
playlistStore.store(this.newPlaylistName, this.songs).then(p => {
this.newPlaylistName = '';
this.newPlaylistName = ''
// 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() {
this.$parent.closeAddToMenu();
},
},
};
close () {
this.$parent.closeAddToMenu()
}
}
}
</script>
<style lang="sass" scoped>

View file

@ -36,29 +36,29 @@
</template>
<script>
import { map } from 'lodash';
import $ from 'jquery';
import { map } from 'lodash'
import $ from 'jquery'
import { pluralize } from '../../utils';
import { queueStore, artistStore, sharedStore } from '../../stores';
import { playback, download } from '../../services';
import { pluralize } from '../../utils'
import { queueStore, artistStore, sharedStore } from '../../stores'
import { playback, download } from '../../services'
export default {
name: 'shared--album-item',
props: ['album'],
filters: { pluralize },
data() {
data () {
return {
sharedState: sharedStore.state,
};
sharedState: sharedStore.state
}
},
computed: {
isNormalArtist() {
return !artistStore.isVariousArtists(this.album.artist)
&& !artistStore.isUnknownArtist(this.album.artist);
},
isNormalArtist () {
return !artistStore.isVariousArtists(this.album.artist) &&
!artistStore.isUnknownArtist(this.album.artist)
}
},
methods: {
@ -66,42 +66,42 @@ export default {
* Play all songs in the current album in track order,
* or queue them up if Ctrl/Cmd key is pressed.
*/
play(e) {
play (e) {
if (e.metaKey || e.ctrlKey) {
queueStore.queue(this.album.songs);
queueStore.queue(this.album.songs)
} else {
playback.playAllInAlbum(this.album, false);
playback.playAllInAlbum(this.album, false)
}
},
/**
* Shuffle all songs in album.
*/
shuffle() {
playback.playAllInAlbum(this.album, true);
shuffle () {
playback.playAllInAlbum(this.album, true)
},
/**
* Download all songs in album.
*/
download() {
download.fromAlbum(this.album);
download () {
download.fromAlbum(this.album)
},
/**
* Allow dragging the album (actually, its songs).
*/
dragStart(e) {
const songIds = map(this.album.songs, 'id');
e.dataTransfer.setData('application/x-koel.text+plain', songIds);
e.dataTransfer.effectAllowed = 'move';
dragStart (e) {
const songIds = map(this.album.songs, 'id')
e.dataTransfer.setData('application/x-koel.text+plain', songIds)
e.dataTransfer.effectAllowed = 'move'
// 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}`);
e.dataTransfer.setDragImage($ghost[0], 0, 0);
},
},
};
const $ghost = $('#dragGhost').text(`All ${songIds.length} song${songIds.length === 1 ? '' : 's'} in ${this.album.name}`)
e.dataTransfer.setDragImage($ghost[0], 0, 0)
}
}
}
</script>
<style lang="sass">

View file

@ -28,22 +28,22 @@
</template>
<script>
import { map } from 'lodash';
import $ from 'jquery';
import { map } from 'lodash'
import $ from 'jquery'
import { pluralize } from '../../utils';
import { artistStore, queueStore, sharedStore } from '../../stores';
import { playback, download } from '../../services';
import { pluralize } from '../../utils'
import { artistStore, queueStore, sharedStore } from '../../stores'
import { playback, download } from '../../services'
export default {
name: 'shared--artist-item',
props: ['artist'],
filters: { pluralize },
data() {
data () {
return {
sharedState: sharedStore.state,
};
sharedState: sharedStore.state
}
},
computed: {
@ -53,8 +53,8 @@ export default {
*
* @return {Boolean}
*/
showing() {
return this.artist.songCount && !artistStore.isVariousArtists(this.artist);
showing () {
return this.artist.songCount && !artistStore.isVariousArtists(this.artist)
}
},
@ -62,35 +62,35 @@ export default {
/**
* Play all songs by the current artist, or queue them up if Ctrl/Cmd key is pressed.
*/
play(e) {
play (e) {
if (e.metaKey || e.ctrlKey) {
queueStore.queue(this.artist.songs);
queueStore.queue(this.artist.songs)
} else {
playback.playAllByArtist(this.artist);
playback.playAllByArtist(this.artist)
}
},
/**
* Download all songs by artist.
*/
download() {
download.fromArtist(this.artist);
download () {
download.fromArtist(this.artist)
},
/**
* Allow dragging the artist (actually, their songs).
*/
dragStart(e) {
const songIds = map(this.artist.songs, 'id');
e.dataTransfer.setData('application/x-koel.text+plain', songIds);
e.dataTransfer.effectAllowed = 'move';
dragStart (e) {
const songIds = map(this.artist.songs, 'id')
e.dataTransfer.setData('application/x-koel.text+plain', songIds)
e.dataTransfer.effectAllowed = 'move'
// 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}`);
e.dataTransfer.setDragImage($ghost[0], 0, 0);
},
},
};
const $ghost = $('#dragGhost').text(`All ${songIds.length} song${songIds.length === 1 ? '' : 's'} by ${this.artist.name}`)
e.dataTransfer.setDragImage($ghost[0], 0, 0)
}
}
}
</script>
<style lang="sass">

View file

@ -21,9 +21,9 @@
</template>
<script>
import { pluralize } from '../../utils';
import { queueStore } from '../../stores';
import { playback } from '../../services';
import { pluralize } from '../../utils'
import { queueStore } from '../../stores'
import { playback } from '../../services'
export default {
name: 'shared--home-song-item',
@ -31,31 +31,31 @@ export default {
filters: { pluralize },
computed: {
showPlayCount() {
return this.topPlayCount && this.song.playCount;
},
showPlayCount () {
return this.topPlayCount && this.song.playCount
}
},
methods: {
play() {
play () {
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') {
this.play(this.song);
this.play(this.song)
} else if (this.song.playbackState === 'paused') {
playback.resume();
playback.resume()
} else {
playback.pause();
playback.pause()
}
},
},
};
}
}
}
</script>
<style lang="sass" scoped>

View file

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

View file

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

View file

@ -6,21 +6,21 @@
</template>
<script>
import isMobile from 'ismobilejs';
import isMobile from 'ismobilejs'
export default {
name: 'shared--song-list-controls-toggler',
props: ['showingControls'],
data() {
data () {
return {
isPhone: isMobile.phone
}
},
methods: {
toggleControls() {
this.$emit('toggleControls');
toggleControls () {
this.$emit('toggleControls')
}
}
}

View file

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

View file

@ -50,17 +50,17 @@
</template>
<script>
import { find, invokeMap, filter, map } from 'lodash';
import isMobile from 'ismobilejs';
import $ from 'jquery';
import { find, invokeMap, filter, map } from 'lodash'
import isMobile from 'ismobilejs'
import $ from 'jquery'
import { filterBy, orderBy, limitBy, event } from '../../utils';
import { playlistStore, queueStore, songStore, favoriteStore } from '../../stores';
import { playback } from '../../services';
import router from '../../router';
import songItem from './song-item.vue';
import songMenu from './song-menu.vue';
import infiniteScroll from '../../mixins/infinite-scroll';
import { filterBy, orderBy, limitBy, event } from '../../utils'
import { playlistStore, queueStore, songStore, favoriteStore } from '../../stores'
import { playback } from '../../services'
import router from '../../router'
import songItem from './song-item.vue'
import songMenu from './song-menu.vue'
import infiniteScroll from '../../mixins/infinite-scroll'
export default {
name: 'song-list',
@ -68,7 +68,7 @@ export default {
mixins: [infiniteScroll],
components: { songItem, songMenu },
data() {
data () {
return {
lastSelectedRow: null,
q: '', // The filter query
@ -77,35 +77,35 @@ export default {
sortingByAlbum: false,
sortingByArtist: false,
selectedSongs: [],
mutatedItems: [],
};
mutatedItems: []
}
},
watch: {
/**
* Watch the items.
*/
items() {
items () {
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.
this.$parent.updateMeta({
songCount: this.items.length,
totalLength: songStore.getLength(this.items, true),
});
totalLength: songStore.getLength(this.items, true)
})
},
selectedSongs(val) {
this.$parent.setSelectedSongs(val);
},
selectedSongs (val) {
this.$parent.setSelectedSongs(val)
}
},
computed: {
displayedItems() {
displayedItems () {
return limitBy(
filterBy(
this.mutatedItems,
@ -113,8 +113,8 @@ export default {
'title', 'album.name', 'artist.name'
),
this.numOfItems
);
},
)
}
},
methods: {
@ -123,43 +123,43 @@ export default {
*
* @param {String} key The sort key. Can be 'title', 'album', 'artist', or 'length'
*/
sort(key) {
sort (key) {
if (this.sortable === false) {
return;
return
}
this.sortKey = key;
this.order = 0 - this.order;
this.sortingByAlbum = Array.isArray(this.sortKey) && this.sortKey[0] === 'album.name';
this.sortingByArtist = Array.isArray(this.sortKey) && this.sortKey[0] === 'album.artist.name';
this.mutatedItems = orderBy(this.items, this.sortKey, this.order);
this.sortKey = key
this.order = 0 - this.order
this.sortingByAlbum = Array.isArray(this.sortKey) && this.sortKey[0] === 'album.name'
this.sortingByArtist = Array.isArray(this.sortKey) && this.sortKey[0] === 'album.artist.name'
this.mutatedItems = orderBy(this.items, this.sortKey, this.order)
},
/**
* Execute the corresponding reaction(s) when the user presses Delete.
*/
handleDelete() {
const songs = this.selectedSongs;
handleDelete () {
const songs = this.selectedSongs
if (!songs.length) {
return;
return
}
switch (this.type) {
case 'queue':
queueStore.unqueue(songs);
break;
queueStore.unqueue(songs)
break
case 'favorites':
favoriteStore.unlike(songs);
break;
favoriteStore.unlike(songs)
break
case 'playlist':
playlistStore.removeSongs(this.playlist, songs);
break;
playlistStore.removeSongs(this.playlist, songs)
break
default:
break;
break
}
this.clearSelection();
this.clearSelection()
},
/**
@ -167,25 +167,25 @@ export default {
*
* @param {Object} e The keydown event.
*/
handleEnter(e) {
const songs = this.selectedSongs;
handleEnter (e) {
const songs = this.selectedSongs
if (!songs.length) {
return;
return
}
if (songs.length === 1) {
// Just play the song
playback.play(songs[0]);
playback.play(songs[0])
return;
return
}
switch (this.type) {
case 'queue':
// Play the first song selected if we're in Queue screen.
playback.play(songs[0]);
break;
playback.play(songs[0])
break
case 'favorites':
case 'playlist':
default:
@ -201,20 +201,20 @@ export default {
// 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(() => {
router.go('queue');
router.go('queue')
if (e.ctrlKey || e.metaKey || songs.length === 1) {
playback.play(songs[0]);
playback.play(songs[0])
}
});
})
break;
break
}
this.clearSelection();
this.clearSelection()
},
/**
@ -224,8 +224,8 @@ export default {
*
* @return {Object} The Vue compoenent
*/
getComponentBySongId(id) {
return find(this.$refs.rows, { song: { id } });
getComponentBySongId (id) {
return find(this.$refs.rows, { song: { id }})
},
/**
@ -233,13 +233,13 @@ export default {
*
* @param {Object} e The keydown event.
*/
handleA(e) {
handleA (e) {
if (!e.metaKey && !e.ctrlKey) {
return;
return
}
invokeMap(this.$refs.rows, 'select');
this.gatherSelected();
invokeMap(this.$refs.rows, 'select')
this.gatherSelected()
},
/**
@ -247,11 +247,11 @@ export default {
*
* @return {Array.<Object>} An array of Song objects
*/
gatherSelected() {
const selectedRows = filter(this.$refs.rows, { selected: true });
const ids = map(selectedRows, row => row.song.id);
gatherSelected () {
const selectedRows = filter(this.$refs.rows, { selected: true })
const ids = map(selectedRows, row => row.song.id)
this.selectedSongs = songStore.byIds(ids);
this.selectedSongs = songStore.byIds(ids)
},
/**
@ -268,33 +268,33 @@ export default {
* @param {String} songId
* @param {Object} e
*/
itemClicked(songId, e) {
const row = this.getComponentBySongId(songId);
itemClicked (songId, e) {
const row = this.getComponentBySongId(songId)
// If we're on a touch device, or if Ctrl/Cmd key is pressed, just toggle selection.
if (isMobile.any) {
this.toggleRow(row);
this.gatherSelected();
this.toggleRow(row)
this.gatherSelected()
return;
return
}
if (e.ctrlKey || e.metaKey) {
this.toggleRow(row);
this.toggleRow(row)
}
if (e.button === 0) {
if (!e.ctrlKey && !e.metaKey && !e.shiftKey) {
this.clearSelection();
this.toggleRow(row);
this.clearSelection()
this.toggleRow(row)
}
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()
},
/**
@ -302,27 +302,27 @@ export default {
*
* @param {Object} row The song-item component
*/
toggleRow(row) {
row.toggleSelectedState();
this.lastSelectedRow = row;
toggleRow (row) {
row.toggleSelectedState()
this.lastSelectedRow = row
},
selectRowsBetweenIndexes(indexes) {
indexes.sort((a, b) => a - b);
selectRowsBetweenIndexes (indexes) {
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) {
this.getComponentBySongId($(rows[i - 1]).data('song-id')).select();
this.getComponentBySongId($(rows[i - 1]).data('song-id')).select()
}
},
/**
* Clear the current selection on this song list.
*/
clearSelection() {
invokeMap(this.$refs.rows, 'deselect');
this.gatherSelected();
clearSelection () {
invokeMap(this.$refs.rows, 'deselect')
this.gatherSelected()
},
/**
@ -332,24 +332,24 @@ export default {
*
* @param {Object} e The event.
*/
dragStart(songId, e) {
dragStart (songId, e) {
// 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) {
this.clearSelection();
currentRow.select();
this.gatherSelected();
this.clearSelection()
currentRow.select()
this.gatherSelected()
}
this.$nextTick(() => {
const songIds = map(this.selectedSongs, 'id');
e.dataTransfer.setData('application/x-koel.text+plain', songIds);
e.dataTransfer.effectAllowed = 'move';
const songIds = map(this.selectedSongs, 'id')
e.dataTransfer.setData('application/x-koel.text+plain', songIds)
e.dataTransfer.effectAllowed = 'move'
// Set a fancy drop image using our ghost element.
const $ghost = $('#dragGhost').text(`${songIds.length} song${songIds.length === 1 ? '' : 's'}`);
e.dataTransfer.setDragImage($ghost[0], 0, 0);
});
const $ghost = $('#dragGhost').text(`${songIds.length} song${songIds.length === 1 ? '' : 's'}`)
e.dataTransfer.setDragImage($ghost[0], 0, 0)
})
},
/**
@ -358,15 +358,15 @@ export default {
* @param {String} songId
* @param {Object} e The dragover event.
*/
allowDrop(songId, e) {
allowDrop (songId, e) {
if (this.type !== 'queue') {
return;
return
}
$(e.target).parents('tr').addClass('droppable');
e.dataTransfer.dropEffect = 'move';
$(e.target).parents('tr').addClass('droppable')
e.dataTransfer.dropEffect = 'move'
return false;
return false
},
/**
@ -375,26 +375,24 @@ export default {
* @param {String} songId
* @param {Object} e
*/
handleDrop(songId, e) {
console.log('dropping into', songId);
handleDrop (songId, e) {
if (this.type !== 'queue') {
return this.removeDroppableState(e) && false;
return this.removeDroppableState(e) && false
}
if (!e.dataTransfer.getData('application/x-koel.text+plain')) {
return this.removeDroppableState(e) && false;
return this.removeDroppableState(e) && false
}
const songs = this.selectedSongs;
console.log('selected songs after drop:', songs);
const songs = this.selectedSongs
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
},
/**
@ -402,25 +400,25 @@ export default {
*
* @param {Object} e
*/
removeDroppableState(e) {
return $(e.target).parents('tr').removeClass('droppable');
removeDroppableState (e) {
return $(e.target).parents('tr').removeClass('droppable')
},
openContextMenu(songId, e) {
openContextMenu (songId, e) {
// If the user is right-clicking an unselected row,
// clear the current selection and select it instead.
const currentRow = this.getComponentBySongId(songId);
const currentRow = this.getComponentBySongId(songId)
if (!currentRow.selected) {
this.clearSelection();
currentRow.select();
this.gatherSelected();
this.clearSelection()
currentRow.select()
this.gatherSelected()
}
this.$nextTick(() => this.$refs.contextMenu.open(e.pageY, e.pageX));
},
this.$nextTick(() => this.$refs.contextMenu.open(e.pageY, e.pageX))
}
},
created() {
created () {
event.on({
/**
* Listen to song:played event to do some logic.
@ -430,21 +428,21 @@ export default {
'song:played': song => {
// If the song is at the end of the current displayed items, load more.
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.
if (this.type === 'queue') {
const $wrapper = $(this.$refs.wrapper);
const $row = $wrapper.find(`.song-item[data-song-id="${song.id}"]`);
const $wrapper = $(this.$refs.wrapper)
const $row = $wrapper.find(`.song-item[data-song-id="${song.id}"]`)
if (!$row.length) {
return;
return
}
if ($wrapper[0].getBoundingClientRect().top + $wrapper[0].getBoundingClientRect().height <
$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.
*/
'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.
@ -463,10 +463,10 @@ export default {
* Listen to 'song:selection-clear' (often broadcasted from the direct parent)
* to clear the selected songs.
*/
'song:selection-clear': () => this.clearSelection(),
});
},
};
'song:selection-clear': () => this.clearSelection()
})
}
}
</script>
<style lang="sass">

View file

@ -30,128 +30,128 @@
</template>
<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 { sharedStore, songStore, queueStore, userStore, playlistStore } from '../../stores';
import { playback, download } from '../../services';
import router from '../../router';
import { event, isClipboardSupported, copyText } from '../../utils'
import { sharedStore, songStore, queueStore, userStore, playlistStore } from '../../stores'
import { playback, download } from '../../services'
import router from '../../router'
export default {
name: 'song-menu',
props: ['songs'],
mixins: [songMenuMethods],
data() {
data () {
return {
playlistState: playlistStore.state,
sharedState: sharedStore.state,
copyable: isClipboardSupported(),
};
copyable: isClipboardSupported()
}
},
computed: {
onlyOneSongSelected() {
return this.songs.length === 1;
onlyOneSongSelected () {
return this.songs.length === 1
},
firstSongPlaying() {
return this.songs[0] ? this.songs[0].playbackState === 'playing' : false;
firstSongPlaying () {
return this.songs[0] ? this.songs[0].playbackState === 'playing' : false
},
isAdmin() {
return userStore.current.is_admin;
},
isAdmin () {
return userStore.current.is_admin
}
},
methods: {
open(top = 0, left = 0) {
open (top = 0, left = 0) {
if (!this.songs.length) {
return;
return
}
this.top = top;
this.left = left;
this.shown = true;
this.top = top
this.left = left
this.shown = true
this.$nextTick(() => {
// Make sure the menu isn't off-screen
if (this.$el.getBoundingClientRect().bottom > window.innerHeight) {
$(this.$el).css({
top: 'auto',
bottom: 0,
});
bottom: 0
})
} else {
$(this.$el).css({
top: this.top,
bottom: 'auto',
});
bottom: 'auto'
})
}
this.$refs.menu.focus();
});
this.$refs.menu.focus()
})
},
/**
* Take the right playback action based on the current playback state.
*/
doPlayback() {
doPlayback () {
switch (this.songs[0].playbackState) {
case 'playing':
playback.pause();
break;
playback.pause()
break
case 'paused':
playback.resume();
break;
playback.resume()
break
default:
if (!queueStore.contains(this.songs[0])) {
queueStore.queueAfterCurrent(this.songs[0]);
queueStore.queueAfterCurrent(this.songs[0])
}
playback.play(this.songs[0]);
break;
playback.play(this.songs[0])
break
}
this.close();
this.close()
},
/**
* Trigger opening the "Edit Song" form/overlay.
*/
openEditForm() {
openEditForm () {
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.
*/
viewAlbumDetails(album) {
router.go(`album/${album.id}`);
this.close();
viewAlbumDetails (album) {
router.go(`album/${album.id}`)
this.close()
},
/**
* Load the artist details screen.
*/
viewArtistDetails(artist) {
router.go(`artist/${artist.id}`);
this.close();
viewArtistDetails (artist) {
router.go(`artist/${artist.id}`)
this.close()
},
download() {
download.fromSongs(this.songs);
this.close();
download () {
download.fromSongs(this.songs)
this.close()
},
copyUrl() {
copyText(songStore.getShareableUrl(this.songs[0]));
},
copyUrl () {
copyText(songStore.getShareableUrl(this.songs[0]))
}
},
/**
@ -159,30 +159,30 @@ export default {
* With this, we can catch when the submenus shown or hidden, and can make sure
* they don't appear off-screen.
*/
mounted() {
mounted () {
$(this.$el).find('.has-sub').hover(e => {
const $submenu = $(e.target).find('.submenu:first');
const $submenu = $(e.target).find('.submenu:first')
if (!$submenu.length) {
return;
return
}
$submenu.show();
$submenu.show()
// Make sure the submenu isn't off-screen
if ($submenu[0].getBoundingClientRect().bottom > window.innerHeight) {
$submenu.css({
top: 'auto',
bottom: 0,
});
bottom: 0
})
}
}, e => {
$(e.target).find('.submenu:first').hide().css({
top: 0,
bottom: 'auto',
});
});
},
};
bottom: 'auto'
})
})
}
}
</script>
<style lang="sass" scoped>

View file

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

View file

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

View file

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

View file

@ -43,21 +43,21 @@
</template>
<script>
import { clone, assign } from 'lodash';
import swal from 'sweetalert';
import { clone, assign } from 'lodash'
import swal from 'sweetalert'
import { userStore } from '../../stores';
import router from '../../router';
import { userStore } from '../../stores'
import router from '../../router'
export default {
props: ['user'],
data() {
data () {
return {
editing: false,
confirmingDelete: false,
cached: {},
};
cached: {}
}
},
computed: {
@ -66,9 +66,9 @@ export default {
*
* @return {Boolean}
*/
isCurrentUser() {
return this.user.id === userStore.current.id;
},
isCurrentUser () {
return this.user.id === userStore.current.id
}
},
methods: {
@ -76,55 +76,55 @@ export default {
* Trigger editing a user.
* If the user is the current logged-in user, redirect to the profile screen instead.
*/
edit() {
edit () {
if (this.isCurrentUser) {
router.go('profile');
router.go('profile')
return;
return
}
// Keep a cached version of the user for rolling back.
this.cached = clone(this.user);
this.editing = true;
this.cached = clone(this.user)
this.editing = true
},
/**
* Cancel editing a user.
*/
cancelEdit() {
cancelEdit () {
// Restore the original user's properties
assign(this.user, this.cached);
this.editing = false;
assign(this.user, this.cached)
this.editing = false
},
/**
* Update the edited user.
*/
update() {
userStore.update(this.user, this.user.name, this.user.email, this.user.password). then(u => {
this.editing = false;
});
update () {
userStore.update(this.user, this.user.name, this.user.email, this.user.password).then(u => {
this.editing = false
})
},
/**
* Kill off the freaking user.
*/
del() {
del () {
swal({
title: 'Hey…',
text: `Youre about to unperson ${this.user.name}. Are you sure?`,
type: 'warning',
showCancelButton: true,
confirmButtonText: 'Certainly',
cancelButtonText: 'Oops',
cancelButtonText: 'Oops'
}, () => {
userStore.destroy(this.user).then(() => {
this.$destroy(true);
});
});
},
},
};
this.$destroy(true)
})
})
}
}
}
</script>
<style lang="sass">

View file

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

View file

@ -40,33 +40,34 @@
</template>
<script>
import { map, cloneDeep } from 'lodash';
import $ from 'jquery';
import rangeslider from 'rangeslider.js';
import { map, cloneDeep } from 'lodash'
import $ from 'jquery'
// eslint-disable-next-line no-unused-vars
import rangeslider from 'rangeslider.js'
import { isAudioContextSupported, event } from '../../utils';
import { equalizerStore, preferenceStore as preferences } from '../../stores';
import { isAudioContextSupported, event } from '../../utils'
import { equalizerStore, preferenceStore as preferences } from '../../stores'
export default {
data() {
data () {
return {
idx: 0,
bands: [],
preampGainValue: 0,
selectedPresetIndex: -1,
};
selectedPresetIndex: -1
}
},
computed: {
presets() {
let clonedPreset = cloneDeep(equalizerStore.presets);
presets () {
const clonedPreset = cloneDeep(equalizerStore.presets)
// Prepend an empty option for instruction purpose.
clonedPreset.unshift({
id: -1,
name: 'Preset',
});
return clonedPreset;
},
name: 'Preset'
})
return clonedPreset
}
},
watch: {
@ -74,16 +75,16 @@ export default {
* Watch selectedPresetIndex and trigger our logic.
* @param {Number} val
*/
selectedPresetIndex(val) {
selectedPresetIndex (val) {
/**
* Save the selected preset (index) into local storage every time the value's changed.
*/
preferences.selectedPreset = val;
preferences.selectedPreset = val
if (~~val !== -1) {
this.loadPreset(equalizerStore.getPresetById(val));
this.loadPreset(equalizerStore.getPresetById(val))
}
},
}
},
methods: {
@ -92,62 +93,63 @@ export default {
*
* @param {Element} player The audio player's DOM.
*/
init(player) {
const settings = equalizerStore.get();
init (player) {
const settings = equalizerStore.get()
const AudioContext = window.AudioContext ||
window.webkitAudioContext ||
window.mozAudioContext ||
window.oAudioContext ||
window.msAudioContext;
window.msAudioContext
const context = new AudioContext();
const context = new AudioContext()
this.preampGainNode = context.createGain();
this.changePreampGain(settings.preamp);
this.preampGainNode = context.createGain()
this.changePreampGain(settings.preamp)
const source = context.createMediaElementSource(player);
source.connect(this.preampGainNode);
const source = context.createMediaElementSource(player)
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.
[60, 170, 310, 600, 1000, 3000, 6000, 12000, 14000, 16000].forEach((f, i) => {
const filter = context.createBiquadFilter();
const freqs = [60, 170, 310, 600, 1000, 3000, 6000, 12000, 14000, 16000]
freqs.forEach((f, i) => {
const filter = context.createBiquadFilter()
if (i === 0) {
filter.type = 'lowshelf';
filter.type = 'lowshelf'
} else if (i === 9) {
filter.type = 'highshelf';
filter.type = 'highshelf'
} else {
filter.type = 'peaking';
filter.type = 'peaking'
}
filter.gain.value = settings.gains[i] ? settings.gains[i] : 0;
filter.Q.value = 1;
filter.frequency.value = f;
filter.gain.value = settings.gains[i] ? settings.gains[i] : 0
filter.Q.value = 1
filter.frequency.value = f
prevFilter ? prevFilter.connect(filter) : this.preampGainNode.connect(filter);
prevFilter = filter;
prevFilter ? prevFilter.connect(filter) : this.preampGainNode.connect(filter)
prevFilter = filter
this.bands.push({
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.
this.selectedPresetIndex = preferences.selectedPreset;
this.selectedPresetIndex = preferences.selectedPreset
},
/**
* Create the UI slider for both the preamp and the normal bands using rangeslider.js.
*/
createRangeSliders() {
createRangeSliders () {
$('#equalizer input[type="range"]').each((i, el) => {
$(el).rangeslider({
/**
@ -165,9 +167,9 @@ export default {
*/
onSlide: (position, value) => {
if ($(el).parents('.band').is('.preamp')) {
this.changePreampGain(value);
this.changePreampGain(value)
} 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.
*/
onSlideEnd: () => {
this.selectedPresetIndex = -1;
this.save();
},
});
});
this.selectedPresetIndex = -1
this.save()
}
})
})
},
/**
@ -187,9 +189,9 @@ export default {
*
* @param {Number} dbValue The value of the gain, in dB.
*/
changePreampGain(dbValue) {
this.preampGainValue = dbValue;
this.preampGainNode.gain.value = Math.pow(10, dbValue / 20);
changePreampGain (dbValue) {
this.preampGainValue = dbValue
this.preampGainNode.gain.value = Math.pow(10, dbValue / 20)
},
/**
@ -198,48 +200,48 @@ export default {
* @param {Object} filter The filter object
* @param {Object} value Value of the gain, in dB.
*/
changeFilterGain(filter, value) {
filter.gain.value = value;
changeFilterGain (filter, value) {
filter.gain.value = value
},
/**
* Load a preset when the user select it from the dropdown.
*/
loadPreset(preset) {
loadPreset (preset) {
$('#equalizer input[type=range]').each((i, input) => {
// We treat our preamp slider differently.
if ($(input).parents('.band').is('.preamp')) {
this.changePreampGain(preset.preamp);
this.changePreampGain(preset.preamp)
} else {
this.changeFilterGain(this.bands[i - 1].filter, preset.gains[i - 1]);
input.value = preset.gains[i - 1];
this.changeFilterGain(this.bands[i - 1].filter, preset.gains[i - 1])
input.value = preset.gains[i - 1]
}
});
})
this.$nextTick(() => {
// 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() {
equalizerStore.set(this.preampGainValue, map(this.bands, 'filter.gain.value'));
},
save () {
equalizerStore.set(this.preampGainValue, map(this.bands, 'filter.gain.value'))
}
},
mounted() {
mounted () {
event.on('equalizer:init', player => {
if (isAudioContextSupported()) {
this.init(player);
this.init(player)
}
});
},
};
})
}
}
</script>
<style lang="sass">

View file

@ -56,17 +56,16 @@
</template>
<script>
import config from '../../config';
import { playback } from '../../services';
import { isAudioContextSupported, event } from '../../utils';
import { songStore, favoriteStore, preferenceStore } from '../../stores';
import { playback } from '../../services'
import { isAudioContextSupported, event } from '../../utils'
import { songStore, favoriteStore, preferenceStore } from '../../stores'
import soundBar from '../shared/sound-bar.vue';
import equalizer from './equalizer.vue';
import volume from './volume.vue';
import soundBar from '../shared/sound-bar.vue'
import equalizer from './equalizer.vue'
import volume from './volume.vue'
export default {
data() {
data () {
return {
song: songStore.stub,
viewingQueue: false,
@ -80,8 +79,8 @@ export default {
*
* @type {Boolean}
*/
useEqualizer: isAudioContextSupported(),
};
useEqualizer: isAudioContextSupported()
}
},
components: { soundBar, equalizer, volume },
@ -92,8 +91,8 @@ export default {
*
* @return {?Object}
*/
prev() {
return playback.previous;
prev () {
return playback.previous
},
/**
@ -101,76 +100,76 @@ export default {
*
* @return {?Object}
*/
next() {
return playback.next;
},
next () {
return playback.next
}
},
methods: {
/**
* Play the previous song in queue.
*/
playPrev() {
return playback.playPrev();
playPrev () {
return playback.playPrev()
},
/**
* Play the next song in queue.
*/
playNext() {
return playback.playNext();
playNext () {
return playback.playNext()
},
/**
* Resume the current song.
* If the current song is the stub, just play the first song in the queue.
*/
resume() {
resume () {
if (!this.song.id) {
return playback.playFirstInQueue();
return playback.playFirstInQueue()
}
playback.resume();
playback.resume()
},
/**
* Pause the playback.
*/
pause() {
playback.pause();
pause () {
playback.pause()
},
/**
* Change the repeat mode.
*/
changeRepeatMode() {
return playback.changeRepeatMode();
changeRepeatMode () {
return playback.changeRepeatMode()
},
/**
* Like the current song.
*/
like() {
like () {
if (!this.song.id) {
return;
return
}
favoriteStore.toggleOne(this.song);
favoriteStore.toggleOne(this.song)
},
/**
* Toggle hide or show the extra panel.
*/
toggleExtraPanel() {
preferenceStore.set('showExtraPanel', !this.prefs.showExtraPanel);
toggleExtraPanel () {
preferenceStore.set('showExtraPanel', !this.prefs.showExtraPanel)
},
closeEqualizer() {
this.showEqualizer = false;
},
closeEqualizer () {
this.showEqualizer = false
}
},
created() {
created () {
event.on({
/**
* Listen to song:played event to set the current playing song and the cover image.
@ -180,20 +179,24 @@ export default {
* @return {Boolean}
*/
'song:played': song => {
this.song = song;
this.cover = this.song.album.cover;
this.song = song
this.cover = this.song.album.cover
},
/**
* Listen to main-content-view:load event and highlight the Queue icon if
* 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>
<style lang="sass">

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,16 +4,16 @@
* @type {Object}
*/
export const clickawayDirective = {
bind(el, { value }) {
bind (el, { value }) {
if (typeof value !== 'function') {
console.warn(`Expect a function, got ${value}`);
return;
console.warn(`Expect a function, got ${value}`)
return
}
document.addEventListener('click', e => {
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.
*/
export const focusDirective = {
inserted(el) {
el.focus();
},
};
inserted (el) {
el.focus()
}
}

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
import $ from 'jquery';
import $ from 'jquery'
import { event } from '../utils';
import toTopButton from '../components/shared/to-top-button.vue';
import { event } from '../utils'
import toTopButton from '../components/shared/to-top-button.vue'
/**
* Add a "infinite scroll" functionality to any component using this mixin.
@ -11,45 +11,45 @@ import toTopButton from '../components/shared/to-top-button.vue';
export default {
components: { toTopButton },
data() {
data () {
return {
numOfItems: 30, // Number of currently loaded and displayed items
perPage: 30, // Number of items to be loaded per "page"
showBackToTop: false,
};
showBackToTop: false
}
},
methods: {
scrolling(e) {
scrolling (e) {
// 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 (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.
*/
displayMore() {
this.numOfItems += this.perPage;
displayMore () {
this.numOfItems += this.perPage
},
/**
* Scroll to top of the wrapper.
*/
scrollToTop() {
$(this.$refs.wrapper).animate({ scrollTop: 0 }, 500);
this.showBackToTop = false;
},
scrollToTop () {
$(this.$refs.wrapper).animate({ scrollTop: 0 }, 500)
this.showBackToTop = false
}
},
created() {
created () {
event.on('koel:teardown', () => {
this.numOfItems = 30;
this.showBackToTop = false;
});
},
};
this.numOfItems = 30
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.
@ -9,56 +9,55 @@ import { queueStore, playlistStore, favoriteStore } from '../stores';
* for example close() and open().
*/
export default {
data() {
data () {
return {
shown: false,
top: 0,
left: 0,
};
left: 0
}
},
methods: {
open() {},
open () {},
/**
* Close all submenus.
*/
close() {
$(this.$el).find('.submenu').hide();
this.shown = false;
close () {
$(this.$el).find('.submenu').hide()
this.shown = false
},
/**
* Queue select songs after the current song.
*/
queueSongsAfterCurrent() {
queueStore.queueAfterCurrent(this.songs);
this.close();
queueSongsAfterCurrent () {
queueStore.queueAfterCurrent(this.songs)
this.close()
},
/**
* Queue selected songs to bottom of queue.
*/
queueSongsToBottom() {
queueStore.queue(this.songs);
this.close();
queueSongsToBottom () {
queueStore.queue(this.songs)
this.close()
},
/**
* Queue selected songs to top of queue.
*/
queueSongsToTop() {
queueStore.queue(this.songs, false, true);
this.close();
queueSongsToTop () {
queueStore.queue(this.songs, false, true)
this.close()
},
/**
* Add the selected songs into Favorites.
*/
addSongsToFavorite() {
favoriteStore.like(this.songs);
this.close();
addSongsToFavorite () {
favoriteStore.like(this.songs)
this.close()
},
/**
@ -66,9 +65,9 @@ export default {
*
* @param {Object} playlist The playlist.
*/
addSongsToExistingPlaylist(playlist) {
playlistStore.addSongs(playlist, this.songs);
this.close();
},
},
};
addSongsToExistingPlaylist (playlist) {
playlistStore.addSongs(playlist, this.songs)
this.close()
}
}
}

View file

@ -1,108 +1,108 @@
import isMobile from 'ismobilejs';
import isMobile from 'ismobilejs'
import { loadMainView } from './utils';
import { artistStore, albumStore, songStore, queueStore, playlistStore, userStore } from './stores';
import { playback } from './services';
import { loadMainView } from './utils'
import { artistStore, albumStore, songStore, queueStore, playlistStore, userStore } from './stores'
import { playback } from './services'
export default {
routes: {
'/home'() {
loadMainView('home');
'/home' () {
loadMainView('home')
},
'/queue'() {
loadMainView('queue');
'/queue' () {
loadMainView('queue')
},
'/songs'() {
loadMainView('songs');
'/songs' () {
loadMainView('songs')
},
'/albums'() {
loadMainView('albums');
'/albums' () {
loadMainView('albums')
},
'/album/(\\d+)'(id) {
const album = albumStore.byId(~~id);
'/album/(\\d+)' (id) {
const album = albumStore.byId(~~id)
if (album) {
loadMainView('album', album);
loadMainView('album', album)
}
},
'/artists'() {
loadMainView('artists');
'/artists' () {
loadMainView('artists')
},
'/artist/(\\d+)'(id) {
const artist = artistStore.byId(~~id);
'/artist/(\\d+)' (id) {
const artist = artistStore.byId(~~id)
if (artist) {
loadMainView('artist', artist);
loadMainView('artist', artist)
}
},
'/favorites'() {
loadMainView('favorites');
'/favorites' () {
loadMainView('favorites')
},
'/playlist/(\\d+)'(id) {
const playlist = playlistStore.byId(~~id);
'/playlist/(\\d+)' (id) {
const playlist = playlistStore.byId(~~id)
if (playlist) {
loadMainView('playlist', playlist);
loadMainView('playlist', playlist)
}
},
'/settings'() {
userStore.current.is_admin && loadMainView('settings');
'/settings' () {
userStore.current.is_admin && loadMainView('settings')
},
'/users'() {
userStore.current.is_admin && loadMainView('users');
'/users' () {
userStore.current.is_admin && loadMainView('users')
},
'/profile'() {
loadMainView('profile');
'/profile' () {
loadMainView('profile')
},
'/login'() {
'/login' () {
},
'/song/([a-z0-9]{32})'(id) {
const song = songStore.byId(id);
if (!song) return;
'/song/([a-z0-9]{32})' (id) {
const song = songStore.byId(id)
if (!song) return
if (isMobile.apple.device) {
// Mobile Safari doesn't allow autoplay, so we just queue.
queueStore.queue(song);
loadMainView('queue');
queueStore.queue(song)
loadMainView('queue')
} else {
playback.queueAndPlay(song);
playback.queueAndPlay(song)
}
},
'/youtube'() {
loadMainView('youtubePlayer');
},
'/youtube' () {
loadMainView('youtubePlayer')
}
},
init() {
this.loadState();
window.addEventListener('popstate', () => this.loadState(), true);
init () {
this.loadState()
window.addEventListener('popstate', () => this.loadState(), true)
},
loadState() {
loadState () {
if (!window.location.hash) {
return this.go('home');
return this.go('home')
}
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) {
const [, ...params] = matches;
this.routes[route](...params);
return false;
const [, ...params] = matches
this.routes[route](...params)
return false
}
});
})
},
/**
@ -110,15 +110,15 @@ export default {
*
* @param {String} path
*/
go(path) {
go (path) {
if (path[0] !== '/') {
path = `/${path}`;
path = `/${path}`
}
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 { map } from 'lodash';
import $ from 'jquery'
import { map } from 'lodash'
import { playlistStore, favoriteStore } from '../stores';
import { ls } from '.';
import { playlistStore, favoriteStore } from '../stores'
import { ls } from '.'
export const download = {
/**
@ -10,12 +10,12 @@ export const download = {
*
* @param {Array.<Object>|Object} songs
*/
fromSongs(songs) {
songs = [].concat(songs);
const ids = map(songs, 'id');
const params = $.param({ songs: ids });
fromSongs (songs) {
songs = [].concat(songs)
const ids = map(songs, 'id')
const params = $.param({ songs: ids })
return this.trigger(`songs?${params}`);
return this.trigger(`songs?${params}`)
},
/**
@ -23,8 +23,8 @@ export const download = {
*
* @param {Object} album
*/
fromAlbum(album) {
return this.trigger(`album/${album.id}`);
fromAlbum (album) {
return this.trigger(`album/${album.id}`)
},
/**
@ -32,11 +32,11 @@ export const download = {
*
* @param {Object} artist
*/
fromArtist(artist) {
fromArtist (artist) {
// It's safe to assume an artist always has songs.
// After all, what's an artist without her songs?
// (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}`)
},
/**
@ -44,25 +44,25 @@ export const download = {
*
* @param {Object} playlist
*/
fromPlaylist(playlist) {
fromPlaylist (playlist) {
if (!playlistStore.getSongs(playlist).length) {
console.warn('Empty playlist.');
return;
console.warn('Empty playlist.')
return
}
return this.trigger(`playlist/${playlist.id}`);
return this.trigger(`playlist/${playlist.id}`)
},
/**
* Download all favorite songs.
*/
fromFavorites() {
fromFavorites () {
if (!favoriteStore.all.length) {
console.warn("You don't like any song? Come on, don't be that grumpy.");
return;
console.warn("You don't like any song? Come on, don't be that grumpy.")
return
}
return this.trigger('favorites');
return this.trigger('favorites')
},
/**
@ -71,10 +71,10 @@ export const download = {
* @param {string} uri The uri segment, corresponding to the song(s),
* artist, playlist, or album.
*/
trigger(uri) {
const sep = uri.indexOf('?') === -1 ? '?' : '&';
const frameId = `downloader${Date.now()}`;
$(`<iframe id="${frameId}" style="display:none"></iframe`).appendTo('body');
document.getElementById(frameId).src = `/api/download/${uri}${sep}jwt-token=${ls.get('jwt-token')}`;
},
trigger (uri) {
const sep = uri.indexOf('?') === -1 ? '?' : '&'
const frameId = `downloader${Date.now()}`
$(`<iframe id="${frameId}" style="display:none"></iframe`).appendTo('body')
document.getElementById(frameId).src = `/api/download/${uri}${sep}jwt-token=${ls.get('jwt-token')}`
}
}

View file

@ -1,64 +1,64 @@
import $ from 'jquery';
import NProgress from 'nprogress';
import $ from 'jquery'
import NProgress from 'nprogress'
import { event } from '../utils';
import { ls } from '../services';
import { event } from '../utils'
import { ls } from '../services'
/**
* Responsible for all HTTP requests.
*/
export const http = {
request(method, url, data, successCb = null, errorCb = null) {
request (method, url, data, successCb = null, errorCb = null) {
return $.ajax({
data,
dataType: 'json',
url: `/api/${url}`,
method: method.toUpperCase(),
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) {
return this.request('get', url, {}, successCb, errorCb);
get (url, successCb = null, errorCb = null) {
return this.request('get', url, {}, successCb, errorCb)
},
post(url, data, successCb = null, errorCb = null) {
return this.request('post', url, data, successCb, errorCb);
post (url, data, successCb = null, errorCb = null) {
return this.request('post', url, data, successCb, errorCb)
},
put(url, data, successCb = null, errorCb = null) {
return this.request('put', url, data, successCb, errorCb);
put (url, data, successCb = null, errorCb = null) {
return this.request('put', url, data, successCb, errorCb)
},
delete(url, data = {}, successCb = null, errorCb = null) {
return this.request('delete', url, data, successCb, errorCb);
delete (url, data = {}, successCb = null, errorCb = null) {
return this.request('delete', url, data, successCb, errorCb)
},
/**
* Init the service.
*/
init() {
init () {
$(document).ajaxComplete((e, r, settings) => {
NProgress.done();
NProgress.done()
if (r.status === 400 || r.status === 401) {
if (!(settings.method === 'POST' && /\/api\/me\/?$/.test(settings.url))) {
// This is not a failed login. Log out then.
event.emit('logout');
return;
event.emit('logout')
return
}
}
const token = r.getResponseHeader('Authorization');
const token = r.getResponseHeader('Authorization')
if (token) {
ls.set('jwt-token', token);
ls.set('jwt-token', token)
}
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 './download';
export * from './http';
export * from './ls';
export * from './playback';
export * from './youtube';
export * from './info'
export * from './download'
export * from './http'
export * from './ls'
export * from './playback'
export * from './youtube'

View file

@ -1,7 +1,7 @@
import { each } from 'lodash';
import { each } from 'lodash'
import { secondsToHis } from '../../utils';
import { http } from '..';
import { secondsToHis } from '../../utils'
import { http } from '..'
export const albumInfo = {
/**
@ -9,18 +9,18 @@ export const albumInfo = {
*
* @param {Object} album
*/
fetch(album) {
fetch (album) {
return new Promise((resolve, reject) => {
if (album.info) {
resolve(album);
return;
resolve(album)
return
}
http.get(`album/${album.id}/info`, data => {
data && this.merge(album, data);
resolve(album);
}, r => reject(r));
});
data && this.merge(album, data)
resolve(album)
}, r => reject(r))
})
},
/**
@ -29,20 +29,22 @@ export const albumInfo = {
* @param {Object} album
* @param {Object} info
*/
merge(album, info) {
merge (album, info) {
// 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 (typeof info.image !== 'string') {
info.image = null;
info.image = null
}
// Set the album cover on the client side to the retrieved image from server.
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 = {
/**
@ -6,18 +6,18 @@ export const artistInfo = {
*
* @param {Object} artist
*/
fetch(artist) {
fetch (artist) {
return new Promise((resolve, reject) => {
if (artist.info) {
resolve(artist);
return;
resolve(artist)
return
}
http.get(`artist/${artist.id}/info`, data => {
data && this.merge(artist, data);
resolve(artist);
}, r => reject(r));
});
data && this.merge(artist, data)
resolve(artist)
}, r => reject(r))
})
},
/**
@ -26,17 +26,17 @@ export const artistInfo = {
* @param {Object} artist
* @param {Object} info
*/
merge(artist, info) {
merge (artist, info) {
// If the artist image is not in a nice form, discard.
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.
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 './artist';
export * from './song';
export * from './album'
export * from './artist'
export * from './song'

View file

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

View file

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

View file

@ -1,12 +1,12 @@
import { shuffle, orderBy } from 'lodash';
import $ from 'jquery';
import plyr from 'plyr';
import Vue from 'vue';
import { shuffle, orderBy } from 'lodash'
import $ from 'jquery'
import plyr from 'plyr'
import Vue from 'vue'
import { event } from '../utils';
import { queueStore, sharedStore, userStore, songStore, artistStore, preferenceStore as preferences } from '../stores';
import config from '../config';
import router from '../router';
import { event } from '../utils'
import { queueStore, sharedStore, userStore, songStore, preferenceStore as preferences } from '../stores'
import config from '../config'
import router from '../router'
export const playback = {
player: null,
@ -17,63 +17,63 @@ export const playback = {
/**
* Initialize the playback service for this whole Koel app.
*/
init() {
init () {
// We don't need to init this service twice, or the media events will be duplicated.
if (this.initialized) {
return;
return
}
this.player = plyr.setup({
controls: [],
})[0];
controls: []
})[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.
*/
document.querySelector('.plyr').addEventListener('error', e => {
this.playNext();
}, true);
this.playNext()
}, true)
/**
* Listen to 'ended' event on the audio player and play the next song in the queue.
*/
document.querySelector('.plyr').addEventListener('ended', e => {
if (sharedStore.state.useLastfm && userStore.current.preferences.lastfm_session_key) {
songStore.scrobble(queueStore.current);
songStore.scrobble(queueStore.current)
}
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.
*/
document.querySelector('.plyr').addEventListener('timeupdate', e => {
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.
const nextSong = queueStore.next;
const nextSong = queueStore.next
if (!nextSong || nextSong.preloaded) {
return;
return
}
const $preloader = $('<audio>');
$preloader.attr('src', songStore.getSourceUrl(nextSong));
const $preloader = $('<audio>')
$preloader.attr('src', songStore.getSourceUrl(nextSong))
nextSong.preloaded = true;
});
nextSong.preloaded = true
})
/**
* Listen to 'input' event on the volume range control.
@ -81,16 +81,16 @@ export const playback = {
* update the volume on the plyr object.
*/
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.
this.setVolume(preferences.volume);
this.setVolume(preferences.volume)
// 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
},
/**
@ -103,66 +103,66 @@ export const playback = {
*
* @param {Object} song The song to play
*/
play(song) {
play (song) {
if (!song) {
return;
return
}
if (queueStore.current) {
queueStore.current.playbackState = 'stopped';
queueStore.current.playbackState = 'stopped'
}
song.playbackState = 'playing';
song.playbackState = 'playing'
// Set the song as the current song
queueStore.current = song;
queueStore.current = song
// 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
// 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}`);
$('.plyr audio').attr('title', `${song.artist.name} - ${song.title}`);
$('title').text(`${song.title}${config.appTitle}`)
$('.plyr audio').attr('title', `${song.artist.name} - ${song.title}`)
// We'll just "restart" playing the song, which will handle notification, scrobbling etc.
this.restart();
this.restart()
},
/**
* Restart playing a song.
*/
restart() {
const song = queueStore.current;
restart () {
const song = queueStore.current
// 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.play();
this.player.restart()
this.player.play()
// Register the play to the server
songStore.registerPlay(song);
songStore.registerPlay(song)
// Show the notification if we're allowed to
if (!window.Notification || !preferences.notify) {
return;
return
}
try {
const notification = new Notification(`${song.title}`, {
const notif = new window.Notification(`${song.title}`, {
icon: song.album.cover,
body: `${song.album.name} ${song.artist.name}`
});
})
notification.onclick = () => window.focus();
notif.onclick = () => window.focus()
// Close the notif after 5 secs.
window.setTimeout(() => notification.close(), 5000);
window.setTimeout(() => notif.close(), 5000)
} catch (e) {
// Notification fails.
// @link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification
@ -175,15 +175,15 @@ export const playback = {
*
* @return {Object} The song
*/
get next() {
const next = queueStore.next;
get next () {
const next = queueStore.next
if (next) {
return next;
return next
}
if (preferences.repeatMode === 'REPEAT_ALL') {
return queueStore.first;
return queueStore.first
}
},
@ -193,15 +193,15 @@ export const playback = {
*
* @return {Object} The song
*/
get previous() {
const prev = queueStore.previous;
get previous () {
const prev = queueStore.previous
if (prev) {
return prev;
return prev
}
if (preferences.repeatMode === 'REPEAT_ALL') {
return queueStore.last;
return queueStore.last
}
},
@ -209,55 +209,55 @@ export const playback = {
* Circle through the repeat mode.
* The selected mode will be stored into local storage as well.
*/
changeRepeatMode() {
let idx = this.repeatModes.indexOf(preferences.repeatMode) + 1;
changeRepeatMode () {
let idx = this.repeatModes.indexOf(preferences.repeatMode) + 1
if (idx >= this.repeatModes.length) {
idx = 0;
idx = 0
}
preferences.repeatMode = this.repeatModes[idx];
preferences.repeatMode = this.repeatModes[idx]
},
/**
* Play the prev song in the queue, if one is found.
* If the prev song is not found and the current mode is NO_REPEAT, we stop completely.
*/
playPrev() {
playPrev () {
// If the song's duration is greater than 5 seconds and we've passed 5 seconds into it,
// restart playing instead.
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') {
this.stop();
this.stop()
return;
return
}
this.play(prev);
this.play(prev)
},
/**
* Play the next song in the queue, if one is found.
* If the next song is not found and the current mode is NO_REPEAT, we stop completely.
*/
playNext() {
const next = this.next;
playNext () {
const next = this.next
if (!next && preferences.repeatMode === 'NO_REPEAT') {
// Nothing lasts forever, even cold November rain.
this.stop();
this.stop()
return;
return
}
this.play(next);
this.play(next)
},
/**
@ -266,63 +266,63 @@ export const playback = {
* @param {Number} volume 0-10
* @param {Boolean=true} persist Whether the volume should be saved into local storage
*/
setVolume(volume, persist = true) {
this.player.setVolume(volume);
setVolume (volume, persist = true) {
this.player.setVolume(volume)
if (persist) {
preferences.volume = volume;
preferences.volume = volume
}
this.$volumeInput.val(volume);
this.$volumeInput.val(volume)
},
/**
* Mute playback.
*/
mute() {
this.setVolume(0, false);
mute () {
this.setVolume(0, false)
},
/**
* Unmute playback.
*/
unmute() {
unmute () {
// If the saved volume is 0, we unmute to the default level (7).
if (preferences.volume === '0' || preferences.volume === 0) {
preferences.volume = 7;
preferences.volume = 7
}
this.setVolume(preferences.volume);
this.setVolume(preferences.volume)
},
/**
* Completely stop playback.
*/
stop() {
$('title').text(config.appTitle);
this.player.pause();
this.player.seek(0);
stop () {
$('title').text(config.appTitle)
this.player.pause()
this.player.seek(0)
if (queueStore.current) {
queueStore.current.playbackState = 'stopped';
queueStore.current.playbackState = 'stopped'
}
},
/**
* Pause playback.
*/
pause() {
this.player.pause();
queueStore.current.playbackState = 'paused';
pause () {
this.player.pause()
queueStore.current.playbackState = 'paused'
},
/**
* Resume playback.
*/
resume() {
this.player.play();
queueStore.current.playbackState = 'playing';
event.emit('song:played', queueStore.current);
resume () {
this.player.play()
queueStore.current.playbackState = 'playing'
event.emit('song:played', queueStore.current)
},
/**
@ -331,41 +331,41 @@ export const playback = {
* @param {?Array.<Object>} songs An array of song objects. Defaults to all songs if null.
* @param {Boolean=false} shuffled Whether to shuffle the songs before playing.
*/
queueAndPlay(songs = null, shuffled = false) {
queueAndPlay (songs = null, shuffled = false) {
if (!songs) {
songs = songStore.all;
songs = songStore.all
}
if (!songs.length) {
return;
return
}
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
// and then play the first song in the queue.
Vue.nextTick(() => {
router.go('queue');
this.play(queueStore.first);
});
router.go('queue')
this.play(queueStore.first)
})
},
/**
* Play the first song in the queue.
* If the current queue is empty, try creating it by shuffling all songs.
*/
playFirstInQueue() {
playFirstInQueue () {
if (!queueStore.all.length) {
this.queueAndPlay();
this.queueAndPlay()
return;
return
}
this.play(queueStore.first);
this.play(queueStore.first)
},
/**
@ -374,8 +374,8 @@ export const playback = {
* @param {Object} artist The artist object
* @param {Boolean=true} shuffled Whether to shuffle the songs
*/
playAllByArtist(artist, shuffled = true) {
this.queueAndPlay(artist.songs, shuffled);
playAllByArtist (artist, shuffled = true) {
this.queueAndPlay(artist.songs, shuffled)
},
/**
@ -384,13 +384,12 @@ export const playback = {
* @param {Object} album The album object
* @param {Boolean=true} shuffled Whether to shuffle the songs
*/
playAllInAlbum(album, shuffled = true) {
playAllInAlbum (album, shuffled = true) {
if (!shuffled) {
this.queueAndPlay(orderBy(album.songs, 'track'));
return;
this.queueAndPlay(orderBy(album.songs, 'track'))
return
}
this.queueAndPlay(album.songs, true);
},
};
this.queueAndPlay(album.songs, true)
}
}

View file

@ -1,7 +1,6 @@
import { http, playback } from '.';
import { assign } from 'lodash';
import { event, loadMainView } from '../utils';
import router from '../router';
import { http } from '.'
import { event } from '../utils'
import router from '../router'
export const youtube = {
/**
@ -10,17 +9,17 @@ export const youtube = {
* @param {Object} song
* @param {Function} cb
*/
searchVideosRelatedToSong(song, cb = null) {
searchVideosRelatedToSong (song, cb = null) {
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 => {
song.youtube.nextPageToken = data.nextPageToken;
song.youtube.items.push(...data.items);
cb && cb();
});
song.youtube.nextPageToken = data.nextPageToken
song.youtube.items.push(...data.items)
cb && cb()
})
},
/**
@ -28,8 +27,8 @@ export const youtube = {
*
* @param {string} id The video ID
*/
play(id) {
event.emit('youtube:play', id);
router.go('youtube');
},
};
play (id) {
event.emit('youtube:play', id)
router.go('youtube')
}
}

View file

@ -1,15 +1,15 @@
import Vue from 'vue';
import { reduce, each, find, union, difference, take, filter, orderBy } from 'lodash';
import Vue from 'vue'
import { reduce, each, find, union, difference, take, filter, orderBy } from 'lodash'
import { secondsToHis } from '../utils';
import stub from '../stubs/album';
import { songStore, artistStore } from '.';
import { secondsToHis } from '../utils'
import stub from '../stubs/album'
import { songStore, artistStore } from '.'
export const albumStore = {
stub,
state: {
albums: [stub],
albums: [stub]
},
/**
@ -17,29 +17,29 @@ export const albumStore = {
*
* @param {Array.<Object>} artists The array of artists to extract album data from.
*/
init(artists) {
init (artists) {
// Traverse through the artists array and add their albums into our master album list.
this.all = reduce(artists, (albums, artist) => {
// While we're doing so, for each album, we get its length
// and keep a back reference to the artist too.
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.
songStore.init(this.all);
songStore.init(this.all)
},
setupAlbum(album, artist) {
Vue.set(album, 'playCount', 0);
Vue.set(album, 'artist', artist);
Vue.set(album, 'info', null);
this.getLength(album);
setupAlbum (album, artist) {
Vue.set(album, 'playCount', 0)
Vue.set(album, 'artist', artist)
Vue.set(album, 'info', null)
this.getLength(album)
return album;
return album
},
/**
@ -47,8 +47,8 @@ export const albumStore = {
*
* @return {Array.<Object>}
*/
get all() {
return this.state.albums;
get all () {
return this.state.albums
},
/**
@ -56,12 +56,12 @@ export const albumStore = {
*
* @param {Array.<Object>} value
*/
set all(value) {
this.state.albums = value;
set all (value) {
this.state.albums = value
},
byId(id) {
return find(this.all, { id });
byId (id) {
return find(this.all, { id })
},
/**
@ -72,11 +72,11 @@ export const albumStore = {
*
* @return {String} The H:i:s format of the album length.
*/
getLength(album) {
Vue.set(album, 'length', reduce(album.songs, (length, song) => length + song.length, 0));
Vue.set(album, 'fmtLength', secondsToHis(album.length));
getLength (album) {
Vue.set(album, 'length', reduce(album.songs, (length, song) => length + song.length, 0))
Vue.set(album, 'fmtLength', secondsToHis(album.length))
return album.fmtLength;
return album.fmtLength
},
/**
@ -84,14 +84,14 @@ export const albumStore = {
*
* @param {Array.<Object>|Object} albums
*/
add(albums) {
albums = [].concat(albums);
add (albums) {
albums = [].concat(albums)
each(albums, a => {
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)
},
/**
@ -100,18 +100,18 @@ export const albumStore = {
* @param {Object} album
* @param {Array.<Object>|Object} song
*/
addSongsIntoAlbum(album, songs) {
songs = [].concat(songs);
addSongsIntoAlbum (album, songs) {
songs = [].concat(songs)
album.songs = union(album.songs ? album.songs : [], songs);
album.songs = union(album.songs ? album.songs : [], songs)
each(songs, song => {
song.album_id = album.id;
song.album = album;
});
song.album_id = album.id
song.album = album
})
album.playCount = reduce(album.songs, (count, song) => count + song.playCount, 0);
this.getLength(album);
album.playCount = reduce(album.songs, (count, song) => count + song.playCount, 0)
this.getLength(album)
},
/**
@ -120,10 +120,10 @@ export const albumStore = {
* @param {Object} album
* @param {Array.<Object>|Object} songs
*/
removeSongsFromAlbum(album, songs) {
album.songs = difference(album.songs, [].concat(songs));
album.playCount = reduce(album.songs, (count, song) => count + song.playCount, 0);
this.getLength(album);
removeSongsFromAlbum (album, songs) {
album.songs = difference(album.songs, [].concat(songs))
album.playCount = reduce(album.songs, (count, song) => count + song.playCount, 0)
this.getLength(album)
},
/**
@ -133,8 +133,8 @@ export const albumStore = {
*
* @return {boolean}
*/
isAlbumEmpty(album) {
return !album.songs.length;
isAlbumEmpty (album) {
return !album.songs.length
},
/**
@ -142,14 +142,14 @@ export const albumStore = {
*
* @param {Array.<Object>|Object} albums
*/
remove(albums) {
albums = [].concat(albums);
this.all = difference(this.all, albums);
remove (albums) {
albums = [].concat(albums)
this.all = difference(this.all, albums)
// Remove from the artist as well
each(albums, album => {
artistStore.removeAlbumsFromArtist(album.artist, album);
});
artistStore.removeAlbumsFromArtist(album.artist, album)
})
},
/**
@ -159,13 +159,13 @@ export const albumStore = {
*
* @return {Array.<Object>}
*/
getMostPlayed(n = 6) {
getMostPlayed (n = 6) {
// Only non-unknown albums with actually play count are applicable.
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)
},
/**
@ -175,9 +175,9 @@ export const albumStore = {
*
* @return {Array.<Object>}
*/
getRecentlyAdded(n = 6) {
const applicable = filter(this.all, album => album.id !== 1);
getRecentlyAdded (n = 6) {
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 { reduce, each, find, union, difference, take, filter, orderBy } from 'lodash';
import Vue from 'vue'
import { reduce, each, find, union, difference, take, filter, orderBy } from 'lodash'
import config from '../config';
import stub from '../stubs/artist';
import { albumStore } from '.';
import config from '../config'
import stub from '../stubs/artist'
import { albumStore } from '.'
const UNKNOWN_ARTIST_ID = 1;
const VARIOUS_ARTISTS_ID = 2;
const UNKNOWN_ARTIST_ID = 1
const VARIOUS_ARTISTS_ID = 2
export const artistStore = {
stub,
state: {
artists: [],
artists: []
},
/**
@ -20,15 +20,15 @@ export const artistStore = {
*
* @param {Array.<Object>} artists The array of artists we got from the server.
*/
init(artists) {
this.all = artists;
init (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.
each(this.all, artist => {
this.setupArtist(artist);
});
this.setupArtist(artist)
})
},
/**
@ -36,9 +36,9 @@ export const artistStore = {
*
* @param {Object} artist
*/
setupArtist(artist) {
this.getImage(artist);
Vue.set(artist, 'playCount', 0);
setupArtist (artist) {
this.getImage(artist)
Vue.set(artist, 'playCount', 0)
// 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.
@ -46,18 +46,18 @@ export const artistStore = {
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 (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.
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
},
/**
@ -65,8 +65,8 @@ export const artistStore = {
*
* @return {Array.<Object>}
*/
get all() {
return this.state.artists;
get all () {
return this.state.artists
},
/**
@ -74,8 +74,8 @@ export const artistStore = {
*
* @param {Array.<Object>} value
*/
set all(value) {
this.state.artists = value;
set all (value) {
this.state.artists = value
},
/**
@ -83,8 +83,8 @@ export const artistStore = {
*
* @param {Number} id
*/
byId(id) {
return find(this.all, { id });
byId (id) {
return find(this.all, { id })
},
/**
@ -92,11 +92,11 @@ export const artistStore = {
*
* @param {Array.<Object>|Object} artists
*/
add(artists) {
artists = [].concat(artists);
each(artists, a => this.setupArtist(a));
add (artists) {
artists = [].concat(artists)
each(artists, a => this.setupArtist(a))
this.all = union(this.all, artists);
this.all = union(this.all, artists)
},
/**
@ -104,8 +104,8 @@ export const artistStore = {
*
* @param {Array.<Object>|Object} artists
*/
remove(artists) {
this.all = difference(this.all, [].concat(artists));
remove (artists) {
this.all = difference(this.all, [].concat(artists))
},
/**
@ -115,16 +115,16 @@ export const artistStore = {
* @param {Array.<Object>|Object} albums
*
*/
addAlbumsIntoArtist(artist, albums) {
albums = [].concat(albums);
addAlbumsIntoArtist (artist, albums) {
albums = [].concat(albums)
artist.albums = union(artist.albums ? artist.albums : [], albums);
artist.albums = union(artist.albums ? artist.albums : [], albums)
each(albums, album => {
album.artist_id = artist.id;
album.artist = artist;
artist.playCount += album.playCount;
});
album.artist_id = artist.id
album.artist = artist
artist.playCount += album.playCount
})
},
/**
@ -133,10 +133,12 @@ export const artistStore = {
* @param {Object} artist
* @param {Array.<Object>|Object} albums
*/
removeAlbumsFromArtist(artist, albums) {
albums = [].concat(albums);
artist.albums = difference(artist.albums, albums);
each(albums, album => artist.playCount -= album.playCount);
removeAlbumsFromArtist (artist, albums) {
albums = [].concat(albums)
artist.albums = difference(artist.albums, albums)
each(albums, album => {
artist.playCount -= album.playCount
})
},
/**
@ -146,8 +148,8 @@ export const artistStore = {
*
* @return {boolean}
*/
isArtistEmpty(artist) {
return !artist.albums.length;
isArtistEmpty (artist) {
return !artist.albums.length
},
/**
@ -157,8 +159,8 @@ export const artistStore = {
*
* @return {Boolean}
*/
isVariousArtists(artist) {
return artist.id === VARIOUS_ARTISTS_ID;
isVariousArtists (artist) {
return artist.id === VARIOUS_ARTISTS_ID
},
/**
@ -168,8 +170,8 @@ export const artistStore = {
*
* @return {Boolean}
*/
isUnknownArtist(artist) {
return artist.id === UNKNOWN_ARTIST_ID;
isUnknownArtist (artist) {
return artist.id === UNKNOWN_ARTIST_ID
},
/**
@ -179,8 +181,8 @@ export const artistStore = {
*
* @return {Array.<Object>}
*/
getSongsByArtist(artist) {
return artist.songs;
getSongsByArtist (artist) {
return artist.songs
},
/**
@ -190,23 +192,23 @@ export const artistStore = {
*
* @return {String}
*/
getImage(artist) {
getImage (artist) {
if (!artist.image) {
// Try to get an image from one of the albums.
artist.image = config.unknownCover;
artist.image = config.unknownCover
artist.albums.every(album => {
// If there's a "real" cover, use it.
if (album.image !== config.unknownCover) {
artist.image = album.cover;
artist.image = album.cover
// I want to break free.
return false;
return false
}
});
})
}
return artist.image;
return artist.image
},
/**
@ -216,15 +218,15 @@ export const artistStore = {
*
* @return {Array.<Object>}
*/
getMostPlayed(n = 6) {
getMostPlayed (n = 6) {
// Only non-unknown artists with actually play count are applicable.
// Also, "Various Artists" doesn't count.
const applicable = filter(this.all, artist => {
return artist.playCount
&& !this.isUnknownArtist(artist)
&& !this.isVariousArtists(artist);
});
return artist.playCount &&
!this.isUnknownArtist(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 = {
presets: [
@ -8,25 +8,25 @@ export const equalizerStore = {
id: 0,
name: 'Default',
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,
name: 'Classical',
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,
name: 'Club',
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,
name: 'Dance',
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,
@ -50,48 +50,48 @@ export const equalizerStore = {
id: 7,
name: 'Large Hall',
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,
name: 'Live',
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,
name: 'Pop',
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,
name: 'Reggae',
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,
name: 'Rock',
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,
name: 'Soft Rock',
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,
name: 'Techno',
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) {
return find(this.presets, { id });
getPresetById (id) {
return find(this.presets, { id })
},
/**
@ -99,9 +99,9 @@ export const equalizerStore = {
*
* @return {Object}
*/
get() {
get () {
if (!this.presets[preferenceStore.selectedPreset]) {
return preferenceStore.equalizer;
return preferenceStore.equalizer
}
// If the user chose a preset (instead of customizing one), just return it.
@ -114,7 +114,7 @@ export const equalizerStore = {
* @param {Number} preamp The preamp value (dB)
* @param {Array.<Number>} gains The band's gain value (dB)
*/
set(preamp, gains) {
preferenceStore.equalizer = { preamp, gains };
},
};
set (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 = {
state: {
songs: [],
length: 0,
fmtLength: '',
fmtLength: ''
},
/**
@ -14,8 +14,8 @@ export const favoriteStore = {
*
* @return {Array.<Object>}
*/
get all() {
return this.state.songs;
get all () {
return this.state.songs
},
/**
@ -23,8 +23,8 @@ export const favoriteStore = {
*
* @param {Array.<Object>} value
*/
set all(value) {
this.state.songs = value;
set all (value) {
this.state.songs = value
},
/**
@ -33,15 +33,15 @@ export const favoriteStore = {
*
* @param {Object} song
*/
toggleOne(song) {
toggleOne (song) {
// 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?
song.liked = !song.liked;
song.liked ? this.add(song) : this.remove(song);
song.liked = !song.liked
song.liked ? this.add(song) : this.remove(song)
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))
})
},
/**
@ -49,8 +49,8 @@ export const favoriteStore = {
*
* @param {Array.<Object>|Object} songs
*/
add(songs) {
this.all = union(this.all, [].concat(songs));
add (songs) {
this.all = union(this.all, [].concat(songs))
},
/**
@ -58,15 +58,15 @@ export const favoriteStore = {
*
* @param {Array.<Object>|Object} songs
*/
remove(songs) {
this.all = difference(this.all, [].concat(songs));
remove (songs) {
this.all = difference(this.all, [].concat(songs))
},
/**
* Remove all favorites.
*/
clear() {
this.all = [];
clear () {
this.all = []
},
/**
@ -74,15 +74,17 @@ export const favoriteStore = {
*
* @param {Array.<Object>} songs
*/
like(songs) {
like (songs) {
// 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?
each(songs, song => song.liked = true);
this.add(songs);
each(songs, song => {
song.liked = true
})
this.add(songs)
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))
})
},
/**
@ -90,12 +92,14 @@ export const favoriteStore = {
*
* @param {Array.<Object>} songs
*/
unlike(songs) {
each(songs, song => song.liked = false);
this.remove(songs);
unlike (songs) {
each(songs, song => {
song.liked = false
})
this.remove(songs)
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 './artist';
export * from './equalizer';
export * from './favorite';
export * from './playlist';
export * from './preference';
export * from './queue';
export * from './setting';
export * from './shared';
export * from './song';
export * from './user';
export * from './album'
export * from './artist'
export * from './equalizer'
export * from './favorite'
export * from './playlist'
export * from './preference'
export * from './queue'
export * from './setting'
export * from './shared'
export * from './song'
export * from './user'

View file

@ -1,20 +1,20 @@
import { each, find, map, difference, union, without } from 'lodash';
import NProgress from 'nprogress';
import { each, find, map, difference, union } from 'lodash'
import NProgress from 'nprogress'
import stub from '../stubs/playlist';
import { http } from '../services';
import { songStore } from '.';
import stub from '../stubs/playlist'
import { http } from '../services'
import { songStore } from '.'
export const playlistStore = {
stub,
state: {
playlists: [],
playlists: []
},
init(playlists) {
this.all = playlists;
each(this.all, this.objectifySongs);
init (playlists) {
this.all = playlists
each(this.all, this.objectifySongs)
},
/**
@ -22,8 +22,8 @@ export const playlistStore = {
*
* @return {Array.<Object>}
*/
get all() {
return this.state.playlists;
get all () {
return this.state.playlists
},
/**
@ -31,8 +31,8 @@ export const playlistStore = {
*
* @param {Array.<Object>} value
*/
set all(value) {
this.state.playlists = value;
set all (value) {
this.state.playlists = value
},
/**
@ -42,8 +42,8 @@ export const playlistStore = {
*
* @return {Object}
*/
byId(id) {
return find(this.all, { id });
byId (id) {
return find(this.all, { id })
},
/**
@ -52,8 +52,8 @@ export const playlistStore = {
*
* @param {Object} playlist
*/
objectifySongs(playlist) {
playlist.songs = songStore.byIds(playlist.songs);
objectifySongs (playlist) {
playlist.songs = songStore.byIds(playlist.songs)
},
/**
@ -63,8 +63,8 @@ export const playlistStore = {
*
* return {Array.<Object>}
*/
getSongs(playlist) {
return playlist.songs;
getSongs (playlist) {
return playlist.songs
},
/**
@ -72,8 +72,8 @@ export const playlistStore = {
*
* @param {Array.<Object>|Object} playlists
*/
add(playlists) {
this.all = union(this.all, [].concat(playlists));
add (playlists) {
this.all = union(this.all, [].concat(playlists))
},
/**
@ -81,8 +81,8 @@ export const playlistStore = {
*
* @param {Array.<Object>|Object} playlist
*/
remove(playlists) {
this.all = difference(this.all, [].concat(playlists));
remove (playlists) {
this.all = difference(this.all, [].concat(playlists))
},
/**
@ -91,22 +91,22 @@ export const playlistStore = {
* @param {String} name Name of the playlist
* @param {Array.<Object>} songs An array of song objects
*/
store(name, songs = []) {
store (name, songs = []) {
if (songs.length) {
// Extract the IDs from the song objects.
songs = map(songs, 'id');
songs = map(songs, 'id')
}
NProgress.start();
NProgress.start()
return new Promise((resolve, reject) => {
http.post('playlist', { name, songs }, playlist => {
playlist.songs = songs;
this.objectifySongs(playlist);
this.add(playlist);
resolve(playlist);
}, r => reject(r));
});
playlist.songs = songs
this.objectifySongs(playlist)
this.add(playlist)
resolve(playlist)
}, r => reject(r))
})
},
/**
@ -114,15 +114,15 @@ export const playlistStore = {
*
* @param {Object} playlist
*/
delete(playlist) {
NProgress.start();
delete (playlist) {
NProgress.start()
return new Promise((resolve, reject) => {
http.delete(`playlist/${playlist.id}`, {}, data => {
this.remove(playlist);
resolve(data);
}, r => reject(r));
});
this.remove(playlist)
resolve(data)
}, r => reject(r))
})
},
/**
@ -131,20 +131,20 @@ export const playlistStore = {
* @param {Object} playlist
* @param {Array.<Object>} songs
*/
addSongs(playlist, songs) {
addSongs (playlist, songs) {
return new Promise((resolve, reject) => {
const count = playlist.songs.length;
playlist.songs = union(playlist.songs, songs);
const count = playlist.songs.length
playlist.songs = union(playlist.songs, songs)
if (count === playlist.songs.length) {
resolve(playlist);
return;
resolve(playlist)
return
}
http.put(`playlist/${playlist.id}/sync`, { songs: map(playlist.songs, 'id') },
data => resolve(playlist),
r => reject(r)
);
)
})
},
@ -154,14 +154,14 @@ export const playlistStore = {
* @param {Object} playlist
* @param {Array.<Object>} songs
*/
removeSongs(playlist, songs) {
playlist.songs = difference(playlist.songs, songs);
removeSongs (playlist, songs) {
playlist.songs = difference(playlist.songs, songs)
return new Promise((resolve, reject) => {
http.put(`playlist/${playlist.id}/sync`, { songs: map(playlist.songs, 'id') },
data => resolve(playlist),
r => reject(r)
);
)
})
},
@ -170,11 +170,11 @@ export const playlistStore = {
*
* @param {Object} playlist
*/
update(playlist) {
NProgress.start();
update (playlist) {
NProgress.start()
return new Promise((resolve, reject) => {
http.put(`playlist/${playlist.id}`, { name: playlist.name }, data => resolve(playlist), r => reject(r));
});
},
};
http.put(`playlist/${playlist.id}`, { name: playlist.name }, 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 { ls } from '../services';
import { userStore } from '.'
import { ls } from '../services'
export const preferenceStore = {
storeKey: '',
@ -14,11 +14,11 @@ export const preferenceStore = {
confirmClosing: false,
equalizer: {
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,
albumsViewMode: null,
selectedPreset: -1,
selectedPreset: -1
},
/**
@ -26,42 +26,42 @@ export const preferenceStore = {
*
* @param {Object} user The user whose preferences we are managing.
*/
init(user = null) {
init (user = null) {
if (!user) {
user = userStore.current;
user = userStore.current
}
this.storeKey = `preferences_${user.id}`;
extend(this.state, ls.get(this.storeKey, this.state));
this.setupProxy();
this.storeKey = `preferences_${user.id}`
extend(this.state, ls.get(this.storeKey, this.state))
this.setupProxy()
},
/**
* Proxy the state properties, so that each can be directly accessed using the key.
*/
setupProxy() {
setupProxy () {
each(Object.keys(this.state), key => {
Object.defineProperty(this, key, {
get: () => this.state[key],
set: (value) => {
this.state[key] = value;
this.save();
this.state[key] = value
this.save()
},
configurable: true,
});
});
configurable: true
})
})
},
set(key, val) {
this.state[key] = val;
this.save();
set (key, val) {
this.state[key] = val
this.save()
},
get(key) {
return has(this.state, key) ? this.state[key] : null;
get (key) {
return has(this.state, key) ? this.state[key] : null
},
save() {
ls.set(this.storeKey, this.state);
},
};
save () {
ls.set(this.storeKey, this.state)
}
}

View file

@ -1,12 +1,12 @@
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 = {
state: {
songs: [],
current: null,
current: null
},
init() {
init () {
// We don't have anything to do here yet.
// How about another song then?
//
@ -36,8 +36,8 @@ export const queueStore = {
*
* @return {Array.<Object>}
*/
get all() {
return this.state.songs;
get all () {
return this.state.songs
},
/**
@ -45,8 +45,8 @@ export const queueStore = {
*
* @param {Array.<Object>}
*/
set all(songs) {
this.state.songs = songs;
set all (songs) {
this.state.songs = songs
},
/**
@ -54,8 +54,8 @@ export const queueStore = {
*
* @return {?Object}
*/
get first() {
return head(this.all);
get first () {
return head(this.all)
},
/**
@ -63,8 +63,8 @@ export const queueStore = {
*
* @return {?Object}
*/
get last() {
return last(this.all);
get last () {
return last(this.all)
},
/**
@ -74,8 +74,8 @@ export const queueStore = {
*
* @return {Boolean}
*/
contains(song) {
return includes(this.all, song);
contains (song) {
return includes(this.all, song)
},
/**
@ -86,13 +86,13 @@ export const queueStore = {
* @param {Boolean} replace Whether to replace the current queue
* @param {Boolean} toTop Whether to prepend or append to the queue
*/
queue(songs, replace = false, toTop = false) {
songs = [].concat(songs);
queue (songs, replace = false, toTop = false) {
songs = [].concat(songs)
if (replace) {
this.all = songs;
this.all = songs
} else {
this.all = toTop ? union(songs, this.all) : union(this.all, songs);
this.all = toTop ? union(songs, this.all) : union(this.all, songs)
}
},
@ -101,18 +101,18 @@ export const queueStore = {
*
* @param {Array.<Object>|Object} songs
*/
queueAfterCurrent(songs) {
songs = [].concat(songs);
queueAfterCurrent (songs) {
songs = [].concat(songs)
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.
this.unqueue(songs);
this.unqueue(songs)
const head = this.all.splice(0, this.indexOf(this.current) + 1);
this.all = head.concat(songs, this.all);
const head = this.all.splice(0, this.indexOf(this.current) + 1)
this.all = head.concat(songs, this.all)
},
/**
@ -120,8 +120,8 @@ export const queueStore = {
*
* @param {Object|String|Array.<Object>} songs The song(s) to unqueue
*/
unqueue(songs) {
this.all = difference(this.all, [].concat(songs));
unqueue (songs) {
this.all = difference(this.all, [].concat(songs))
},
/**
@ -130,21 +130,21 @@ export const queueStore = {
* @param {Array.<Object>} songs Songs to move
* @param {Object} target The target song object
*/
move(songs, target) {
const $targetIndex = this.indexOf(target);
move (songs, target) {
const $targetIndex = this.indexOf(target)
each(songs, song => {
this.all.splice(this.indexOf(song), 1);
this.all.splice($targetIndex, 0, song);
});
this.all.splice(this.indexOf(song), 1)
this.all.splice($targetIndex, 0, song)
})
},
/**
* Clear the current queue.
*/
clear() {
this.all = [];
this.current = null;
clear () {
this.all = []
this.current = null
},
/**
@ -154,8 +154,8 @@ export const queueStore = {
*
* @return {?Integer}
*/
indexOf(song) {
return this.all.indexOf(song);
indexOf (song) {
return this.all.indexOf(song)
},
/**
@ -163,14 +163,14 @@ export const queueStore = {
*
* @return {?Object}
*/
get next() {
get next () {
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]
},
/**
@ -178,14 +178,14 @@ export const queueStore = {
*
* @return {?Object}
*/
get previous() {
get previous () {
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]
},
/**
@ -193,8 +193,8 @@ export const queueStore = {
*
* @return {Object}
*/
get current() {
return this.state.current;
get current () {
return this.state.current
},
/**
@ -204,8 +204,9 @@ export const queueStore = {
*
* @return {Object} The queued song.
*/
set current(song) {
return this.state.current = song;
set current (song) {
this.state.current = song
return this.state.current
},
/**
@ -213,7 +214,8 @@ export const queueStore = {
*
* @return {Array.<Object>} The shuffled array of song objects
*/
shuffle() {
return this.all = _shuffle(this.all);
},
};
shuffle () {
this.all = _shuffle(this.all)
return this.all
}
}

View file

@ -1,24 +1,24 @@
import { http } from '../services';
import stub from '../stubs/settings';
import { http } from '../services'
import stub from '../stubs/settings'
export const settingStore = {
stub,
state: {
settings: [],
settings: []
},
init(settings) {
this.state.settings = settings;
init (settings) {
this.state.settings = settings
},
get all() {
return this.state.settings;
get all () {
return this.state.settings
},
update() {
update () {
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 isMobile from 'ismobilejs';
import { assign } from 'lodash'
import isMobile from 'ismobilejs'
import { http } from '../services';
import { userStore, preferenceStore, artistStore, songStore, playlistStore, queueStore, settingStore } from '.';
import { http } from '../services'
import { userStore, preferenceStore, artistStore, songStore, playlistStore, queueStore, settingStore } from '.'
export const sharedStore = {
state: {
@ -22,58 +22,58 @@ export const sharedStore = {
currentVersion: '',
latestVersion: '',
cdnUrl: '',
originalMediaPath: '',
originalMediaPath: ''
},
init() {
this.reset();
init () {
this.reset()
return new Promise((resolve, reject) => {
http.get('data', data => {
// 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.
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.state.currentUser.preferences) {
this.state.currentUser.preferences = {};
this.state.currentUser.preferences = {}
}
userStore.init(this.state.users, this.state.currentUser);
preferenceStore.init(this.state.preferences);
artistStore.init(this.state.artists); // This will init album and song stores as well.
songStore.initInteractions(this.state.interactions);
playlistStore.init(this.state.playlists);
queueStore.init();
settingStore.init(this.state.settings);
userStore.init(this.state.users, this.state.currentUser)
preferenceStore.init(this.state.preferences)
artistStore.init(this.state.artists) // This will init album and song stores as well.
songStore.initInteractions(this.state.interactions)
playlistStore.init(this.state.playlists)
queueStore.init()
settingStore.init(this.state.settings)
// 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)
}, r => reject(r));
});
}, r => reject(r))
})
},
reset() {
this.state.songs = [];
this.state.albums = [];
this.state.artists = [];
this.state.favorites = [];
this.state.queued = [];
this.state.interactions = [];
this.state.users = [];
this.state.settings = [];
this.state.currentUser = null;
this.state.playlists = [];
this.state.useLastfm = false;
this.state.allowDownload = false;
this.state.currentVersion = '';
this.state.latestVersion = '';
this.state.cdnUrl = '';
},
};
reset () {
this.state.songs = []
this.state.albums = []
this.state.artists = []
this.state.favorites = []
this.state.queued = []
this.state.interactions = []
this.state.users = []
this.state.settings = []
this.state.currentUser = null
this.state.playlists = []
this.state.useLastfm = false
this.state.allowDownload = false
this.state.currentVersion = ''
this.state.latestVersion = ''
this.state.cdnUrl = ''
}
}

View file

@ -1,10 +1,10 @@
import Vue from 'vue';
import { without, map, take, remove, orderBy, each, union } from 'lodash';
import Vue from 'vue'
import { without, map, take, remove, orderBy, each, union } from 'lodash'
import { secondsToHis } from '../utils';
import { http, ls } from '../services';
import { sharedStore, favoriteStore, userStore, albumStore, artistStore } from '.';
import stub from '../stubs/song';
import { secondsToHis } from '../utils'
import { http, ls } from '../services'
import { sharedStore, favoriteStore, albumStore, artistStore } from '.'
import stub from '../stubs/song'
export const songStore = {
stub,
@ -24,7 +24,7 @@ export const songStore = {
*
* @type {Array}
*/
recent: [],
recent: []
},
/**
@ -32,39 +32,39 @@ export const songStore = {
*
* @param {Array.<Object>} albums The array of albums to extract our songs from
*/
init(albums) {
init (albums) {
// Iterate through the albums. With each, add its songs into our master song list.
// While doing so, we populate some other information into the songs as well.
this.all = albums.reduce((songs, album) => {
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) {
song.fmtLength = secondsToHis(song.length);
setupSong (song, album) {
song.fmtLength = secondsToHis(song.length)
// Manually set these additional properties to be reactive
Vue.set(song, 'playCount', 0);
Vue.set(song, 'album', album);
Vue.set(song, 'liked', false);
Vue.set(song, 'lyrics', null);
Vue.set(song, 'playbackState', 'stopped');
Vue.set(song, 'playCount', 0)
Vue.set(song, 'album', album)
Vue.set(song, 'liked', false)
Vue.set(song, 'lyrics', null)
Vue.set(song, 'playbackState', 'stopped')
if (song.contributing_artist_id) {
const artist = artistStore.byId(song.contributing_artist_id);
artist.albums = union(artist.albums, [album]);
artistStore.setupArtist(artist);
Vue.set(song, 'artist', artist);
const artist = artistStore.byId(song.contributing_artist_id)
artist.albums = union(artist.albums, [album])
artistStore.setupArtist(artist)
Vue.set(song, 'artist', artist)
} 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
this.cache[song.id] = song;
this.cache[song.id] = song
},
/**
@ -72,25 +72,25 @@ export const songStore = {
*
* @param {Array.<Object>} interactions The array of interactions of the current user
*/
initInteractions(interactions) {
favoriteStore.clear();
initInteractions (interactions) {
favoriteStore.clear()
each(interactions, interaction => {
const song = this.byId(interaction.song_id);
const song = this.byId(interaction.song_id)
if (!song) {
return;
return
}
song.liked = interaction.liked;
song.playCount = interaction.play_count;
song.album.playCount += song.playCount;
song.artist.playCount += song.playCount;
song.liked = interaction.liked
song.playCount = interaction.play_count
song.album.playCount += song.playCount
song.artist.playCount += song.playCount
if (song.liked) {
favoriteStore.add(song);
favoriteStore.add(song)
}
});
})
},
/**
@ -101,10 +101,10 @@ export const songStore = {
*
* @return {Float|String}
*/
getLength(songs, toHis) {
const duration = songs.reduce((length, song) => length + song.length, 0);
getLength (songs, toHis) {
const duration = songs.reduce((length, song) => length + song.length, 0)
return toHis ? secondsToHis(duration) : duration;
return toHis ? secondsToHis(duration) : duration
},
/**
@ -112,8 +112,8 @@ export const songStore = {
*
* @return {Array.<Object>}
*/
get all() {
return this.state.songs;
get all () {
return this.state.songs
},
/**
@ -121,8 +121,8 @@ export const songStore = {
*
* @param {Array.<Object>} value
*/
set all(value) {
this.state.songs = value;
set all (value) {
this.state.songs = value
},
/**
@ -132,8 +132,8 @@ export const songStore = {
*
* @return {Object}
*/
byId(id) {
return this.cache[id];
byId (id) {
return this.cache[id]
},
/**
@ -143,8 +143,8 @@ export const songStore = {
*
* @return {Array.<Object>}
*/
byIds(ids) {
return ids.map(id => this.byId(id));
byIds (ids) {
return ids.map(id => this.byId(id))
},
/**
@ -152,19 +152,19 @@ export const songStore = {
*
* @param {Object} song
*/
registerPlay(song) {
registerPlay (song) {
return new Promise((resolve, reject) => {
const oldCount = song.playCount;
const oldCount = song.playCount
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.
song.playCount = data.play_count;
song.album.playCount += song.playCount - oldCount;
song.artist.playCount += song.playCount - oldCount;
song.playCount = data.play_count
song.album.playCount += song.playCount - oldCount
song.artist.playCount += song.playCount - oldCount
resolve(data);
}, r => reject(r));
});
resolve(data)
}, r => reject(r))
})
},
/**
@ -172,12 +172,12 @@ export const songStore = {
*
* @param {Object}
*/
addRecent(song) {
addRecent (song) {
// 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.
this.state.recent.unshift(song);
this.state.recent.unshift(song)
},
/**
@ -185,9 +185,9 @@ export const songStore = {
*
* @param {Object} song
*/
scrobble(song) {
scrobble (song) {
return new Promise((resolve, reject) => {
http.post(`${song.id}/scrobble/${song.playStartTime}`, {}, data => resolve(data), r => reject(r));
http.post(`${song.id}/scrobble/${song.playStartTime}`, {}, data => resolve(data), r => reject(r))
})
},
@ -197,16 +197,16 @@ export const songStore = {
* @param {Array.<Object>} songs An array of song
* @param {Object} data
*/
update(songs, data) {
update (songs, data) {
return new Promise((resolve, reject) => {
http.put('songs', {
data,
songs: map(songs, 'id'),
songs: map(songs, 'id')
}, songs => {
each(songs, song => this.syncUpdatedSong(song));
resolve(songs);
}, r => reject(r));
});
each(songs, song => this.syncUpdatedSong(song))
resolve(songs)
}, r => reject(r))
})
},
/**
@ -221,7 +221,7 @@ export const songStore = {
*
* @return {?Object} The updated song.
*/
syncUpdatedSong(updatedSong) {
syncUpdatedSong (updatedSong) {
// Cases:
// 1. Album doesn't change (and then, artist doesn't either)
// 2. Album changes (note that a new album might have been created) and
@ -229,77 +229,77 @@ export const songStore = {
// 2.b. Artist changes as well. Note that an artist might have been created.
// Find the original song,
const originalSong = this.byId(updatedSong.id);
const originalSong = this.byId(updatedSong.id)
if (!originalSong) {
return;
return
}
// and keep track of original album/artist.
const originalAlbumId = originalSong.album.id;
const originalArtistId = originalSong.artist.id;
const originalAlbumId = originalSong.album.id
const originalArtistId = originalSong.artist.id
// First, we update the title, lyrics, and track #
originalSong.title = updatedSong.title;
originalSong.lyrics = updatedSong.lyrics;
originalSong.track = updatedSong.track;
originalSong.title = updatedSong.title
originalSong.lyrics = updatedSong.lyrics
originalSong.track = updatedSong.track
if (updatedSong.album.id === originalAlbumId) { // case 1
// Nothing to do
} else { // case 2
// 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 newAlbumCreated = !existingAlbum;
const existingAlbum = albumStore.byId(updatedSong.album.id)
const newAlbumCreated = !existingAlbum
if (!newAlbumCreated) {
// The song changed to an existing album. We now add it to such album.
albumStore.addSongsIntoAlbum(existingAlbum, originalSong);
albumStore.addSongsIntoAlbum(existingAlbum, originalSong)
} else {
// A new album was created. We:
// - Add the new album into our collection
// - Add the song into it
albumStore.addSongsIntoAlbum(updatedSong.album, originalSong);
albumStore.add(updatedSong.album);
albumStore.addSongsIntoAlbum(updatedSong.album, originalSong)
albumStore.add(updatedSong.album)
}
if (updatedSong.album.artist.id === originalArtistId) { // case 2.a
// Same artist, but what if the album is new?
if (newAlbumCreated) {
artistStore.addAlbumsIntoArtist(artistStore.byId(originalArtistId), updatedSong.album);
artistStore.addAlbumsIntoArtist(artistStore.byId(originalArtistId), updatedSong.album)
}
} else { // case 2.b
// The artist changes.
const existingArtist = artistStore.byId(updatedSong.album.artist.id);
const existingArtist = artistStore.byId(updatedSong.album.artist.id)
if (existingArtist) {
originalSong.artist = existingArtist;
originalSong.artist = existingArtist
} else {
// New artist created. We:
// - Add the album into it, because now it MUST BE a new album
// (there's no "new artist with existing album" in our system).
// - Add the new artist into our collection
artistStore.addAlbumsIntoArtist(updatedSong.album.artist, updatedSong.album);
artistStore.add(updatedSong.album.artist);
originalSong.artist = updatedSong.album.artist;
artistStore.addAlbumsIntoArtist(updatedSong.album.artist, updatedSong.album)
artistStore.add(updatedSong.album.artist)
originalSong.artist = updatedSong.album.artist
}
}
// As a last step, we purify our library of empty albums/artists.
if (albumStore.isAlbumEmpty(albumStore.byId(originalAlbumId))) {
albumStore.remove(albumStore.byId(originalAlbumId));
albumStore.remove(albumStore.byId(originalAlbumId))
}
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.
originalSong.infoRetrieved = false;
originalSong.infoRetrieved = false
}
return originalSong;
return originalSong
},
/**
@ -309,8 +309,8 @@ export const songStore = {
*
* @return {string} The source URL, with JWT token appended.
*/
getSourceUrl(song) {
return `${sharedStore.state.cdnUrl}api/${song.id}/play?jwt-token=${ls.get('jwt-token')}`;
getSourceUrl (song) {
return `${sharedStore.state.cdnUrl}api/${song.id}/play?jwt-token=${ls.get('jwt-token')}`
},
/**
@ -321,8 +321,8 @@ export const songStore = {
*
* @return {string}
*/
getShareableUrl(song) {
return `${window.location.origin}/#!/song/${song.id}`;
getShareableUrl (song) {
return `${window.location.origin}/#!/song/${song.id}`
},
/**
@ -332,8 +332,8 @@ export const songStore = {
*
* @return {Array.<Object>}
*/
getRecent(n = 10) {
return take(this.state.recent, n);
getRecent (n = 10) {
return take(this.state.recent, n)
},
/**
@ -343,13 +343,13 @@ export const songStore = {
*
* @return {Array.<Object>}
*/
getMostPlayed(n = 10) {
const songs = take(orderBy(this.all, 'playCount', 'desc'), n);
getMostPlayed (n = 10) {
const songs = take(orderBy(this.all, 'playCount', 'desc'), n)
// Remove those with playCount=0
remove(songs, song => !song.playCount);
remove(songs, song => !song.playCount)
return songs;
return songs
},
/**
@ -357,15 +357,15 @@ export const songStore = {
* @param {Number} n
* @return {Array.<Object>}
*/
getRecentlyAdded(n = 10) {
return take(orderBy(this.all, 'created_at', 'desc'), n);
getRecentlyAdded (n = 10) {
return take(orderBy(this.all, 'created_at', 'desc'), n)
},
/**
* Called when the application is torn down.
* Reset stuff.
*/
teardown() {
this.state.recent = [];
},
};
teardown () {
this.state.recent = []
}
}

View file

@ -1,17 +1,17 @@
import { each, find, without } from 'lodash';
import md5 from 'blueimp-md5';
import Vue from 'vue';
import NProgress from 'nprogress';
import { each, find, without } from 'lodash'
import md5 from 'blueimp-md5'
import Vue from 'vue'
import NProgress from 'nprogress'
import { http } from '../services';
import stub from '../stubs/user';
import { http } from '../services'
import stub from '../stubs/user'
export const userStore = {
stub,
state: {
users: [],
current: stub,
current: stub
},
/**
@ -20,15 +20,15 @@ export const userStore = {
* @param {Array.<Object>} users The users in the system. Empty array if current user is not an admin.
* @param {Object} currentUser The current user.
*/
init(users, currentUser) {
this.all = users;
this.current = currentUser;
init (users, currentUser) {
this.all = users
this.current = currentUser
// Set the avatar for each of the users…
each(this.all, this.setAvatar);
each(this.all, this.setAvatar)
// …and the current user as well.
this.setAvatar();
this.setAvatar()
},
/**
@ -36,8 +36,8 @@ export const userStore = {
*
* @return {Array.<Object>}
*/
get all() {
return this.state.users;
get all () {
return this.state.users
},
/**
@ -45,8 +45,8 @@ export const userStore = {
*
* @param {Array.<Object>} value
*/
set all(value) {
this.state.users = value;
set all (value) {
this.state.users = value
},
/**
@ -56,8 +56,8 @@ export const userStore = {
*
* @return {Object}
*/
byId(id) {
return find(this.all, { id });
byId (id) {
return find(this.all, { id })
},
/**
@ -65,8 +65,8 @@ export const userStore = {
*
* @return {Object}
*/
get current() {
return this.state.current;
get current () {
return this.state.current
},
/**
@ -76,8 +76,9 @@ export const userStore = {
*
* @return {Object}
*/
set current(user) {
return this.state.current = user;
set current (user) {
this.state.current = user
return this.state.current
},
/**
@ -85,12 +86,12 @@ export const userStore = {
*
* @param {?Object} user The user. If null, the current user.
*/
setAvatar(user = null) {
setAvatar (user = null) {
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`)
},
/**
@ -99,21 +100,21 @@ export const userStore = {
* @param {String} email
* @param {String} password
*/
login(email, password) {
NProgress.start();
login (email, password) {
NProgress.start()
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))
})
},
/**
* Log the current user out.
*/
logout() {
logout () {
return new Promise((resolve, reject) => {
http.delete('me', {}, data => resolve(data), r => reject(r));
});
http.delete('me', {}, data => resolve(data), r => reject(r))
})
},
/**
@ -121,20 +122,20 @@ export const userStore = {
*
* @param {string} password Can be an empty string if the user is not changing his password.
*/
updateProfile(password) {
NProgress.start();
updateProfile (password) {
NProgress.start()
return new Promise((resolve, reject) => {
http.put('me', {
password,
name: this.current.name,
email: this.current.email
}, () => {
this.setAvatar();
resolve(this.current)
}, r => reject(r)
);
});
password,
name: this.current.name,
email: this.current.email
}, () => {
this.setAvatar()
resolve(this.current)
},
r => reject(r))
})
},
/**
@ -144,16 +145,16 @@ export const userStore = {
* @param {string} email
* @param {string} password
*/
store(name, email, password) {
NProgress.start();
store (name, email, password) {
NProgress.start()
return new Promise((resolve, reject) => {
http.post('user', { name, email, password }, user => {
this.setAvatar(user);
this.all.unshift(user);
resolve(user);
}, r => reject(r));
});
this.setAvatar(user)
this.all.unshift(user)
resolve(user)
}, r => reject(r))
})
},
/**
@ -164,16 +165,16 @@ export const userStore = {
* @param {String} email
* @param {String} password
*/
update(user, name, email, password) {
NProgress.start();
update (user, name, email, password) {
NProgress.start()
return new Promise((resolve, reject) => {
http.put(`user/${user.id}`, { name, email, password }, () => {
this.setAvatar(user);
user.password = '';
resolve(user);
}, r => reject(r));
});
this.setAvatar(user)
user.password = ''
resolve(user)
}, r => reject(r))
})
},
/**
@ -181,12 +182,12 @@ export const userStore = {
*
* @param {Object} user
*/
destroy(user) {
NProgress.start();
destroy (user) {
NProgress.start()
return new Promise((resolve, reject) => {
http.delete(`user/${user.id}`, {}, data => {
this.all = without(this.all, user);
this.all = without(this.all, user)
// Mama, just killed a man
// Put a gun against his head
@ -210,8 +211,8 @@ export const userStore = {
/**
* Brian May enters the stage.
*/
resolve(data);
}, r => reject(r));
});
},
};
resolve(data)
}, r => reject(r))
})
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,7 +3,7 @@ export default {
id: 1,
name: 'Phan An',
email: 'me@phanan.net',
is_admin: true,
is_admin: true
},
users: [
@ -11,13 +11,13 @@ export default {
id: 1,
name: 'Phan An',
email: 'me@phanan.net',
is_admin: true,
is_admin: true
},
{
id: 2,
name: 'John Doe',
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 win = doc.defaultView;
const doc = jsdom('<!doctype html><html><body></body></html>')
const win = doc.defaultView
global.document = doc;
global.window = win;
global.document = doc
global.window = win
Object.keys(window).forEach((key) => {
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 { ls } from '../../services';
import localStorage from 'local-storage'
import { ls } from '../../services'
describe('services/ls', () => {
beforeEach(() => localStorage.remove('foo'));
beforeEach(() => localStorage.remove('foo'))
describe('#get', () => {
it('correctly gets an existing item from local storage', () => {
localStorage('foo', 'bar');
ls.get('foo').should.equal('bar');
});
localStorage('foo', 'bar')
ls.get('foo').should.equal('bar')
})
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', () => {
it('correctly sets an item into local storage', () => {
ls.set('foo', 'bar');
localStorage('foo').should.equal('bar');
});
});
ls.set('foo', 'bar')
localStorage('foo').should.equal('bar')
})
})
describe('#remove', () => {
it('correctly removes an item from local storage', () => {
localStorage('foo', 'bar');
ls.remove('foo');
(localStorage('foo') === null).should.be.true;
});
});
});
localStorage('foo', 'bar')
ls.remove('foo')
var result = localStorage('foo') === null
result.should.be.true
})
})
})

View file

@ -1,102 +1,102 @@
require('chai').should();
import { cloneDeep, last } from 'lodash';
require('chai').should()
import { cloneDeep, last } from 'lodash'
import { albumStore, artistStore } from '../../stores';
import { default as artists, singleAlbum, singleSong } from '../blobs/media';
import { albumStore, artistStore } from '../../stores'
import { default as artists, singleAlbum, singleSong } from '../blobs/media'
describe('stores/album', () => {
beforeEach(() => albumStore.init(cloneDeep(artists)));
beforeEach(() => albumStore.init(cloneDeep(artists)))
afterEach(() => albumStore.state.albums = []);
afterEach(() => albumStore.state.albums = [])
describe('#init', () => {
it('correctly gathers albums', () => {
albumStore.state.albums.length.should.equal(7);
});
albumStore.state.albums.length.should.equal(7)
})
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', () => {
albumStore.state.albums[0].artist.id.should.equal(1);
});
});
albumStore.state.albums[0].artist.id.should.equal(1)
})
})
describe('#all', () => {
it('correctly returns all songs', () => {
albumStore.all.length.should.equal(7);
});
});
albumStore.all.length.should.equal(7)
})
})
describe('#getLength', () => {
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…
});
});
})
})
describe('#add', () => {
beforeEach(() => {
albumStore.add(cloneDeep(singleAlbum));
});
albumStore.add(cloneDeep(singleAlbum))
})
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', () => {
last(albumStore.state.albums).length.should.equal(300);
});
last(albumStore.state.albums).length.should.equal(300)
})
it('correctly recalculates the play count', () => {
last(albumStore.state.albums).playCount.should.equal(11);
});
});
last(albumStore.state.albums).playCount.should.equal(11)
})
})
describe('#remove', () => {
beforeEach(() => {
albumStore.remove(albumStore.state.albums[0]); // ID 1193
});
})
it('correctly removes an album', () => {
albumStore.state.albums.length.should.equal(6);
});
});
albumStore.state.albums.length.should.equal(6)
})
})
describe('#addSongsIntoAlbum', () => {
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', () => {
albumStore.state.albums[0].songs.length.should.equal(2);
});
albumStore.state.albums[0].songs.length.should.equal(2)
})
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', () => {
albumStore.state.albums[0].length.should.equal(359.92);
});
});
albumStore.state.albums[0].length.should.equal(359.92)
})
})
describe('#removeSongsFromAlbum', () => {
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', () => {
albumStore.state.albums[0].songs.length.should.equal(0);
});
albumStore.state.albums[0].songs.length.should.equal(0)
})
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', () => {
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();
import { cloneDeep, last } from 'lodash';
require('chai').should()
import { cloneDeep, last } from 'lodash'
import { artistStore } from '../../stores';
import { default as artists, singleAlbum, singleArtist } from '../blobs/media';
import { artistStore } from '../../stores'
import { default as artists, singleAlbum, singleArtist } from '../blobs/media'
describe('stores/artist', () => {
beforeEach(() => artistStore.init(cloneDeep(artists)));
afterEach(() => artistStore.state.artists = []);
beforeEach(() => artistStore.init(cloneDeep(artists)))
afterEach(() => artistStore.state.artists = [])
describe('#init', () => {
it('correctly gathers artists', () => {
artistStore.state.artists.length.should.equal(3);
});
artistStore.state.artists.length.should.equal(3)
})
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', () => {
artistStore.state.artists[0].songCount = 3;
});
});
artistStore.state.artists[0].songCount = 3
})
})
describe('#getImage', () => {
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', () => {
beforeEach(() => artistStore.add(cloneDeep(singleArtist)));
beforeEach(() => artistStore.add(cloneDeep(singleArtist)))
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', () => {
beforeEach(() => artistStore.remove(artistStore.state.artists[0]));
beforeEach(() => artistStore.remove(artistStore.state.artists[0]))
it('correctly removes an artist', () => {
artistStore.state.artists.length.should.equal(2);
artistStore.state.artists[0].name.should.equal('Bob Dylan');
});
});
artistStore.state.artists.length.should.equal(2)
artistStore.state.artists[0].name.should.equal('Bob Dylan')
})
})
describe('#addAlbumsIntoArtist', () => {
beforeEach(() => {
artistStore.addAlbumsIntoArtist(artistStore.state.artists[0], cloneDeep(singleAlbum));
});
artistStore.addAlbumsIntoArtist(artistStore.state.artists[0], cloneDeep(singleAlbum))
})
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', () => {
const addedAlbum = last(artistStore.state.artists[0].albums);
addedAlbum.artist.should.equal(artistStore.state.artists[0]);
addedAlbum.artist_id.should.equal(artistStore.state.artists[0].id);
});
});
const addedAlbum = last(artistStore.state.artists[0].albums)
addedAlbum.artist.should.equal(artistStore.state.artists[0])
addedAlbum.artist_id.should.equal(artistStore.state.artists[0].id)
})
})
describe('#removeAlbumsFromArtist', () => {
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', () => {
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 { preferenceStore } from '../../stores';
import localStorage from 'local-storage'
import { preferenceStore } from '../../stores'
const user = { id: 0 };
const user = { id: 0 }
const preferences = {
volume: 8,
notify: false,
};
notify: false
}
describe('stores/preference', () => {
beforeEach(() => {
localStorage.set(`preferences_${user.id}`, preferences);
preferenceStore.init(user);
});
localStorage.set(`preferences_${user.id}`, preferences)
preferenceStore.init(user)
})
describe("#set", () => {
it('correctly sets preferences', () => {
preferenceStore.set('volume', 5);
localStorage.get(`preferences_${user.id}`).volume.should.equal(5);
preferenceStore.set('volume', 5)
localStorage.get(`preferences_${user.id}`).volume.should.equal(5)
// Test the proxy
preferenceStore.volume = 6;
localStorage.get(`preferences_${user.id}`).volume.should.equal(6);
});
});
preferenceStore.volume = 6
localStorage.get(`preferences_${user.id}`).volume.should.equal(6)
})
})
describe("#get", () => {
it('returns correct preference values', () => {
preferenceStore.get('volume').should.equal(8);
preferenceStore.get('volume').should.equal(8)
// 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 artists from '../blobs/media';
import { queueStore } from '../../stores'
import artists from '../blobs/media'
const songs = artists[2].albums[0].songs;
const songs = artists[2].albums[0].songs
describe('stores/queue', () => {
beforeEach(() => {
queueStore.state.songs = songs;
queueStore.state.current = songs[1];
});
queueStore.state.songs = songs
queueStore.state.current = songs[1]
})
describe('#all', () => {
it('correctly returns all queued songs', () => {
queueStore.all.should.equal(songs);
});
});
queueStore.all.should.equal(songs)
})
})
describe('#first', () => {
it('correctly returns the first queued song', () => {
queueStore.first.title.should.equal('No bravery');
});
});
queueStore.first.title.should.equal('No bravery')
})
})
describe('#last', () => {
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', () => {
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', () => {
queueStore.queue(song);
queueStore.last.title.should.equal('I Swear');
});
queueStore.queue(song)
queueStore.last.title.should.equal('I Swear')
})
it('correctly prepends a song to top of the queue', () => {
queueStore.queue(song, false, true);
queueStore.first.title.should.equal('I Swear');
});
queueStore.queue(song, false, true)
queueStore.first.title.should.equal('I Swear')
})
it('correctly replaces the whole queue', () => {
queueStore.queue(song, true);
queueStore.all.length.should.equal(1);
queueStore.first.title.should.equal('I Swear');
});
});
queueStore.queue(song, true)
queueStore.all.length.should.equal(1)
queueStore.first.title.should.equal('I Swear')
})
})
describe('#unqueue', () => {
beforeEach(() => queueStore.state.songs = songs);
beforeEach(() => queueStore.state.songs = songs)
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.
});
})
it('correctly removes mutiple songs from queue', () => {
queueStore.unqueue([queueStore.state.songs[0], queueStore.state.songs[1]]);
queueStore.first.title.should.equal('Wisemen');
});
});
queueStore.unqueue([queueStore.state.songs[0], queueStore.state.songs[1]])
queueStore.first.title.should.equal('Wisemen')
})
})
describe('#clear', () => {
it('correctly clears all songs from queue', () => {
queueStore.clear();
queueStore.state.songs.length.should.equal(0);
});
});
queueStore.clear()
queueStore.state.songs.length.should.equal(0)
})
})
describe('#current', () => {
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', () => {
queueStore.current = queueStore.state.songs[0];
queueStore.current.title.should.equal('No bravery');
});
});
queueStore.current = queueStore.state.songs[0]
queueStore.current.title.should.equal('No bravery')
})
})
describe('#getNextSong', () => {
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', () => {
queueStore.current = queueStore.state.songs[queueStore.state.songs.length - 1];
(queueStore.next === null).should.be.true;
});
});
queueStore.current = queueStore.state.songs[queueStore.state.songs.length - 1]
var result = queueStore.next === null
result.should.be.true
})
})
describe('#getPrevSong', () => {
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', () => {
queueStore.current = queueStore.state.songs[0];
(queueStore.previous === null).should.be.true;
});
});
});
queueStore.current = queueStore.state.songs[0]
var result = queueStore.previous === null
result.should.be.true
})
})
})

View file

@ -1,61 +1,61 @@
require('chai').should();
import { cloneDeep, last } from 'lodash';
require('chai').should()
import { cloneDeep, last } from 'lodash'
import { songStore, albumStore, artistStore } from '../../stores';
import artists from '../blobs/media';
import interactions from '../blobs/interactions';
import { songStore, albumStore, artistStore } from '../../stores'
import artists from '../blobs/media'
import interactions from '../blobs/interactions'
describe('stores/song', () => {
beforeEach(() => {
artistStore.init(artists);
});
artistStore.init(artists)
})
describe('#init', () => {
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', () => {
songStore.state.songs[0].fmtLength.should.be.a.string;
});
songStore.state.songs[0].fmtLength.should.be.a.string
})
it('correctly sets albums', () => {
songStore.state.songs[0].album.id.should.equal(1193);
});
});
songStore.state.songs[0].album.id.should.equal(1193)
})
})
describe('#all', () => {
it('correctly returns all songs', () => {
songStore.all.length.should.equal(14);
});
});
songStore.all.length.should.equal(14)
})
})
describe('#byId', () => {
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', () => {
it('correctly gets multiple songs by IDs', () => {
const songs = songStore.byIds(['e6d3977f3ffa147801ca5d1fdf6fa55e', 'aa16bbef6a9710eb9a0f41ecc534fad5']);
songs[0].title.should.equal('Like a rolling stone');
songs[1].title.should.equal("Knockin' on heaven's door");
});
});
const songs = songStore.byIds(['e6d3977f3ffa147801ca5d1fdf6fa55e', 'aa16bbef6a9710eb9a0f41ecc534fad5'])
songs[0].title.should.equal('Like a rolling stone')
songs[1].title.should.equal("Knockin' on heaven's door")
})
})
describe('#initInteractions', () => {
beforeEach(() => songStore.initInteractions(interactions));
beforeEach(() => songStore.initInteractions(interactions))
it('correctly sets interaction status', () => {
const song = songStore.byId('cb7edeac1f097143e65b1b2cde102482');
song.liked.should.be.true;
song.playCount.should.equal(3);
});
});
const song = songStore.byId('cb7edeac1f097143e65b1b2cde102482')
song.liked.should.be.true
song.playCount.should.equal(3)
})
})
describe('#syncUpdatedSong', () => {
beforeEach(() => artistStore.init(artists));
beforeEach(() => artistStore.init(artists))
const updatedSong = {
id: "39189f4545f9d5671fb3dc964f0080a0",
@ -66,19 +66,19 @@ describe('stores/song', () => {
arist_id: 1,
artist: {
id: 1,
name: 'All-4-One',
},
},
};
name: 'All-4-One'
}
}
}
it ('correctly syncs an updated song with no album changes', () => {
songStore.syncUpdatedSong(cloneDeep(updatedSong));
songStore.byId(updatedSong.id).title.should.equal('I Swear A Lot');
});
songStore.syncUpdatedSong(cloneDeep(updatedSong))
songStore.byId(updatedSong.id).title.should.equal('I Swear A Lot')
})
it ('correctly syncs an updated song into an existing album of same artist', () => {
const song = cloneDeep(updatedSong);
song.album_id = 1194;
const song = cloneDeep(updatedSong)
song.album_id = 1194
song.album = {
id: 1194,
artist_id: 1,
@ -86,62 +86,62 @@ describe('stores/song', () => {
id: 1,
name: 'All-4-One',
},
};
}
songStore.syncUpdatedSong(song);
songStore.byId(song.id).album.name.should.equal('And The Music Speaks');
});
songStore.syncUpdatedSong(song)
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', () => {
const song = cloneDeep(updatedSong);
song.album_id = 9999;
const song = cloneDeep(updatedSong)
song.album_id = 9999
song.album = {
id: 9999,
artist_id: 1,
name: 'Brand New Album from All-4-One',
artist: {
id: 1,
name: 'All-4-One',
},
};
name: 'All-4-One'
}
}
songStore.syncUpdatedSong(song);
songStore.syncUpdatedSong(song)
// 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.
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', () => {
const song = cloneDeep(updatedSong);
song.album_id = 10000;
const song = cloneDeep(updatedSong)
song.album_id = 10000
song.album = {
id: 10000,
name: "It's... John Cena!!!",
artist_id: 10000,
artist: {
id: 10000,
name: 'John Cena',
},
};
name: 'John Cena'
}
}
songStore.syncUpdatedSong(song);
songStore.syncUpdatedSong(song)
// A new artist should be created...
const lastArtist = last(artistStore.all);
lastArtist.name.should.equal('John Cena');
const lastArtist = last(artistStore.all)
lastArtist.name.should.equal('John Cena')
// A new album should be created
const lastAlbum = last(albumStore.all);
lastAlbum.name.should.equal("It's... John Cena!!!");
const lastAlbum = last(albumStore.all)
lastAlbum.name.should.equal("It's... John Cena!!!")
// 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.
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 data from '../blobs/users';
import { userStore } from '../../stores'
import data from '../blobs/users'
describe('stores/user', () => {
beforeEach(() => userStore.init(data.users, data.currentUser));
beforeEach(() => userStore.init(data.users, data.currentUser))
describe('#init', () => {
it('correctly sets data state', () => {
userStore.state.users.should.equal(data.users);
userStore.state.current.should.equal(data.currentUser);
});
});
userStore.state.users.should.equal(data.users)
userStore.state.current.should.equal(data.currentUser)
})
})
describe('#all', () => {
it('correctly returns all users', () => {
userStore.all.should.equal(data.users);
});
});
userStore.all.should.equal(data.users)
})
})
describe('#byId', () => {
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', () => {
it('correctly gets the current user', () => {
userStore.current.id.should.equal(1);
});
userStore.current.id.should.equal(1)
})
it('correctly sets the current user', () => {
userStore.current = data.users[1];
userStore.current.id.should.equal(2);
});
});
userStore.current = data.users[1]
userStore.current.id.should.equal(2)
})
})
describe('#setAvatar', () => {
it('correctly sets the current users avatar', () => {
userStore.setAvatar();
userStore.current.avatar.should.equal('https://www.gravatar.com/avatar/b9611f1bba1aacbe6f5de5856695a202?s=256');
});
userStore.setAvatar()
userStore.current.avatar.should.equal('https://www.gravatar.com/avatar/b9611f1bba1aacbe6f5de5856695a202?s=256')
})
it('correctly sets a users avatar', () => {
userStore.setAvatar(data.users[1]);
data.users[1].avatar.should.equal('https://www.gravatar.com/avatar/5024672cfe53f113b746e1923e373058?s=256');
});
});
describe('#updateProfile', () => {
});
describe('#store', () => {
});
describe('#update', () => {
});
describe('#destroy', () => {
});
});
userStore.setAvatar(data.users[1])
data.users[1].avatar.should.equal('https://www.gravatar.com/avatar/5024672cfe53f113b746e1923e373058?s=256')
})
})
})

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('#secondsToHis', () => {
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', () => {
secondsToHis(314).should.equal('05:14');
});
});
secondsToHis(314).should.equal('05:14')
})
})
describe('#parseValidationError', () => {
it('correctly parses single-level validation 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', () => {
const error = {
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.
*/
import select from 'select';
import select from 'select'
import { event } from '../utils'
/**
@ -10,18 +10,18 @@ import { event } from '../utils'
* @param {String} view The view, which can be found under components/main-wrapper/main-content.
* @param {...*} Extra data to attach to the view.
*/
export function loadMainView(view, ...args) {
event.emit('main-content-view:load', view, ...args);
};
export function loadMainView (view, ...args) {
event.emit('main-content-view:load', view, ...args)
}
/**
* Force reloading window regardless of "Confirm before reload" setting.
* This is handy for certain cases, for example Last.fm connect/disconnect.
*/
export function forceReloadWindow() {
window.onbeforeunload = function() {};
window.location.reload();
};
export function forceReloadWindow () {
window.onbeforeunload = function () {}
window.location.reload()
}
/**
* Show the overlay.
@ -30,26 +30,26 @@ export function forceReloadWindow() {
* @param {String} type
* @param {Boolean} dismissable
*/
export function showOverlay(message = 'Just a little patience…', type = 'loading', dismissable = false) {
event.emit('overlay:show', { message, type, dismissable });
};
export function showOverlay (message = 'Just a little patience…', type = 'loading', dismissable = false) {
event.emit('overlay:show', { message, type, dismissable })
}
/**
* Hide the overlay.
*/
export function hideOverlay() {
event.emit('overlay:hide');
};
export function hideOverlay () {
event.emit('overlay:hide')
}
/**
* Copy a text into clipboard.
*
* @param {string} txt
*/
export function copyText(txt) {
const copyArea = document.querySelector('#copyArea');
copyArea.style.top = (window.pageYOffset || document.documentElement.scrollTop) + 'px';
copyArea.value = txt;
select(copyArea);
document.execCommand('copy');
};
export function copyText (txt) {
const copyArea = document.querySelector('#copyArea')
copyArea.style.top = (window.pageYOffset || document.documentElement.scrollTop) + 'px'
copyArea.value = txt
select(copyArea)
document.execCommand('copy')
}

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